diff --git a/Changelog.md b/Changelog.md index 29a4ba83..8e4dd056 100644 --- a/Changelog.md +++ b/Changelog.md @@ -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) @@ -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 diff --git a/src/OpenRiaServices.Client.DomainClients.Http/Framework/BinaryHttpDomainClientFactory.cs b/src/OpenRiaServices.Client.DomainClients.Http/Framework/BinaryHttpDomainClientFactory.cs index cd718c71..a7ac9d9f 100644 --- a/src/OpenRiaServices.Client.DomainClients.Http/Framework/BinaryHttpDomainClientFactory.cs +++ b/src/OpenRiaServices.Client.DomainClients.Http/Framework/BinaryHttpDomainClientFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Net.Http; using OpenRiaServices.Client.DomainClients.Http; @@ -11,7 +12,7 @@ namespace OpenRiaServices.Client.DomainClients public class BinaryHttpDomainClientFactory : DomainClientFactory { - private readonly Func _httpClientFactory; + private readonly Func _httpClientFactory; /// /// Create a where all requests share a single @@ -31,6 +32,25 @@ public BinaryHttpDomainClientFactory(Uri serverBaseUri, HttpMessageHandler messa /// The value base all service Uris on (see ) /// method creating a new HttpClient each time, should never return null public BinaryHttpDomainClientFactory(Uri serverBaseUri, Func 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; + }; + } + + /// + /// Constructor intended for .Net Core where the actual creation is handled by IHttpClientFactory or similar + /// + /// The value base all service Uris on (see ) + /// method creating a new HttpClient each time, should never return null + public BinaryHttpDomainClientFactory(Uri serverBaseUri, Func httpClientFactory) { base.ServerBaseUri = serverBaseUri; this._httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); @@ -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; } } diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesConfigurationBuilder.cs b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesConfigurationBuilder.cs index 135122f9..c268c1fc 100644 --- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesConfigurationBuilder.cs +++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesConfigurationBuilder.cs @@ -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; @@ -19,19 +20,60 @@ internal OpenRiaServicesConfigurationBuilder(OpenRiaServicesEndpointDataSource d _typeIsService = typeIsService; } + public IEndpointConventionBuilder AddDomainService() 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() 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(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()?.EndpointRoutePattern + ?? Assembly.GetEntryAssembly().GetCustomAttribute()?.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(), + }; } } } diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesEndpointDataSource.cs b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesEndpointDataSource.cs index 0880094c..5d79d1a0 100644 --- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesEndpointDataSource.cs +++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesEndpointDataSource.cs @@ -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 _endpointBuilders = new(); + private readonly HashSet _paths = new(); + private readonly Dictionary _endpointBuilders = new(); private List _endpoints; public OpenRiaServicesEndpointDataSource() @@ -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 Endpoints { get @@ -82,14 +92,14 @@ private List 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; @@ -111,6 +121,8 @@ public DomainServiceEndpointBuilder(DomainServiceDescription description) public DomainServiceDescription Description { get { return _description; } } + public List Paths { get; } = new(); + public void Add(Action convention) { _conventions.Add(convention); @@ -136,16 +148,23 @@ public void ApplyFinallyConventions(EndpointBuilder endpointBuilder) } } - private Endpoint BuildEndpoint(string domainService, OperationInvoker invoker, DomainServiceEndpointBuilder domainServiceEndpointBuilder, List additionalMetadata) + private void AddEndpoints(List endpoints, OperationInvoker invoker, DomainServiceEndpointBuilder domainServiceEndpointBuilder, List 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 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); diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/OpenRiaServices.Hosting.AspNetCore.csproj b/src/OpenRiaServices.Hosting.AspNetCore/Framework/OpenRiaServices.Hosting.AspNetCore.csproj index c7b50bf3..0783712c 100644 --- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/OpenRiaServices.Hosting.AspNetCore.csproj +++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/OpenRiaServices.Hosting.AspNetCore.csproj @@ -6,7 +6,7 @@ OpenRiaServices.Hosting $(NoWarn);CS1574;CS1573;CS1591;CS1572 - 1.1.0 + 1.2.0 true Daniel-Svensson OpenRiaServices AspNetCore Hosting DomainServices AspNet WCF RIA Services Server diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/README.md b/src/OpenRiaServices.Hosting.AspNetCore/Framework/README.md index 9843f257..c64cfe32 100644 --- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/README.md +++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/README.md @@ -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 @@ -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.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 diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainServiceEndpointRoutePatternAttribute.cs b/src/OpenRiaServices.Server/Framework/Data/DomainServiceEndpointRoutePatternAttribute.cs new file mode 100644 index 00000000..f964d23b --- /dev/null +++ b/src/OpenRiaServices.Server/Framework/Data/DomainServiceEndpointRoutePatternAttribute.cs @@ -0,0 +1,49 @@ +using System; + +namespace OpenRiaServices.Server +{ +#if NET + /// + /// Determine how endpoints routes (Uris to access DomainServices) are generated + /// + /// + /// IMPORTANT: If any value is changed here, then the corresponding value must be changed in "OpenRiaServices.Tools.EndpointRoutePattern" + /// + public enum EndpointRoutePattern + { + /// + /// Enpoints routes match "My-Namespace-TypeName/MethodName" + /// + FullName, + + /// + /// Enpoints routes match "TypeName/MethodName" + /// + Name, + + /// + /// Enpoints routes match "My-Namespace-TypeName.svc/binary/MethodName" which is the same schema as in WCF hosting (and old WCF RIA Services) + /// + WCF + } + + /// + /// Attribute to configure the pattern used for endpoint, see + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = true)] + public sealed class DomainServiceEndpointRoutePatternAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// to use + public DomainServiceEndpointRoutePatternAttribute(EndpointRoutePattern endpointRoutePattern) + => EndpointRoutePattern = endpointRoutePattern; + + /// + /// that should be used + /// + public EndpointRoutePattern EndpointRoutePattern { get; } + } +#endif +} diff --git a/src/OpenRiaServices.Tools.TextTemplate/Framework/CSharpGenerators/CSharpDomainContextGenerator.cs b/src/OpenRiaServices.Tools.TextTemplate/Framework/CSharpGenerators/CSharpDomainContextGenerator.cs index a9f0ad74..1bb03150 100644 --- a/src/OpenRiaServices.Tools.TextTemplate/Framework/CSharpGenerators/CSharpDomainContextGenerator.cs +++ b/src/OpenRiaServices.Tools.TextTemplate/Framework/CSharpGenerators/CSharpDomainContextGenerator.cs @@ -85,13 +85,13 @@ protected virtual void GenerateClassDeclaration() } - /// + /// /// Generates the DomainContext class constructors. /// 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 "); @@ -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)); diff --git a/src/OpenRiaServices.Tools.TextTemplate/Framework/CSharpGenerators/CSharpDomainContextGenerator.partial.cs b/src/OpenRiaServices.Tools.TextTemplate/Framework/CSharpGenerators/CSharpDomainContextGenerator.partial.cs index 51c5bc85..5943bea8 100644 --- a/src/OpenRiaServices.Tools.TextTemplate/Framework/CSharpGenerators/CSharpDomainContextGenerator.partial.cs +++ b/src/OpenRiaServices.Tools.TextTemplate/Framework/CSharpGenerators/CSharpDomainContextGenerator.partial.cs @@ -1,6 +1,7 @@ namespace OpenRiaServices.Tools.TextTemplate.CSharpGenerators { using System; + using System.Reflection; using OpenRiaServices.Server; /// @@ -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()?.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 + } } } diff --git a/src/OpenRiaServices.Tools.TextTemplate/Framework/CSharpGenerators/CSharpDomainContextGenerator.tt b/src/OpenRiaServices.Tools.TextTemplate/Framework/CSharpGenerators/CSharpDomainContextGenerator.tt index 1515025a..91b358b8 100644 --- a/src/OpenRiaServices.Tools.TextTemplate/Framework/CSharpGenerators/CSharpDomainContextGenerator.tt +++ b/src/OpenRiaServices.Tools.TextTemplate/Framework/CSharpGenerators/CSharpDomainContextGenerator.tt @@ -48,19 +48,19 @@ public sealed partial class <#= this.DomainContextTypeName #> : <#= baseType #> <#+ } - /// + /// /// Generates the DomainContext class constructors. /// 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() #>)) { diff --git a/src/OpenRiaServices.Tools.TextTemplate/Framework/CodeGeneratorTextTemplate.cs b/src/OpenRiaServices.Tools.TextTemplate/Framework/CodeGeneratorTextTemplate.cs index a07b494a..8e49286e 100644 --- a/src/OpenRiaServices.Tools.TextTemplate/Framework/CodeGeneratorTextTemplate.cs +++ b/src/OpenRiaServices.Tools.TextTemplate/Framework/CodeGeneratorTextTemplate.cs @@ -260,6 +260,15 @@ public System.IFormatProvider FormatProvider } } } + + /// + /// String specialization of + /// + public string ToStringWithCulture(string objectToConvert) + { + return objectToConvert; + } + /// /// This is called from the compile/run appdomain to convert objects within an expression block to a string /// diff --git a/src/OpenRiaServices.Tools/Framework/ClientCodeGenerationDispatcher.cs b/src/OpenRiaServices.Tools/Framework/ClientCodeGenerationDispatcher.cs index 94987a87..0c77a23f 100644 --- a/src/OpenRiaServices.Tools/Framework/ClientCodeGenerationDispatcher.cs +++ b/src/OpenRiaServices.Tools/Framework/ClientCodeGenerationDispatcher.cs @@ -75,12 +75,22 @@ internal string GenerateCode(ClientCodeGenerationOptions options, SharedCodeServ // We load the ".Server" assembly actually from the server projects output folder since it was previously required to mix stongly named and not stronly named LoadOpenRiaServicesServerAssembly(parameters, loggingService); #else - // The current AssemblyLoadContext has been previously setup to load the server projects dependencies, no additional action is needed + // The current AssemblyLoadContext has been previously setup to load the server projects dependencies, no additional action is needed #endif using (SharedCodeService sharedCodeService = new SharedCodeService(parameters, loggingService)) { CodeGenerationHost host = new CodeGenerationHost(loggingService, sharedCodeService); + +#if NET + // If the server assembly has a DomainServiceEndpointRoutePatternAttribute, we use it to set the default EndpointRoutePattern + Assembly linkedServerAssembly = AssemblyUtilities.LoadAssembly(parameters.ServerAssemblies.First() + ".dll", loggingService); + if (linkedServerAssembly?.GetCustomAttribute() is { } routeAttribute) + { + options.DefaultEndpointRoutePattern = (EndpointRoutePattern)(int)routeAttribute.EndpointRoutePattern; + } +#endif + return this.GenerateCode(host, options, parameters.ServerAssemblies, codeGeneratorName); } } diff --git a/src/OpenRiaServices.Tools/Framework/ClientCodeGenerationOptions.cs b/src/OpenRiaServices.Tools/Framework/ClientCodeGenerationOptions.cs index 1ddfdb95..c67ab8b0 100644 --- a/src/OpenRiaServices.Tools/Framework/ClientCodeGenerationOptions.cs +++ b/src/OpenRiaServices.Tools/Framework/ClientCodeGenerationOptions.cs @@ -78,6 +78,7 @@ public string Language /// /// Gets or sets a value the target platform of the client. /// + [Obsolete("ClientProjectTargetPlatform is no longer used")] public TargetPlatform ClientProjectTargetPlatform { get; set; } /// @@ -90,5 +91,13 @@ public string Language /// generate fully qualified type names and avoid adding unnecessary imports. /// public bool UseFullTypeNames { get; set; } + + + /// + /// See "Server.EndpointRoutePattern" determining how default Uri's for accessing DomainServices are generated. + /// + public EndpointRoutePattern DefaultEndpointRoutePattern { get; set; } + = EndpointRoutePattern.WCF; + } } diff --git a/src/OpenRiaServices.Tools/Framework/DomainServiceProxyGenerator.cs b/src/OpenRiaServices.Tools/Framework/DomainServiceProxyGenerator.cs index aaa040cc..3d37283e 100644 --- a/src/OpenRiaServices.Tools/Framework/DomainServiceProxyGenerator.cs +++ b/src/OpenRiaServices.Tools/Framework/DomainServiceProxyGenerator.cs @@ -350,6 +350,30 @@ private CodeTypeDeclaration GenEntityContainerInnerClass(CodeTypeDeclaration pro return innerClass; } + + private string GetDomainServiceUri() + { + Type type = _domainServiceDescription.DomainServiceType; +#if NET + // Lookup DomainServiceEndpointRoutePatternAttribute first in same assembly as DomainService + // Then in the entry point assembly + // - Fallback + EndpointRoutePattern routePattern = type.Assembly.GetCustomAttribute()?.EndpointRoutePattern is { } endpointRoutePattern + ? (EndpointRoutePattern)(int)(endpointRoutePattern) + : base.ClientProxyGenerator.ClientProxyCodeGenerationOptions.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 + } + private void GenerateConstructors(CodeTypeDeclaration proxyClass, CodeTypeDeclaration contractInterface, EnableClientAccessAttribute enableClientAccessAttribute, CodeMethodInvokeExpression onCreatedExpression) { CodeTypeReference uriTypeRef = CodeGenUtilities.GetTypeReference(typeof(Uri), this.ClientProxyGenerator, proxyClass); @@ -363,7 +387,7 @@ private void GenerateConstructors(CodeTypeDeclaration proxyClass, CodeTypeDeclar true); // construct relative URI - string relativeServiceUri = string.Format(CultureInfo.InvariantCulture, "{0}.svc", this._domainServiceDescription.DomainServiceType.FullName.Replace('.', '-')); + string relativeServiceUri = GetDomainServiceUri(); CodeExpression relativeUriExpression = new CodeObjectCreateExpression( uriTypeRef, new CodePrimitiveExpression(relativeServiceUri), diff --git a/src/OpenRiaServices.Tools/Framework/EndpointRoutePattern.cs b/src/OpenRiaServices.Tools/Framework/EndpointRoutePattern.cs new file mode 100644 index 00000000..7b2fd491 --- /dev/null +++ b/src/OpenRiaServices.Tools/Framework/EndpointRoutePattern.cs @@ -0,0 +1,29 @@ +#if NETFRAMEWORK +#pragma warning disable 1574 // Server.EndpointRoutePattern is not available +#endif + +namespace OpenRiaServices.Tools +{ + /// + /// IMPORTANT: THIS IS AN EXACT copy of where all values are identical. + /// We don't use the server version in the options because we don't want to load in the Server assembly until a bit later + /// after the options are created. + /// + public enum EndpointRoutePattern + { + /// + /// Enpoints routes match "My-Namespace-TypeName/MethodName" + /// + FullName, + + /// + /// Enpoints routes match "TypeName/MethodName" + /// + Name, + + /// + /// Enpoints routes match "My-Namespace-TypeName.svc/binary/MethodName" which is the same schema as in WCF hosting (and old WCF RIA Services) + /// + WCF + } +} diff --git a/src/OpenRiaServices.Tools/Test/TestHelper.cs b/src/OpenRiaServices.Tools/Test/TestHelper.cs index c62b353f..e8710c74 100644 --- a/src/OpenRiaServices.Tools/Test/TestHelper.cs +++ b/src/OpenRiaServices.Tools/Test/TestHelper.cs @@ -750,7 +750,9 @@ internal static string ValidateLanguageCodeGen(CodeGenValidationOptions codeGenO ClientRootNamespace = codeGenOptions.RootNamespace, ClientProjectPath = "MockProject.proj", IsApplicationContextGenerationEnabled = codeGenOptions.GenerateApplicationContexts, - UseFullTypeNames = codeGenOptions.UseFullTypeNames + // Since we use the same baseline for NETFRAMEWORK and NET we need to set the same endpoint pattern for both + DefaultEndpointRoutePattern = EndpointRoutePattern.WCF, + UseFullTypeNames = codeGenOptions.UseFullTypeNames, }; MockCodeGenerationHost host = TestHelper.CreateMockCodeGenerationHost(codeGenOptions.Logger, codeGenOptions.SharedCodeService); diff --git a/src/Test/AspNetCoreWebsite/Program.cs b/src/Test/AspNetCoreWebsite/Program.cs index 43b5aa8b..2d3f08a8 100644 --- a/src/Test/AspNetCoreWebsite/Program.cs +++ b/src/Test/AspNetCoreWebsite/Program.cs @@ -12,6 +12,8 @@ using RootNamespace.TestNamespace; using TestDomainServices.Testing; +[assembly: DomainServiceEndpointRoutePattern(EndpointRoutePattern.FullName)] + var builder = WebApplication.CreateBuilder(args); builder.Services.AddOpenRiaServices(); @@ -55,6 +57,10 @@ // Types in this assembly builder.AddDomainService(); + + // Add services with old endpoint structure to allow unit tests to work + // REMARKDS: The unit tests should be rewritten so this is not needed + builder.AddDomainService("Cities-CityDomainService.svc/binary"); }); // TestDatabase diff --git a/src/Test/OpenRiaservices.EndToEnd.AspNetCore.Test/Main.cs b/src/Test/OpenRiaservices.EndToEnd.AspNetCore.Test/Main.cs index 8e4a3970..8c5a8ed5 100644 --- a/src/Test/OpenRiaservices.EndToEnd.AspNetCore.Test/Main.cs +++ b/src/Test/OpenRiaservices.EndToEnd.AspNetCore.Test/Main.cs @@ -26,16 +26,33 @@ public static void AssemblyInit(TestContext context) StartWebServer(); - DomainContext.DomainClientFactory = new BinaryHttpDomainClientFactory(TestURIs.RootURI, new HttpClientHandler() + var clientHandler = new HttpClientHandler() { CookieContainer = new CookieContainer(), UseCookies = true, AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, - }); + }; - // Note: Below gives errors when running (at least BinaryHttpDomainClientFactory) against AspNetCore - // It seems to cache results even with "private, no-store" - //HttpWebRequest.DefaultCachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.Default); + // Map enpoint names from WCF format to "FullName" format + // We do this since all DomainContext were generated usign old WCF format + Func httpClientFactory = uri => + { + HttpClient httpClient = new HttpClient(clientHandler, disposeHandler: false); + + // Remove ".svc/binary" from the URI + const string toRemove = ".svc/binary/"; + string uriString = uri.AbsoluteUri; + + if (uriString.EndsWith(toRemove, StringComparison.Ordinal)) + { + uri = new Uri(uriString.Remove(uriString.Length - toRemove.Length)); + } + + httpClient.BaseAddress = uri; + return httpClient; + }; + + DomainContext.DomainClientFactory = new BinaryHttpDomainClientFactory(TestURIs.RootURI, httpClientFactory); } [AssemblyCleanup] diff --git a/src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/EntityTestsE2E.cs b/src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/EntityTestsE2E.cs index cecb3b70..1eb4c6a8 100644 --- a/src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/EntityTestsE2E.cs +++ b/src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/EntityTestsE2E.cs @@ -28,7 +28,8 @@ public void Entity_RejectChanges_ParentAssociationRestored() List employeeList = new List(); ConfigurableEntityContainer container = new ConfigurableEntityContainer(); container.CreateSet(EntitySetOperations.All); - ConfigurableDomainContext catalog = new ConfigurableDomainContext(new WebDomainClient(TestURIs.EFCore_Catalog), container); + var domainClient = DomainContext.DomainClientFactory.CreateDomainClient(typeof(TestDomainServices.LTS.Catalog.ICatalogContract), TestURIs.EFCore_Catalog, false); + ConfigurableDomainContext catalog = new ConfigurableDomainContext(domainClient, container); var load = catalog.Load(catalog.GetEntityQuery("GetEmployees"), throwOnError:false); this.EnqueueCompletion(() => load);