using System; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ModelBinding; using ZhonTai.DynamicApi.Attributes; using ZhonTai.DynamicApi.Enums; using ZhonTai.DynamicApi.Helpers; namespace ZhonTai.DynamicApi; public class DynamicApiConvention : IApplicationModelConvention { private readonly ISelectController _selectController; private readonly IActionRouteFactory _actionRouteFactory; public DynamicApiConvention(ISelectController selectController, IActionRouteFactory actionRouteFactory) { _selectController = selectController; _actionRouteFactory = actionRouteFactory; } public string GetSeparateWords(string value, NamingConventionEnum namingConvention = NamingConventionEnum.KebabCase) { if (string.IsNullOrWhiteSpace(value)) return value; var separator = "-"; if (namingConvention == NamingConventionEnum.SnakeCase) { separator = "_"; } else if (namingConvention == NamingConventionEnum.ExtensionCase) { separator = "."; } return Regex.Replace( value, "(?(type.GetTypeInfo()); if (!(_selectController is DefaultSelectController) && _selectController.IsController(type)) { controller.ControllerName = controller.ControllerName.RemovePostFix(AppConsts.ControllerPostfixes.ToArray()); if (AppConsts.NamingConvention == NamingConventionEnum.Custom) { controller.ControllerName = GetRestFulControllerName(controller.ControllerName); } else { controller.ControllerName = GetFormatName(controller.ControllerName, AppConsts.NamingConvention); } ConfigureDynamicApi(controller, DynamicApiAttr); } else { if (typeof(IDynamicApi).GetTypeInfo().IsAssignableFrom(type)) { controller.ControllerName = controller.ControllerName.RemovePostFix(AppConsts.ControllerPostfixes.ToArray()); if (AppConsts.NamingConvention == NamingConventionEnum.Custom) { controller.ControllerName = GetRestFulControllerName(controller.ControllerName); } else { controller.ControllerName = GetFormatName(controller.ControllerName, AppConsts.NamingConvention); } ConfigureArea(controller, DynamicApiAttr); ConfigureDynamicApi(controller, DynamicApiAttr); } else { if (DynamicApiAttr != null) { ConfigureArea(controller, DynamicApiAttr); ConfigureDynamicApi(controller, DynamicApiAttr); } } } } } private void ConfigureArea(ControllerModel controller, DynamicApiAttribute attr) { if (!controller.RouteValues.ContainsKey("area")) { if (attr == null) { throw new ArgumentException(nameof(attr)); } if (!string.IsNullOrEmpty(attr.Area)) { controller.RouteValues["area"] = attr.Area; } else if (!string.IsNullOrEmpty(AppConsts.DefaultAreaName)) { controller.RouteValues["area"] = AppConsts.DefaultAreaName; } } } private void ConfigureDynamicApi(ControllerModel controller, DynamicApiAttribute controllerAttr) { ConfigureApiExplorer(controller); ConfigureSelector(controller, controllerAttr); ConfigureParameters(controller); if (AppConsts.FormatResult) { ConfigureFormatResult(controller); } } private void ConfigureFormatResult(ControllerModel controller) { foreach (var action in controller.Actions) { if (!CheckNoMapMethod(action)) { var returnType = action.ActionMethod.GetReturnType(); if (returnType == typeof(void)) return; action.Filters.Add(new FormatResultAttribute(returnType)); } } } private void ConfigureParameters(ControllerModel controller) { foreach (var action in controller.Actions) { if (!CheckNoMapMethod(action)) foreach (var para in action.Parameters) { if (para.BindingInfo != null) { continue; } if (!TypeHelper.IsPrimitiveExtendedIncludingNullable(para.ParameterInfo.ParameterType)) { if (CanUseFormBodyBinding(action, para)) { para.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromBodyAttribute() }); } } } } } private bool CanUseFormBodyBinding(ActionModel action, ParameterModel parameter) { if (AppConsts.FormBodyBindingIgnoredTypes.Any(t => t.IsAssignableFrom(parameter.ParameterInfo.ParameterType))) { return false; } foreach (var selector in action.Selectors) { if (selector.ActionConstraints == null) { continue; } foreach (var actionConstraint in selector.ActionConstraints) { var httpMethodActionConstraint = actionConstraint as HttpMethodActionConstraint; if (httpMethodActionConstraint == null) { continue; } if (httpMethodActionConstraint.HttpMethods.All(hm => hm.IsIn("GET", "DELETE", "TRACE", "HEAD"))) { return false; } } } return true; } #region ConfigureApiExplorer private void ConfigureApiExplorer(ControllerModel controller) { if (controller.ApiExplorer.GroupName.IsNullOrEmpty()) { controller.ApiExplorer.GroupName = controller.ControllerName; } if (controller.ApiExplorer.IsVisible == null) { controller.ApiExplorer.IsVisible = true; } foreach (var action in controller.Actions) { if (!CheckNoMapMethod(action)) ConfigureApiExplorer(action); } } private void ConfigureApiExplorer(ActionModel action) { if (action.ApiExplorer.IsVisible == null) { action.ApiExplorer.IsVisible = true; } } #endregion /// /// //不映射指定的方法 /// /// /// private bool CheckNoMapMethod(ActionModel action) { bool isExist = false; var noMapMethod = ReflectionHelper.GetSingleAttributeOrDefault(action.ActionMethod); if (noMapMethod != null) { action.ApiExplorer.IsVisible = false;//对应的Api不映射 isExist = true; } return isExist; } private void ConfigureSelector(ControllerModel controller, DynamicApiAttribute controllerAttr) { if (controller.Selectors.Any(selector => selector.AttributeRouteModel != null)) { return; } var areaName = string.Empty; if (controllerAttr != null) { areaName = controllerAttr.Area; } foreach (var action in controller.Actions) { if (!CheckNoMapMethod(action)) ConfigureSelector(areaName, controller.ControllerName, action); } } private void ConfigureSelector(string areaName, string controllerName, ActionModel action) { var nonAttr = ReflectionHelper.GetSingleAttributeOrDefault(action.ActionMethod); if (nonAttr != null) { return; } if (action.Selectors.IsNullOrEmpty() || action.Selectors.Any(a => a.ActionConstraints.IsNullOrEmpty())) { if (!CheckNoMapMethod(action)) AddAppServiceSelector(areaName, controllerName, action); } else { NormalizeSelectorRoutes(areaName, controllerName, action); } } private void AddAppServiceSelector(string areaName, string controllerName, ActionModel action) { var verb = GetHttpVerb(action); if (AppConsts.NamingConvention == NamingConventionEnum.Custom) { action.ActionName = GetRestFulActionName(action.ActionName); } else { action.ActionName = GetFormatName(action.ActionName, AppConsts.NamingConvention); } var appServiceSelectorModel = action.Selectors[0]; if (appServiceSelectorModel.AttributeRouteModel == null) { appServiceSelectorModel.AttributeRouteModel = CreateActionRouteModel(areaName, controllerName, action); } if (!appServiceSelectorModel.ActionConstraints.Any()) { appServiceSelectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { verb })); switch (verb) { case "GET": appServiceSelectorModel.EndpointMetadata.Add(new HttpGetAttribute()); break; case "POST": appServiceSelectorModel.EndpointMetadata.Add(new HttpPostAttribute()); break; case "PUT": appServiceSelectorModel.EndpointMetadata.Add(new HttpPutAttribute()); break; case "DELETE": appServiceSelectorModel.EndpointMetadata.Add(new HttpDeleteAttribute()); break; default: throw new Exception($"Unsupported http verb: {verb}."); } } } /// /// Processing action name /// /// /// private static string GetRestFulActionName(string actionName) { // custom process action name var appConstsActionName = AppConsts.GetRestFulActionName?.Invoke(actionName); if (appConstsActionName != null) { return appConstsActionName; } // default process action name. // Remove Postfix actionName = actionName.RemovePostFix(AppConsts.ActionPostfixes.ToArray()); // Remove Prefix var verbKey = actionName.GetPascalOrCamelCaseFirstWord().ToLower(); if (AppConsts.HttpVerbs.ContainsKey(verbKey)) { if (actionName.Length == verbKey.Length) { return ""; } else { return actionName.Substring(verbKey.Length); } } else { return actionName; } } private static string GetRestFulControllerName(string controllerName) { // custom process action name var appConstsControllerName = AppConsts.GetRestFulControllerName?.Invoke(controllerName); if (appConstsControllerName != null) { return appConstsControllerName; } else { return controllerName; } } private void NormalizeSelectorRoutes(string areaName, string controllerName, ActionModel action) { if (AppConsts.NamingConvention == NamingConventionEnum.Custom) { action.ActionName = GetRestFulActionName(action.ActionName); } else { action.ActionName = GetFormatName(action.ActionName, AppConsts.NamingConvention); } foreach (var selector in action.Selectors) { selector.AttributeRouteModel = selector.AttributeRouteModel == null ? CreateActionRouteModel(areaName, controllerName, action) : AttributeRouteModel.CombineAttributeRouteModel(CreateActionRouteModel(areaName, controllerName, action), selector.AttributeRouteModel); } } private static string GetHttpVerb(ActionModel action) { var getValueSuccess = AppConsts.AssemblyDynamicApiOptions .TryGetValue(action.Controller.ControllerType.Assembly, out AssemblyDynamicApiOptions assemblyDynamicApiOptions); if (getValueSuccess && !string.IsNullOrWhiteSpace(assemblyDynamicApiOptions?.HttpVerb)) { return assemblyDynamicApiOptions.HttpVerb; } var verbKey = action.ActionName.GetPascalOrCamelCaseFirstWord().ToLower(); var verb = AppConsts.HttpVerbs.ContainsKey(verbKey) ? AppConsts.HttpVerbs[verbKey] : AppConsts.DefaultHttpVerb; return verb; } private AttributeRouteModel CreateActionRouteModel(string areaName, string controllerName, ActionModel action) { var route = _actionRouteFactory.CreateActionRouteModel(areaName, controllerName, action); return new AttributeRouteModel(new RouteAttribute(route)); } }