.net core

Exploring the new Assembly unloading feature in .NET Core 3.0 by building a simple plugin system running on ASP.NET Core Blazor

A Primer

.NET Core 3.0 has brought a swath of exciting new features, some of which I have blogged about before, such as Blazor (if you haven't already check out my course here 'Zero to Azure Hero with ASP.NET Core, Blazor, Azure DevOps, Cognitive Services, SonarCloud and App Services' here). One feature I have been looking forward to for some time however, is the ability to unload assemblies. This is particularly useful in plugin based architectures where you may want to push a new version of a plugin and have it reload without needing to restart the full application.

.NET Core 3.0 and later versions support assembly unloading through the AssemblyLoadContext . You can load a set of assemblies into a collectible AssemblyLoadContext , execute methods in them or just inspect them using reflection, and finally unload the AssemblyLoadContext and contained assemblies.

Assembly unloading was available in the .Net Framework, however was part of the AppDomain functionality and was not ported to .Net Core. A noteworthy difference between the two implementations is how the assemblies are unloaded, in AppDomains this was forced, however in the new implementation it is 'cooperative' and unloading will only occur when several conditions are met such as assemblies in the load context having no  references outside of the AssemblyLoadContext itself. You can find more on these conditions and on unloading from the official documentation here.

In this blog post I walk through creating a basic plugin system that supports loading and unloading of plugins/assemblies and the ability to author and deploy a plugin directly from the browser. This blog post was inspired by the following post from StrathWeb (https://www.strathweb.com/2019/01/collectible-assemblies-in-net-core-3-0/) so please check that out too. I have also included the link at the bottom of this post.

I have used Blazor as my UI in this case as its another feature of the new .NET Core 3.0 release I have been looking forward too, however my implementation (and this post) is purely POC for exploring some of these new features, and is not a post on plugin architecture. A lot of work would be needed to move this towards anything close to production, especially from a security perspective so please be warned.

With that said..lets get cracking! The final version of this demo code can be found on my GitHub repo here https://github.com/stevenknox/PluginsDemo

You can also watch a quick clip of the final demo in action here

Create our project structure and basic plugins

Lets build out our project structure and initial plugins. First create our solution and navigate into that folder

dotnet new sln -o PluginApp

cd PluginApp

Next create 2 c# class libraries for our plugins and our Blazor web app

dotnet new classlib -o Plugins/HelloWorldPlugin
dotnet new classlib -o Plugins/WhatsTheTimePlugin
dotnet new blazorserver -o WebApp

Add our new projects to our solution

dotnet sln add Plugins/HelloWorldPlugin
dotnet sln add Plugins/WhatsTheTimePlugin
dotnet sln add WebApp/WebApp.App
dotnet sln add WebApp/WebApp.Server

Next lets create a folder in our WebApp that will contain the compiled plugins

mkdir WebApp/Plugins

and finally lets build our solution to make sure everything has bootstrapped correctly

dotnet build

If everything looks good, lets open VSCode

code .

First thing we want to do is update our plugin projects to deploy to the Plugins folder we created under the WebApp folder.

Open the HelloWorldPlugin.csproj and WhatsTheTimePlugin.csproj and update to incude a target element to copy the compiled output to the plugins directory and a PropertyGroup element for the CopyDestionationPath. Your project file should look like

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
     <CopyDestionationPath>..\..\WebApp\Plugins</CopyDestionationPath>
  </PropertyGroup>

 <Target Name="CopyOutputToDestination" AfterTargets="AfterBuild">

    <ItemGroup>
      <OutputFiles Include="$(OutDir)**\*"></OutputFiles>
    </ItemGroup>

    <Message Text="Copying output file to destination: @(OutputFiles)" Importance="high" />

    <Copy SourceFiles="@(OutputFiles)" DestinationFolder="$(CopyDestionationPath)\$(ProjectName)\%(RecursiveDir)" OverwriteReadOnlyFiles="true"></Copy>

  </Target>
</Project>

Now lets add our basic plugin code, create a new class in the root of the HelloWorldPlugin folder called Plugin.cs with the following code

namespace PluginSystem
{
    public class Plugin
    {
        public string Execute(string input)
        {
            return $"Hello World! (Request {input})";
        }
    }
}

This is a very simple plugin that just contains an Execute method that returns a Hello World string with a request number.

Create another Plugin.cs file, this time in the WhatsTheTime plugin folder with the following

using System;

namespace PluginSystem
{
    public class Plugin
    {
        public string Execute(string input)
        {
            return $"The time is {DateTime.Now.ToString("hh:mm:ss")} (Request {input})";
        }
    }
}

Again, a very simple plugin, this time displaying the time, along with the current request number.

Create our PluginService with Assembly Unloading functionality

We now need to create a service in our WebApp to find and load the Plugins.

Create a folder in the root of the WebApp called PluginSystem and a new c# class called CollectibleAssemblieContext.cs with the following:

using System.Reflection;
using System.Runtime.Loader;

namespace WebApp.PluginSystem
{
    public class CollectibleAssemblyContext : AssemblyLoadContext
    {
        public CollectibleAssemblyContext() : base(isCollectible: true)
        {
        }

        protected override Assembly Load(AssemblyName assemblyName)
        {
            return null;
        }
    }
}

The above class derives from the AssemblyLoadContext however note we are passing in true for the isCollectible variable in the base class. This is what allows us to unload our assemblies.

Now lets create our PluginService class in the same folder, this is where most of the work will occur.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace WebApp.PluginSystem
{
    public class PluginService
    {
        private Assembly SystemRuntime = Assembly.Load(new AssemblyName("System.Runtime"));
        public Dictionary<string, List<string>> PluginResponses { get; private set; } = new Dictionary<string, List<string>>();
        public List<HostedPlugin> Plugins { get; set; } = new List<HostedPlugin>();

        public void LoadPlugins()
        {
            var assembliesPath = Path.Combine(Directory.GetCurrentDirectory(), "Plugins");

            foreach (var pluginfolder in Directory.EnumerateDirectories(assembliesPath))
            {
                var pluginName = Path.GetFileName(pluginfolder);
                if (Plugins.FirstOrDefault(f => f.Name == pluginName) == null)
                {
                    Plugins.Add(new HostedPlugin
                    {
                        Name = pluginName,
                        FilePath = Path.Combine(pluginfolder, $"{pluginName}.dll"),
                    });
                }
            }
        }

        // put entire UnloadableAssemblyLoadContext in a method to avoid caller
        // holding on to the reference
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void ExecuteAssembly(HostedPlugin plugin, string input)
        {
            var context = new CollectibleAssemblyContext();
            var assemblyPath = Path.Combine(plugin.FilePath);
            using (var fs = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read))
            {
                var assembly = context.LoadFromStream(fs);

                var type = assembly.GetType("PluginSystem.Plugin");
                var executeMethod = type.GetMethod("Execute");

                var instance = Activator.CreateInstance(type);

                var dic = PluginResponses.GetOrCreate(plugin.Name);

                dic.Add(executeMethod.Invoke(instance, new object[] { input }).ToString());
            }

            context.Unload();
        }


        public void RunPlugin(HostedPlugin plugin, string input)
        {
            ExecuteAssembly(plugin, input);

            RunGarbageCollection();
        }


        private static void RunGarbageCollection()
        {
            try
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }
            catch (System.Exception)
            {
                //sometimes GC.Collet/WaitForPendingFinalizers crashes, just ignore for this blog post
            }
        }
    }
}

Ok, so lets break down whats happening in the above. First of all we are finding any existing compiled assemblies from the Plugins folder and adding them to a collection of HostedPlugin types (we will create this model class below).

 public void LoadPlugins()
        {
            var assembliesPath = Path.Combine(Directory.GetCurrentDirectory(), "Plugins");

            foreach (var pluginfolder in Directory.EnumerateDirectories(assembliesPath))
            {
                var pluginName = Path.GetFileName(pluginfolder);
                if (Plugins.FirstOrDefault(f => f.Name == pluginName) == null)
                {
                    Plugins.Add(new HostedPlugin
                    {
                        Name = pluginName,
                        FilePath = Path.Combine(pluginfolder, $"{pluginName}.dll"),
                    });
                }
            }
        }

Next we add a method to allow us to execute the compiled assembly within our LoadContext and unload it after its been executed.

// put entire UnloadableAssemblyLoadContext in a method to avoid caller
        // holding on to the reference
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void ExecuteAssembly(HostedPlugin plugin, string input)
        {
            var context = new CollectibleAssemblyContext();
            var assemblyPath = Path.Combine(plugin.FilePath);
            using (var fs = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read))
            {
                var assembly = context.LoadFromStream(fs);

                var type = assembly.GetType("PluginSystem.Plugin");
                var executeMethod = type.GetMethod("Execute");

                var instance = Activator.CreateInstance(type);

                var dic = PluginResponses.GetOrCreate(plugin.Name);

                dic.Add(executeMethod.Invoke(instance, new object[] { input }).ToString());
            }

            context.Unload();
        }

Here we provide a HostedPlugin parameter that contains some information about the plugin, including the path to the plugin binary. We create an instance of our CollectibleAssemblyContext, then create a filestream using the FilePath property on the HostedPlugin and pass the stream to the 'LoadFromStream' method on the context. Our plugin assembly will now be loaded into our CollectibleAssemblyContext.

Now that our assembly is loaded, we can use Reflection to find our Plugin type followed by finding the 'Execute' method on the type. Again using reflection, we can create an instance of that type and finally invoke that method.

We capture the the result of the executed method and add it to the PluginResponses dictionary, this will be used to display the output in our web app.

*Note in this case i have used a well-known Namespace and Class name for my plugins to make this example a bit easier. In a real world you would have a more robust plugin architecture in place that would support plugin discovery and execution. You can find more information build plugins for .NET Core here

https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support

Finally we added a helper method that can be called from our WebApp to load and execute our plugin. Once the plugin has been ran, it will the force the garbage collector to clean up resources. I have wrapped this call in a Try/Catch as it sometimes crashes however for the scope of this post, the resulting behaviour works ok.

      public void RunPlugin(HostedPlugin plugin, string input)
      {
          ExecuteAssembly(plugin, input);

          RunGarbageCollection();
      }

We have two additional classes when need to create before we can move onto the Blazor app, first our HostedPlugin:

namespace WebApp.PluginSystem
{
   public class HostedPlugin
   {
       public string Name { get; set; }
       public string FilePath { get; set; }
   }
}

We also need to create an extension method to allow us to 'GetOrCreate' our PluginResonses dictionary item.

using System.Collections.Generic;

namespace WebApp.PluginSystem
{
    public static class Extensions
    {
        public static TValue GetOrCreate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key) 
            where TValue : new()
        {
            TValue val;

            if (!dict.TryGetValue(key, out val))
            {
                val = new TValue();
                dict.Add(key, val);
            }

            return val;
        }
    }
}

That should be us ready to start building out our Blazor WebApp functionality. Return to your command line at the root of the solution and ensure everything builds

dotnet build

Add plugin support to our Blazor WebApp

Ok now that we have our Plugins and PluginService ready, the first thing we need to do now is register our service in our Blazor app. Open your startup.cs file and add the following line under the ConfigureServices method

 services.AddSingleton<PluginService>();

You will also need to add the following using statement to the top of the startup class

using WebApp.PluginSystem;

Next lets create our blazor page; under the pages folder, create a new page 'Plugins.razor' with the following content


@page "/plugins"

@inject PluginService service

<h1>Plugins</h1>

<div class="row">
  <div class="col-md-9">
        @foreach (var plugin in plugins)
        {
            <div class="card plugin ml-1">
                <h5 class="card-header">
                    @plugin.Name
                    <button @[email protected](() => LoadPlugin(@plugin)) class="float-right"><i class="oi oi-reload"></i></button>
                </h5>
                <div class="card-body plugin-body">
                    <p class="card-text">
                        <ul>
                            @if (service.PluginResponses.TryGetValue(@plugin.Name, out List<string> responses))
                            {
                                @foreach (var response in responses)
                                {
                                    <li> @response</li>
                                }
                            }
                        </ul>
                    </p>
                </div>
            </div>
        }
    </div>
 </div>


@code {
    int iteration = 0;
    List<HostedPlugin> plugins = new List<HostedPlugin>();
    
    protected override void OnInitialized()
    {
        service.LoadPlugins();
        plugins = service.Plugins;
    }

    private void LoadPlugin(HostedPlugin currentPlugin)
    {
        iteration++;
        service.RunPlugin(currentPlugin, iteration.ToString());
    }

}

The above page loads our Plugins using the PluginService within the OnInitialized() lifecycle method and the razor foreach loops over each plugin rendering the name, any responses that have been stored in the PluginResponses dictionary for that plugin and a button that allows us to reload that plugin.

Before this will build and run we need to update our 'Imports.razor' file to include the following:

@using WebApp.PluginSystem

And we can also update our 'NavMenu.razor' file to include our plugins page in the menu

 <li class="nav-item px-3">
    <NavLink class="nav-link" href="plugins">
        <span class="oi oi-bolt" aria-hidden="true"></span> Plugins
    </NavLink>
</li>

Finally add the following to the bottom of site.css found under `wwwroot/css'

  .plugin-body {
        min-height: 300px;
    }
    .plugin {
        width: 390px;
        float: left;
    }
     .full-height {
        height: 70vh;
    }

That should be us ready to test our app. From the command line, navigate into the WebApp folder and run your application

cd WebApp
dotnet run

Your application should now be available (usually under the default port https://localhost:5001) to test. Try clicking the reload button against our plugins to see some responses

blog-plugin1

Ok, lets go and make a change to our HelloWorldPlugin source code, build the plugin and see if that will hot reload in our WebApp. Leave the WebApp running from your command line and open a new command line shell, navigating to the HelloWorldPlugin folder. Next return to VSCode and open Plugin.cs under the HelloWorldPlugin project and update to:

 return $"Hello World! This is a fully recompiled version !! (Request {input})";

From your newly opened shell, run

dotnet build

You should see something like the following in your build output showing build output was copied to the plugins folder under WebApp

blog-buildpng

If you now return to your browser and click the reload button in the HelloWorld plugin, you should see our updated message

blog-plugin2

Create and run a new Plugin

We can even create a completely new plugin and have it registered and displayed, return to the shell you just used to build HelloWorld and run the following:

cd ../
dotnet new classlib -o GitHubPlugin
cd GitHubPlugin
code .

This should create out new Plugin project and launch VSCode ready for editing.

First open the 'GitHubPlugin.csproj' and update as follows (to enable copying build output to our Plugins directory

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
     <CopyDestionationPath>..\..\WebApp\Plugins</CopyDestionationPath>
  </PropertyGroup>

 <Target Name="CopyOutputToDestination" AfterTargets="AfterBuild">

    <ItemGroup>
      <OutputFiles Include="$(OutDir)**\*"></OutputFiles>
    </ItemGroup>

    <Message Text="Copying output file to destination: @(OutputFiles)" Importance="high" />

    <Copy SourceFiles="@(OutputFiles)" DestinationFolder="$(CopyDestionationPath)\$(ProjectName)\%(RecursiveDir)" OverwriteReadOnlyFiles="true"></Copy>

  </Target>
</Project>

Next create a 'Plugin.cs' class with the following:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.Serialization.Json;

namespace PluginSystem
{
    public class Plugin
    {
        private readonly HttpClient client = new HttpClient();
        public string Execute(string input)
        {
        
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(
                new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
            client.DefaultRequestHeaders.Add("User-Agent", "GitHub Repository Reporter");

            var serializer = new DataContractJsonSerializer(typeof(List<repo>));
            var streamTask = client.GetStreamAsync("https://api.github.com/users/stevenknox/repos").Result;
            
            var repositories = serializer.ReadObject(streamTask) as List<repo>;
            
            return string.Join(", ", repositories.Where(f=> f.fork == false)
                                                .Take(15)
                                                .OrderByDescending(o=> o.stargazers_count)
                                                .Select(s => s.name));
        }
    }
     public class repo
    {
        public string name;
        public int stargazers_count;
        public bool fork { get; set; }
    }
}


Save everything, then return to your command line and build the project. Once complete it should have copied its binaries to the plugin folder under WebApp. Now if you return to your browser, and refresh you should see your new plugin has loaded, and clicking the reload button for that plugin should display a comma-delimited list of GitHub repos.

blog-plugin4

Add dynamic plugin creation and deployment

The final part of this demo is allowing us to author a plugin from the browser, compile it on the fly using Roslyn then have it dynamically load and reload in the browser.

To use the Rosyln features, we need to add the 'Microsoft.CodeAnalysis.CSharp' package to our WebApp. Stop the running WebApp from the command line and run the following (you should still be in the WebApp project in the shell)

dotnet add package Microsoft.CodeAnalysis.CSharp

Next lets make a few changes to some existing classes. Open our HostedPlugin class in the WebApp project and update to include the InMemory and Code properties

namespace WebApp.PluginSystem
{
    public class HostedPlugin
    {
        public string Name { get; set; }
        public string FilePath { get; set; }
        public bool InMemory { get; set; }
        public string Code { get; set; }
    }
}

Next open our PluginService and update to the following, we have added some extra methods to enable loading and compiling dynamically generated plugins.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace WebApp.PluginSystem
{
    public class PluginService
    {
        private Assembly SystemRuntime = Assembly.Load(new AssemblyName("System.Runtime"));
        public Dictionary<string, List<string>> PluginResponses { get; private set; } = new Dictionary<string, List<string>>();
        public List<HostedPlugin> Plugins { get; set; } = new List<HostedPlugin>();

        public void LoadPlugins()
        {
            var assembliesPath = Path.Combine(Directory.GetCurrentDirectory(), "Plugins");

            foreach (var pluginfolder in Directory.EnumerateDirectories(assembliesPath))
            {
                var pluginName = Path.GetFileName(pluginfolder);
                if (Plugins.FirstOrDefault(f => f.Name == pluginName) == null)
                {
                    Plugins.Add(new HostedPlugin
                    {
                        Name = pluginName,
                        FilePath = Path.Combine(pluginfolder, $"{pluginName}.dll"),
                    });
                }
            }
        }

        // put entire UnloadableAssemblyLoadContext in a method to avoid caller
        // holding on to the reference
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void ExecuteAssembly(HostedPlugin plugin, string input)
        {
            var context = new CollectibleAssemblyContext();
            var assemblyPath = Path.Combine(plugin.FilePath);
            using (var fs = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read))
            {
                var assembly = context.LoadFromStream(fs);

                var type = assembly.GetType("PluginSystem.Plugin");
                var executeMethod = type.GetMethod("Execute");

                var instance = Activator.CreateInstance(type);

                var dic = PluginResponses.GetOrCreate(plugin.Name);

                dic.Add(executeMethod.Invoke(instance, new object[] { input }).ToString());
            }

            context.Unload();
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private void ExecuteInMemoryAssembly(HostedPlugin plugin, string input)
        {
            var context = new CollectibleAssemblyContext();

            using (var ms = new MemoryStream())
            {
                var compilation = CSharpCompilation.Create(plugin.Name, new[] { CSharpSyntaxTree.ParseText(plugin.Code) },
                new[]
                {
                    MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
                    MetadataReference.CreateFromFile(typeof(Console).GetTypeInfo().Assembly.Location),
                    MetadataReference.CreateFromFile(SystemRuntime.Location),
                },
                new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

                var cr = compilation.Emit(ms);
                ms.Seek(0, SeekOrigin.Begin);
                var assembly = context.LoadFromStream(ms);

                var type = assembly.GetType("Plugin");
                var executeMethod = type.GetMethod("Execute");

                var instance = Activator.CreateInstance(type);

                var dic = PluginResponses.GetOrCreate(plugin.Name);

                dic.Add(executeMethod.Invoke(instance, new object[] { input }).ToString());
            }

            context.Unload();
        }

        public void RunPlugin(HostedPlugin plugin, string input)
        {
            if (plugin.InMemory)
                RunDynamicPlugin(plugin, input);
            else
                ExecuteAssembly(plugin, input);

            RunGarbageCollection();
        }

        public void CreateOrRunDynamicPlugin(string syntax, string input)
        {
            //if hosted assembly, just execute, else create
            var name = $"DynamicPlugin{input}";
            var plugin = Plugins.FirstOrDefault(f => f.Name == name);
            if (plugin == null)
            {

                plugin = new HostedPlugin();
                plugin.Name = name;
                plugin.Code = syntax;

            }

            Plugins.Add(plugin);

            ExecuteInMemoryAssembly(plugin, input);

            RunGarbageCollection();
        }
        public void RunDynamicPlugin(HostedPlugin plugin, string input)
        {
            ExecuteInMemoryAssembly(plugin, input);

            RunGarbageCollection();
        }


        private static void RunGarbageCollection()
        {
            try
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }
            catch (System.Exception)
            {
                //sometimes GC.Collet/WaitForPendingFinalizers crashes, just ignore for this blog post
            }
        }

    }
}

'ExecuteInMemoryAssembly' is the method worth noting in the above changes to the PluginService. Its similar to the ExecuteAssembly method, in that it adds an in memory assembly to our CollectibleAssemblyContext, however, rather then loading a compiled assembly from disk, it compiles c# code on-the-fly and creates the assembly in-memory, the main snippet below shows the compiltation and assembly generation steps


            using (var ms = new MemoryStream())
            {
                var compilation = CSharpCompilation.Create(plugin.Name, new[] { CSharpSyntaxTree.ParseText(plugin.Code) },
                new[]
                {
                    MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
                    MetadataReference.CreateFromFile(typeof(Console).GetTypeInfo().Assembly.Location),
                    MetadataReference.CreateFromFile(SystemRuntime.Location),
                },
                new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

                var cr = compilation.Emit(ms);
                ms.Seek(0, SeekOrigin.Begin);
                var assembly = context.LoadFromStream(ms);
    
                //omited for brevity
               
            }

            context.Unload();
        }

Finally we can update our Pages.razor page to add the plugin editor and loader.

@page "/plugins"

@inject PluginService service

<h1>Plugins</h1>

<div class="row">
<div class="col-md-9">
        @foreach (var plugin in plugins)
        {
            <div class="card plugin ml-1">
                <h5 class="card-header">
                    @plugin.Name
                    <button @[email protected](() => LoadPlugin(@plugin)) class="float-right"><i class="oi oi-reload"></i></button>
                </h5>
                <div class="card-body plugin-body">
                    <p class="card-text">
                        <ul>
                            @if (service.PluginResponses.TryGetValue(@plugin.Name, out List<string> responses))
                            {
                                @foreach (var response in responses)
                                {
                                    <li> @response</li>
                                }
                            }
                        </ul>
                    </p>
                </div>
            </div>
        }
    </div>
    	
    <div class="col-md-2">
                <div class="card text-white bg-dark plugin ml-1">
                    <h5 class="card-header">
                        Dynamic Plugin
                    </h5>
                    <div class="card-body plugin-body full-height">
                        <p class="card-text">
                            Enter C# code to create a plugin 'on-the-fly' (the method must return a string)<br />
                            <textarea @bind="@syntax" cols="40" rows="10" id="code" name="code"></textarea>
                            <ul>
                                @if (service.PluginResponses.TryGetValue("DynamicPlugin", out List<string> dynamicResponses))
                                {
                                    @foreach (var response in dynamicResponses)
                                    {
                                        <li> @response</li>
                                    }
                                }
                            </ul>
                        </p>
                    </div>
                    <div class="card-footer">
                        <button @onclick=LoadDynamicPlugin>Create Dynamic Plugin</button>
                    </div>
            </div>
    </div>
</div>

@code {
    int iteration = 0;
    List<HostedPlugin> plugins = new List<HostedPlugin>();
    public string syntax { get; set; } =  @"public class Plugin
        {
            public string Execute(string input)
            {
               return ""Hello from a Dynamic Plugin!"";
            }
        }";

    protected override void OnInitialized()
    {
        service.LoadPlugins();
        plugins = service.Plugins;
    }

    private void LoadPlugin(HostedPlugin currentPlugin)
    {
        iteration++;
        service.RunPlugin(currentPlugin, iteration.ToString());
    }

    private void LoadDynamicPlugin()
    {
        iteration++;
        service.CreateOrRunDynamicPlugin(syntax, iteration.ToString());
    }

}

Now if you stop your WebApp from running in the command line, build, then run again, you should see the plugin author section on the plugins page
.
dynamic1

Click 'Create Dynamic Plugin' at the bottom to run the default dynamic plugin. It should load the plugin with the default content. To do something a bit more exciting, try adding the following into the editor and clicking 'Create Dynamic Plugin' again

public class Plugin
{
    public string Execute(string input)
    {
       var x = 100 * 100;
       return $"100 * 100 = {x}"; 
    }
}

You should see the following output

dynamic2

Wrapping up and further reading

As I have mentioned several times in the article this is a very simple approach to explore some technologies, and is not a post on plugin architecture in a web application. If you want to explore more real world plugin scenario's on Asp.Net Core check out some of the posts and GitHub repos below.

I hope you have enjoyed exploring some of these technologies in this post and drop me a comment below if you have any comments, queries or feedback.

Blogs posts and github repos for further reading:

https://github.com/natemcmaster/DotNetCorePlugins/

https://github.com/dotnet/samples/tree/master/core/extensions/AppWithPlugin

https://www.strathweb.com/2019/01/collectible-assemblies-in-net-core-3-0/

https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support