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 9 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
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,22 @@ 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.FullName;
Daniel-Svensson marked this conversation as resolved.
Show resolved Hide resolved

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
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,13 @@ protected virtual void GenerateClassDeclaration()

}

/// <summary>
/// <summary>
/// Generates the DomainContext class constructors.
/// </summary>
protected virtual void GenerateConstructors()
{
bool requiresSecureEndpoint = this.GetRequiresSecureEndpoint();
string relativeServiceUri = string.Format(CultureInfo.InvariantCulture, "{0}.svc", this.DomainServiceDescription.DomainServiceType.FullName.Replace('.', '-'));
string relativeServiceUri = GetDomainServiceUri();

this.Write("public ");

Expand All @@ -101,7 +101,7 @@ protected virtual void GenerateConstructors()

this.Write(this.ToStringHelper.ToStringWithCulture(relativeServiceUri));

this.Write("\", UriKind.Relative))\r\n{\r\n}\r\n\t\t\r\npublic ");
this.Write("\", UriKind.Relative))\r\n{\r\n}\r\n\r\npublic ");

this.Write(this.ToStringHelper.ToStringWithCulture(this.DomainContextTypeName));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace OpenRiaServices.Tools.TextTemplate.CSharpGenerators
{
using System;
using System.Reflection;
using OpenRiaServices.Server;

/// <summary>
Expand Down Expand Up @@ -145,6 +146,29 @@ internal static string GetEndOperationReturnType(DomainOperationEntry operation)
}
return returnTypeName;
}

private string GetDomainServiceUri()
{
Type type = this.DomainServiceDescription.DomainServiceType;
#if NET
// Lookup DomainServiceEndpointRoutePatternAttribute first in same assembly as DomainService
// Then in the entry point assembly
// - Fallback
EndpointRoutePattern routePattern = type.Assembly.GetCustomAttribute<DomainServiceEndpointRoutePatternAttribute>()?.EndpointRoutePattern is { } endpointRoutePattern
? (EndpointRoutePattern)(int)(endpointRoutePattern)
: (EndpointRoutePattern)(int)this.ClientCodeGenerator.Options.DefaultEndpointRoutePattern;

return routePattern switch
{
EndpointRoutePattern.Name => type.Name,
EndpointRoutePattern.WCF => type.FullName.Replace('.', '-') + ".svc",
EndpointRoutePattern.FullName => type.FullName.Replace('.', '-'),
_ => throw new NotImplementedException(),
};
#else
return type.FullName.Replace('.', '-') + ".svc";
#endif
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,19 @@ public sealed partial class <#= this.DomainContextTypeName #> : <#= baseType #>
<#+
}

/// <summary>
/// <summary>
/// Generates the DomainContext class constructors.
/// </summary>
protected virtual void GenerateConstructors()
{
bool requiresSecureEndpoint = this.GetRequiresSecureEndpoint();
string relativeServiceUri = string.Format(CultureInfo.InvariantCulture, "{0}.svc", this.DomainServiceDescription.DomainServiceType.FullName.Replace('.', '-'));
string relativeServiceUri = GetDomainServiceUri();
#>
public <#= this.DomainContextTypeName #>() :
this(new Uri("<#= relativeServiceUri #>", UriKind.Relative))
{
}

public <#= this.DomainContextTypeName #>(Uri serviceUri) :
this(OpenRiaServices.Client.DomainContext.CreateDomainClient(typeof(<#= this.ContractInterfaceName #>), serviceUri, <#= requiresSecureEndpoint.ToString().ToLower() #>))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,15 @@ public System.IFormatProvider FormatProvider
}
}
}

/// <summary>
/// String specialization of <see cref="ToStringWithCulture(object)"/>
/// </summary>
public string ToStringWithCulture(string objectToConvert)
{
return objectToConvert;
}

/// <summary>
/// This is called from the compile/run appdomain to convert objects within an expression block to a string
/// </summary>
Expand Down
Loading
Loading