diff --git a/Infrastructure/Helper/AssemblyUtils.cs b/Infrastructure/Helper/AssemblyUtils.cs index 3934851e03d42ebc9de856b5ba412fd6063e28df..9f6519691f7b157be05c58f14f82c0a3411018b0 100644 --- a/Infrastructure/Helper/AssemblyUtils.cs +++ b/Infrastructure/Helper/AssemblyUtils.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Infrastructure.Helper { @@ -32,5 +30,37 @@ namespace Infrastructure.Helper var assemblies = GetAssemblies(); return assemblies.SelectMany(p => p.GetTypes()); } + //获取泛型类名 + public static Type GetGenericTypeByName(string genericTypeName) + { + Type type = null; + foreach (var assembly in GetAssemblies()) + { + var baseType = assembly.GetTypes() + .FirstOrDefault(t => t.IsGenericType && + t.GetGenericTypeDefinition().Name.Equals(genericTypeName, StringComparison.Ordinal)); + if (baseType != null) + { + return baseType?.GetGenericTypeDefinition(); + } + + + } + + return type; + } + public static bool IsDerivedFromGenericBaseRepository(this Type? type, Type genericBase) + { + while (type != null && type != typeof(object)) + { + var cur = type.IsGenericType ? type.GetGenericTypeDefinition() : type; + if (genericBase == cur) + { + return true; + } + type = type.BaseType; + } + return false; + } } } diff --git a/ZR.Admin.WebApi/Controllers/CommonController.cs b/ZR.Admin.WebApi/Controllers/CommonController.cs index a70bf33ad35d1c51e4d2a9165c1733f05355f409..9aa0c11e2685fe86839376dbd70d9daf88d5ea71 100644 --- a/ZR.Admin.WebApi/Controllers/CommonController.cs +++ b/ZR.Admin.WebApi/Controllers/CommonController.cs @@ -22,18 +22,16 @@ namespace ZR.Admin.WebApi.Controllers private IWebHostEnvironment WebHostEnvironment; private ISysFileService SysFileService; - private IHelloService HelloService; + public CommonController( IOptions options, IWebHostEnvironment webHostEnvironment, - ISysFileService fileService, - IHelloService helloService) + ISysFileService fileService) { WebHostEnvironment = webHostEnvironment; SysFileService = fileService; OptionsSetting = options.Value; - HelloService = helloService; } /// @@ -48,18 +46,6 @@ namespace ZR.Admin.WebApi.Controllers "如果觉得项目有用,打赏作者喝杯咖啡作为奖励\n☛☛http://www.izhaorui.cn/vip\n"); } - /// - /// hello - /// - /// - /// - [Route("/hello")] - [HttpGet] - public IActionResult Hello(string name) - { - return Ok(HelloService.SayHello(name)); - } - /// /// 企业消息测试 /// diff --git a/ZR.Admin.WebApi/Extensions/SwaggerExtension.cs b/ZR.Admin.WebApi/Extensions/SwaggerExtension.cs index a8a7336781dce9cbb97a179242a2a2a3d95094c1..ce97d989cf27148a3905c7e6bc1df915a4fa20c5 100644 --- a/ZR.Admin.WebApi/Extensions/SwaggerExtension.cs +++ b/ZR.Admin.WebApi/Extensions/SwaggerExtension.cs @@ -118,19 +118,28 @@ namespace ZR.Admin.WebApi.Extensions } }); - //判断接口归于哪个分组 - c.DocInclusionPredicate((docName, apiDescription) => + try { - if (docName == "v1") - { - //当分组为NoGroup时,只要没加特性的都属于这个组 - return string.IsNullOrEmpty(apiDescription.GroupName); - } - else + //判断接口归于哪个分组 + c.DocInclusionPredicate((docName, apiDescription) => { - return apiDescription.GroupName == docName; - } - }); + if (docName == "v1") + { + //当分组为NoGroup时,只要没加特性的都属于这个组 + return string.IsNullOrEmpty(apiDescription.GroupName); + } + else + { + return apiDescription.GroupName == docName; + } + }); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + }); } } diff --git a/ZR.Admin.WebApi/Program.cs b/ZR.Admin.WebApi/Program.cs index 80172463fa3fac3fda7ecccbac5343ec2c6cbf2f..f21d9089e3ea433cb28c83fbb01da47c8ad9335a 100644 --- a/ZR.Admin.WebApi/Program.cs +++ b/ZR.Admin.WebApi/Program.cs @@ -6,6 +6,8 @@ using SqlSugar; using System.Text.Json; using ZR.Admin.WebApi.Extensions; using ZR.Common.Cache; +using ZR.Common.DynamicApiSimple; +using ZR.Common.DynamicApiSimple.Extens; using ZR.Infrastructure.WebExtensions; using ZR.ServiceCore.Signalr; using ZR.ServiceCore.SqlSugar; @@ -15,8 +17,10 @@ var builder = WebApplication.CreateBuilder(args); //builder.Logging.ClearProviders(); builder.Host.UseNLog(); +builder.Services.AddDynamicApi(); // Add services to the container. -builder.Services.AddControllers(); +builder.Services.AddControllers(options=>options.ModelBinderProviders.Insert(0, new JsonModelBinderProvider())); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); diff --git a/ZR.Common/DynamicApiSimple/ApiConvention.cs b/ZR.Common/DynamicApiSimple/ApiConvention.cs new file mode 100644 index 0000000000000000000000000000000000000000..4ab2c0318031ad24bb74e64991074b0ffe11c141 --- /dev/null +++ b/ZR.Common/DynamicApiSimple/ApiConvention.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Infrastructure.Helper; +using Microsoft.AspNetCore.Mvc.ActionConstraints; +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +namespace ZR.Common.DynamicApiSimple; + +class ApiConvention : IApplicationModelConvention +{ + public void Apply(ApplicationModel application) + { + foreach (var controller in application.Controllers) + { + var type = controller.ControllerType; + if (typeof(IDynamicApi).IsAssignableFrom(type) || type.IsDefined(typeof(DynamicApiAttribute),true)) + { + ClearAction(controller); + ConfigureApiExplorer(controller); + ConfigureSelector(controller); + } + } + } + + private void ClearAction(ControllerModel controller) + { + Type genericBaseType = AssemblyUtils.GetGenericTypeByName("BaseService`1"); + var needRemoveAction = controller.Actions + .Where(action => !action.ActionMethod.DeclaringType.IsDerivedFromGenericBaseRepository(genericBaseType)) + .ToList(); + + foreach (var actionModel in needRemoveAction) + { + controller.Actions.Remove(actionModel); + } + } + + + private static 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(selector => selector.AttributeRouteModel != null)) + return; + + foreach (var action in controller.Actions) + { + ConfigureSelector(action); + } + } + + private static void RemoveEmptySelectors(IList 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); + } + } + } + + private void ConfigureSelector(ActionModel action) + { + RemoveEmptySelectors(action.Selectors); + + if (action.Selectors.Count <= 0) + AddServiceSelector(action); + else + NormalizeSelectorRoutes(action); + } + + private void AddServiceSelector(ActionModel action) + { + var template = new Microsoft.AspNetCore.Mvc.RouteAttribute(GetRouteTemplate(action)); + var selector = new SelectorModel + { + AttributeRouteModel = new AttributeRouteModel(template) + }; + selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { GetHttpMethod(action) })); + action.Selectors.Add(selector); + } + + private void NormalizeSelectorRoutes(ActionModel action) + { + foreach (var selector in action.Selectors) + { + var template = new Microsoft.AspNetCore.Mvc.RouteAttribute(GetRouteTemplate(action)); + selector.AttributeRouteModel = new AttributeRouteModel(template); + if (selector.ActionConstraints.OfType().FirstOrDefault()?.HttpMethods?.FirstOrDefault() == null) + selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { GetHttpMethod(action) })); + + } + } + + private string GetRouteTemplate(ActionModel action) + { + var routeTemplate = new StringBuilder(); + var names = action.Controller.ControllerType.Namespace.Split('.'); + if (names.Length > 2) + { + routeTemplate.Append(names[^2]); + } + + // Controller + var controllerName = action.Controller.ControllerName; + if (controllerName.EndsWith("Service")) + controllerName = controllerName[0..^7]; + + routeTemplate.Append($"api/{controllerName}"); + + // Action + var actionName = action.ActionName; + if (actionName.EndsWith("Async")||actionName.EndsWith("async")) + actionName = actionName[..^"Async".Length]; + + if (!string.IsNullOrEmpty(actionName)) + { + routeTemplate.Append($"/{RemoveHttpMethodPrefix(actionName)}"); + } + + + return routeTemplate.ToString(); + } + + private static string GetHttpMethod(ActionModel action) + { + var actionName = action.ActionName.ToLower(); + string Method = string.Empty; + if (!string.IsNullOrEmpty(actionName)) + { + Method = GetName(actionName); + } + return Method; + } + + private static string GetName(string actionName) + { + string result = "POST"; + foreach (string key in Methods.Keys) + { + if (actionName.Contains(key)) + { + result = Methods[key]; + break; + } + + } + return result; + } + internal static Dictionary Methods { get; private set; } + static ApiConvention() + { + Methods = new Dictionary() + { + + ["get"] = "GET", + ["find"] = "GET", + ["fetch"] = "GET", + ["query"] = "GET", + ["post"] = "POST", + ["add"] = "POST", + ["create"] = "POST", + ["insert"] = "POST", + ["submit"] = "POST", + ["put"] = "POST", + ["update"] = "POST", + ["delete"] = "DELETE", + ["remove"] = "DELETE", + ["clear"] = "DELETE", + ["patch"] = "PATCH" + }; + + } + private static string RemoveHttpMethodPrefix(string actionName) + { + foreach (var method in Methods.Keys) + { + if (actionName.StartsWith(method, StringComparison.OrdinalIgnoreCase)) + { + // 移除前缀并返回结果 + return actionName.Substring(method.Length); + } + } + + return actionName; // 如果没有找到前缀,返回原始名称 + } +} diff --git a/ZR.Common/DynamicApiSimple/ApiFeatureProvider.cs b/ZR.Common/DynamicApiSimple/ApiFeatureProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..820f9104a0a14e6dc85183ffbae7e3c4a40dd519 --- /dev/null +++ b/ZR.Common/DynamicApiSimple/ApiFeatureProvider.cs @@ -0,0 +1,28 @@ +using System; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; + +namespace ZR.Common.DynamicApiSimple; + +class ApiFeatureProvider : ControllerFeatureProvider +{ + protected override bool IsController(TypeInfo typeInfo) + { + Type type = typeInfo.AsType(); + // 不能是非公开的、值类型、抽象类、泛型类或基元类型 + if (!type.IsPublic || type.IsValueType || type.IsAbstract || type.IsGenericType || type.IsPrimitive || string.IsNullOrWhiteSpace(type.Namespace)) return false; + + // 原生层或者实现IDynamicApiController(类),[DynamicApi](接口) + if ((!typeof(Controller).IsAssignableFrom(type) && typeof(ControllerBase).IsAssignableFrom(type))|| type.IsDefined(typeof(DynamicApiAttribute), true) || typeof(IDynamicApi).IsAssignableFrom(type)) + { + // 如果是忽略的则跳过自定义的接口在前面会报错,所以必须在后面 + if (type.IsDefined(typeof(ApiExplorerSettingsAttribute), true) && type.GetCustomAttribute(true).IgnoreApi) + { + return false; + } + return true; + } + return false; + } +} diff --git a/ZR.Common/DynamicApiSimple/DynamicApiAttribute.cs b/ZR.Common/DynamicApiSimple/DynamicApiAttribute.cs new file mode 100644 index 0000000000000000000000000000000000000000..dad1dfe9bccfe5d425a1bd5f899ab1626164d777 --- /dev/null +++ b/ZR.Common/DynamicApiSimple/DynamicApiAttribute.cs @@ -0,0 +1,21 @@ +using System; + +namespace ZR.Common.DynamicApiSimple +{ + public class DynamicApiAttribute:Attribute + { + public string Name; + public string Order; + public string Description; + public DynamicApiAttribute() + { + + } + public DynamicApiAttribute(string _name,string _order,string _description) + { + Name = _name; + Order = _order; + Description = _description; + } + } +} diff --git a/ZR.Common/DynamicApiSimple/Extens/DynamicApiExtens.cs b/ZR.Common/DynamicApiSimple/Extens/DynamicApiExtens.cs new file mode 100644 index 0000000000000000000000000000000000000000..c3a59ace02cc114718b706c10630766455ec2c11 --- /dev/null +++ b/ZR.Common/DynamicApiSimple/Extens/DynamicApiExtens.cs @@ -0,0 +1,33 @@ +using System.Linq; +using System.Reflection; +using Infrastructure.Helper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.DependencyInjection; + +namespace ZR.Common.DynamicApiSimple.Extens +{ + public static class DynamicApiExtens + { + public static IServiceCollection AddDynamicApi(this IServiceCollection services) + { + services.AddMvc().ConfigureApplicationPartManager(m => + { + foreach (Assembly assembly in AssemblyUtils.GetAssemblies()) + { + + if (m.ApplicationParts.Any(it => it.Name.Equals(assembly.FullName.Split(',')[0]))) continue; + + m.ApplicationParts.Add(new AssemblyPart(assembly)); + } + m.FeatureProviders.Add(new ApiFeatureProvider()); + }); + + services.Configure(o => + { + o.Conventions.Add(new ApiConvention()); + }); + return services; + } + } +} diff --git a/ZR.Common/DynamicApiSimple/IDynamicApi.cs b/ZR.Common/DynamicApiSimple/IDynamicApi.cs new file mode 100644 index 0000000000000000000000000000000000000000..780410c0d1ee9c474a0e93be30d71c149b0e2eda --- /dev/null +++ b/ZR.Common/DynamicApiSimple/IDynamicApi.cs @@ -0,0 +1,6 @@ +namespace ZR.Common.DynamicApiSimple +{ + public interface IDynamicApi + { + } +} diff --git a/ZR.Common/DynamicApiSimple/JsonModelBinder.cs b/ZR.Common/DynamicApiSimple/JsonModelBinder.cs new file mode 100644 index 0000000000000000000000000000000000000000..3604c56ddea0a275ac9c1f3563d4e995804e0fc1 --- /dev/null +++ b/ZR.Common/DynamicApiSimple/JsonModelBinder.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Newtonsoft.Json; + +namespace ZR.Common.DynamicApiSimple; + +public class JsonModelBinder:IModelBinder +{ + private readonly IModelBinder _fallbackBinder; + + public JsonModelBinder(IModelBinder fallbackBinder) + { + _fallbackBinder = fallbackBinder; + } + + public async Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext == null) + { + throw new ArgumentNullException(nameof(bindingContext)); + } + + var request = bindingContext.HttpContext.Request; + if ((request.Method == "POST" || request.Method == "PUT") && request.ContentType != null && request.ContentType.Contains("application/json")) + { + using (var reader = new StreamReader(request.Body)) + { + var body = await reader.ReadToEndAsync(); + if (!string.IsNullOrEmpty(body)) + { + var result = JsonConvert.DeserializeObject(body, bindingContext.ModelType); + bindingContext.Result = ModelBindingResult.Success(result); + return; + } + } + } + + if (_fallbackBinder != null) + { + await _fallbackBinder.BindModelAsync(bindingContext); + } + else + { + bindingContext.Result = ModelBindingResult.Failed(); + } + } +} \ No newline at end of file diff --git a/ZR.Common/DynamicApiSimple/JsonModelBinderProvider.cs b/ZR.Common/DynamicApiSimple/JsonModelBinderProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..5354d01b556165aa48324c00dd83aa1e22a597c1 --- /dev/null +++ b/ZR.Common/DynamicApiSimple/JsonModelBinderProvider.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; + +namespace ZR.Common.DynamicApiSimple; + +public class JsonModelBinderProvider:IModelBinderProvider +{ + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Metadata.IsComplexType) + { + var fallbackBinder = new ComplexTypeModelBinderProvider().GetBinder(context); + return new JsonModelBinder(fallbackBinder); + } + + return null; + } +} \ No newline at end of file diff --git a/ZR.ServiceCore/BaseService.cs b/ZR.ServiceCore/BaseService.cs index fd5c703c76b3921fe6efe7c8a25872dedd2fe532..078e7b82e30dc4b50e435dd88202084e84454fc2 100644 --- a/ZR.ServiceCore/BaseService.cs +++ b/ZR.ServiceCore/BaseService.cs @@ -8,5 +8,14 @@ namespace ZR.ServiceCore /// public class BaseService : BaseRepository where T : class, new() { + public override Task InsertAsync(T insertObj, CancellationToken cancellationToken) + { + return base.InsertAsync(insertObj, cancellationToken); + } + + public override Task InsertRangeAsync(List insertObjs, CancellationToken cancellationToken) + { + return base.InsertRangeAsync(insertObjs, cancellationToken); + } } }