分析路由时动态添加 Asp .NET Core Controller

Posted

技术标签:

【中文标题】分析路由时动态添加 Asp .NET Core Controller【英文标题】:Adding Asp .NET Core Controller dynamically when analyzing route 【发布时间】:2021-06-04 13:02:15 【问题描述】:

总结:

假设我们正在调用 url http://localhost:5600/Student/All/ 我的目标是我的 Asp .NET Core Web Api 在调用该路由时即时编译一个学生控制器,然后使用该控制器来响应该请求。

详细说明:

我想在分析路由时编译一个 Asp .NET Core Controller。所以我使用 DynamicRouteValueTransfer 来分析路由并在需要时对其进行修改。

app.UseEndpoints(endpoints =>

    //...
    endpoints.MapDynamicControllerRoute<SearchValueTransformer>("controller/action/**params");
    //...

在 SearchValue Transfer(继承自 DynamicRouteValueTransfer)中我可以分析和重写路由。在那里,我正在编译一个带有 Get 方法的控制器,该方法返回一个学生列表(带有一个 OData - [EnableQuery] - 属性,因为最后我想为 OData 访问提供任何数据)。

然后我尝试将这个新编译的 StudentController 添加到要在该请求中使用的控制器中。但这行不通

我尝试保存在 ConfigureServices 启动时获得的 IMvcBuilder:

//inside ConfigureServices - MvcManager is my static class
MvcManager.Builder = services.AddMvc();

//in my static class
public static IMvcBuilder Builder  get; set; 

编译控制器后,我尝试通过调用添加该程序集

MvcManager.Builder.AddApplicationPart(assembly).AddControllersAsServices();

问题是我无法进入那个控制器。我发现了一些关于动态添加的文章,但在请求已经启动时没有人这样做。

【问题讨论】:

看看这个页面:strathweb.com/2018/04/generic-and-dynamically-generated-controllers-in-asp-net-core-mvc 是的,这个例子已经非常动态了。它们在运行时生成实体类。但对我来说,这还不够,因为我真的需要编译后的控制器。 让我们准确地说:我想在请求进入时生成控制器。在您共享的链接中,控制器是在启动时创建的。 @Dennis1679:您的链接帮助了我!我错过的缺少的想法是使用中间件添加控制器,而不是在链接上的启动时添加控制器。谢谢丹尼斯! 【参考方案1】:

经过一番搜索,我终于找到了根据传入的请求路径在运行时创建控制器的最佳解决方案。

这是解决方案 - 您必须使用中间件。在中间件中,您可以根据请求路由进行编译,并将其作为 AssemblyPart 添加到您当前的程序集中。例子: 请求 http://localhost:56002/Account/getdata 进来。您可以在中间件中获取“/Account/getdata”,并使用内部名称为getdata的get方法编译一个帐户控制器。中间件编译控制器,将其作为AssemblyPart添加到当前运行的程序集中,中间件完成后,控制器就可以直接使用了。

首先您必须添加一个中间件。在那里你可以

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    
    //...
        app.UseMiddleware<MyMiddleware>();

中间件可能如下所示:

public class MyMiddleware

    public MyMiddleware(RequestDelegate requestDel, ControllerGenerator generator)
    
        RequestDel = requestDel;
        Generator = generator;
    

    public RequestDelegate RequestDel  get; 
    public ControllerGenerator Generator  get; 

    public async Task Invoke(HttpContext context)
    
        if (context.Request.Path.HasValue)
        
            var queryParams = context.Request.Path.Value;
            var entityName = queryParams.Split("/").Where(s => !string.IsNullOrEmpty(s)).FirstOrDefault();
            var result = Generator.AppendController(entityName);
            Console.WriteLine(result + ", " + queryParams);
        

        await RequestDel.Invoke(context);
    

在我的例子中,控制器生成器看起来像这样。这里重要的是ApplicationPartManager,它可以用来将程序集(部件)添加到当前程序集。

public class ControllerGenerator

    private readonly ApplicationPartManager _partManager;
    private readonly IHostingEnvironment _hostingEnvironment;

    public ControllerGenerator(
        ApplicationPartManager partManager,
        IHostingEnvironment env)
    
        _partManager = partManager;
        _hostingEnvironment = env;
    

    public bool AppendController(string className)
    
        var generator = new WebApiGenerator();

        Assembly assembly = generator.Exists(className) ?
            generator.GetAssembly(className) : generator.CreateDll(className);

        if (assembly != null)
        
            _partManager.ApplicationParts.Add(new AssemblyPart(assembly));
            // Notify change
            MyActionDescriptorChangeProvider.Instance.HasChanged = true;
            MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
            return true;
        
        return false;
    

ActionDescriptorChangeProvider 用于通知框架某些内容已更改并且必须重新加载。

public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider

    public static MyActionDescriptorChangeProvider Instance  get;  = new MyActionDescriptorChangeProvider();

    public CancellationTokenSource TokenSource  get; private set; 

    public bool HasChanged  get; set; 

    public IChangeToken GetChangeToken()
    
        TokenSource = new CancellationTokenSource();
        return new CancellationChangeToken(TokenSource.Token);
    

别忘了在启动时注册那个动作描述符:

    public void ConfigureServices(IServiceCollection services)
    
        services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
        services.AddSingleton(MyActionDescriptorChangeProvider.Instance);

我的代码生成器只是创建了一个控制器和一个实体类。控制器返回一个启用 OData 的实体列表,其中包含三个属性。没什么特别的,只是在这里为想要尝试的人提供一个工作示例。

public class WebApiGenerator

    public WebApiGenerator()
    
    

    private static CSharpCompilation GenerateCode(string sourceCode, string className)
    
        var codeString = SourceText.From(sourceCode);
        var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp9);

        var parsedSyntaxTree = SyntaxFactory.ParseSyntaxTree(codeString, options);

        var references = new List<MetadataReference>
        
            MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
            MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
            MetadataReference.CreateFromFile(typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly.Location),
            MetadataReference.CreateFromFile(typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly.Location),
        ;

        var referencedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
        foreach (var referenced in referencedAssemblies)
        
            string location = null;
            try
            
                location = referenced.Location;
            
            catch
            
            
            if (location != null)
            
                references.Add(MetadataReference.CreateFromFile(location));
            
        

        string outputDll = className + ".dll";
        return CSharpCompilation.Create(outputDll,
            new[]  parsedSyntaxTree ,
            references: references,
            options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
                optimizationLevel: OptimizationLevel.Release,
                assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default));
    

    public bool Exists(string className)
    
        string outputDll = className + ".dll";
        return File.Exists(outputDll);
    

    public Assembly GetAssembly(string className)
    
        string outputDll = className + ".dll";
        return Assembly.LoadFrom(outputDll);
    

    public Assembly CreateDll(string className)
    
        className = className.Replace(" ", string.Empty);

        string outputDll = className + ".dll";
        if (File.Exists(outputDll)) return Assembly.LoadFrom(outputDll);

        var code = new StringBuilder()
            .AppendLine("using Microsoft.AspNetCore.Mvc;")
            .AppendLine("using Microsoft.Extensions.Logging;")
            .AppendLine("using System;")
            .AppendLine("using System.Collections.Generic;")
            .AppendLine("using System.Linq;")
            .AppendLine("using Microsoft.AspNet.OData;")
            .AppendLine("")
            .AppendLine("namespace ControllerLibrary")
            .AppendLine("")
            .AppendLine($"public class className")
            .AppendLine("")
            .AppendLine("   public string FirstProperty  get; set; ")
            .AppendLine("   public string SecondProperty  get; set; ")
            .AppendLine("   public int IntValue  get; set; ")
            .AppendLine("")
            .AppendLine($"[ApiController]")
            .AppendLine($"[Route(\"[controller]\")]")
            .AppendLine($"public class classNameController : ControllerBase")
            .AppendLine(" ")
            .AppendLine("  [HttpGet(\"GetData\")]")
            .AppendLine("  [EnableQuery]")
            .AppendLine($"  public IList<className> Get()")
            .AppendLine("  ")
            .AppendLine($"  var list = new List<className>();");
        for (int i = 0; i < 3; i++)
        
            code.AppendLine($"list.Add(new className  FirstProperty = \"First prop of className\", SecondProperty = \"asd\", IntValue = i );");
        
        code
            .AppendLine("  return list;")
            .AppendLine("  ")
            .AppendLine(" ")
            .AppendLine("");

        File.WriteAllText("code.txt", code.ToString());

        var result = GenerateCode(code.ToString(), className).Emit(outputDll);
        //CompilerResults results = codeProvider.CompileAssemblyFromSource(parameters, code.ToString());
        if (!result.Success)
        
            Console.WriteLine("Compilation done with error.");
            var failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error);

            foreach (var diagnostic in failures)
            
                Console.Error.WriteLine("0: 1", diagnostic.Id, diagnostic.GetMessage());
            
        
        else
        
            Console.WriteLine("Build Succeeded");
            return Assembly.LoadFrom(outputDll);
        

        return null;
    

【讨论】:

以上是关于分析路由时动态添加 Asp .NET Core Controller的主要内容,如果未能解决你的问题,请参考以下文章

当 url 的一部分已经是另一个路由时,ASP.NET Core 3.1 中的动态路由

ASP.Net Core 5.0 中的子域路由

ASP.NET Core API:添加自定义路由令牌解析器

ASP.NET Core MVC 中两种路由的简单配置

ASP.NET Core MVC 数据库重定向后不添加对象

asp.net core 系列 5 MVC框架路由(上)