本文在webapi的基础上进行后续的扩展,也可以实现不依赖项目类型的模式,只需要添加webapi对应的Nuget即可。
- 首先创建接口来识别动态API的实现类
public interface IAutoAPIService
{
}
- ControllerFeatureProvider
创建一个类继承ControllerFeatureProvider并重写IsController方法,此方法将告诉容器指定的类型是否为控制器。 也可以继承IApplicationFeatureProvider, IApplicationFeatureProvider然后分别实现PopulateFeature、IsController两个方法
public class AutoAPIControllerFeatureProvider :
ControllerFeatureProvider
//IApplicationFeatureProvider<ControllerFeature>, IApplicationFeatureProvider
{
//public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
//{
// foreach (var part in parts.OfType<IApplicationPartTypeProvider>())
// {
// foreach (var type in part.Types)
// {
// if (IsController(type) && !feature.Controllers.Contains(type))
// {
// feature.Controllers.Add(type);
// }
// }
// }
//}
//protected override bool IsController(TypeInfo typeInfo)
protected override bool IsController(TypeInfo typeInfo)
{
//判断是否继承了指定的接口
if (typeof(IAutoAPIService).IsAssignableFrom(typeInfo))
{
if (!typeInfo.IsInterface &&
!typeInfo.IsAbstract &&
!typeInfo.IsGenericType &&
typeInfo.IsPublic)
{
return true;
}
}
return false;
//return base.IsController(typeInfo);
}
- IApplicationModelConvention
public class AutoAPIApplicationModelConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
if (typeof(IAutoAPIService).IsAssignableFrom(controller.ControllerType))
{
ConfigureApplicationService(controller);
}
}
}
private void ConfigureApplicationService(ControllerModel controller)
{
ConfigureApiExplorer(controller);//api是否允许被发现
ConfigureSelector(controller);//路由配置
ConfigureParameters(controller);//参数配置
}
private void ConfigureApiExplorer(ControllerModel controller)
{
if (!controller.ApiExplorer.IsVisible.HasValue)
{
controller.ApiExplorer.IsVisible = true;
}
foreach (var action in controller.Actions)
{
if (!action.ApiExplorer.IsVisible.HasValue)
{
action.ApiExplorer.IsVisible = true;
}
}
}
private void ConfigureSelector(ControllerModel controller)
{
RemoveEmptySelectors(controller.Selectors);
if (controller.Selectors.Any(temp => temp.AttributeRouteModel != null))
{
return;
}
foreach (var action in controller.Actions)
{
ConfigureSelector(action);
}
}
private void ConfigureSelector(ActionModel action)
{
RemoveEmptySelectors(action.Selectors);
if (action.Selectors.Count <= 0)
{
AddApplicationServiceSelector(action);
}
else
{
NormalizeSelectorRoutes(action);
}
}
private void ConfigureParameters(ControllerModel controller)
{
foreach (var action in controller.Actions)
{
foreach (var parameter in action.Parameters)
{
if (parameter.BindingInfo != null)
{
continue;
}
if (parameter.ParameterType.IsClass &&
parameter.ParameterType != typeof(string) &&
parameter.ParameterType != typeof(IFormFile))
{
var httpMethods = action.Selectors.SelectMany(temp => temp.ActionConstraints).OfType<HttpMethodActionConstraint>().SelectMany(temp => temp.HttpMethods).ToList();
if (httpMethods.Contains("GET") ||
httpMethods.Contains("DELETE") ||
httpMethods.Contains("TRACE") ||
httpMethods.Contains("HEAD"))
{
continue;
}
parameter.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromBodyAttribute() });
}
}
}
}
private void NormalizeSelectorRoutes(ActionModel action)
{
foreach (var selector in action.Selectors)
{
if (selector.AttributeRouteModel == null)
{
selector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(CalculateRouteTemplate(action)));
}
if (selector.ActionConstraints.OfType<HttpMethodActionConstraint>().FirstOrDefault()?.HttpMethods?.FirstOrDefault() == null)
{
selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { GetHttpMethod(action) }));
}
}
}
private void AddApplicationServiceSelector(ActionModel action)
{
var selector = new SelectorModel();
selector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(CalculateRouteTemplate(action)));
selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { GetHttpMethod(action) }));
action.Selectors.Add(selector);
}
private string CalculateRouteTemplate(ActionModel action)
{
var routeTemplate = new StringBuilder();
routeTemplate.Append("api");
// 控制器名称部分
var controllerName = action.Controller.ControllerName;
if (controllerName.EndsWith("ApplicationService"))
{
controllerName = controllerName.Substring(0, controllerName.Length - "ApplicationService".Length);
}
else if (controllerName.EndsWith("AppService"))
{
controllerName = controllerName.Substring(0, controllerName.Length - "AppService".Length);
}
controllerName += "s";
routeTemplate.Append($"/{controllerName}");
// id 部分
if (action.Parameters.Any(temp => temp.ParameterName == "id"))
{
routeTemplate.Append("/{id}");
}
// Action 名称部分
var actionName = action.ActionName;
if (actionName.EndsWith("Async"))
{
actionName = actionName.Substring(0, actionName.Length - "Async".Length);
}
var trimPrefixes = new[]
{
"GetAll","GetList","Get",
"Post","Create","Add","Insert",
"Put","Update",
"Delete","Remove",
"Patch"
};
foreach (var trimPrefix in trimPrefixes)
{
if (actionName.StartsWith(trimPrefix))
{
actionName = actionName.Substring(trimPrefix.Length);
break;
}
}
if (!string.IsNullOrEmpty(actionName))
{
routeTemplate.Append($"/{actionName}");
}
return routeTemplate.ToString();
}
private string GetHttpMethod(ActionModel action)
{
var actionName = action.ActionName;
if (actionName.StartsWith("Get"))
{
return "GET";
}
if (actionName.StartsWith("Put") || actionName.StartsWith("Update"))
{
return "PUT";
}
if (actionName.StartsWith("Delete") || actionName.StartsWith("Remove"))
{
return "DELETE";
}
if (actionName.StartsWith("Patch"))
{
return "PATCH";
}
return "POST";
}
private void RemoveEmptySelectors(IList<SelectorModel> selectors)
{
for (var i = selectors.Count - 1; i >= 0; i--)
{
var selector = selectors[i];
if (selector.AttributeRouteModel == null &&
(selector.ActionConstraints == null || selector.ActionConstraints.Count <= 0) &&
(selector.EndpointMetadata == null || selector.EndpointMetadata.Count <= 0))
{
selectors.Remove(selector);
}
}
}
}
- 注入
添加扩展方法来注入
public static class AutoAPIEx
{
public static IMvcBuilder AddDynamicWebApi(this IMvcBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.ConfigureApplicationPartManager(applicationPartManager =>
{
applicationPartManager.FeatureProviders.Add(new AutoAPIControllerFeatureProvider());
});
builder.Services.Configure<MvcOptions>(options =>
{
options.Conventions.Add(new AutoAPIApplicationModelConvention());
});
return builder;
}
public static IMvcCoreBuilder AddDynamicWebApi(this IMvcCoreBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.ConfigureApplicationPartManager(applicationPartManager =>
{
applicationPartManager.FeatureProviders.Add(new AutoAPIControllerFeatureProvider());
});
builder.Services.Configure<MvcOptions>(options =>
{
options.Conventions.Add(new AutoAPIApplicationModelConvention());
});
return builder;
}
}
随后将ConfigureServices中的
services.AddControllers();调整为
services.AddControllers().AddDynamicWebApi();
- 测试使用
public class AutoTestService : IAutoAPIService
{
public IEnumerable<int> Getx(int value)
{
yield return value;
}
public InDto PostX(InDto value)
{
return value;
}
}
5.效果
- 尾注
也可以用IApplicationModelConvention来做一些特殊的事情,如根据Controller继承的特性来添加一些特殊的过滤器从而实现扩展
测试代码
https://files.cnblogs.com/files/ives/DynamicAPI.zip?t=1683815091&download=true
[参考]
[.Net Core后端架构实战【2-实现动态路由与Dynamic API】]https://www.cnblogs.com/zhangnever/p/17131504.html)
ASP.NET Core 奇淫技巧之动态WebApi
.net core 实现动态 Web API
.NET 7.0 架构实战 动态路由与Dynamic API
.net mvc core 关于项目中分离控制器
FeatureProviders
wpf 动态修改表头_【asp.net core】实现动态 Web API
5.1.6.1 控制器特性提供器
在Asp.Net Core中使用ModelConvention实现全局过滤器隔离
ASP.NET Core中为指定类添加WebApi服务功能
.NET Core POCOController在动态Web API中的应用