Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AspNetCore: Enable shorter Uris #508

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 5.4.4 / EF Core 3.1.0
# 5.5.0 / EF Core 3.1.0

### EF Core 3.1.0
* Initial support for Owned Entities for one-to-one navigation properties (#500)
Expand All @@ -11,9 +11,29 @@
* reduces code that needs to be written and works both with and without "OriginalEntity" (`RoundTripAttribute`)
* Add package README to `OpenRiaServices.Server.EntityFrameworkCore`

### AspNetCore 1.2.0
* Add support for specifying endpoints routes (#508, issue: #507)
You can choose between 3 different approaches to how the endpoint routes are generated.
See AspNetCore readme for more details.
* `WCF` will generate the same routes as WCF RIA Services `Some-Namespace-TypeName.svc/binary/Method`
* `FullName` will generate routes with the full name of the DomainService `Some-Namespace-TypeName/Method`
* `Name` will generate routes with the short name of the DomainService `TypeName/Method`

### Code generation
* Log whole Exceptions in DomainServiceCatalog instead of just message (#502), for better error messages on code generation failure
* Call "dotnet CodeGenTask.dll" instead of "CodeGenTask.exe" #503
* Support for the 3 different approaches to how the endpoint routes are generated for AspNetCore hosting (#508)
* Replace obsolete AssociationAttribute with new EntityAssociationAttribute on client (#509)

### Client
* Replace obsolete AssociationAttribute with new EntityAssociationAttribute on client (#509)
* The client currently detect `AssociationAttribute` but it will be removed in future versions.
* Ensure you have the corresponding version of the Code generation


### Client

### AspNetCore 1.2.0

# EF Core 3.0.0

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Net.Http;
using OpenRiaServices.Client.DomainClients.Http;

Expand All @@ -11,7 +12,7 @@ namespace OpenRiaServices.Client.DomainClients
public class BinaryHttpDomainClientFactory
: DomainClientFactory
{
private readonly Func<HttpClient> _httpClientFactory;
private readonly Func<Uri, HttpClient> _httpClientFactory;

/// <summary>
/// Create a <see cref="BinaryHttpDomainClientFactory"/> where all requests share a single <see cref="HttpMessageHandler"/>
Expand All @@ -31,6 +32,25 @@ public BinaryHttpDomainClientFactory(Uri serverBaseUri, HttpMessageHandler messa
/// <param name="serverBaseUri">The value base all service Uris on (see <see cref="DomainClientFactory.ServerBaseUri"/>)</param>
/// <param name="httpClientFactory">method creating a new HttpClient each time, should never return null</param>
public BinaryHttpDomainClientFactory(Uri serverBaseUri, Func<HttpClient> httpClientFactory)
{
base.ServerBaseUri = serverBaseUri;
if (httpClientFactory is null)
throw new ArgumentNullException(nameof(httpClientFactory));

this._httpClientFactory = (Uri uri) =>
{
HttpClient httpClient = httpClientFactory();
httpClient.BaseAddress = uri;
return httpClient;
};
}

/// <summary>
/// Constructor intended for .Net Core where the actual creation is handled by <c>IHttpClientFactory</c> or similar
/// </summary>
/// <param name="serverBaseUri">The value base all service Uris on (see <see cref="DomainClientFactory.ServerBaseUri"/>)</param>
/// <param name="httpClientFactory">method creating a new HttpClient each time, should never return null</param>
public BinaryHttpDomainClientFactory(Uri serverBaseUri, Func<Uri, HttpClient> httpClientFactory)
{
base.ServerBaseUri = serverBaseUri;
this._httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
Expand All @@ -48,8 +68,21 @@ protected override DomainClient CreateDomainClientCore(Type serviceContract, Uri
// what parameters to support, it might make sens to do changes per DomainService/DomainContext
private HttpClient CreateHttpClient(Uri serviceUri)
{
var httpClient = _httpClientFactory();
httpClient.BaseAddress = new Uri(serviceUri.AbsoluteUri + "/binary/", UriKind.Absolute);
// Add /binary only for WCF style Uris
if (serviceUri.AbsolutePath.EndsWith(".svc", StringComparison.Ordinal))
{
serviceUri = new Uri(serviceUri.AbsoluteUri + "/binary/");
}

var httpClient = _httpClientFactory(serviceUri);
httpClient.BaseAddress ??= serviceUri;

// Ensure Uri always end with "/" so that we can call Get and Post with just the method name
if (!httpClient.BaseAddress.AbsoluteUri.EndsWith("/", StringComparison.Ordinal))
{
httpClient.BaseAddress = new Uri(httpClient.BaseAddress.AbsoluteUri + '/');
}

return httpClient;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using OpenRiaServices.Server;
Expand All @@ -19,19 +20,60 @@ internal OpenRiaServicesConfigurationBuilder(OpenRiaServicesEndpointDataSource d
_typeIsService = typeIsService;
}

public IEndpointConventionBuilder AddDomainService<T>() where T : DomainService
{
return AddDomainService(typeof(T));
}

public IEndpointConventionBuilder AddDomainService(Type type)
{
var longName = type.FullName.Replace('.', '-') + ".svc";
ArgumentNullException.ThrowIfNull(nameof(type));

if (!_typeIsService.IsService(type))
throw new InvalidOperationException($"Domainservice {type} cannot be resolved by container, register it before calling map");

return _dataSource.AddDomainService(longName + "/binary", type);
return _dataSource.AddDomainService(GetDomainServiceRoute(type), type);
}

public IEndpointConventionBuilder AddDomainService<T>() where T : DomainService
public IEndpointConventionBuilder AddDomainService(Type type, string path)
{
return AddDomainService(typeof(T));
ArgumentNullException.ThrowIfNull(nameof(type));
#if NET7_0_OR_GREATER
ArgumentNullException.ThrowIfNullOrEmpty(nameof(path));
#endif
if (path.EndsWith('/'))
throw new ArgumentException("Path should not end with /", nameof(path));

if (!_typeIsService.IsService(type))
throw new InvalidOperationException($"Domainservice {type} cannot be resolved by container, register it before calling map");

return _dataSource.AddDomainService(path, type);
}

public IEndpointConventionBuilder AddDomainService<T>(string path) where T : DomainService
{
return AddDomainService(typeof(T), path);
}

private static string GetDomainServiceRoute(Type type)
{
// 1. TODO: Look at EnableClientAccessAttribute if we can set route there
// 2. Lookup DomainServiceEndpointRoutePatternAttribute in same assembly as DomainService
// 3. Lookup DomainServiceEndpointRoutePatternAttribute in startup assembly
// 4. Fallback to default (FullName)
// - Fallback to FullName
EndpointRoutePattern pattern =
type.Assembly.GetCustomAttribute<DomainServiceEndpointRoutePatternAttribute>()?.EndpointRoutePattern
?? Assembly.GetEntryAssembly().GetCustomAttribute<DomainServiceEndpointRoutePatternAttribute>()?.EndpointRoutePattern
?? EndpointRoutePattern.WCF;

return pattern switch
{
EndpointRoutePattern.Name => type.Name,
EndpointRoutePattern.WCF => type.FullName.Replace('.', '-') + ".svc/binary",
EndpointRoutePattern.FullName => type.FullName.Replace('.', '-'),
_ => throw new NotImplementedException(),
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ internal sealed class OpenRiaServicesEndpointDataSource : EndpointDataSource, IE
private readonly HttpMethodMetadata _getOrPost = new(new[] { "GET", "POST" });
private readonly HttpMethodMetadata _postOnly = new(new[] { "POST" });

private readonly Dictionary<string, DomainServiceEndpointBuilder> _endpointBuilders = new();
private readonly HashSet<string> _paths = new();
private readonly Dictionary<Type, DomainServiceEndpointBuilder> _endpointBuilders = new();
private List<Endpoint> _endpoints;

public OpenRiaServicesEndpointDataSource()
Expand All @@ -31,13 +32,22 @@ public OpenRiaServicesEndpointDataSource()

internal IEndpointConventionBuilder AddDomainService(string path, Type type)
{
var description = DomainServiceDescription.GetDescription(type);
var endpointBuilder = new DomainServiceEndpointBuilder(description);
if (!_paths.Add(path))
throw new ArgumentException($"Endpoint {path} is already in use for a DomainService", paramName: path);

if (!_endpointBuilders.TryGetValue(type, out var endpointBuilder))
{
var description = DomainServiceDescription.GetDescription(type);
endpointBuilder = new DomainServiceEndpointBuilder(description);
_endpointBuilders.Add(type, endpointBuilder);
}

endpointBuilder.Paths.Add(path);

_endpointBuilders.Add(path, endpointBuilder);
return endpointBuilder;
}


public override IReadOnlyList<Endpoint> Endpoints
{
get
Expand Down Expand Up @@ -82,14 +92,14 @@ private List<Endpoint> BuildEndpoints()
else // Submit related methods are not directly accessible
continue;

endpoints.Add(BuildEndpoint(name, invoker, domainServiceBuilder, additionalMetadata));
AddEndpoints(endpoints, invoker, domainServiceBuilder, additionalMetadata);
}

var submit = new ReflectionDomainServiceDescriptionProvider.ReflectionDomainOperationEntry(domainService.DomainServiceType,
typeof(DomainService).GetMethod(nameof(DomainService.SubmitAsync)), DomainOperation.Custom);

var submitOperationInvoker = new SubmitOperationInvoker(submit, serializationHelper);
endpoints.Add(BuildEndpoint(name, submitOperationInvoker, domainServiceBuilder, additionalMetadata));
AddEndpoints(endpoints, submitOperationInvoker, domainServiceBuilder, additionalMetadata);
}

return endpoints;
Expand All @@ -111,6 +121,8 @@ public DomainServiceEndpointBuilder(DomainServiceDescription description)

public DomainServiceDescription Description { get { return _description; } }

public List<string> Paths { get; } = new();

public void Add(Action<EndpointBuilder> convention)
{
_conventions.Add(convention);
Expand All @@ -136,16 +148,23 @@ public void ApplyFinallyConventions(EndpointBuilder endpointBuilder)
}
}

private Endpoint BuildEndpoint(string domainService, OperationInvoker invoker, DomainServiceEndpointBuilder domainServiceEndpointBuilder, List<object> additionalMetadata)
private void AddEndpoints(List<Endpoint> endpoints, OperationInvoker invoker, DomainServiceEndpointBuilder domainServiceEndpointBuilder, List<object> additionalMetadata)
{
var route = RoutePatternFactory.Parse($"{Prefix}/{domainService}/{invoker.OperationName}");
foreach(string path in domainServiceEndpointBuilder.Paths)
{
var route = RoutePatternFactory.Parse($"{Prefix}/{path}/{invoker.OperationName}");
endpoints.Add(BuildEndpoint(route, invoker, domainServiceEndpointBuilder, additionalMetadata));
}
}

private Endpoint BuildEndpoint(RoutePattern route, OperationInvoker invoker, DomainServiceEndpointBuilder domainServiceEndpointBuilder, List<object> additionalMetadata)
{
var endpointBuilder = new RouteEndpointBuilder(
invoker.Invoke,
route,
0)
{
DisplayName = $"{domainService}.{invoker.OperationName}"
DisplayName = $"{invoker.DomainOperation.DomainServiceType.Name}.{invoker.OperationName}"
};

endpointBuilder.Metadata.Add(invoker.HasSideEffects ? _postOnly : _getOrPost);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<RootNamespace>OpenRiaServices.Hosting</RootNamespace>
<!-- Ignore documentation varnings while experimental-->
<NoWarn>$(NoWarn);CS1574;CS1573;CS1591;CS1572</NoWarn>
<VersionPrefix>1.1.0</VersionPrefix>
<VersionPrefix>1.2.0</VersionPrefix>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Daniel-Svensson</Authors>
<PackageTags>OpenRiaServices AspNetCore Hosting DomainServices AspNet WCF RIA Services Server</PackageTags>
Expand Down
46 changes: 38 additions & 8 deletions src/OpenRiaServices.Hosting.AspNetCore/Framework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,13 @@ This excludes usage by the Russian state, Russian state-owned companies, Russian
- You allow anonymized telemetry to collected and sent during the preview releases to gather feedback about usage


## Production Ready - "preview"
## Sample

The package is production ready, but does not yet contain all features planened for 1.0.
Please look at TODO in project's folder for more details.

**Public API will change before 1.0.0 release**

There is no documentation yet, please see AspNetCoreWebsite project in repository for usage.
There is no documentation except for this yet readme, please see AspNetCoreWebsite project in repository for usage.

* For a sample see [WpfCore_AspNetCore in Samples repository](https://github.com/OpenRIAServices/Samples/tree/main/WpfCore_AspNetCore)



## Getting Started

1. Create a new dotnet 6 web application `dotnet new web` or similar
Expand Down Expand Up @@ -79,6 +73,42 @@ app.MapOpenRiaServices(builder =>
app.Run();
```

## Advanced

### Specifying endpoint routes

You can choose between 3 different approaches to how the endpoint routes are generated.
You do this by adding the `DomainServiceEndpointRoutePattern` attribute to your assembly.
- If the attribute is defined in the assembly of a specific DomainService, then that will be used
- Otherwise if there is an attribute in the "startup" assembly then that will be used
(since the code generation cannot know what project is the startup project,
it will always treat the "LinkedServerProject" as the startup project)

The options are `WCF`, `FullName` and `ShortName`.
* `WCF` will generate the same routes as WCF RIA Services `Some-Namespace-TypeName.svc/binary/Method`
* This is the only option that works with the (obsolete) WCF based DomainClient
* `FullName` will generate routes with the full name of the DomainService `Some-Namespace-TypeName/Method`
* `Name` will generate routes with the short name of the DomainService `TypeName/Method`

The default will be changed to `FullName` which is the same as in WCF RIA Services.
```csharp
[assembly: DomainServiceEndpointRoutePattern(EndpointRoutePattern.WCF)]
// or
[assembly: DomainServiceEndpointRoutePattern(EndpointRoutePattern.FullName)]
// or
[assembly: DomainServiceEndpointRoutePattern(EndpointRoutePattern.Name)]
```

If you want to change the route for a specific DomainService or need to map a DomainService to multiple routes you can specify
a route directly when adding domainservices during the `MapOpenRiaServices` call.

```csharp
app.MapOpenRiaServices(builder =>
{
builder.AddDomainService<Cities.CityDomainService>("Cities-CityDomainService.svc/binary");
});
```

## Asp.Net Core integration

Since 0.4.0 any attivbute applied to Invoke/Query are added to the corresponding AspNetCore-Endpoint allowing the use
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;

namespace OpenRiaServices.Server
{
#if NET
/// <summary>
/// Determine how endpoints routes (Uris to access DomainServices) are generated
/// </summary>
/// <remarks>
/// IMPORTANT: If any value is changed here, then the corresponding value must be changed in "OpenRiaServices.Tools.EndpointRoutePattern"
/// </remarks>
public enum EndpointRoutePattern
{
/// <summary>
/// Enpoints routes match "My-Namespace-TypeName/MethodName"
/// </summary>
FullName,

/// <summary>
/// Enpoints routes match "TypeName/MethodName"
/// </summary>
Name,

/// <summary>
/// Enpoints routes match "My-Namespace-TypeName.svc/binary/MethodName" which is the same schema as in WCF hosting (and old WCF RIA Services)
/// </summary>
WCF
}

/// <summary>
/// Attribute to configure the pattern used for endpoint, see <see cref="Server.EndpointRoutePattern"/>
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = true)]
public sealed class DomainServiceEndpointRoutePatternAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DomainServiceEndpointRoutePatternAttribute"/> class.
/// </summary>
/// <param name="endpointRoutePattern"><see cref="Server.EndpointRoutePattern"/> to use</param>
public DomainServiceEndpointRoutePatternAttribute(EndpointRoutePattern endpointRoutePattern)
=> EndpointRoutePattern = endpointRoutePattern;

/// <summary>
/// <see cref="EndpointRoutePattern"/> that should be used
/// </summary>
public EndpointRoutePattern EndpointRoutePattern { get; }
}
#endif
}
Loading
Loading