| | |
| | | using Furion; |
| | | using Consul; |
| | | using Furion; |
| | | using Furion.DatabaseAccessor; |
| | | using Furion.DataEncryption; |
| | | using Furion.DistributedIDGenerator; |
| | | using Furion.DynamicApiController; |
| | | using Furion.FriendlyException; |
| | | using Furion.HttpRemote; |
| | | using Mapster; |
| | | using MediatR; |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using Microsoft.AspNetCore.Mvc.ActionConstraints; |
| | | using Microsoft.AspNetCore.Mvc.Controllers; |
| | | using Microsoft.AspNetCore.Routing; |
| | | using Microsoft.EntityFrameworkCore; |
| | | using Microsoft.Extensions.Caching.Distributed; |
| | | using Microsoft.Extensions.DependencyInjection; |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.ComponentModel; |
| | | using System.Diagnostics; |
| | | using System.Linq; |
| | | using System.Net.Http; |
| | | using System.Net.Http.Headers; |
| | | using System.Reflection; |
| | | using System.Resources; |
| | | using System.Text; |
| | | using System.Text.RegularExpressions; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | using static System.Collections.Specialized.BitVector32; |
| | | |
| | | namespace FlexJobApi.Core |
| | | { |
| | |
| | | public static class ResourceUtils |
| | | { |
| | | /// <summary> |
| | | /// 生成资源 |
| | | /// 发送HTTP请求 |
| | | /// </summary> |
| | | /// <param name="cancellationToken"></param> |
| | | /// <typeparam name="TRequest"></typeparam> |
| | | /// <typeparam name="TResponse"></typeparam> |
| | | /// <param name="request"></param> |
| | | /// <param name="provider"></param> |
| | | /// <returns></returns> |
| | | public static async Task BuildWebApis(CancellationToken cancellationToken = default) |
| | | public static Task<TResponse> SendHttpAsync<TRequest, TResponse>( |
| | | TRequest request, |
| | | IResourceHttpProvider provider = null) |
| | | where TRequest : class, new() |
| | | { |
| | | return App.GetRequiredService<ResourceHttpUtils>().SendHttpAsync<TRequest, TResponse>(request, provider); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 生成动态控制器 |
| | | /// </summary> |
| | | public static async Task BuildDynamicControllersAsync() |
| | | { |
| | | var traceId = App.GetTraceId() ?? IDGen.NextID().ToString(); |
| | | var scopeFactory = App.GetService<IServiceScopeFactory>(); |
| | | var serviceScope = scopeFactory.CreateScope(); |
| | | var provider = serviceScope.ServiceProvider.GetRequiredService<IDynamicApiRuntimeChangeProvider>(); |
| | | var rep = serviceScope.ServiceProvider.GetRequiredService<IRepository<Resource>>(); |
| | | var endpointDataSource = App.GetService<EndpointDataSource>(); |
| | | var routeEndpoints = endpointDataSource.Endpoints.OfType<RouteEndpoint>(); |
| | | var xmlDoc = await XmlDocUtils.GetXmlDocAsync(); |
| | | var enumWebApiMethods = await EnumUtils.GetModel<EnumWebApiMethod>(); |
| | | var webApis = await rep.AsQueryable().ToListAsync(); |
| | | foreach (var routeEndpoint in routeEndpoints) |
| | | var enumWebApiMethods = await EnumUtils.GetModel<EnumResourceMethod>(); |
| | | var resourceControllers = await EnumUtils.GetModel<EnumResourceController>(); |
| | | var requests = App.Assemblies |
| | | .SelectMany(it => it.GetTypes()) |
| | | .Where(it => !it.IsAbstract && typeof(IBaseRequest).IsAssignableFrom(it)) |
| | | .ToList(); |
| | | var models = new List<ResourceModel>(); |
| | | foreach (var request in requests) |
| | | { |
| | | var model = new ResourceModel(); |
| | | model.TraceId = traceId; |
| | | model.Route = "/" + routeEndpoint.RoutePattern.RawText; |
| | | var controllerActionDescriptor = routeEndpoint.Metadata.GetMetadata<ControllerActionDescriptor>(); |
| | | var methodInfo = controllerActionDescriptor.MethodInfo; |
| | | var methodInfoXmlDoc = await controllerActionDescriptor.MethodInfo.GetXmlDocMemberAsync(xmlDoc); |
| | | var controllerTypeXmlDoc = await methodInfo.DeclaringType?.GetXmlDocMemberAsync(); |
| | | model.Code = methodInfoXmlDoc.Name; |
| | | model.Service = methodInfo.Module.Name.Split(".")[1]; |
| | | model.Method = enumWebApiMethods.GetEnum((controllerActionDescriptor.ActionConstraints[0] as HttpMethodActionConstraint).HttpMethods.FirstOrDefault()); |
| | | model.Name = $"{controllerTypeXmlDoc?.Summary ?? "没写注释"}-{methodInfoXmlDoc?.Summary ?? "没写注释"}"; |
| | | model.RequestTypeName = methodInfo.GetParameters().FirstOrDefault().ParameterType.FullName; |
| | | var returnType = methodInfo.ReturnType; |
| | | if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) |
| | | returnType = returnType.GetGenericArguments()[0]; |
| | | if (returnType != null && returnType != typeof(Task)) |
| | | model.ResponseTypeName = methodInfo.ReturnType.FullName; |
| | | var resourceAttribute = request.GetCustomAttribute<ResourceAttribute>(); |
| | | if (resourceAttribute == null) throw Oops.Oh(EnumErrorCodeType.s404, $"请给资源{request.Name}分配服务特性Resource"); |
| | | |
| | | var webApi = webApis.FirstOrDefault(it => it.Route == model.Route && it.Method == model.Method); |
| | | if (webApi == null) |
| | | foreach (var controller in resourceAttribute.Controllers) |
| | | { |
| | | webApi = new Resource(); |
| | | model.Adapt(webApi); |
| | | await rep.InsertAsync(webApi); |
| | | webApis.Add(webApi); |
| | | var requestXmlDoc = await request.GetXmlDocMemberAsync(xmlDoc); |
| | | var resourceController = controller.GetCustomAttribute<EnumResourceController, ResourceControllerAttribute>(); |
| | | var resourceService = resourceController.Service.GetCustomAttribute<EnumResourceService, ResourceServiceAttribute>(); |
| | | |
| | | var model = new ResourceModel(); |
| | | model.TraceId = traceId; |
| | | model.ApplicationName = resourceService.ApplicationName; |
| | | model.Controller = controller; |
| | | model.ControllerSummary = resourceControllers.GetDescription(controller); |
| | | model.ActionName = Regex.Replace(request.Name, @"(Command|Query)$", "", RegexOptions.None); |
| | | model.ActionSummary = requestXmlDoc?.Summary; |
| | | model.Service = resourceController.Service; |
| | | model.ServiceName = resourceService.ServiceName; |
| | | model.RouteArea = resourceService.RouteArea; |
| | | model.Route = $"/api/{resourceService.RouteArea ?? "main"}/{controller}/{model.ActionName}"; |
| | | model.Method = |
| | | request.BaseType?.IsGenericType == true && request.BaseType.GetGenericTypeDefinition() == typeof(PagedListQuery<,>) |
| | | ? EnumResourceMethod.Post |
| | | : new List<string> { "Post", "Add", "Create", "Insert", "Submit" }.Any(it => request.Name.StartsWith(it, StringComparison.OrdinalIgnoreCase)) |
| | | ? EnumResourceMethod.Post |
| | | : new List<string> { "GetAll", "GetList", "Get", "Find", "Fetch", "Query" }.Any(it => request.Name.StartsWith(it, StringComparison.OrdinalIgnoreCase)) |
| | | ? EnumResourceMethod.Get |
| | | : new List<string> { "Put", "Update ", "Set" }.Any(it => request.Name.StartsWith(it, StringComparison.OrdinalIgnoreCase)) |
| | | ? EnumResourceMethod.Put |
| | | : new List<string> { "Delete", "Remove ", "Clear" }.Any(it => request.Name.StartsWith(it, StringComparison.OrdinalIgnoreCase)) |
| | | ? EnumResourceMethod.Delete |
| | | : EnumResourceMethod.Post; |
| | | model.Code = requestXmlDoc?.Name; |
| | | model.Name = $"{model.ControllerSummary}-{model.ActionSummary}"; |
| | | model.AllowAnonymous = resourceAttribute.AllowAnonymous; |
| | | model.RequestTypeName = request.Name; |
| | | model.RequestTypeFullName = request.FullName; |
| | | var iRequestType = request.GetInterface("IRequest`1"); |
| | | if (iRequestType != null && iRequestType.IsGenericType) |
| | | { |
| | | var responseType = iRequestType.GenericTypeArguments[0]; |
| | | model.ResponseTypeName = responseType.GetCSharpFriendlyName(); |
| | | model.ResponseTypeFullName = responseType.FullName; |
| | | } |
| | | models.Add(model); |
| | | |
| | | await App.GetRequiredService<IDistributedCache>().SetStringAsync($"ResourceModel|{model.RequestTypeFullName}", model.ToJson()); |
| | | } |
| | | } |
| | | |
| | | var resources = await SaveResourcesAsync(models, rep); |
| | | |
| | | DynamicControllersHotPlug(resources, provider); |
| | | |
| | | await rep.SaveNowAsync(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 保存资源 |
| | | /// </summary> |
| | | /// <param name="models"></param> |
| | | /// <param name="traceId"></param> |
| | | /// <param name="rep"></param> |
| | | /// <returns></returns> |
| | | private static async Task<List<Resource>> SaveResourcesAsync(List<ResourceModel> models, IRepository<Resource> rep = null) |
| | | { |
| | | rep = rep ?? Db.GetRepository<Resource>(); |
| | | var resources = await rep.AsQueryable() |
| | | .Where(it => !it.IsExpired) |
| | | .ToListAsync(); |
| | | foreach (var model in models) |
| | | { |
| | | var resource = resources.FirstOrDefault(it => it.Route == model.Route && it.Method == model.Method); |
| | | if (resource == null) |
| | | { |
| | | resource = new Resource(); |
| | | resource.Id = IDGen.NextID(); |
| | | resource.CreatedTime = DateTimeOffset.Now; |
| | | model.Adapt(resource); |
| | | await rep.InsertAsync(resource); |
| | | resources.Add(resource); |
| | | } |
| | | else |
| | | { |
| | | model.Adapt(webApi); |
| | | await rep.UpdateAsync(webApi); |
| | | var resourceBakModel = new ResourceModel(); |
| | | resource.Adapt(resourceBakModel); |
| | | resourceBakModel.TraceId = model.TraceId; |
| | | resourceBakModel.DynamicAssemblyName = model.DynamicAssemblyName; |
| | | if (resourceBakModel.ToJson() != model.ToJson()) |
| | | { |
| | | model.Adapt(resource); |
| | | resource.UpdatedTime = DateTimeOffset.Now; |
| | | await rep.UpdateAsync(resource); |
| | | } |
| | | } |
| | | } |
| | | var expiredWebApis = webApis.Where(it => it.TraceId != traceId).ToList(); |
| | | foreach (var webApi in expiredWebApis) |
| | | |
| | | var expiredResources = resources.Where(it => !models.Any(m => m.Route == it.Route && m.Method == it.Method)).ToList(); |
| | | foreach (var expiredResource in expiredResources) |
| | | { |
| | | webApi.IsExpired = true; |
| | | await rep.UpdateAsync(webApi); |
| | | //expiredResource.IsExpired = true; |
| | | //await rep.UpdateAsync(expiredResource); |
| | | await rep.DeleteAsync(expiredResource); |
| | | } |
| | | |
| | | await rep.SaveNowAsync(); |
| | | return resources.Where(it => !it.IsExpired).ToList(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 动态控制器热插 |
| | | /// </summary> |
| | | /// <param name="resources"></param> |
| | | /// <param name="provider"></param> |
| | | public static void DynamicControllersHotPlug(List<Resource> resources, IDynamicApiRuntimeChangeProvider provider = null) |
| | | { |
| | | provider = provider ?? App.GetRequiredService<IDynamicApiRuntimeChangeProvider>(); |
| | | foreach (var resource in resources) |
| | | { |
| | | var code = $@" |
| | | using FlexJobApi.Core; |
| | | using Furion.DynamicApiController; |
| | | using Furion.FriendlyException; |
| | | using MediatR; |
| | | using Microsoft.AspNetCore.Authorization; |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using System; |
| | | using System.Threading.Tasks; |
| | | using System.Collections.Generic; |
| | | using System.ComponentModel; |
| | | |
| | | namespace {resource.ApplicationName}.{resource.Controller}.{resource.ActionName}Request |
| | | {{ |
| | | /// <summary> |
| | | /// {resource.ControllerSummary} |
| | | /// </summary> |
| | | [Route(""api/{resource.RouteArea}/[controller]"")] |
| | | public class {resource.Controller}AppService(IMediator mediator) : IDynamicApiController |
| | | {{ |
| | | private readonly IMediator mediator = mediator; |
| | | |
| | | /// <summary> |
| | | /// {resource.ActionSummary} |
| | | /// </summary> |
| | | /// <param name=""request""></param> |
| | | /// <returns></returns>"; |
| | | if (resource.AllowAnonymous) |
| | | { |
| | | code += $@" |
| | | [AllowAnonymous]"; |
| | | } |
| | | code += $@" |
| | | [Http{resource.Method}] |
| | | public Task<{resource.ResponseTypeName}> {resource.ActionName}({resource.RequestTypeName} request) |
| | | {{ |
| | | return mediator.Send(request); |
| | | }} |
| | | |
| | | }} |
| | | }} |
| | | "; |
| | | var dynamicAssembly = App.CompileCSharpClassCode(code); |
| | | provider.AddAssembliesWithNotifyChanges(dynamicAssembly); |
| | | var dynamicAssemblyName = dynamicAssembly.GetName().Name; |
| | | resource.DynamicAssemblyName = dynamicAssemblyName; |
| | | } |
| | | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 动态控制器热拔 |
| | | /// </summary> |
| | | /// <param name="resource"></param> |
| | | /// <param name="provider"></param> |
| | | public static void DynamicControllerHotPluck(Resource resource, IDynamicApiRuntimeChangeProvider provider = null) |
| | | { |
| | | provider = provider ?? App.GetRequiredService<IDynamicApiRuntimeChangeProvider>(); |
| | | provider.RemoveAssembliesWithNotifyChanges(resource.DynamicAssemblyName); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取C#友好名称 |
| | | /// </summary> |
| | | /// <param name="type"></param> |
| | | /// <returns></returns> |
| | | private static string GetCSharpFriendlyName(this Type type) |
| | | { |
| | | // 处理可空类型 |
| | | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) |
| | | { |
| | | Type underlyingType = Nullable.GetUnderlyingType(type)!; |
| | | return $"{GetCSharpFriendlyName(underlyingType)}?"; |
| | | } |
| | | |
| | | // 处理基础类型 |
| | | if (type.FullName.IsNotNull()) |
| | | { |
| | | var baseTypes = new Dictionary<string, string> |
| | | { |
| | | { "System.Byte", "byte" }, |
| | | { "System.SByte", "sbyte" }, |
| | | { "System.Int16", "short" }, |
| | | { "System.UInt16", "ushort" }, |
| | | { "System.Int32", "int" }, |
| | | { "System.UInt32", "uint" }, |
| | | { "System.Int64", "long" }, |
| | | { "System.UInt64", "ulong" }, |
| | | { "System.Single", "float" }, |
| | | { "System.Double", "double" }, |
| | | { "System.Decimal", "decimal" }, |
| | | { "System.Char", "char" }, |
| | | { "System.Boolean", "bool" }, |
| | | { "System.String", "string" }, |
| | | { "System.Object", "object" } |
| | | }; |
| | | if (baseTypes.TryGetValue(type.FullName, out string? friendlyName) && friendlyName.IsNotNull()) |
| | | { |
| | | return friendlyName; |
| | | } |
| | | } |
| | | |
| | | // 处理非泛型类型 |
| | | if (!type.IsGenericType) |
| | | { |
| | | return type.Name; |
| | | } |
| | | |
| | | // 处理泛型类型 |
| | | string genericTypeName = type.GetGenericTypeDefinition().Name; |
| | | if (genericTypeName.Contains('`')) |
| | | genericTypeName = genericTypeName.Substring(0, genericTypeName.IndexOf('`')); |
| | | |
| | | string[] genericArgs = type.GetGenericArguments() |
| | | .Select(GetCSharpFriendlyName) |
| | | .ToArray(); |
| | | |
| | | return $"{genericTypeName}<{string.Join(", ", genericArgs)}>"; |
| | | } |
| | | } |
| | | } |