Introduction to Plugin Architecture in C#
Summary
TLDRIn this Rock Coding YouTube tutorial, Anton teaches viewers about plugin architecture in C#, focusing on extending applications at runtime. He explains how plugins work using examples like Visual Studio and WordPress, then dives into creating a plugin system for an ASP.NET Core application. Anton demonstrates building a server, a plugin development kit (PDK), and a custom middleware to load and unload plugins dynamically. He also addresses challenges like unloading plugins and avoiding memory leaks, providing practical insights into plugin development in C#.
Takeaways
- ๐ Plugin architecture in C# allows for extending applications at runtime, similar to how extensions work in Visual Studio, VS Code, or WordPress plugins.
- ๐ The presenter introduces a 'Plugin Development Kit (PDK)' which is a specialized SDK for creating plugins, including components like 'IPluginEndpoint' and 'PathAttribute'.
- ๐๏ธ The server project is designed to be extended with plugins, and it references the PDK to incorporate HTTP context functionalities.
- ๐ ๏ธ Custom middleware is created to handle plugin loading and execution, enabling dynamic addition of endpoints to an ASP.NET Core application.
- ๐ The process of loading and unloading plugins involves using reflection to instantiate types and execute methods based on the plugin's assembly.
- ๐งฉ The concept of 'Assembly Load Context' is highlighted as a way to manage the scope of assembly loading, allowing for the possibility of unloading assemblies.
- ๐๏ธ Unloading plugins is not immediate and requires the use of garbage collection to clear references and finalize the unloading process.
- ๐ The presenter demonstrates the use of a weak reference to an assembly load context to check if an assembly has been successfully unloaded.
- ๐ The importance of managing references carefully is emphasized to avoid preventing assemblies from being unloaded due to lingering references.
- ๐ซ The script mentions that certain operations, like serialization with the JSON serializer, can inadvertently leak references and affect the unloading process.
- ๐ง The video concludes with recommendations for plugin development, such as aiming for pure functions and being cautious of side effects that could impact the plugin lifecycle.
Q & A
What is plugin architecture in the context of C#?
-Plugin architecture in C# refers to the ability to extend an application at runtime, similar to how extensions or plugins add functionality to applications like Visual Studio or WordPress.
Why is plugin architecture useful in application development?
-Plugin architecture is useful because it allows developers to add new features and functionality to an application without modifying the core application code, providing flexibility and extensibility.
What is the role of a Plugin Development Kit (PDK) in creating plugins?
-A Plugin Development Kit (PDK) provides the necessary components and interfaces for developers to create plugins. It includes elements like 'IPluginEndpoint' and 'PathAttribute' to define entry points and routing for plugins.
How does the 'PathAttribute' in the PDK contribute to plugin development?
-'PathAttribute' in the PDK is used to specify the endpoint path for a plugin, allowing the server to recognize and route requests to the appropriate plugin functionality.
What is the purpose of the 'EndpointPDK' in the server project?
-The 'EndpointPDK' in the server project references the framework and provides the ability to include HTTP context within the plugin, enabling the plugin to interact with the server's HTTP requests and responses.
Can you explain the custom middleware mentioned in the script for handling plugins?
-The custom middleware, referred to as 'Plugin Middleware', is used to dynamically load and execute plugin code during runtime. It matches incoming requests to plugin endpoints and activates the corresponding plugin functionality.
What is the significance of unloading assemblies in a plugin architecture?
-Unloading assemblies is important for updating or removing plugins without restarting the entire application. It allows for dynamic changes to the application's functionality.
Why is it challenging to unload a plugin in a C# application?
-Unloading a plugin can be challenging due to reference leaks where parts of the plugin may still be referenced by the application, preventing the garbage collector from unloading the assembly.
What is the role of 'AssemblyLoadContext' in managing plugin assemblies?
-'AssemblyLoadContext' provides a way to load and unload assemblies independently from the main application domain. It allows for creating a separate scope for plugin assemblies, facilitating their dynamic loading and unloading.
How can you ensure that a plugin is properly unloaded from an application?
-To ensure a plugin is properly unloaded, you can use techniques like forcing garbage collection, waiting for finalizers to run, and using weak references to check if the assembly has been collected.
What are some potential issues with using the 'System.Text.Json' serializer in plugins?
-The 'System.Text.Json' serializer can cause reference leaks because it caches the types it serializes, which can prevent the plugin assembly from being unloaded if the serializer holds references to plugin types.
Outlines
๐ Introduction to Plugin Architecture in C#
Anton, the host of the Rock Coding YouTube channel, introduces the topic of plugin architecture in C#. He explains that plugins allow for extending applications at runtime, using examples like Visual Studio, VS Code, and WordPress. He also touches on cloud functions as a form of plugin architecture. Anton invites viewers to like, subscribe, and ask questions in the comments. He promotes a C# course and outlines the structure of the video, which includes three projects: a server, an endpoint PDK (Plugin Development Kit), and an example endpoint. The server is the extendable application, while the PDK provides the necessary components for plugin development, such as an entry point and a path attribute for adding endpoints to an ASP.NET Core application.
๐ ๏ธ Building a Custom Middleware for Plugins
The video script delves into the technical process of creating custom middleware to dynamically add endpoints to a server application. Anton discusses the limitations of ASP.NET Core's default behavior, which does not allow for adding endpoints post-startup. To overcome this, he demonstrates how to write a middleware that can load and execute plugin endpoints at runtime. This involves creating a 'Plugin Middleware' class, defining a request delegate, and implementing a method to match HTTP requests with plugin endpoints. The middleware is designed to intercept requests to specific paths and execute the corresponding plugin logic, showcasing a practical example with a 'test endpoint'.
๐ Dynamically Loading and Unloading Assemblies
Anton explains the process of dynamically loading and unloading plugin assemblies in a C# application. He details the use of reflection to load assemblies from a specified path and to instantiate types that have been marked with a 'Path Attribute'. The script covers the technical steps to activate a plugin by creating an instance of the plugin type and executing it with the HTTP context. It also touches on the challenges of unloading assemblies, explaining that the 'AssemblyLoadContext' can be used to manage the lifecycle of dynamically loaded assemblies and enable their unloading by marking them as collectible.
๐ซ Challenges with Unloading Plugins
The script discusses the difficulties associated with unloading plugins in C#. It explains that unloading is not immediate and is subject to garbage collection. Anton demonstrates how to force garbage collection to ensure that unloaded assemblies are removed from memory. He also highlights potential issues with references that can prevent assemblies from being unloaded, such as the use of the 'JsonSerializer' which caches types and can inadvertently hold references to plugin code. The video emphasizes the importance of managing references carefully to ensure successful unloading of plugins.
๐ง Best Practices for Plugin Development
In the final paragraph, Anton provides recommendations for developing plugins in C#. He suggests aiming for pure functions with no side effects to minimize the risk of leaking references and to ensure that plugins can be safely unloaded. The script wraps up with a reminder that the demonstrated solution is not production-ready and outlines further considerations for a complete plugin system, such as using a file watcher to automatically detect changes in the plugins folder and handle the loading and unloading of plugins. Anton concludes by thanking viewers for their support and inviting them to engage with the content through likes, subscriptions, and Patreon support.
Mindmap
Keywords
๐กPlugin Architecture
๐กC#
๐กVisual Studio
๐กASP.NET Core
๐กEndpoint
๐กPath Attribute
๐กMiddleware
๐กAssembly
๐กReflection
๐กGarbage Collection
๐กFinalizer
๐กSystem.Text.Json
Highlights
Introduction to plugin architecture in C#, allowing applications to be extended at runtime.
Explanation of how plugins enhance functionality in applications like Visual Studio, VS Code, and WordPress.
The concept of cloud functions as a form of plugin architecture, extending cloud capabilities.
The importance of leaving likes and subscribing to the channel for support and updates.
Overview of the three projects involved in the plugin architecture: server, endpoint PDK, and test endpoint.
Description of the Plugin Development Kit (PDK) and its components for marking plugin entry points.
Details on the server project, which serves as the base application to be extended by plugins.
The process of creating custom middleware for dynamically adding endpoints to an ASP.NET Core application.
Demonstration of a test endpoint project reference to the endpoint PDK for plugin development.
Instructions on avoiding conflicts by correctly setting the 'Private' property in plugin assembly output.
The use of reflection for loading and executing plugin types at runtime.
The challenge of unloading plugins and the role of the AssemblyLoadContext in managing assembly loading and unloading.
Techniques for forcing garbage collection to unload plugins and the limitations of this approach.
The impact of method implementation attributes on the inlining of code and its effect on garbage collection.
The use of weak references to check if an assembly has been successfully unloaded.
The subtle ways references can leak and prevent plugin unloading, such as through JSON serialization.
Recommendations for creating pure functions within plugins to avoid side effects and reference leakage.
The need for a robust system to handle plugin updates and edge cases in a production environment.
Closing remarks, encouraging viewers to support the channel, ask questions, and check for additional resources.
Transcripts
welcome to the Rock coding YouTube
channel my name is Anton and today we're
going to be learning about plug-in
architecture in c-sharp plugins and
plug-in architecture is essentially
being able to extend your application
while at runtime so for example if you
have Visual Studio vs code or writer to
edit code you can have your extensions
or plugins to add more functionality to
it if you've ever used WordPress you
know the power of plugins and perhaps
you can take it as far as saying Cloud
functions because the cloud is just one
big computer you're taking your code and
you're extending the cloud as always
don't forget if you're enjoying the
video leave a like And subscribe that
helps out the channel massively if you
have any questions leave them in the
comments section don't forget to check
out the description I have a course that
is out if you want to know c-sharp as I
do it I'm sure it will be useful to you
with that let's go ahead and get started
here we have three projects the server
this is the application that we're going
to be extending we then have an endpoint
pdk so usually you would have an SDK a
software development kit here because
we're developing plugins it's a plug-in
development kit hence pdk let's quickly
take a look at this and we really have
super simple two components I plug in
endpoint this is the thing which is
going to essentially Mark a entry point
to a plugin and then we have a path
attribute because we essentially want to
add endpoints to our server by the way
that is not the only way that you can
extend you if you can write a plugin you
can extend your server in any way that
you design here the design is
specifically we want to be able to add
endpoints to our asp.net core
application so hence the plugin endpoint
and the path attribute these are the
only two things that we have here the
endpoint pdk also has a reference to the
framework so we can actually have HTTP
context in here okay we then have the
server and the server references the
plugin development kit nothing else in
in here and it is pretty much an empty
template now to skip some time of me
writing out code here is an example
endpoint that we essentially want to add
to our server at runtime the test
endpoint has a project reference to
endpoint pdk and if you're developing a
plug-in this setting is important
because basically when you're going to
build your plugin it is only going to
Output the dll for the individual
assembly it is not going to include the
endpoint pdk dll if you remove private
it's going to be included in the output
artifact and may collide with the one
that is included in the server with all
of that let's go ahead and go to our
program Cs and we're going to think
about how can we possibly add more
endpoints to our application perhaps
after we start our application we hold a
reference to the app and we do something
like map get and long story short that
is not going to work as soon as you run
your application you cannot add more
endpoints that way so what we're going
to do is we're going to write custom
middleware so use middleware we will say
plugin middleware create this type
semicolon over here and because I can't
remember all the types that are needed
for the middleware we're going to go to
use Authentication
go inside of here the authentication
middleware we're going to copy the
request delegate that we need in the
Constructor so ctor place that over here
this is going to be the next function
that is going to be executing and then
we just need to match the signature of
this method so a method that returns a
task and accepts an HTTP context so we
grab this function place it over here
give it a body and now we have to fill
it out let's get rid of this use
authentication over here before we dive
straight into loading the plugin Etc we
can play about with this and get a feel
for how this is going to work first of
all let's say that we're going to have
some kind of context that's going to
have a request and it's going to have a
path if this path is equal to something
like plugin slash test we want to go
ahead and well write something to the
response so write async this will be
test we are going to await here and then
whether this has triggered or not we can
basically catch all the scenarios if we
go to context a response and then there
is a has started flag if the response
hasn't started then we actually want to
fall through to the next so we're gonna
take next invoke it pass the context
down in there and oh wait so if we hit
next we're gonna hit hello world let's
go ahead and start this up so opening
this dot watch here we see hello world
if we go to plug and test and that
actually says plug test we want plug-in
test so let me amend this a little bit
plugin and there we can see test now
essentially what we want to do is
instead of hard coding this here we want
to be able to load up this class over
here so what do we do we're going to
open test endpoint over here we're gonna
delete the assemblies just to be sure
and then we're gonna build this so.net
build this is going to Reaper produce
the artifacts with the latest
information and all we need to do is
just have a path to this assembly the
process of installation of a plugin is
essentially just taking artifacts and
putting them in the correct place in
order for it to be load and then perhaps
telling the system go over here and load
this stuff up so let's go ahead and grab
the full path we're going to drop down
the terminal we're going to come back to
program Cs and we're gonna say here is
the path to the full assembly we can
then use assembly load from and this is
going to accept a path and now we have
an assembly object and from here on out
if you know reflection you know what
you're doing you can go to assembly you
can get type let's say that we're
looking after the test endpoint and
whatever class I gave it so an end point
in the same namespace this is going to
be end point we're then going to have
the path information from this endpoint
get custom attribute this is going to be
path attribute and by the way just for
Clarity if I open a path attribute this
is the same path attribute that is
coming from this endpoint BDK okay to
check for Noble we can place a question
mark over here if path info doesn't
equal null or we can just say method and
path equals to that of the context
request so let me scroll down and
quickly skipped past this part and there
we have it if path info is a null and
the method in the path are basically
equal and we ignore the cases we want to
go ahead take this endpoint we want to
activate it so we're going to go to the
activator we're going to create an
instance of this type and this is going
to be a plug-in endpoint so for actual
endpoint this we can call it type so
endpoint type replace it here and here
then on the individual endpoint we can
execute it and pass the HTTP context
into there wait and that should be good
so the application at this point should
restart if I come back over here and
actually let me remind my self of the
route it was plug test so coming back
over here we're gonna remove the i n and
go to plug test and we get test if we
duplicate this and I'm just going to go
to the root route here we're still
getting hello world so we've managed to
successfully plug in our additional
functionality now if we come back and
we're at this plugable endpoint we
change it we go to rebuild it we're
still pointing to the same dll if we
come back and we well refresh
essentially it's gonna break if we come
back over here and we say let's reload
the app the app restarts so it's working
again and again if we hit the plug test
endpoint we can see the change so the
behavior that we're seeing over here is
once we have managed to load an assembly
and its types into our application once
it is very hard to load it a second time
because there is something around
basically being able able to identify
that that dll is that unique thing and
we cannot load it more than once so
that's basically the new ones how do we
walk around this what can we do well
first of all we have a special type
called an assembly below the context and
this type specifically is giving us a
scope in which assemblies are loaded so
instead of just having your whole
application This Global namespace This
Global application essentially your
assembly is like a drop added to this
pool and essentially diluted through it
you're putting a little space in there
that's saying this is where I'm going to
be adding my new assembly and then you
can go ahead and remove it as well and
that is what the assembly load context
is so let's go ahead and create a new
assembly load context and here in the
Constructor you're gonna see that you
can provide a name to it and then you
can mark it as is collectible or not if
you mark it as is collectible that gives
you the ability to actually unload the
assemblies that you have loaded for the
name we're just going to give it the
path and then for is collectible we're
gonna say that it is true let's say that
this is the load context and now instead
of using assembly load from and by the
way if you go into the load from method
you're gonna see that it's actually
using an assembly load context
internally it's just not using a
separate one it's using the default one
coming back to program CS we can go to
the load context use it and say load
from assembly path this is still going
to give us an assembly and finally let's
say that we're gonna wrap this whole
thing and try catch or more of a try
finally and by the end this whole thing
is done we are going to call unload and
unload behave slightly weirdly although
also logically and we're going to talk
about this in just a second let's remove
this space over here and go over the
code once more so we understand what it
does we create this scope for where we
are going to load up an assembly so we
have this context and we're maintaining
a reference to this place where we are
going to be loading assemblies we go
ahead load the assembly we do exactly
the same thing as we were doing before
and then after we've managed to execute
it or not because we already did the
loading we're just going to unload and
by the way we're doing this for every
request this is not a production ready
solution we are just trying to
understand what the heck is actually
going on here with what we currently
have let's go ahead open up the terminal
it's going to restart we're going to
come back to this endpoint over here I'm
going to refresh and we're still going
to see the same extended endpoint coming
back over here we're going to go to test
endpoint we're gonna change it back to
test we're gonna rebuild
we're gonna come back and we're gonna
refresh and we see the signature is
incorrect error the logic behind why it
displays the error of the signature is
incorrect I won't be able to explain to
you however I will be able to tell you
why this is happening the reason this is
happening is because in program CS when
we're calling unload over here it
doesn't actually instantly unload
anything imagine this so you are loading
assemblies from a file you are
essentially having this compiled c-sharp
code you are bringing it into your own
application and then at some point
you're saying unload what that is going
to do is just remove references and if
you're somehow still referencing any of
that code the unloading is not going to
happen and it's actually only going to
occur when the garbage collection runs
so unload over here even though it's a
synchronous method it's more of a like
remove markers the actual unloading is
going to happen when the garbage
collection is going to run so if I open
a memory profiler and I reattach to the
server process over here and I'm going
to force garbage collection and then I'm
going to come back over here and refresh
the endpoint we now see the change so
when you want to actually unload a
plugin and reload it you actually need
to force garbage collection and collect
the old plugin from memory if we come
back to the code and we take a look at
this we're loading a type from this
assembly if we take this type and we
stick it somewhere on the plug-in
middleware which outlives the unloading
of assembly the assembly is never going
to unload this is why if you are
uninstalling an extension or you're
removing a plug-in it's going to ask you
please restart because perhaps for
performance reasons the main application
needs to take the things inside the
plugin and dilute it in its essentially
Global space so furthermore what can we
do about this let's go ahead and first
of all create a private static task
where we're going to process us the HTTP
context so CTX we're going to take this
whole Malarkey place it over here inside
this body so we're working directly with
this everywhere where we have CTX or
context let's go ahead and replace it
make the function asynchronous and we're
going to call process right over here on
the context and await on it give it a
little bit of space the first reason for
taking out the logic into its own method
is because of the stack we have this
endpoint type variable and it's
essentially capturing this type so until
we actually exit the method even though
we have unloaded so if we would try to
force garbage collection over here
nothing would happen because the stack
is still keeping a reference to this
type over here so before we can actually
start garbage collection we need to do
it outside we need to exit this method
to clear all the references from the
stack something that you need to be
aware of also in this situation is you
can supply and an attribute Mark that is
basically called method implementation
attribute and then here you can specify
things like aggressive inline aggressive
optimizations and think about it like
this if the dotnet runtime deems it
necessary or just I don't know feels
like or basically decides to it's going
to say there is no method it's going to
take the code that is over here and it's
just going to execute it as part of this
method that basically means even if
we're going to put the garbage
collection logic over here if we're not
going to Define this boundary of a
method explicitly we may be holding on
to this reference a little bit longer
than we intended to so let's say that
method implementation no inlining again
inlining means taking all of the
instructions over here and essentially
inlining them into this invoke method
another way of giving it this guarantee
essentially we can go to the garbage
collector we can collect and another
thing that we can call is finalizer's
weight for pending finalizers if you're
wondering what a finalizer is
essentially on classes you will have
things like destructors so something
that looks like this without any
parameters and when the object is
actually being garbage collected this is
what is going to run this finalizer is
essentially going to get called sometime
after the actual garbage collection runs
and it's going to be placed on the
finalizer queue where one after another
the objects which are being garbage
collected will get their finalizers
called one by one so here we're
basically just saying everything that we
have marked as garbage collected or that
is being garbage collected wait for all
of those finalizers to be called okay so
we're doing this we're still flying a
little bit blind because we don't have
any insurance whether anything has been
cleared from memory one way that we can
double check that stuff has actually
been unloaded is using a week not week
week reference to the load context so a
strong reference is going to prevent
garbage collection week reference
doesn't and it just says is there still
an object on the endless let's take the
weak reference return it from this
process we will then have it over here
so assembly load context reference we
can take this and let's say We'll place
a for each Loop we'll retry 10 times
while I is less than 10 and the
reference is alive we want to go ahead
and try garbage collecting by the end of
this we can go to the console right line
and say unloading successful always
forget that s over there take the LC ref
and say is alive and we'll say not so if
it's not alive it's successful if it is
alive then it's unsuccessful with what
we have over here let's come back wait
for the application to restart let's
double check that everything is working
so we're just going to refresh the
endpoint come back over here and the
unloading is successful we can take a
quick step over here and say what kind
of unloading attempt is it to just to
satisfy our curiosity how many times are
we forcing garbage collection to run
before the stuff actually gets unloaded
there we have it let's go ahead and
refresh and looks like two times again
refresh and another two times why does
it take two times I don't know which
generation is it trying to collect I
don't know as well I don't really care
all that much again the loading and
unloading of the plugin is going to be
slow it's really the experience of once
the plugin is loaded how well can you
use it and can you actually update the
plugin to well update the plugin because
updating stuff actually matters so we're
capable of unloading the plugin by the
end of it let's drop down the terminal
over here go to the test endpoint add
some more changes we are going to
rebuild the application come back over
here refresh and the plugin
automatically updates one last thing
that I wanted to highlight over here is
the point about references again so
we've seen how if we are essentially
keeping references to something the
unloading doesn't happen and we actually
need to run a garbage collector so how
can you essentially leak a reference so
you can prevent your assembly from being
unloaded and the points about this can
be very subtle for example if we go to
the test endpoint and we're going to say
look we're gonna have some kind of Json
string over here and we're going to use
the Json serializer to serialize some
the object where we're gonna have a
message and say yo okay we're gonna take
the Json string that is what we're going
to write over here and if we're smart
you know we're going to use write as
Json async we're going to take this
object and we're going to put it here
okay and Json serializer you don't even
see it it's not even there right this is
what you're using all fine and dandy
let's go ahead over here re-run the
build come back to the application
refresh and everything is working fine
no problem we come back we add all of
our changes we rebuilt we come back we
refresh and the signature is incorrect
if we come back to the application over
here we're gonna see that we've went
through all of our attempts and the
unloading was unsuccessful if you didn't
know now you're basically going to know
the Json serializer from
system.text.json I don't know about the
Newton soft one essentially the type
that you're trying to serialize that is
going to get cached so effectively the
Json serializer coming from this Global
namespace down into your plugin and if
you're utilizing it in your plugin
through your plugin your leaking
references into the global space so this
is how easy it is to mess this stuff up
my recommendations for plugins even
though it's sometimes not going to be
possible you want those to be pure
functions a pure function is a function
without side effects although this being
essentially an internal solution for
Microsoft I think I haven't looked far
enough but I think through Json
serialization options you can actually
go ahead and disable it however this is
going to be the end of this video thank
you very much for watching hopefully
this gives you a good starting point on
how to write a plugin and what to watch
out for or rather than writing a plugin
essentially utilizing plugin
architecture in your c-sharp application
please note it's not fully production
ready we've left the loading and
unloading of the plugin in the Middle
where you want to take it out there is a
class like system file Watcher you want
to watch the plugins folder for changes
if there is a change emits an event try
to unload disable usage of the plugin
you know you basically have to do all
that crazy ceremony as you usually do
with edge cases Etc what if you can't
unload the plugin what do you do in that
scenario you know voice your scenarios
and test them as always if you enjoyed
the video don't forget to leave a like
subscribe if you have any questions
leave them in the comment section if you
would like the code for this video as
well as my other videos please come
support me on patreon I will really
appreciate it a very very big and
special thank you to all my current
patreon supporters your help is very
appreciated don't forget to check the
description for extra documentation that
you can use around this topic as always
thank you for watching and have a good
day
Browse More Related Video
Custom Fleet Plugins for Your Kotlin Codebase | Vitaly Bragilevsky
๐๏ธ TOP 10 BEST Obsidian Plugins ๐๏ธ
Ultimate Solution for Zero Latency Monitoring Without DSP (Works with any Interface & any Daw)
Introduction to WordPress Theme Development in Hindi #1 - WsCube Tech
Docker Setup for Local WordPress Development
MASTER Obsidian's Powerful METADATA MENU Plugin - Step by Step
5.0 / 5 (0 votes)