Introduce
The concept of middleware has been around since ASP.NET MVC (before .NET Core) and OWIN . Basically, a middleware component in a pipeline processes requests and acts as a series of jobs, and is entrusted with any subsequent componennt middleware registered in the pipeline. after it. The image below (taken from the Microsoft website) indicates this.
MVC itself is implemented (implemented) as a middleware component, such as redirection, exception handling, buffering, etc.
A middleware component can be added in several ways, but in ASP.NET Core, it’s all through the Use
method in IApplicationBuilder
. Many API-specific methods rely on it to add middleware components.
For now, we will use the IMiddleware
interface that comes with ASP.NET Core. It provides a simple contract with no dependencies other than HTTP abstractions.
A common request can download and inject middleware components automatically into the pipeline. Let’s see how we can do that.
Managed Extensibility Framework
The .NET Core has a Managed Extensibility Framework (MEF), which you can find out in my previous article Using MEF in .NET Core . MEF proposes an API that can be used to find and initialize plugins from assemblies, which makes it an interesting candidate for discovering and installing middleware components.
We will use the System.Composition
Nuget package. As in the previous article, we will loop through all assemblies in the provided paths (usually the bin directory of ASP.NET Core) and try to find all implementations of interfaces. We will then register them all with MEF configuration.
Deployment
Our interface will be called IPlugin
and it is actually inherited from IMiddleware
. If we wish, we can add more members to it, now it doesn’t really matter:
1 2 3 4 | <span class="token keyword">public</span> <span class="token keyword">interface</span> <span class="token class-name">IPlugin</span> <span class="token punctuation">:</span> <span class="token class-name">IMiddleware</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span> |
IMiddleware
proposes an InvonkeAsync
method that can be called asynchronous and holds the current context and a pointer to the next delegate (or middleware component).
I wrote the extension method below for IApplicationBuilder
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">ApplicationBuilderExtensions</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token class-name">IApplicationBuilder</span> <span class="token function">UsePlugins</span> <span class="token punctuation">(</span> <span class="token keyword">this</span> <span class="token class-name">IApplicationBuilder</span> app <span class="token punctuation">,</span> <span class="token keyword">string</span> path <span class="token operator">=</span> <span class="token keyword">null</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> conventions <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ConventionBuilder</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> conventions <span class="token punctuation">.</span> <span class="token generic-method"><span class="token function">ForTypesDerivedFrom</span> <span class="token punctuation"><</span> <span class="token class-name">IPlugin</span> <span class="token punctuation">></span></span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token generic-method"><span class="token function">Export</span> <span class="token punctuation"><</span> <span class="token class-name">IPlugin</span> <span class="token punctuation">></span></span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">Shared</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> path <span class="token operator">=</span> path <span class="token operator">?</span> <span class="token operator">?</span> AppContext <span class="token punctuation">.</span> BaseDirectory <span class="token punctuation">;</span> <span class="token keyword">var</span> configuration <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ContainerConfiguration</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">WithAssembliesInPath</span> <span class="token punctuation">(</span> path <span class="token punctuation">,</span> conventions <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">using</span> <span class="token punctuation">(</span> <span class="token keyword">var</span> container <span class="token operator">=</span> configuration <span class="token punctuation">.</span> <span class="token function">CreateContainer</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> plugins <span class="token operator">=</span> container <span class="token punctuation">.</span> <span class="token generic-method"><span class="token function">GetExports</span> <span class="token punctuation"><</span> <span class="token class-name">IPlugin</span> <span class="token punctuation">></span></span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">OrderBy</span> <span class="token punctuation">(</span> p <span class="token operator">=</span> <span class="token operator">></span> p <span class="token punctuation">.</span> <span class="token function">GetType</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token generic-method"><span class="token function">GetCustomAttributes</span> <span class="token punctuation"><</span> <span class="token class-name">ExportMetadataAttribute</span> <span class="token punctuation">></span></span> <span class="token punctuation">(</span> <span class="token keyword">true</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">SingleOrDefault</span> <span class="token punctuation">(</span> x <span class="token operator">=</span> <span class="token operator">></span> x <span class="token punctuation">.</span> Name <span class="token operator">==</span> <span class="token string">"Order"</span> <span class="token punctuation">)</span> <span class="token operator">?</span> <span class="token punctuation">.</span> <span class="token class-name">Value</span> <span class="token keyword">as</span> IComparable <span class="token operator">?</span> <span class="token operator">?</span> <span class="token keyword">int</span> <span class="token punctuation">.</span> MaxValue <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">foreach</span> <span class="token punctuation">(</span> <span class="token keyword">var</span> plugin <span class="token keyword">in</span> plugins <span class="token punctuation">)</span> <span class="token punctuation">{</span> app <span class="token punctuation">.</span> <span class="token function">Use</span> <span class="token punctuation">(</span> <span class="token keyword">async</span> <span class="token punctuation">(</span> ctx <span class="token punctuation">,</span> next <span class="token punctuation">)</span> <span class="token operator">=</span> <span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">await</span> plugin <span class="token punctuation">.</span> <span class="token function">InvokeAsync</span> <span class="token punctuation">(</span> ctx <span class="token punctuation">,</span> <span class="token keyword">null</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">await</span> <span class="token function">next</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> app <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
We have defined a convention
in which each type is found to implement Iplugin
we will register as a share, meaning a singleton
form.
As you can see, if the path parameter is not provided, it will default to AppContext.BaseDirectory
.
We can add in the plugin / middleware implementation an ExportMetadataAttribute
with an Order value specifying the order by which the plugins will load, I’ll explain in detail in a moment.
With the extionsion WithAssembliesInPath
method from my previous post but I’ll add it here for you to trust:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">ContainerConfigurationExtensions</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token class-name">ContainerConfiguration</span> <span class="token function">WithAssembliesInPath</span> <span class="token punctuation">(</span> <span class="token keyword">this</span> <span class="token class-name">ContainerConfiguration</span> configuration <span class="token punctuation">,</span> <span class="token keyword">string</span> path <span class="token punctuation">,</span> <span class="token class-name">SearchOption</span> searchOption <span class="token operator">=</span> SearchOption <span class="token punctuation">.</span> TopDirectoryOnly <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">WithAssembliesInPath</span> <span class="token punctuation">(</span> configuration <span class="token punctuation">,</span> path <span class="token punctuation">,</span> <span class="token keyword">null</span> <span class="token punctuation">,</span> searchOption <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token class-name">ContainerConfiguration</span> <span class="token function">WithAssembliesInPath</span> <span class="token punctuation">(</span> <span class="token keyword">this</span> <span class="token class-name">ContainerConfiguration</span> configuration <span class="token punctuation">,</span> <span class="token keyword">string</span> path <span class="token punctuation">,</span> <span class="token class-name">AttributedModelProvider</span> conventions <span class="token punctuation">,</span> <span class="token class-name">SearchOption</span> searchOption <span class="token operator">=</span> SearchOption <span class="token punctuation">.</span> TopDirectoryOnly <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> assemblyFiles <span class="token operator">=</span> Directory <span class="token punctuation">.</span> <span class="token function">GetFiles</span> <span class="token punctuation">(</span> path <span class="token punctuation">,</span> <span class="token string">"*.dll"</span> <span class="token punctuation">,</span> searchOption <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">var</span> assemblies <span class="token operator">=</span> assemblyFiles <span class="token punctuation">.</span> <span class="token function">Select</span> <span class="token punctuation">(</span> AssemblyLoadContext <span class="token punctuation">.</span> Default <span class="token punctuation">.</span> LoadFromAssemblyPath <span class="token punctuation">)</span> <span class="token punctuation">;</span> configuration <span class="token operator">=</span> configuration <span class="token punctuation">.</span> <span class="token function">WithAssemblies</span> <span class="token punctuation">(</span> assemblies <span class="token punctuation">,</span> conventions <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">return</span> configuration <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
If you want to find all assemblies in nested directories, you need to pass SearchOption.AllDirectories
as the searchOption
parameter, but of course this will have a trade-off if you have multiple subdirectories.
Putting it all together
As such, write a few classes that implement the Iplugin
interface and are therefore appropriate to use middleware components:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token punctuation">[</span> <span class="token class-name">Export</span> <span class="token punctuation">(</span> <span class="token keyword">typeof</span> <span class="token punctuation">(</span> IPlugin <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">]</span> <span class="token punctuation">[</span> <span class="token class-name">ExportMetadata</span> <span class="token punctuation">(</span> <span class="token string">"Order"</span> <span class="token punctuation">,</span> <span class="token number">1</span> <span class="token punctuation">)</span> <span class="token punctuation">]</span> <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">MyPlugin1</span> <span class="token punctuation">:</span> <span class="token class-name">IPlugin</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">async</span> <span class="token class-name">Task</span> <span class="token function">InvokeAsync</span> <span class="token punctuation">(</span> <span class="token class-name">HttpContext</span> context <span class="token punctuation">,</span> <span class="token class-name">RequestDelegate</span> next <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">//do something here</span> <span class="token comment">//this is needed because this can be the last middleware in the pipeline (next = null)</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> next <span class="token operator">!=</span> <span class="token keyword">null</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">await</span> <span class="token function">next</span> <span class="token punctuation">(</span> context <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">//do something here </span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Notice how we apply an ExportMetadataAttribute
to a class with an Order value; This is not necessary and if not provided, it will default to the maximum value of type integer ( int.MaxValue
), meaning it will be loaded after all other plugins. But this class needs to be public and has a public parameters contructor. You can get any of the registered services from the RequestServices
property of the HttpContext
.
Now, all we need to do is add a pair of assemblies to the bin directory of the web application (or some other path that is passed to UsePlugins
) and call this extension method inside Configue
:
1 2 3 4 5 6 7 8 9 | <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">Configure</span> <span class="token punctuation">(</span> <span class="token class-name">IApplicationBuilder</span> app <span class="token punctuation">,</span> <span class="token class-name">IHostingEnvironment</span> env <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">//rest goes here</span> app <span class="token punctuation">.</span> <span class="token function">UsePlugins</span> <span class="token punctuation">(</span> <span class="token comment">/*path: “some path”*/</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token comment">//rest goes here</span> <span class="token punctuation">}</span> |
And here you have it: ASP.NET Core will find middleware from any assemblies it can find on the path provided.
Conclusion
Middleware is not a new concept, it has been in previous ASP.NET MVC versions, as well as other web programming languages. Here I would like to introduce to us how to register middleware through assemplies (dll files) in ASP.NET Core. This method is fully loaded dynamically with the support of ASP.NET Core. It is very suitable when we attach external plugins to our application. Hope to help you in need of it.
The article is translated from the source:
https://weblogs.asp.net/ricardoperes/dynamically-loading-middleware-in-asp-net-core