Created
November 18, 2020 07:52
-
-
Save maliming/5a76e6d65840530f60ab00e5b5d56c80 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Reflection; | |
using System.Text; | |
using JetBrains.Annotations; | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.AspNetCore.Mvc.Abstractions; | |
using Microsoft.AspNetCore.Mvc.ApiExplorer; | |
using Microsoft.AspNetCore.Mvc.ModelBinding; | |
using Microsoft.Extensions.Logging; | |
using Microsoft.Extensions.Logging.Abstractions; | |
using Microsoft.Extensions.Options; | |
using Volo.Abp; | |
using Volo.Abp.Application.Services; | |
using Volo.Abp.AspNetCore.Mvc; | |
using Volo.Abp.AspNetCore.Mvc.Conventions; | |
using Volo.Abp.DependencyInjection; | |
using Volo.Abp.Http.Modeling; | |
using Volo.Abp.Reflection; | |
using Volo.Abp.Threading; | |
namespace MyCompanyName.MyProjectName.Web | |
{ | |
[Dependency(ReplaceServices = true)] | |
public class MyAspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvider, ITransientDependency | |
{ | |
public ILogger<AspNetCoreApiDescriptionModelProvider> Logger { get; set; } | |
private readonly IApiDescriptionGroupCollectionProvider _descriptionProvider; | |
private readonly AbpAspNetCoreMvcOptions _options; | |
private readonly AbpApiDescriptionModelOptions _modelOptions; | |
public MyAspNetCoreApiDescriptionModelProvider( | |
IApiDescriptionGroupCollectionProvider descriptionProvider, | |
IOptions<AbpAspNetCoreMvcOptions> options, | |
IOptions<AbpApiDescriptionModelOptions> modelOptions) | |
{ | |
_descriptionProvider = descriptionProvider; | |
_options = options.Value; | |
_modelOptions = modelOptions.Value; | |
Logger = NullLogger<AspNetCoreApiDescriptionModelProvider>.Instance; | |
} | |
public ApplicationApiDescriptionModel CreateApiModel(ApplicationApiDescriptionModelRequestDto input) | |
{ | |
var model = ApplicationApiDescriptionModel.Create(); | |
foreach (var descriptionGroupItem in _descriptionProvider.ApiDescriptionGroups.Items) | |
{ | |
foreach (var apiDescription in descriptionGroupItem.Items) | |
{ | |
if (!apiDescription.ActionDescriptor.IsControllerAction()) | |
{ | |
continue; | |
} | |
if (apiDescription.ActionDescriptor.AsControllerActionDescriptor().ControllerName.StartsWith("XXX")) | |
{ | |
continue; | |
} | |
AddApiDescriptionToModel(apiDescription, model, input); | |
} | |
} | |
return model; | |
} | |
private void AddApiDescriptionToModel( | |
ApiDescription apiDescription, | |
ApplicationApiDescriptionModel applicationModel, | |
ApplicationApiDescriptionModelRequestDto input) | |
{ | |
var controllerType = apiDescription | |
.ActionDescriptor | |
.AsControllerActionDescriptor() | |
.ControllerTypeInfo; | |
var setting = FindSetting(controllerType); | |
var moduleModel = applicationModel.GetOrAddModule( | |
GetRootPath(controllerType, setting), | |
GetRemoteServiceName(controllerType, setting) | |
); | |
var controllerModel = moduleModel.GetOrAddController( | |
controllerType.FullName, | |
CalculateControllerName(controllerType, setting), | |
controllerType, | |
_modelOptions.IgnoredInterfaces | |
); | |
var method = apiDescription.ActionDescriptor.GetMethodInfo(); | |
var uniqueMethodName = GetUniqueActionName(method); | |
if (controllerModel.Actions.ContainsKey(uniqueMethodName)) | |
{ | |
Logger.LogWarning( | |
$"Controller '{controllerModel.ControllerName}' contains more than one action with name '{uniqueMethodName}' for module '{moduleModel.RootPath}'. Ignored: " + | |
method); | |
return; | |
} | |
Logger.LogDebug($"ActionApiDescriptionModel.Create: {controllerModel.ControllerName}.{uniqueMethodName}"); | |
var actionModel = controllerModel.AddAction( | |
uniqueMethodName, | |
ActionApiDescriptionModel.Create( | |
uniqueMethodName, | |
method, | |
apiDescription.RelativePath, | |
apiDescription.HttpMethod, | |
GetSupportedVersions(controllerType, method, setting) | |
) | |
); | |
if (input.IncludeTypes) | |
{ | |
AddCustomTypesToModel(applicationModel, method); | |
} | |
AddParameterDescriptionsToModel(actionModel, method, apiDescription); | |
} | |
private static string CalculateControllerName(Type controllerType, ConventionalControllerSetting setting) | |
{ | |
var controllerName = controllerType.Name.RemovePostFix("Controller") | |
.RemovePostFix(ApplicationService.CommonPostfixes); | |
if (setting?.UrlControllerNameNormalizer != null) | |
{ | |
controllerName = | |
setting.UrlControllerNameNormalizer( | |
new UrlControllerNameNormalizerContext(setting.RootPath, controllerName)); | |
} | |
return controllerName; | |
} | |
private static string GetUniqueActionName(MethodInfo method) | |
{ | |
var methodNameBuilder = new StringBuilder(method.Name); | |
var parameters = method.GetParameters(); | |
if (parameters.Any()) | |
{ | |
methodNameBuilder.Append("By"); | |
for (var i = 0; i < parameters.Length; i++) | |
{ | |
if (i > 0) | |
{ | |
methodNameBuilder.Append("And"); | |
} | |
methodNameBuilder.Append(parameters[i].Name.ToPascalCase()); | |
} | |
} | |
return methodNameBuilder.ToString(); | |
} | |
private static List<string> GetSupportedVersions(Type controllerType, MethodInfo method, | |
ConventionalControllerSetting setting) | |
{ | |
var supportedVersions = new List<ApiVersion>(); | |
var mapToAttributes = method.GetCustomAttributes<MapToApiVersionAttribute>().ToArray(); | |
if (mapToAttributes.Any()) | |
{ | |
supportedVersions.AddRange( | |
mapToAttributes.SelectMany(a => a.Versions) | |
); | |
} | |
else | |
{ | |
supportedVersions.AddRange( | |
controllerType.GetCustomAttributes<ApiVersionAttribute>().SelectMany(a => a.Versions) | |
); | |
setting?.ApiVersions.ForEach(supportedVersions.Add); | |
} | |
return supportedVersions.Select(v => v.ToString()).Distinct().ToList(); | |
} | |
private void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel, MethodInfo method) | |
{ | |
foreach (var parameterInfo in method.GetParameters()) | |
{ | |
AddCustomTypesToModel(applicationModel, parameterInfo.ParameterType); | |
} | |
AddCustomTypesToModel(applicationModel, method.ReturnType); | |
} | |
private static void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel, | |
[CanBeNull] Type type) | |
{ | |
if (type == null) | |
{ | |
return; | |
} | |
if (type.IsGenericParameter) | |
{ | |
return; | |
} | |
type = AsyncHelper.UnwrapTask(type); | |
if (type == typeof(object) || | |
type == typeof(void) || | |
type == typeof(Enum) || | |
type == typeof(ValueType) || | |
TypeHelper.IsPrimitiveExtended(type)) | |
{ | |
return; | |
} | |
if (TypeHelper.IsDictionary(type, out var keyType, out var valueType)) | |
{ | |
AddCustomTypesToModel(applicationModel, keyType); | |
AddCustomTypesToModel(applicationModel, valueType); | |
return; | |
} | |
if (TypeHelper.IsEnumerable(type, out var itemType)) | |
{ | |
AddCustomTypesToModel(applicationModel, itemType); | |
return; | |
} | |
if (type.IsGenericType && !type.IsGenericTypeDefinition) | |
{ | |
var genericTypeDefinition = type.GetGenericTypeDefinition(); | |
AddCustomTypesToModel(applicationModel, genericTypeDefinition); | |
foreach (var genericArgument in type.GetGenericArguments()) | |
{ | |
AddCustomTypesToModel(applicationModel, genericArgument); | |
} | |
return; | |
} | |
var typeName = CalculateTypeName(type); | |
if (applicationModel.Types.ContainsKey(typeName)) | |
{ | |
return; | |
} | |
applicationModel.Types[typeName] = TypeApiDescriptionModel.Create(type); | |
AddCustomTypesToModel(applicationModel, type.BaseType); | |
foreach (var propertyInfo in type.GetProperties().Where(p => p.DeclaringType == type)) | |
{ | |
AddCustomTypesToModel(applicationModel, propertyInfo.PropertyType); | |
} | |
} | |
private static string CalculateTypeName(Type type) | |
{ | |
if (!type.IsGenericTypeDefinition) | |
{ | |
return TypeHelper.GetFullNameHandlingNullableAndGenerics(type); | |
} | |
var i = 0; | |
var argumentList = type | |
.GetGenericArguments() | |
.Select(_ => "T" + i++) | |
.JoinAsString(","); | |
return $"{type.FullName.Left(type.FullName.IndexOf('`'))}<{argumentList}>"; | |
} | |
private void AddParameterDescriptionsToModel(ActionApiDescriptionModel actionModel, MethodInfo method, | |
ApiDescription apiDescription) | |
{ | |
if (!apiDescription.ParameterDescriptions.Any()) | |
{ | |
return; | |
} | |
var parameterDescriptionNames = apiDescription | |
.ParameterDescriptions | |
.Select(p => p.Name) | |
.ToArray(); | |
var methodParameterNames = method | |
.GetParameters() | |
.Where(IsNotFromServicesParameter) | |
.Select(GetMethodParamName) | |
.ToArray(); | |
var matchedMethodParamNames = ArrayMatcher.Match( | |
parameterDescriptionNames, | |
methodParameterNames | |
); | |
for (var i = 0; i < apiDescription.ParameterDescriptions.Count; i++) | |
{ | |
var parameterDescription = apiDescription.ParameterDescriptions[i]; | |
var matchedMethodParamName = matchedMethodParamNames.Length > i | |
? matchedMethodParamNames[i] | |
: parameterDescription.Name; | |
actionModel.AddParameter(ParameterApiDescriptionModel.Create( | |
parameterDescription.Name, | |
matchedMethodParamName, | |
parameterDescription.Type, | |
parameterDescription.RouteInfo?.IsOptional ?? false, | |
parameterDescription.RouteInfo?.DefaultValue, | |
parameterDescription.RouteInfo?.Constraints?.Select(c => c.GetType().Name).ToArray(), | |
parameterDescription.Source.Id, | |
parameterDescription.ModelMetadata?.ContainerType != null | |
? parameterDescription.ParameterDescriptor?.Name ?? string.Empty | |
: string.Empty | |
) | |
); | |
} | |
} | |
private static bool IsNotFromServicesParameter(ParameterInfo parameterInfo) | |
{ | |
return !parameterInfo.IsDefined(typeof(FromServicesAttribute), true); | |
} | |
public string GetMethodParamName(ParameterInfo parameterInfo) | |
{ | |
var modelNameProvider = parameterInfo.GetCustomAttributes() | |
.OfType<IModelNameProvider>() | |
.FirstOrDefault(); | |
if (modelNameProvider == null) | |
{ | |
return parameterInfo.Name; | |
} | |
return modelNameProvider.Name ?? parameterInfo.Name; | |
} | |
private static string GetRootPath([NotNull] Type controllerType, | |
[CanBeNull] ConventionalControllerSetting setting) | |
{ | |
if (setting != null) | |
{ | |
return setting.RootPath; | |
} | |
var areaAttr = controllerType.GetCustomAttributes().OfType<AreaAttribute>().FirstOrDefault(); | |
if (areaAttr != null) | |
{ | |
return areaAttr.RouteValue; | |
} | |
return ModuleApiDescriptionModel.DefaultRootPath; | |
} | |
private string GetRemoteServiceName(Type controllerType, [CanBeNull] ConventionalControllerSetting setting) | |
{ | |
if (setting != null) | |
{ | |
return setting.RemoteServiceName; | |
} | |
var remoteServiceAttr = | |
controllerType.GetCustomAttributes().OfType<RemoteServiceAttribute>().FirstOrDefault(); | |
if (remoteServiceAttr?.Name != null) | |
{ | |
return remoteServiceAttr.Name; | |
} | |
return ModuleApiDescriptionModel.DefaultRemoteServiceName; | |
} | |
[CanBeNull] | |
private ConventionalControllerSetting FindSetting(Type controllerType) | |
{ | |
foreach (var controllerSetting in _options.ConventionalControllers.ConventionalControllerSettings) | |
{ | |
if (controllerSetting.ControllerTypes.Contains(controllerType)) | |
{ | |
return controllerSetting; | |
} | |
} | |
return null; | |
} | |
} | |
internal static class ArrayMatcher | |
{ | |
public static T[] Match<T>(T[] sourceArray, T[] destinationArray) | |
{ | |
var result = new List<T>(); | |
var currentMethodParamIndex = 0; | |
var parentItem = default(T); | |
foreach (var sourceItem in sourceArray) | |
{ | |
if (currentMethodParamIndex < destinationArray.Length) | |
{ | |
var destinationItem = destinationArray[currentMethodParamIndex]; | |
if (EqualityComparer<T>.Default.Equals(sourceItem, destinationItem)) | |
{ | |
parentItem = default; | |
currentMethodParamIndex++; | |
} | |
else | |
{ | |
if (parentItem == null) | |
{ | |
parentItem = destinationItem; | |
currentMethodParamIndex++; | |
} | |
} | |
} | |
var resultItem = EqualityComparer<T>.Default.Equals(parentItem, default) ? sourceItem : parentItem; | |
result.Add(resultItem); | |
} | |
return result.ToArray(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment