| | |
| | | using Furion.DynamicApiController; |
| | | using Furion.FriendlyException; |
| | | using Furion.HttpRemote; |
| | | using Furion.Logging; |
| | | using Furion.Logging.Extensions; |
| | | using Mapster; |
| | | using MediatR; |
| | | using Microsoft.AspNetCore.Mvc; |
| | |
| | | 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; |
| | |
| | | /// </summary> |
| | | public static class ResourceUtils |
| | | { |
| | | /// <summary> |
| | | /// 发送HTTP请求 |
| | | /// </summary> |
| | | /// <typeparam name="TRequest"></typeparam> |
| | | /// <typeparam name="TResponse"></typeparam> |
| | | /// <param name="request"></param> |
| | | /// <returns></returns> |
| | | public static async Task<TResponse> SendHttpAsync<TRequest, TResponse>(TRequest request) |
| | | where TRequest : class, new() |
| | | { |
| | | var requestTypeFullName = typeof(TRequest).FullName; |
| | | var resource = await Db.GetRepository<Resource>().AsQueryable().AsNoTracking() |
| | | .Where(it => !it.IsExpired && it.RequestTypeFullName == requestTypeFullName) |
| | | .FirstOrDefaultAsync(); |
| | | var domain = await GetHealthyServiceDomain(resource); |
| | | var httpRemoteService = App.GetRequiredService<IHttpRemoteService>(); |
| | | var token = App.HttpContext.Request.Headers["Authorization"].ToString(); |
| | | var refreshToken = App.HttpContext.Request.Headers["X-Authorization"].ToString(); |
| | | TResponse response; |
| | | switch (resource.Method) |
| | | { |
| | | case EnumResourceMethod.Get: |
| | | response = await httpRemoteService.GetAsAsync<TResponse>( |
| | | $"{domain}{resource.Route}", |
| | | it => |
| | | it.WithQueryParameters(request) |
| | | .AddAuthentication(new AuthenticationHeaderValue("Authorization", token)) |
| | | .AddAuthentication(new AuthenticationHeaderValue("X-Authorization", refreshToken))); |
| | | break; |
| | | case EnumResourceMethod.Post: |
| | | response = await httpRemoteService.PostAsAsync<TResponse>( |
| | | $"{domain}{resource.Route}", |
| | | it => |
| | | it.SetJsonContent(request) |
| | | .WithHeader("Authorization", token) |
| | | .WithHeader("X-Authorization", refreshToken)); |
| | | break; |
| | | case EnumResourceMethod.Put: |
| | | response = await httpRemoteService.PutAsAsync<TResponse>( |
| | | $"{domain}{resource.Route}", |
| | | it => |
| | | it.SetJsonContent(request) |
| | | .AddAuthentication(new AuthenticationHeaderValue("Authorization", token)) |
| | | .AddAuthentication(new AuthenticationHeaderValue("X-Authorization", refreshToken))); |
| | | break; |
| | | case EnumResourceMethod.Delete: |
| | | response = await httpRemoteService.DeleteAsAsync<TResponse>( |
| | | $"{domain}{resource.Route}", |
| | | it => |
| | | it.SetJsonContent(request) |
| | | .AddAuthentication(new AuthenticationHeaderValue("Authorization", token)) |
| | | .AddAuthentication(new AuthenticationHeaderValue("X-Authorization", refreshToken))); |
| | | break; |
| | | default: |
| | | throw Oops.Oh(EnumErrorCodeType.s400, $"不支持请求方式{resource.Method}"); |
| | | } |
| | | return response; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取健康服务域名 |
| | | /// </summary> |
| | | /// <param name="resource"></param> |
| | | /// <returns></returns> |
| | | public static async Task<string> GetHealthyServiceDomain(Resource resource) |
| | | { |
| | | var client = App.GetRequiredService<IConsulClient>(); |
| | | var queryResult = await client.Health.Service(resource.ServiceName, null, true); |
| | | if (queryResult.StatusCode != System.Net.HttpStatusCode.OK) |
| | | throw Oops.Oh(EnumErrorCodeType.s404, $"微服务{resource.Service}"); |
| | | |
| | | var domains = queryResult.Response |
| | | .Select(s => $"http://{s.Service.Address}:{s.Service.Port}") |
| | | .ToList(); |
| | | if (domains.IsNull()) |
| | | throw Oops.Oh(EnumErrorCodeType.s404, $"微服务{resource.Service}"); |
| | | // 轮询选择实例 |
| | | int randomIndex = new Random().Next(domains.Count); |
| | | return domains[randomIndex]; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 生成动态控制器 |
| | | /// </summary> |
| | |
| | | var model = new ResourceModel(); |
| | | model.TraceId = traceId; |
| | | model.ApplicationName = resourceService.ApplicationName; |
| | | model.Controller = controller; |
| | | model.ControllerName = resourceController.ControllerName; |
| | | 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.ServiceName = resourceController.Service.ToString(); |
| | | model.RouteArea = resourceService.RouteArea; |
| | | model.Route = $"/api/{resourceService.RouteArea ?? "main"}/{controller}/{model.ActionName}"; |
| | | model.Route = $"/api/{resourceService.RouteArea ?? "main"}/{model.ControllerName}/{model.ActionName}"; |
| | | model.Method = |
| | | request.BaseType?.IsGenericType == true && request.BaseType.GetGenericTypeDefinition() == typeof(PagedListQuery<,>) |
| | | resourceAttribute.Method != EnumResourceMethod.None |
| | | ? resourceAttribute.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> { "Delete", "Remove ", "Clear" }.Any(it => request.Name.StartsWith(it, StringComparison.OrdinalIgnoreCase)) |
| | | ? EnumResourceMethod.Delete |
| | | : EnumResourceMethod.Post; |
| | | model.FileUpload = resourceAttribute.FileUpload; |
| | | model.Code = requestXmlDoc?.Name; |
| | | model.Name = $"{model.ControllerSummary}-{model.ActionSummary}"; |
| | | model.AllowAnonymous = resourceAttribute.AllowAnonymous; |
| | |
| | | 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, traceId, rep); |
| | | var resources = await SaveResourcesAsync(models, rep); |
| | | |
| | | DynamicControllersHotPlug(resources, provider); |
| | | |
| | |
| | | /// <param name="traceId"></param> |
| | | /// <param name="rep"></param> |
| | | /// <returns></returns> |
| | | private static async Task<List<Resource>> SaveResourcesAsync(List<ResourceModel> models, string traceId, IRepository<Resource> rep = null) |
| | | private static async Task<List<Resource>> SaveResourcesAsync(List<ResourceModel> models, IRepository<Resource> rep = null) |
| | | { |
| | | rep = rep ?? Db.GetRepository<Resource>(); |
| | | var resources = await rep.AsQueryable() |
| | |
| | | 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(resource); |
| | | await rep.UpdateAsync(resource); |
| | | 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 expiredResources = resources.Where(it => it.TraceId != traceId).ToList(); |
| | | var expiredResources = resources.Where(it => !models.Any(m => m.Route == it.Route && m.Method == it.Method)).ToList(); |
| | | foreach (var expiredResource in expiredResources) |
| | | { |
| | | expiredResource.IsExpired = true; |
| | | await rep.UpdateAsync(expiredResource); |
| | | resources.Remove(expiredResource); |
| | | await rep.DeleteAsync(expiredResource); |
| | | } |
| | | |
| | | return resources.Where(it => !it.IsExpired).ToList(); |
| | |
| | | public static void DynamicControllersHotPlug(List<Resource> resources, IDynamicApiRuntimeChangeProvider provider = null) |
| | | { |
| | | provider = provider ?? App.GetRequiredService<IDynamicApiRuntimeChangeProvider>(); |
| | | foreach (var resource in resources) |
| | | var controllers = resources |
| | | .GroupBy(it => new |
| | | { |
| | | it.ApplicationName, |
| | | it.ControllerName, |
| | | it.ControllerSummary, |
| | | it.RouteArea |
| | | }) |
| | | .Select(it => new |
| | | { |
| | | it.Key, |
| | | Actions = it.ToList() |
| | | }) |
| | | .ToList(); |
| | | foreach (var controller in controllers) |
| | | { |
| | | var code = $@" |
| | | using FlexJobApi.Core; |
| | |
| | | using System.Collections.Generic; |
| | | using System.ComponentModel; |
| | | |
| | | namespace {resource.ApplicationName}.{resource.Controller}.{resource.ActionName}Request |
| | | |
| | | namespace {controller.Key.ApplicationName}.{controller.Key.ControllerName} |
| | | {{ |
| | | /// <summary> |
| | | /// {resource.ControllerSummary} |
| | | /// {controller.Key.ControllerSummary} |
| | | /// </summary> |
| | | [Route(""api/{resource.RouteArea}/[controller]"")] |
| | | public class {resource.Controller}AppService(IMediator mediator) : IDynamicApiController |
| | | [Route(""api/{controller.Key.RouteArea}/[controller]"")] |
| | | public class {controller.Key.ControllerName}AppService(IMediator mediator) : IDynamicApiController |
| | | {{ |
| | | private readonly IMediator mediator = mediator; |
| | | private readonly IMediator mediator = mediator;"; |
| | | |
| | | foreach (var action in controller.Actions) |
| | | { |
| | | var result = action.ResponseTypeName.IsNull() ? "Task" : $"Task<{action.ResponseTypeName}>"; |
| | | code += @$" |
| | | |
| | | /// <summary> |
| | | /// {resource.ActionSummary} |
| | | /// {action.ActionSummary} |
| | | /// </summary> |
| | | /// <param name=""request""></param> |
| | | /// <returns></returns>"; |
| | | if (resource.AllowAnonymous) |
| | | { |
| | | code += $@" |
| | | if (action.AllowAnonymous) |
| | | { |
| | | code += $@" |
| | | [AllowAnonymous]"; |
| | | } |
| | | code += $@" |
| | | [Http{resource.Method}] |
| | | public Task<{resource.ResponseTypeName}> {resource.ActionName}({resource.RequestTypeName} request) |
| | | } |
| | | code += $@" |
| | | [Http{action.Method}]"; |
| | | if (action.FileUpload) |
| | | { |
| | | code += @" |
| | | [Consumes(""multipart/form-data"")]"; |
| | | } |
| | | code += @$" |
| | | public {result} {action.ActionName}("; |
| | | if (action.FileUpload) |
| | | { |
| | | code += "[FromForm] "; |
| | | } |
| | | code += $@"{action.RequestTypeName} request) |
| | | {{ |
| | | return mediator.Send(request); |
| | | }} |
| | | }}"; |
| | | } |
| | | |
| | | }} |
| | | }} |
| | | code += @" |
| | | } |
| | | } |
| | | "; |
| | | var dynamicAssembly = App.CompileCSharpClassCode(code); |
| | | provider.AddAssembliesWithNotifyChanges(dynamicAssembly); |
| | | var dynamicAssemblyName = dynamicAssembly.GetName().Name; |
| | | resource.DynamicAssemblyName = dynamicAssemblyName; |
| | | try |
| | | { |
| | | var dynamicAssembly = App.CompileCSharpClassCode(code); |
| | | provider.AddAssembliesWithNotifyChanges(dynamicAssembly); |
| | | var dynamicAssemblyName = dynamicAssembly.GetName().Name; |
| | | foreach (var action in controller.Actions) |
| | | { |
| | | action.DynamicAssemblyName = dynamicAssemblyName; |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | code.LogError(ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | } |