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)