diff --git a/Masa.Framework.sln b/Masa.Framework.sln index d4ad33263..c694fc2a3 100644 --- a/Masa.Framework.sln +++ b/Masa.Framework.sln @@ -549,6 +549,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.Contrib.Authentication EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masa.Contrib.Authentication.OpenIdConnect.EFCore.Test", "src\Contrib\Authentication\Tests\Masa.Contrib.Authentication.OpenIdConnect.EFCore.Test\Masa.Contrib.Authentication.OpenIdConnect.EFCore.Test.csproj", "{F242EF1B-6951-4EE5-AF9C-CDC69D9A0260}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masa.Utils.Extensions.DotNet.Tests", "src\Utils\Extensions\Tests\Masa.Utils.Extensions.DotNet.Tests\Masa.Utils.Extensions.DotNet.Tests.csproj", "{9E9122FD-8E27-4524-862F-FFEFA97E404A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1957,6 +1959,14 @@ Global {F242EF1B-6951-4EE5-AF9C-CDC69D9A0260}.Release|Any CPU.Build.0 = Release|Any CPU {F242EF1B-6951-4EE5-AF9C-CDC69D9A0260}.Release|x64.ActiveCfg = Release|Any CPU {F242EF1B-6951-4EE5-AF9C-CDC69D9A0260}.Release|x64.Build.0 = Release|Any CPU + {9E9122FD-8E27-4524-862F-FFEFA97E404A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E9122FD-8E27-4524-862F-FFEFA97E404A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E9122FD-8E27-4524-862F-FFEFA97E404A}.Debug|x64.ActiveCfg = Debug|Any CPU + {9E9122FD-8E27-4524-862F-FFEFA97E404A}.Debug|x64.Build.0 = Debug|Any CPU + {9E9122FD-8E27-4524-862F-FFEFA97E404A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E9122FD-8E27-4524-862F-FFEFA97E404A}.Release|Any CPU.Build.0 = Release|Any CPU + {9E9122FD-8E27-4524-862F-FFEFA97E404A}.Release|x64.ActiveCfg = Release|Any CPU + {9E9122FD-8E27-4524-862F-FFEFA97E404A}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2227,6 +2237,7 @@ Global {583ECD6A-5960-4A60-833A-DB6DD93BE9C3} = {94D15C26-7204-4299-BC23-B89F5A0B0BF9} {60A02980-398C-40CD-9FAA-ACAD7A9A866C} = {94D15C26-7204-4299-BC23-B89F5A0B0BF9} {F242EF1B-6951-4EE5-AF9C-CDC69D9A0260} = {94D15C26-7204-4299-BC23-B89F5A0B0BF9} + {9E9122FD-8E27-4524-862F-FFEFA97E404A} = {C2FAC276-9D6E-498A-BBA2-F3F14ADF4D0D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {40383055-CC50-4600-AD9A-53C14F620D03} diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Attributes/ExcludeMappingAttribute.cs b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Attributes/IgnoreRouteAttribute.cs similarity index 64% rename from src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Attributes/ExcludeMappingAttribute.cs rename to src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Attributes/IgnoreRouteAttribute.cs index dfb039db0..2a3cbd3db 100644 --- a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Attributes/ExcludeMappingAttribute.cs +++ b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Attributes/IgnoreRouteAttribute.cs @@ -1,10 +1,10 @@ -// Copyright (c) MASA Stack All rights reserved. +// Copyright (c) MASA Stack All rights reserved. // Licensed under the MIT License. See LICENSE.txt in the project root for license information. namespace Microsoft.AspNetCore.Mvc; [AttributeUsage(AttributeTargets.Method)] -public class ExcludeMappingAttribute: Attribute +public class IgnoreRouteAttribute: Attribute { } diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Attributes/RoutePatternAttribute .cs b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Attributes/RoutePatternAttribute .cs new file mode 100644 index 000000000..c440c44a0 --- /dev/null +++ b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Attributes/RoutePatternAttribute .cs @@ -0,0 +1,23 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Mvc; + +[AttributeUsage(AttributeTargets.Method)] +public class RoutePatternAttribute : Attribute +{ + public string Pattern { get; set; } + + /// + /// The request method, the default is null (the request method is automatically identified according to the method name prefix) + /// + public string? HttpMethod { get; set; } + + public bool StartWithBaseUri { get; set; } + + public RoutePatternAttribute(string pattern, bool startWithBaseUri = false) + { + Pattern = pattern; + StartWithBaseUri = startWithBaseUri; + } +} diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Helpers/ServiceBaseHelper.cs b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Helpers/ServiceBaseHelper.cs new file mode 100644 index 000000000..76c232d53 --- /dev/null +++ b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Helpers/ServiceBaseHelper.cs @@ -0,0 +1,31 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for license information. + +namespace Masa.Contrib.Service.MinimalAPIs; + +public static class ServiceBaseHelper +{ + public static Delegate CreateDelegate(MethodInfo methodInfo, object targetInstance) + { + var type = Expression.GetDelegateType(methodInfo.GetParameters().Select(parameterInfo => parameterInfo.ParameterType) + .Concat(new List + { methodInfo.ReturnType }).ToArray()); + return Delegate.CreateDelegate(type, targetInstance, methodInfo); + } + + public static string CombineUris(params string[] uris) => string.Join("/", uris.Select(u => u.Trim('/'))); + + public static string TrimEndMethodName(string methodName) + => methodName.TrimEnd("Async", StringComparison.OrdinalIgnoreCase); + + public static string ParseMethodPrefix(string[] prefixes, string methodName) + { + var newMethodName = methodName; + var prefix = prefixes.FirstOrDefault(prefix => newMethodName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + + if (prefix is not null) + return prefix; + + return string.Empty; + } +} diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Masa.Contrib.Service.MinimalAPIs.csproj b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Masa.Contrib.Service.MinimalAPIs.csproj index 5b49091be..0148a036b 100644 --- a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Masa.Contrib.Service.MinimalAPIs.csproj +++ b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/Masa.Contrib.Service.MinimalAPIs.csproj @@ -12,9 +12,9 @@ - + diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/MasaService.cs b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/MasaService.cs deleted file mode 100644 index df99f640b..000000000 --- a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/MasaService.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) MASA Stack All rights reserved. -// Licensed under the MIT License. See LICENSE.txt in the project root for license information. - -namespace Masa.Contrib.Service.MinimalAPIs; - -public static class MasaService -{ - public static bool DisableRestful { get; set; } -} diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/README.md b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/README.md index 08c62d7ce..7b5df99b7 100644 --- a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/README.md +++ b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/README.md @@ -7,7 +7,7 @@ Original usage: ```C# var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); -app.MapGet("/api/v1/helloworld", ()=>"Hello World"); +app.MapGet("/api/v1/Demo/HelloWorld", () => "Hello World"); app.Run(); ``` @@ -21,20 +21,14 @@ Install-Package Masa.Contrib.Service.MinimalAPIs ```c# var builder = WebApplication.CreateBuilder(args); -var app = builder.Services - .AddServices(builder); +var app = builder.Services.AddServices(builder); ``` 2. Custom Service and inherit ServiceBase ```c# -public class IntegrationEventService : ServiceBase +public class DemoService : ServiceBase { - public IntegrationEventService(IServiceCollection services) : base(services) - { - App.MapGet("/api/v1/payment/HelloWorld", HelloWorld); - } - public string HelloWorld() { return "Hello World"; diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/README.zh-CN.md b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/README.zh-CN.md index 294d0cab0..535ffdd33 100644 --- a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/README.zh-CN.md +++ b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/README.zh-CN.md @@ -7,7 +7,7 @@ ```C# var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); -app.MapGet("/api/v1/helloworld", ()=>"Hello World"); +app.MapGet("/api/v1/Demo/HelloWorld", () => "Hello World"); app.Run(); ``` @@ -21,20 +21,14 @@ Install-Package Masa.Contrib.Service.MinimalAPIs ```c# var builder = WebApplication.CreateBuilder(args); -var app = builder.Services - .AddServices(builder); +var app = builder.Services.AddServices(builder); ``` 2. 自定义Service并继承ServiceBase,如: ```c# -public class IntegrationEventService : ServiceBase +public class DemoService : ServiceBase { - public IntegrationEventService(IServiceCollection services) : base(services) - { - App.MapGet("/api/v1/payment/HelloWorld", HelloWorld); - } - public string HelloWorld() { return "Hello World"; diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceBase.cs b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceBase.cs index d6e81871f..f24170d4e 100644 --- a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceBase.cs +++ b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceBase.cs @@ -3,138 +3,164 @@ namespace Microsoft.AspNetCore.Builder; -public abstract class ServiceBase : ServiceBaseOptions, IService +public abstract class ServiceBase : IService { public WebApplication App => MasaApp.GetRequiredService(); public string BaseUri { get; init; } + public ServiceRouteOptions RouteOptions { get; } = new(); + public string? ServiceName { get; init; } - public bool DisableRestful { get; init; } = MasaService.DisableRestful; + /// + /// Based on the RouteHandlerBuilder extension, it is used to extend the mapping method, such as + /// RouteHandlerBuilder = routeHandlerBuilder => + /// { + /// routeHandlerBuilder.RequireAuthorization("AtLeast21"); + /// }; + /// + public Action? RouteHandlerBuilder { get; init; } public IServiceCollection Services => MasaApp.Services; +#pragma warning disable S4136 + protected ServiceBase() { } + protected ServiceBase(string baseUri) { BaseUri = baseUri; } +#pragma warning restore S4136 public TService? GetService() => GetServiceProvider().GetService(); public TService GetRequiredService() where TService : notnull => GetServiceProvider().GetRequiredService(); +#pragma warning disable CA2208 protected virtual IServiceProvider GetServiceProvider() - => MasaApp.GetService()?.HttpContext?.RequestServices ?? MasaApp.RootServiceProvider; + => MasaApp.GetService()?.HttpContext?.RequestServices ?? throw new MasaException("Failed to get ServiceProvider of current request"); +#pragma warning restore CA2208 + + internal void AutoMapRoute(ServiceGlobalRouteOptions globalOptions, PluralizationService pluralizationService) + { + var type = GetType(); + + var methodInfos = type + .GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance) + .Where(methodInfo => methodInfo.CustomAttributes.All(attr => attr.AttributeType != typeof(IgnoreRouteAttribute))) + .Concat(type.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(methodInfo => methodInfo.CustomAttributes.Any(attr => attr.AttributeType == typeof(RoutePatternAttribute)))) + .Distinct(); - protected virtual string GetBaseUri(ServiceBaseOptions globalOptions, PluralizationService pluralizationService) + foreach (var method in methodInfos) + { + var handler = ServiceBaseHelper.CreateDelegate(method, this); + + string? pattern = null; + string? httpMethod = null; + string? methodName = null; + var attribute = method.GetCustomAttribute(); + if (attribute != null) + { + httpMethod = attribute.HttpMethod; + if (attribute.StartWithBaseUri) + methodName = attribute.Pattern; + else + pattern = attribute.Pattern; + } + + string newMethodName = method.Name; + + if (httpMethod == null || pattern == null) + { + var result = ParseMethod(globalOptions, newMethodName); + httpMethod ??= result.HttpMethod; + newMethodName = result.MethodName; + } + + pattern ??= ServiceBaseHelper.CombineUris(GetBaseUri(globalOptions, pluralizationService), + methodName ?? GetMethodName(method, newMethodName, globalOptions)); + var routeHandlerBuilder = MapMethods(pattern, httpMethod, handler); + (RouteHandlerBuilder ?? globalOptions.RouteHandlerBuilder)?.Invoke(routeHandlerBuilder); + } + } + + RouteHandlerBuilder MapMethods(string pattern, string? httpMethod, Delegate handler) { - if (DisableRestful) return string.Empty; + if (httpMethod != null) + return App.MapMethods(pattern, new[] { httpMethod }, handler); - return string.IsNullOrWhiteSpace(BaseUri) ? GetUrl(globalOptions, pluralizationService) : BaseUri; + return App.Map(pattern, handler); } - private string GetUrl(ServiceBaseOptions globalOptions, PluralizationService pluralizationService) + protected virtual string GetBaseUri(ServiceRouteOptions globalOptions, PluralizationService pluralizationService) { + if (!string.IsNullOrWhiteSpace(BaseUri)) + return BaseUri; + var list = new List() { - Prefix ?? globalOptions.Prefix ?? string.Empty, - Version ?? globalOptions.Version ?? string.Empty, - ServiceName ?? - GetServiceName((PluralizeServiceName ?? globalOptions.PluralizeServiceName) is true ? pluralizationService : null) + RouteOptions.Prefix ?? globalOptions.Prefix ?? string.Empty, + RouteOptions.Version ?? globalOptions.Version ?? string.Empty, + ServiceName ?? GetServiceName(RouteOptions.PluralizeServiceName ?? globalOptions.PluralizeServiceName ?? false ? + pluralizationService : + null) }; - return string.Join('/', list.Where(x => !string.IsNullOrWhiteSpace(x))); + return string.Join('/', list.Where(x => !string.IsNullOrWhiteSpace(x)).Select(u => u.Trim('/'))); } private string GetServiceName(PluralizationService? pluralizationService) { - var typeName = GetType().Name; - var index = typeName.LastIndexOf("Service", StringComparison.OrdinalIgnoreCase); - var serviceName = typeName.Remove(index); + var serviceName = GetType().Name.TrimEnd("Service", StringComparison.OrdinalIgnoreCase); if (pluralizationService == null) return serviceName; return pluralizationService.Pluralize(serviceName); } - internal void AutoMapRoute(ServiceBaseOptions globalOptions, PluralizationService pluralizationService) + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + protected virtual string GetMethodName(MethodInfo methodInfo, string methodName, ServiceRouteOptions globalOptions) { - var type = GetType(); + if (!(RouteOptions.AutoAppendId ?? globalOptions.AutoAppendId ?? false)) + return ServiceBaseHelper.TrimEndMethodName(methodName); - var methodInfos = type - .GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance) - .Where(methodInfo => methodInfo.CustomAttributes.All(attr => attr.AttributeType != typeof(ExcludeMappingAttribute))); - - foreach (var method in methodInfos) + var idParameter = methodInfo.GetParameters().FirstOrDefault(p => p.Name!.Equals("id", StringComparison.OrdinalIgnoreCase)); + if (idParameter != null) { - var @delegate = CreateDelegate(method, this); - - var pattern = CombineUris(GetBaseUri(globalOptions, pluralizationService), GetMethodName(method)); - var httpMethod = GetHttpMethod(globalOptions, method.Name); - - if (httpMethod != null) - App.MapMethods(pattern, new[] { httpMethod }, @delegate); - else - App.Map(pattern, @delegate); + var id = idParameter.ParameterType.IsNullableType() || idParameter.HasDefaultValue ? "{id?}" : "{id}"; + return $"{ServiceBaseHelper.TrimEndMethodName(methodName)}/{id}"; } - } - - protected virtual string? GetHttpMethod(ServiceBaseOptions globalOptions, string methodName) - { - if (ExistPrefix(GetPrefixs ?? globalOptions.GetPrefixs!, methodName)) - return "GET"; - - if (ExistPrefix(PostPrefixs ?? globalOptions.PostPrefixs!, methodName)) - return "POST"; - - if (ExistPrefix(PutPrefixs ?? globalOptions.PutPrefixs!, methodName)) - return "PUT"; - - if (ExistPrefix(DeletePrefixs ?? globalOptions.DeletePrefixs!, methodName)) - return "DELETE"; - return null; + return ServiceBaseHelper.TrimEndMethodName(methodName); } - public static bool ExistPrefix(string[] prefixs, string methodName) - => prefixs.Any(prefix => methodName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); - - private static Delegate CreateDelegate(MethodInfo methodInfo, object targetInstance) + protected virtual (string? HttpMethod, string MethodName) ParseMethod(ServiceRouteOptions globalOptions, string methodName) { - var type = Expression.GetDelegateType(methodInfo.GetParameters().Select(parameterInfo => parameterInfo.ParameterType) - .Concat(new List - { methodInfo.ReturnType }).ToArray()); - return Delegate.CreateDelegate(type, targetInstance, methodInfo); - } + var prefix = ServiceBaseHelper.ParseMethodPrefix(RouteOptions.GetPrefixes ?? globalOptions.GetPrefixes!, methodName); + if (!string.IsNullOrEmpty(prefix)) + return ("GET", methodName.Substring(prefix.Length)); - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - protected virtual string GetMethodName(MethodInfo methodInfo) - { - var parameters = methodInfo.GetParameters(); - if (parameters.Length >= 1 && parameters.Any(parameter => parameter.Name != null && parameter.Name.Equals("id", StringComparison.OrdinalIgnoreCase))) - return parameters[0].ParameterType.IsNullableType() ? "{id?}" : "{id}"; + prefix = ServiceBaseHelper.ParseMethodPrefix(RouteOptions.PostPrefixes ?? globalOptions.PostPrefixes!, methodName); + if (!string.IsNullOrEmpty(prefix)) + return ("POST", methodName.Substring(prefix.Length)); - return FormatMethodName(methodInfo.Name); - } + prefix = ServiceBaseHelper.ParseMethodPrefix(RouteOptions.PutPrefixes ?? globalOptions.PutPrefixes!, methodName); + if (!string.IsNullOrEmpty(prefix)) + return ("PUT", methodName.Substring(prefix.Length)); - public static string FormatMethodName(string methodName) - { - if (methodName.EndsWith("Async")) - { - var i = methodName.LastIndexOf("Async", StringComparison.Ordinal); - methodName = methodName[..i]; - } + prefix = ServiceBaseHelper.ParseMethodPrefix(RouteOptions.DeletePrefixes ?? globalOptions.DeletePrefixes!, methodName); + if (!string.IsNullOrEmpty(prefix)) + return ("DELETE", methodName.Substring(prefix.Length)); - return methodName; + return (null, string.Empty); } - public static string CombineUris(params string[] uris) - => string.Join("/", uris.Select(u => u.Trim('/'))); - #region Obsolete +#pragma warning disable S4136 [Obsolete("service can be ignored")] protected ServiceBase(IServiceCollection services) { @@ -145,6 +171,7 @@ protected ServiceBase(IServiceCollection services, string baseUri) : this(servic { } +#pragma warning restore S4136 #region [Obsolete] Map GET,POST,PUT,DELETE @@ -155,14 +182,14 @@ protected ServiceBase(IServiceCollection services, string baseUri) : this(servic /// The delegate executed when the endpoint is matched. It's name is a part of pattern if is null. /// The custom uri. It is a part of pattern if it is not null. /// Determines whether to remove the string 'Async' at the end. - /// A that can be used to further customize the endpoint. + /// A that can be used to further customize the endpoint. [Obsolete("It is recommended to map according to the automatic mapping rules")] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] protected RouteHandlerBuilder MapGet(Delegate handler, string? customUri = null, bool trimEndAsync = true) { - customUri ??= FormatMethodName(handler.Method.Name); + customUri ??= ServiceBaseHelper.TrimEndMethodName(handler.Method.Name); - var pattern = CombineUris(BaseUri, customUri); + var pattern = ServiceBaseHelper.CombineUris(BaseUri, customUri); return App.MapGet(pattern, handler); } @@ -174,14 +201,14 @@ protected RouteHandlerBuilder MapGet(Delegate handler, string? customUri = null, /// The delegate executed when the endpoint is matched. It's name is a part of pattern if is null. /// The custom uri. It is a part of pattern if it is not null. /// Determines whether to remove the string 'Async' at the end. - /// A that can be used to further customize the endpoint. + /// A that can be used to further customize the endpoint. [Obsolete("It is recommended to map according to the automatic mapping rules")] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] protected RouteHandlerBuilder MapPost(Delegate handler, string? customUri = null, bool trimEndAsync = true) { - customUri ??= FormatMethodName(handler.Method.Name); + customUri ??= ServiceBaseHelper.TrimEndMethodName(handler.Method.Name); - var pattern = CombineUris(BaseUri, customUri); + var pattern = ServiceBaseHelper.CombineUris(BaseUri, customUri); return App.MapPost(pattern, handler); } @@ -193,14 +220,14 @@ protected RouteHandlerBuilder MapPost(Delegate handler, string? customUri = null /// The delegate executed when the endpoint is matched. It's name is a part of pattern if is null. /// The custom uri. It is a part of pattern if it is not null. /// Determines whether to remove the string 'Async' at the end. - /// A that can be used to further customize the endpoint. + /// A that can be used to further customize the endpoint. [Obsolete("It is recommended to map according to the automatic mapping rules")] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] protected RouteHandlerBuilder MapPut(Delegate handler, string? customUri = null, bool trimEndAsync = true) { - customUri ??= FormatMethodName(handler.Method.Name); + customUri ??= ServiceBaseHelper.TrimEndMethodName(handler.Method.Name); - var pattern = CombineUris(BaseUri, customUri); + var pattern = ServiceBaseHelper.CombineUris(BaseUri, customUri); return App.MapPut(pattern, handler); } @@ -212,14 +239,14 @@ protected RouteHandlerBuilder MapPut(Delegate handler, string? customUri = null, /// The delegate executed when the endpoint is matched. It's name is a part of pattern if is null. /// The custom uri. It is a part of pattern if it is not null. /// Determines whether to remove the string 'Async' at the end. - /// A that can be used to further customize the endpoint. + /// A that can be used to further customize the endpoint. [Obsolete("It is recommended to map according to the automatic mapping rules")] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] protected RouteHandlerBuilder MapDelete(Delegate handler, string? customUri = null, bool trimEndAsync = true) { - customUri ??= FormatMethodName(handler.Method.Name); + customUri ??= ServiceBaseHelper.TrimEndMethodName(handler.Method.Name); - var pattern = CombineUris(BaseUri, customUri); + var pattern = ServiceBaseHelper.CombineUris(BaseUri, customUri); return App.MapDelete(pattern, handler); } diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceCollectionExtensions.cs b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceCollectionExtensions.cs index ed5bea294..72bcaa8f1 100644 --- a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceCollectionExtensions.cs +++ b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceCollectionExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) MASA Stack All rights reserved. // Licensed under the MIT License. See LICENSE.txt in the project root for license information. -using Microsoft.Extensions.Options; - namespace Microsoft.Extensions.DependencyInjection; public static class ServiceCollectionExtensions @@ -28,7 +26,7 @@ public static WebApplication AddServices( /// public static WebApplication AddServices( this WebApplicationBuilder builder, - Action action) + Action action) => builder.Services.AddServices(builder, action); /// @@ -51,18 +49,16 @@ public static WebApplication AddServices( public static WebApplication AddServices( this IServiceCollection services, WebApplicationBuilder builder, - Action action) + Action action) { if (services.All(service => service.ImplementationType != typeof(MinimalApisMarkerService))) { services.AddSingleton(); + services.AddHttpContextAccessor(); services.Configure(action); -#pragma warning disable CA1822 - //todo: Version 1.0 will be removed - services.TryAddScoped(sp => services); -#pragma warning restore CA1822 + services.TryAddScoped(sp => services);// Version 1.0 will be removed services.AddSingleton(new Lazy(builder.Build, LazyThreadSafetyMode.ExecutionAndPublication)) .AddTransient(serviceProvider => serviceProvider.GetRequiredService>().Value); @@ -70,11 +66,14 @@ public static WebApplication AddServices( MasaApp.Services = services; MasaApp.Build(services.BuildServiceProvider()); - var serviceMapOptions = MasaApp.GetRequiredService>().Value; + var serviceMapOptions = MasaApp.GetRequiredService>().Value; services.AddServices(true, (_, serviceInstance) => { var instance = (ServiceBase)serviceInstance; - if (!instance.DisableRestful) instance.AutoMapRoute(serviceMapOptions, serviceMapOptions.Pluralization); + if (instance.RouteOptions.DisableAutoMapRoute ?? serviceMapOptions.DisableAutoMapRoute ?? false) + return; + + instance.AutoMapRoute(serviceMapOptions, serviceMapOptions.Pluralization); }, serviceMapOptions.Assemblies); } diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceGlobalRouteOptions.cs b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceGlobalRouteOptions.cs new file mode 100644 index 000000000..f132c6d5f --- /dev/null +++ b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceGlobalRouteOptions.cs @@ -0,0 +1,28 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for license information. + +namespace Masa.Contrib.Service.MinimalAPIs; + +public class ServiceGlobalRouteOptions : ServiceRouteOptions +{ + public Assembly[] Assemblies { get; set; } + + public Action? RouteHandlerBuilder { get; set; } + + internal PluralizationService Pluralization { get; set; } + + public ServiceGlobalRouteOptions() + { + DisableAutoMapRoute = false; + Prefix = "api"; + Version = "v1"; + AutoAppendId = true; + PluralizeServiceName = true; + GetPrefixes = new[] { "Get", "Select" }; + PostPrefixes = new[] { "Post", "Add", "Upsert", "Create" }; + PutPrefixes = new[] { "Put", "Update", "Modify" }; + DeletePrefixes = new[] { "Delete", "Remove" }; + Assemblies = AppDomain.CurrentDomain.GetAssemblies(); + Pluralization = PluralizationService.CreateService(CultureInfo.CreateSpecificCulture("en")); + } +} diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceMapOptions.cs b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceMapOptions.cs deleted file mode 100644 index 1c706f177..000000000 --- a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceMapOptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) MASA Stack All rights reserved. -// Licensed under the MIT License. See LICENSE.txt in the project root for license information. - -namespace Masa.Contrib.Service.MinimalAPIs; - -public class ServiceMapOptions : ServiceBaseOptions -{ - public Assembly[] Assemblies { get; set; } - - internal PluralizationService Pluralization { get; set; } - - public ServiceMapOptions() - { - Assemblies = AppDomain.CurrentDomain.GetAssemblies(); - Prefix = "api"; - Version = "v1"; - PluralizeServiceName = false; - GetPrefixs = new[] { "GET", "SELECT" }; - PostPrefixs = new[] { "POST", "ADD", "Upsert" }; - PutPrefixs = new[] { "PUT", "Update", "Modify" }; - DeletePrefixs = new[] { "DELETE", "REMOVE" }; - Pluralization = PluralizationService.CreateService(CultureInfo.CreateSpecificCulture("en")); - } -} diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceBaseOptions.cs b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceRouteOptions.cs similarity index 63% rename from src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceBaseOptions.cs rename to src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceRouteOptions.cs index fb68f801a..4cd4e730f 100644 --- a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceBaseOptions.cs +++ b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/ServiceRouteOptions.cs @@ -1,10 +1,12 @@ -// Copyright (c) MASA Stack All rights reserved. +// Copyright (c) MASA Stack All rights reserved. // Licensed under the MIT License. See LICENSE.txt in the project root for license information. namespace Masa.Contrib.Service.MinimalAPIs; -public class ServiceBaseOptions +public class ServiceRouteOptions { + public bool? DisableAutoMapRoute { get; set; } + /// /// The prefix, the default is null /// Formatter is $"{Prefix}/{Version}/{ServiceName}", any one IsNullOrWhiteSpace would be ignored. @@ -17,13 +19,15 @@ public class ServiceBaseOptions /// public string? Version { get; set; } + public bool? AutoAppendId { get; set; } + public bool? PluralizeServiceName { get; set; } - public string[]? GetPrefixs { get; set; } + public string[]? GetPrefixes { get; set; } - public string[]? PostPrefixs { get; set; } + public string[]? PostPrefixes { get; set; } - public string[]? PutPrefixs { get; set; } + public string[]? PutPrefixes { get; set; } - public string[]? DeletePrefixs { get; set; } + public string[]? DeletePrefixes { get; set; } } diff --git a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/_Imports.cs b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/_Imports.cs index 5563a4a34..9c5591190 100644 --- a/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/_Imports.cs +++ b/src/Contrib/Service/Masa.Contrib.Service.MinimalAPIs/_Imports.cs @@ -10,6 +10,7 @@ global using Microsoft.AspNetCore.Routing; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Options; global using System; global using System.Collections.Generic; global using System.Globalization; diff --git a/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/MinimalAPITest.cs b/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/MinimalAPITest.cs index cab67d769..092a9a31a 100644 --- a/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/MinimalAPITest.cs +++ b/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/MinimalAPITest.cs @@ -38,8 +38,7 @@ public void AddService() Assert.ReferenceEquals(customService.Services, _builder.Services); - Assert.IsNotNull(customService.GetRequiredService()); - Assert.IsNotNull(customService.GetService()); + Assert.ThrowsException(() => customService.GetRequiredService()); Assert.IsTrue(customService.GetTest2() == 1); diff --git a/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/ServiceBaseTest.cs b/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/ServiceBaseTest.cs index ac4f7f784..552d80c21 100644 --- a/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/ServiceBaseTest.cs +++ b/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/ServiceBaseTest.cs @@ -9,15 +9,12 @@ public class ServiceBaseTest [TestMethod] public void TestGetBaseUri() { - var serviceMapOptions = new ServiceMapOptions(); + var serviceMapOptions = new ServiceGlobalRouteOptions(); var serviceBase = GetCustomService(); - Assert.AreEqual("api/v1/Custom", serviceBase.TestGetBaseUri(serviceMapOptions)); + Assert.AreEqual("api/v1/Customs", serviceBase.TestGetBaseUri(serviceMapOptions)); serviceBase = GetUserService(); - Assert.AreEqual("api/v1/User", serviceBase.TestGetBaseUri(serviceMapOptions)); - - serviceBase = GetOrderService(); - Assert.AreEqual(string.Empty, serviceBase.TestGetBaseUri(serviceMapOptions)); + Assert.AreEqual("api/v1/Users", serviceBase.TestGetBaseUri(serviceMapOptions)); serviceBase = GetGoodsService(); Assert.AreEqual("api/v2/Goods", serviceBase.TestGetBaseUri(serviceMapOptions)); @@ -29,7 +26,7 @@ public void TestGetBaseUri() [DataRow("Order/Get", "Order/Get")] public void TestFormatMethods(string methodName, string result) { - Assert.AreEqual(result, ServiceBase.FormatMethodName(methodName)); + Assert.AreEqual(result, ServiceBaseHelper.TrimEndMethodName(methodName)); } [TestMethod] @@ -41,7 +38,16 @@ public void TestCombineUris() "v1", "order" }; - Assert.AreEqual("api/v1/order", ServiceBase.CombineUris(uris)); + Assert.AreEqual("api/v1/order", ServiceBaseHelper.CombineUris(uris)); + } + + [DataTestMethod] + [DataRow("Update,Modify,Put", "AddGoods", "")] + [DataRow("Add, Upsert, Create, AddGoods", "AddGoods", "Add")] + public void TestTryParseHttpMethod(string prefixes, string methodName, string prefix) + { + var result = ServiceBaseHelper.ParseMethodPrefix(prefixes.Split(','), methodName); + Assert.AreEqual(prefix, result); } #region private methods @@ -52,9 +58,6 @@ private static CustomServiceBase GetCustomService() private static CustomServiceBase GetUserService() => new UserService(); - private static CustomServiceBase GetOrderService() - => new OrderService(); - private static CustomServiceBase GetGoodsService() => new GoodsService(); diff --git a/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/Services/CustomServiceBase.cs b/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/Services/CustomServiceBase.cs index 7802dcf0c..e31fa6370 100644 --- a/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/Services/CustomServiceBase.cs +++ b/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/Services/CustomServiceBase.cs @@ -5,7 +5,7 @@ namespace Masa.Contrib.Service.MinimalAPIs.Tests.Services; public abstract class CustomServiceBase : ServiceBase { - protected CustomServiceBase() : base(new ServiceCollection()) + protected CustomServiceBase() { } @@ -13,6 +13,6 @@ protected CustomServiceBase(string baseUri) : base(baseUri) { } - public string TestGetBaseUri(ServiceBaseOptions globalOptions) => base.GetBaseUri(globalOptions, + public string TestGetBaseUri(ServiceRouteOptions globalOptions) => base.GetBaseUri(globalOptions, PluralizationService.CreateService(System.Globalization.CultureInfo.CreateSpecificCulture("en"))); } diff --git a/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/Services/GoodsService.cs b/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/Services/GoodsService.cs index 4d4c15b4f..ee966805f 100644 --- a/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/Services/GoodsService.cs +++ b/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/Services/GoodsService.cs @@ -7,7 +7,7 @@ public class GoodsService : CustomServiceBase { public GoodsService() { - Prefix = "api"; - Version = "v2"; + RouteOptions.Prefix = "api"; + RouteOptions.Version = "v2"; } } diff --git a/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/Services/OrderService.cs b/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/Services/OrderService.cs deleted file mode 100644 index 3c3e3ff34..000000000 --- a/src/Contrib/Service/Tests/Masa.Contrib.Service.MinimalAPIs.Tests/Services/OrderService.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) MASA Stack All rights reserved. -// Licensed under the MIT License. See LICENSE.txt in the project root for license information. - -namespace Masa.Contrib.Service.MinimalAPIs.Tests.Services; - -public class OrderService: CustomServiceBase -{ - public OrderService() - { - DisableRestful = true; - } -} diff --git a/src/Utils/Extensions/Masa.Utils.Extensions.DotNet/StringExtensions.cs b/src/Utils/Extensions/Masa.Utils.Extensions.DotNet/StringExtensions.cs new file mode 100644 index 000000000..e6624a39b --- /dev/null +++ b/src/Utils/Extensions/Masa.Utils.Extensions.DotNet/StringExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for license information. + +namespace System; + +public static class StringExtensions +{ + public static string TrimStart(this string value, string trimParameter) + => value.TrimStart(trimParameter, StringComparison.CurrentCulture); + + public static string TrimStart(this string value, + string trimParameter, + StringComparison stringComparison) + { + if (!value.StartsWith(trimParameter, stringComparison)) + return value; + + return value.Substring(trimParameter.Length); + } + + public static string TrimEnd(this string value, string trimParameter) + => value.TrimEnd(trimParameter, StringComparison.CurrentCulture); + + public static string TrimEnd(this string value, + string trimParameter, + StringComparison stringComparison) + { + if (!value.EndsWith(trimParameter, stringComparison)) + return value; + + return value.Substring(0, value.Length - trimParameter.Length); + } +} diff --git a/src/Utils/Extensions/Tests/Masa.Utils.Extensions.DotNet.Tests/Masa.Utils.Extensions.DotNet.Tests.csproj b/src/Utils/Extensions/Tests/Masa.Utils.Extensions.DotNet.Tests/Masa.Utils.Extensions.DotNet.Tests.csproj new file mode 100644 index 000000000..56749d35b --- /dev/null +++ b/src/Utils/Extensions/Tests/Masa.Utils.Extensions.DotNet.Tests/Masa.Utils.Extensions.DotNet.Tests.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/Utils/Extensions/Tests/Masa.Utils.Extensions.DotNet.Tests/StringTest.cs b/src/Utils/Extensions/Tests/Masa.Utils.Extensions.DotNet.Tests/StringTest.cs new file mode 100644 index 000000000..9a9220ef3 --- /dev/null +++ b/src/Utils/Extensions/Tests/Masa.Utils.Extensions.DotNet.Tests/StringTest.cs @@ -0,0 +1,24 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for license information. + +namespace Masa.Utils.Extensions.DotNet.Tests; + +[TestClass] +public class StringTest +{ + [DataTestMethod] + [DataRow("abcdawDf", "abc", "dawDf")] + [DataRow("abcdawDf", "abd", "abcdawDf")] + public void TestTrimStart(string value, string trimParameter, string result) + { + Assert.AreEqual(result, value.TrimStart(trimParameter, StringComparison.OrdinalIgnoreCase)); + } + + [DataTestMethod] + [DataRow("abcdawDf", "df", "abcdaw")] + [DataRow("abcdawDf", "adf", "abcdawDf")] + public void TestTrimEnd(string value, string trimParameter, string result) + { + Assert.AreEqual(result, value.TrimEnd(trimParameter, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/src/Utils/Extensions/Tests/Masa.Utils.Extensions.DotNet.Tests/_Imports.cs b/src/Utils/Extensions/Tests/Masa.Utils.Extensions.DotNet.Tests/_Imports.cs new file mode 100644 index 000000000..3004a9f37 --- /dev/null +++ b/src/Utils/Extensions/Tests/Masa.Utils.Extensions.DotNet.Tests/_Imports.cs @@ -0,0 +1,4 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for license information. + +global using Microsoft.VisualStudio.TestTools.UnitTesting;