diff --git a/docs/en/API/Auto-API-Controllers.md b/docs/en/API/Auto-API-Controllers.md index 77819c48c9..e139c16d20 100644 --- a/docs/en/API/Auto-API-Controllers.md +++ b/docs/en/API/Auto-API-Controllers.md @@ -75,17 +75,48 @@ Configure(options => Then the route for getting a book will be '**/api/volosoft/book-store/book/{id}**'. This sample uses two-level root path, but you generally use a single level depth. -* Continues with the **normalized controller/service name**. Normalization removes 'AppService', 'ApplicationService' and 'Service' postfixes and converts it to **camelCase**. If your application service class name is 'BookAppService' then it becomes only '/book'. +* Continues with the **normalized controller/service name**. Normalization removes 'AppService', 'ApplicationService' and 'Service' postfixes and converts it to **kebab-case**. If your application service class name is 'ReadingBookAppService' then it becomes only '/reading-book'. * If you want to customize naming, then set the `UrlControllerNameNormalizer` option. It's a func delegate which allows you to determine the name per controller/service. * If the method has an '**id**' parameter then it adds '**/{id}**' ro the route. * Then it adds the action name if necessary. Action name is obtained from the method name on the service and normalized by; * Removing '**Async**' postfix. If the method name is 'GetPhonesAsync' then it becomes 'GetPhones'. * Removing **HTTP method prefix**. 'GetList', 'GetAll', 'Get', 'Put', 'Update', 'Delete', 'Remove', 'Create', 'Add', 'Insert', 'Post' and 'Patch' prefixes are removed based on the selected HTTP method. So, 'GetPhones' becomes 'Phones' since 'Get' prefix is a duplicate for a GET request. - * Converting the result to **camelCase**. + * Converting the result to **kebab-case**. * If the resulting action name is **empty** then it's not added to the route. If it's not empty, it's added to the route (like '/phones'). For 'GetAllAsync' method name it will be empty, for 'GetPhonesAsync' method name it will be 'phones'. * Normalization can be customized by setting the `UrlActionNameNormalizer` option. It's an action delegate that is called for every method. * If there is another parameter with 'Id' postfix, then it's also added to the route as the final route segment (like '/phoneId'). +#### Customizing the Route Calculation + +`IConventionalRouteBuilder` is used to build the route. It is implemented by the `ConventionalRouteBuilder` by default and works as explained above. You can replace/override this service to customize the route calculation strategy. + +#### Version 3.x Style Route Calculation + +The route calculation was different before the version 4.0. It was using camelCase conventions, while the ABP Framework version 4.0+ uses kebab-case. If you use the old route calculation strategy, follow one of the approaches; + +* Set `UseV3UrlStyle` to `true` in the options of the `options.ConventionalControllers.Create(...)` method. Example: + +````csharp +options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.UseV3UrlStyle = true; + }); +```` + +This approach effects only the controllers for the `BookStoreApplicationModule`. + +* Set `UseV3UrlStyle` to `true` for the `AbpConventionalControllerOptions` to set it globally. Example: + +```csharp +Configure(options => +{ + options.UseV3UrlStyle = true; +}); +``` + +Setting it globally effects all the modules in a modular application. + ## Service Selection Creating conventional HTTP API controllers are not unique to application services actually. diff --git a/docs/en/Migration-Guides/Abp-4_0.md b/docs/en/Migration-Guides/Abp-4_0.md index af88b7b768..b562c8573c 100644 --- a/docs/en/Migration-Guides/Abp-4_0.md +++ b/docs/en/Migration-Guides/Abp-4_0.md @@ -1,5 +1,42 @@ # ABP Framework 3.3 to 4.0 Migration Guide +## Auto API Controller Route Changes + +The route calculation for the [Auto API Controllers](https://docs.abp.io/en/abp/latest/API/Auto-API-Controllers) is changing with the ABP Framework version 4.0 ([#5325](https://github.com/abpframework/abp/issues/5325)). Previously, **camelCase** route paths were being used. Beginning from the version 4.0, it uses **kebab-case** route paths where it is possible. + +**A typical auto API before v4.0** + +![route-before-4](images/route-before-4.png) + +**camelCase route parts become kebab-case with 4.0** + +![route-4](images/route-4.png) + +If it is hard to change it for your application, you can continue to use the version 3.x route strategy, by following one of the approaches; + +* Set `UseV3UrlStyle` to `true` in the options of the `options.ConventionalControllers.Create(...)` method. Example: + +````csharp +options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.UseV3UrlStyle = true; + }); +```` + +This approach effects only the controllers for the `BookStoreApplicationModule`. + +* Set `UseV3UrlStyle` to `true` for the `AbpConventionalControllerOptions` to set it globally. Example: + +```csharp +Configure(options => +{ + options.UseV3UrlStyle = true; +}); +``` + +Setting it globally effects all the modules in a modular application. + ## Identity Server Changes ABP Framework upgrades the [IdentityServer4](https://www.nuget.org/packages/IdentityServer4) library from 3.x to 4.x with the ABP Framework version 4.0. IdentityServer 4.x has a lot of changes, some of them are **breaking changes in the data structure**. diff --git a/docs/en/Migration-Guides/images/route-4.png b/docs/en/Migration-Guides/images/route-4.png new file mode 100644 index 0000000000..0d57a57f3e Binary files /dev/null and b/docs/en/Migration-Guides/images/route-4.png differ diff --git a/docs/en/Migration-Guides/images/route-before-4.png b/docs/en/Migration-Guides/images/route-before-4.png new file mode 100644 index 0000000000..48d87c60c0 Binary files /dev/null and b/docs/en/Migration-Guides/images/route-before-4.png differ diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpConventionalControllerOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpConventionalControllerOptions.cs index e21e6ee939..d22fb78aab 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpConventionalControllerOptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpConventionalControllerOptions.cs @@ -12,7 +12,13 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions public ConventionalControllerSettingList ConventionalControllerSettings { get; } public List FormBodyBindingIgnoredTypes { get; } - + + /// + /// Set true to use the old style URL path style. + /// Default: false. + /// + public bool UseV3UrlStyle { get; set; } + public AbpConventionalControllerOptions() { ConventionalControllerSettings = new ConventionalControllerSettingList(); @@ -24,7 +30,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions } public AbpConventionalControllerOptions Create( - Assembly assembly, + Assembly assembly, [CanBeNull] Action optionsAction = null) { var setting = new ConventionalControllerSetting( @@ -39,4 +45,4 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions return this; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs index ab2003a595..3198e73e45 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs @@ -23,12 +23,15 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions { public ILogger Logger { get; set; } - private readonly AbpAspNetCoreMvcOptions _options; + protected AbpAspNetCoreMvcOptions Options { get; } + protected IConventionalRouteBuilder ConventionalRouteBuilder { get; } public AbpServiceConvention( - IOptions options) + IOptions options, + IConventionalRouteBuilder conventionalRouteBuilder) { - _options = options.Value; + ConventionalRouteBuilder = conventionalRouteBuilder; + Options = options.Value; Logger = NullLogger.Instance; } @@ -131,7 +134,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions return false; } - if (_options.ConventionalControllers + if (Options.ConventionalControllers .FormBodyBindingIgnoredTypes .Any(t => t.IsAssignableFrom(parameter.ParameterInfo.ParameterType))) { @@ -306,95 +309,14 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions [CanBeNull] protected virtual ConventionalControllerSetting GetControllerSettingOrNull(Type controllerType) { - return _options.ConventionalControllers.ConventionalControllerSettings.GetSettingOrNull(controllerType); + return Options.ConventionalControllers.ConventionalControllerSettings.GetSettingOrNull(controllerType); } protected virtual AttributeRouteModel CreateAbpServiceAttributeRouteModel(string rootPath, string controllerName, ActionModel action, string httpMethod, [CanBeNull] ConventionalControllerSetting configuration) { return new AttributeRouteModel( new RouteAttribute( - CalculateRouteTemplate(rootPath, controllerName, action, httpMethod, configuration) - ) - ); - } - - protected virtual string CalculateRouteTemplate(string rootPath, string controllerName, ActionModel action, string httpMethod, [CanBeNull] ConventionalControllerSetting configuration) - { - var controllerNameInUrl = NormalizeUrlControllerName(rootPath, controllerName, action, httpMethod, configuration); - - var url = $"api/{rootPath}/{controllerNameInUrl.ToCamelCase()}"; - - //Add {id} path if needed - var idParameterModel = action.Parameters.FirstOrDefault(p => p.ParameterName == "id"); - if (idParameterModel != null) - { - if (TypeHelper.IsPrimitiveExtended(idParameterModel.ParameterType, includeEnums: true)) - { - url += "/{id}"; - } - else - { - var properties = idParameterModel - .ParameterType - .GetProperties(BindingFlags.Instance | BindingFlags.Public); - - foreach (var property in properties) - { - url += "/{" + property.Name + "}"; - } - } - } - - //Add action name if needed - var actionNameInUrl = NormalizeUrlActionName(rootPath, controllerName, action, httpMethod, configuration); - if (!actionNameInUrl.IsNullOrEmpty()) - { - url += $"/{actionNameInUrl.ToCamelCase()}"; - - //Add secondary Id - var secondaryIds = action.Parameters.Where(p => p.ParameterName.EndsWith("Id", StringComparison.Ordinal)).ToList(); - if (secondaryIds.Count == 1) - { - url += $"/{{{secondaryIds[0].ParameterName}}}"; - } - } - - return url; - } - - protected virtual string NormalizeUrlActionName(string rootPath, string controllerName, ActionModel action, string httpMethod, [CanBeNull] ConventionalControllerSetting configuration) - { - var actionNameInUrl = HttpMethodHelper - .RemoveHttpMethodPrefix(action.ActionName, httpMethod) - .RemovePostFix("Async"); - - if (configuration?.UrlActionNameNormalizer == null) - { - return actionNameInUrl; - } - - return configuration.UrlActionNameNormalizer( - new UrlActionNameNormalizerContext( - rootPath, - controllerName, - action, - actionNameInUrl, - httpMethod - ) - ); - } - - protected virtual string NormalizeUrlControllerName(string rootPath, string controllerName, ActionModel action, string httpMethod, [CanBeNull] ConventionalControllerSetting configuration) - { - if (configuration?.UrlControllerNameNormalizer == null) - { - return controllerName; - } - - return configuration.UrlControllerNameNormalizer( - new UrlControllerNameNormalizerContext( - rootPath, - controllerName + ConventionalRouteBuilder.Build(rootPath, controllerName, action, httpMethod, configuration) ) ); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalControllerSetting.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalControllerSetting.cs index bae2522d20..5c2ae1e36c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalControllerSetting.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalControllerSetting.cs @@ -18,6 +18,12 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions [NotNull] public HashSet ControllerTypes { get; } //TODO: Internal? + /// + /// Set true to use the old style URL path style. + /// Default: null (uses the value of the ). + /// + public bool? UseV3UrlStyle { get; set; } + [NotNull] public string RootPath { @@ -57,9 +63,9 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions public List ApiVersions { get; } public Action ApiVersionConfigurer { get; set; } - + public ConventionalControllerSetting( - [NotNull] Assembly assembly, + [NotNull] Assembly assembly, [NotNull] string rootPath, [NotNull] string remoteServiceName) { @@ -104,4 +110,4 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions return false; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalRouteBuilder.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalRouteBuilder.cs new file mode 100644 index 0000000000..58a59f37e8 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalRouteBuilder.cs @@ -0,0 +1,159 @@ +using System; +using System.Linq; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http; +using Volo.Abp.Reflection; + +namespace Volo.Abp.AspNetCore.Mvc.Conventions +{ + public class ConventionalRouteBuilder : IConventionalRouteBuilder, ITransientDependency + { + protected AbpConventionalControllerOptions Options { get; } + + public ConventionalRouteBuilder(IOptions options) + { + Options = options.Value; + } + + public virtual string Build( + string rootPath, + string controllerName, + ActionModel action, + string httpMethod, + [CanBeNull] ConventionalControllerSetting configuration) + { + var controllerNameInUrl = NormalizeUrlControllerName(rootPath, controllerName, action, httpMethod, configuration); + + var url = $"api/{rootPath}/{NormalizeControllerNameCase(controllerNameInUrl, configuration)}"; + + //Add {id} path if needed + var idParameterModel = action.Parameters.FirstOrDefault(p => p.ParameterName == "id"); + if (idParameterModel != null) + { + if (TypeHelper.IsPrimitiveExtended(idParameterModel.ParameterType, includeEnums: true)) + { + url += "/{id}"; + } + else + { + var properties = idParameterModel + .ParameterType + .GetProperties(BindingFlags.Instance | BindingFlags.Public); + + foreach (var property in properties) + { + url += "/{" + NormalizeIdPropertyNameCase(property, configuration) + "}"; + } + } + } + + //Add action name if needed + var actionNameInUrl = NormalizeUrlActionName(rootPath, controllerName, action, httpMethod, configuration); + if (!actionNameInUrl.IsNullOrEmpty()) + { + url += $"/{NormalizeActionNameCase(actionNameInUrl, configuration)}"; + + //Add secondary Id + var secondaryIds = action.Parameters + .Where(p => p.ParameterName.EndsWith("Id", StringComparison.Ordinal)).ToList(); + if (secondaryIds.Count == 1) + { + url += $"/{{{NormalizeSecondaryIdNameCase(secondaryIds[0], configuration)}}}"; + } + } + + return url; + } + + protected virtual string NormalizeUrlActionName(string rootPath, string controllerName, ActionModel action, + string httpMethod, [CanBeNull] ConventionalControllerSetting configuration) + { + var actionNameInUrl = HttpMethodHelper + .RemoveHttpMethodPrefix(action.ActionName, httpMethod) + .RemovePostFix("Async"); + + if (configuration?.UrlActionNameNormalizer == null) + { + return actionNameInUrl; + } + + return configuration.UrlActionNameNormalizer( + new UrlActionNameNormalizerContext( + rootPath, + controllerName, + action, + actionNameInUrl, + httpMethod + ) + ); + } + + protected virtual string NormalizeUrlControllerName(string rootPath, string controllerName, ActionModel action, + string httpMethod, [CanBeNull] ConventionalControllerSetting configuration) + { + if (configuration?.UrlControllerNameNormalizer == null) + { + return controllerName; + } + + return configuration.UrlControllerNameNormalizer( + new UrlControllerNameNormalizerContext( + rootPath, + controllerName + ) + ); + } + + protected virtual string NormalizeControllerNameCase(string controllerName, [CanBeNull] ConventionalControllerSetting configuration) + { + if (configuration?.UseV3UrlStyle ?? Options.UseV3UrlStyle) + { + return controllerName.ToCamelCase(); + } + else + { + return controllerName.ToKebabCase(); + } + } + + protected virtual string NormalizeActionNameCase(string actionName, [CanBeNull] ConventionalControllerSetting configuration) + { + if (configuration?.UseV3UrlStyle ?? Options.UseV3UrlStyle) + { + return actionName.ToCamelCase(); + } + else + { + return actionName.ToKebabCase(); + } + } + + protected virtual string NormalizeIdPropertyNameCase(PropertyInfo property, [CanBeNull] ConventionalControllerSetting configuration) + { + if (configuration?.UseV3UrlStyle ?? Options.UseV3UrlStyle) + { + return property.Name; + } + else + { + return property.Name.ToKebabCase(); + } + } + + protected virtual string NormalizeSecondaryIdNameCase(ParameterModel secondaryId, [CanBeNull] ConventionalControllerSetting configuration) + { + if (configuration?.UseV3UrlStyle ?? Options.UseV3UrlStyle) + { + return secondaryId.ParameterName; + } + else + { + return secondaryId.ParameterName.ToKebabCase(); + } + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/IConventionalRouteBuilder.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/IConventionalRouteBuilder.cs new file mode 100644 index 0000000000..c30d7c5693 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/IConventionalRouteBuilder.cs @@ -0,0 +1,16 @@ +using JetBrains.Annotations; +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +namespace Volo.Abp.AspNetCore.Mvc.Conventions +{ + public interface IConventionalRouteBuilder + { + string Build( + string rootPath, + string controllerName, + ActionModel action, + string httpMethod, + [CanBeNull] ConventionalControllerSetting configuration + ); + } +}