ASP.NET Core : Add controllers at runtime and detecting changes done by others
Part 1 Adding controllers at runtime
Adding controllers at runtime in ASP.NET Core involves the ApplicationPartManager and IActionDescriptorChangeProvider. Let’s say that we hardcode the creation of the controller
private Assembly CreateController(string name) { string code = new StringBuilder() .AppendLine("using System;") .AppendLine("using Microsoft.AspNetCore.Mvc;") .AppendLine("namespace TestBlocklyHtml.Controllers") .AppendLine("{") .AppendLine("[Route(\"api/[controller]\")]") .AppendLine("[ApiController]") .AppendLine(string.Format("public class {0} : ControllerBase", name)) .AppendLine(" {") .AppendLine(" public string Get()") .AppendLine(" {") .AppendLine(string.Format("return \"test - {0}\";", name)) .AppendLine(" }") .AppendLine(" }") .AppendLine("}") .ToString(); var codeString = SourceText.From(code); var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3); var parsedSyntaxTree = SyntaxFactory.ParseSyntaxTree(codeString, options); var references = new 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), MetadataReference.CreateFromFile(typeof(RouteAttribute).Assembly.Location), MetadataReference.CreateFromFile(typeof(ApiControllerAttribute).Assembly.Location), MetadataReference.CreateFromFile(typeof(ControllerBase).Assembly.Location), }; var codeRun = CSharpCompilation.Create("Hello.dll", new[] { parsedSyntaxTree }, references: references, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release, assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); using (var peStream = new MemoryStream()) { if (!codeRun.Emit(peStream).Success) { return null; } return Assembly.Load(peStream.ToArray()); } }
Then we will load into ApplicationParts
[HttpGet] public string AddRuntimeController([FromServices] ApplicationPartManager partManager, [FromServices]MyActionDescriptorChangeProvider provider) { string name = "andrei" + DateTime.Now.ToString("yyyyMMddHHmmss"); var ass = CreateController(name); if (ass != null) { partManager.ApplicationParts.Add(new AssemblyPart(ass)); // Notify change provider.HasChanged = true; provider.TokenSource.Cancel(); return "api/"+ name; } throw new Exception("controller not generated"); }
and the code for is
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); } }
and it is added to the DI services by
services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance); services.AddSingleton(MyActionDescriptorChangeProvider.Instance);
Usually , you do not need Part 2 – if you do not construct something like Blockly for .NET Core (https://netcoreblockly.herokuapp.com/), that needs to see controllers added by the application
Part 2 Detecting controller added at runtime by others
You usually do not need this – Part 1 is enough. I need because I do construct something like Blockly for .NET Core (https://netcoreblockly.herokuapp.com/), that needs to see controllers added by the application.
So we need ActionDescriptorCollectionProvider – read the remarks at https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.infrastructure.actiondescriptorcollectionprovider?view=aspnetcore-3.1 . We can obtain by DI from IActionDescriptorCollectionProvider and convert to ActionDescriptorCollectionProvider
The code is
//in the class , obtained by DI from IActionDescriptorCollectionProvider private readonly ActionDescriptorCollectionProvider cp; internal void registerCallback() { cp.GetChangeToken().RegisterChangeCallback(a => { //a is your class because of this parameter below //do your work s.registerCallback(); }, this); }
( All code is taken from https://github.com/ignatandrei/netcoreblockly)
Leave a Reply