Browse Source

Merge pull request #5925 from abpframework/issue-5325

Implemented: Create auto controller routes in kebap-case
pull/5938/head
Halil İbrahim Kalkan 6 years ago
committed by GitHub
parent
commit
fbdf9fef03
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 35
      docs/en/API/Auto-API-Controllers.md
  2. 37
      docs/en/Migration-Guides/Abp-4_0.md
  3. BIN
      docs/en/Migration-Guides/images/route-4.png
  4. BIN
      docs/en/Migration-Guides/images/route-before-4.png
  5. 12
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpConventionalControllerOptions.cs
  6. 96
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs
  7. 12
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalControllerSetting.cs
  8. 159
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalRouteBuilder.cs
  9. 16
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/IConventionalRouteBuilder.cs

35
docs/en/API/Auto-API-Controllers.md

@ -75,17 +75,48 @@ Configure<AbpAspNetCoreMvcOptions>(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. 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 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. * 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; * 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 '**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. * 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'. * 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. * 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'). * 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<AbpConventionalControllerOptions>(options =>
{
options.UseV3UrlStyle = true;
});
```
Setting it globally effects all the modules in a modular application.
## Service Selection ## Service Selection
Creating conventional HTTP API controllers are not unique to application services actually. Creating conventional HTTP API controllers are not unique to application services actually.

37
docs/en/Migration-Guides/Abp-4_0.md

@ -1,5 +1,42 @@
# ABP Framework 3.3 to 4.0 Migration Guide # 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<AbpConventionalControllerOptions>(options =>
{
options.UseV3UrlStyle = true;
});
```
Setting it globally effects all the modules in a modular application.
## Identity Server Changes ## 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**. 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**.

BIN
docs/en/Migration-Guides/images/route-4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/en/Migration-Guides/images/route-before-4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

12
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 ConventionalControllerSettingList ConventionalControllerSettings { get; }
public List<Type> FormBodyBindingIgnoredTypes { get; } public List<Type> FormBodyBindingIgnoredTypes { get; }
/// <summary>
/// Set true to use the old style URL path style.
/// Default: false.
/// </summary>
public bool UseV3UrlStyle { get; set; }
public AbpConventionalControllerOptions() public AbpConventionalControllerOptions()
{ {
ConventionalControllerSettings = new ConventionalControllerSettingList(); ConventionalControllerSettings = new ConventionalControllerSettingList();
@ -24,7 +30,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
} }
public AbpConventionalControllerOptions Create( public AbpConventionalControllerOptions Create(
Assembly assembly, Assembly assembly,
[CanBeNull] Action<ConventionalControllerSetting> optionsAction = null) [CanBeNull] Action<ConventionalControllerSetting> optionsAction = null)
{ {
var setting = new ConventionalControllerSetting( var setting = new ConventionalControllerSetting(
@ -39,4 +45,4 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
return this; return this;
} }
} }
} }

96
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<AbpServiceConvention> Logger { get; set; } public ILogger<AbpServiceConvention> Logger { get; set; }
private readonly AbpAspNetCoreMvcOptions _options; protected AbpAspNetCoreMvcOptions Options { get; }
protected IConventionalRouteBuilder ConventionalRouteBuilder { get; }
public AbpServiceConvention( public AbpServiceConvention(
IOptions<AbpAspNetCoreMvcOptions> options) IOptions<AbpAspNetCoreMvcOptions> options,
IConventionalRouteBuilder conventionalRouteBuilder)
{ {
_options = options.Value; ConventionalRouteBuilder = conventionalRouteBuilder;
Options = options.Value;
Logger = NullLogger<AbpServiceConvention>.Instance; Logger = NullLogger<AbpServiceConvention>.Instance;
} }
@ -131,7 +134,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
return false; return false;
} }
if (_options.ConventionalControllers if (Options.ConventionalControllers
.FormBodyBindingIgnoredTypes .FormBodyBindingIgnoredTypes
.Any(t => t.IsAssignableFrom(parameter.ParameterInfo.ParameterType))) .Any(t => t.IsAssignableFrom(parameter.ParameterInfo.ParameterType)))
{ {
@ -306,95 +309,14 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
[CanBeNull] [CanBeNull]
protected virtual ConventionalControllerSetting GetControllerSettingOrNull(Type controllerType) 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) protected virtual AttributeRouteModel CreateAbpServiceAttributeRouteModel(string rootPath, string controllerName, ActionModel action, string httpMethod, [CanBeNull] ConventionalControllerSetting configuration)
{ {
return new AttributeRouteModel( return new AttributeRouteModel(
new RouteAttribute( new RouteAttribute(
CalculateRouteTemplate(rootPath, controllerName, action, httpMethod, configuration) ConventionalRouteBuilder.Build(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
) )
); );
} }

12
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalControllerSetting.cs

@ -18,6 +18,12 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
[NotNull] [NotNull]
public HashSet<Type> ControllerTypes { get; } //TODO: Internal? public HashSet<Type> ControllerTypes { get; } //TODO: Internal?
/// <summary>
/// Set true to use the old style URL path style.
/// Default: null (uses the value of the <see cref="AbpConventionalControllerOptions.UseV3UrlStyle"/>).
/// </summary>
public bool? UseV3UrlStyle { get; set; }
[NotNull] [NotNull]
public string RootPath public string RootPath
{ {
@ -57,9 +63,9 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
public List<ApiVersion> ApiVersions { get; } public List<ApiVersion> ApiVersions { get; }
public Action<ApiVersioningOptions> ApiVersionConfigurer { get; set; } public Action<ApiVersioningOptions> ApiVersionConfigurer { get; set; }
public ConventionalControllerSetting( public ConventionalControllerSetting(
[NotNull] Assembly assembly, [NotNull] Assembly assembly,
[NotNull] string rootPath, [NotNull] string rootPath,
[NotNull] string remoteServiceName) [NotNull] string remoteServiceName)
{ {
@ -104,4 +110,4 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
return false; return false;
} }
} }
} }

159
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<AbpConventionalControllerOptions> 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();
}
}
}
}

16
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
);
}
}
Loading…
Cancel
Save