();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/samples/aspnetcore-mvc/MvcApp/Startup.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.IO;
6 | using Microsoft.AspNetCore.Builder;
7 | using Microsoft.AspNetCore.Mvc;
8 | using Microsoft.Extensions.DependencyInjection;
9 |
10 | namespace MvcWebApp
11 | {
12 | public class Startup
13 | {
14 | public void ConfigureServices(IServiceCollection services)
15 | {
16 | var mvcBuilder = services.AddMvc();
17 |
18 | foreach (var dir in Directory.GetDirectories(Path.Combine(AppContext.BaseDirectory, "plugins")))
19 | {
20 | var pluginFile = Path.Combine(dir, Path.GetFileName(dir) + ".dll");
21 | // The AddPluginFromAssemblyFile method comes from McMaster.NETCore.Plugins.Mvc
22 | mvcBuilder.AddPluginFromAssemblyFile(pluginFile);
23 | }
24 | }
25 |
26 | public void Configure(IApplicationBuilder app)
27 | {
28 | app.UseDeveloperExceptionPage();
29 | app
30 | .UseRouting()
31 | .UseEndpoints(r =>
32 | {
33 | r.MapDefaultControllerRoute();
34 | });
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/samples/aspnetcore-mvc/MvcApp/Views/Home/Index.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewData["Title"] = "Home Page";
3 | }
4 |
5 | Hello from the web app.
6 |
7 |
10 |
--------------------------------------------------------------------------------
/samples/aspnetcore-mvc/MvcApp/Views/_ViewImports.cshtml:
--------------------------------------------------------------------------------
1 | @using MvcWebApp
2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
3 |
--------------------------------------------------------------------------------
/samples/aspnetcore-mvc/MvcAppPlugin1/MvcAppPlugin1.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | true
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/samples/aspnetcore-mvc/MvcAppPlugin1/MyPluginController.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | namespace MvcAppPlugin1
7 | {
8 | public class MyPluginController : Controller
9 | {
10 | public IActionResult Index() => View();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/samples/aspnetcore-mvc/MvcAppPlugin1/Views/MyPlugin/Index.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewData["Title"] = "Plugin1";
3 | }
4 |
5 | Hello world from Plugin1!
6 |
--------------------------------------------------------------------------------
/samples/aspnetcore-mvc/README.md:
--------------------------------------------------------------------------------
1 | ASP.NET Core MVC Sample
2 | =======================
3 |
4 | This sample contains 2 projects which demonstrate a simple plugin scenario.
5 |
6 | 1. 'MvcWebApp' is an ASP.NET Core application which scans for a 'plugins' folder in its base directory and attempts to load any plugins it finds
7 | 2. 'MvcAppPlugin1' which implements MVC controllers.
8 |
9 | Normally, an ASP.NET Core MVC application must have a direct dependency on any assemblies
10 | which provide controllers. However, as this sample demonstrates, an MVC application
11 | can load controllers from a list of assemblies which is not known ahead of time when the
12 | host app is built.
13 |
14 | ## Running the sample
15 |
16 | Open a command line to this folder and run:
17 |
18 | ```
19 | dotnet restore
20 | dotnet run --project MvcApp/
21 | ```
22 |
23 | Then open
24 |
--------------------------------------------------------------------------------
/samples/aspnetcore-mvc/aspnetcore-mvc.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcApp", "MvcApp\MvcApp.csproj", "{4F363469-7783-4A40-B1B5-455187AF1C76}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcAppPlugin1", "MvcAppPlugin1\MvcAppPlugin1.csproj", "{B6316996-A470-470F-9CF1-475096A0510A}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Debug|x64 = Debug|x64
14 | Debug|x86 = Debug|x86
15 | Release|Any CPU = Release|Any CPU
16 | Release|x64 = Release|x64
17 | Release|x86 = Release|x86
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Debug|x64.ActiveCfg = Debug|Any CPU
26 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Debug|x64.Build.0 = Debug|Any CPU
27 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Debug|x86.ActiveCfg = Debug|Any CPU
28 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Debug|x86.Build.0 = Debug|Any CPU
29 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Release|x64.ActiveCfg = Release|Any CPU
32 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Release|x64.Build.0 = Release|Any CPU
33 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Release|x86.ActiveCfg = Release|Any CPU
34 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Release|x86.Build.0 = Release|Any CPU
35 | {B6316996-A470-470F-9CF1-475096A0510A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36 | {B6316996-A470-470F-9CF1-475096A0510A}.Debug|Any CPU.Build.0 = Debug|Any CPU
37 | {B6316996-A470-470F-9CF1-475096A0510A}.Debug|x64.ActiveCfg = Debug|Any CPU
38 | {B6316996-A470-470F-9CF1-475096A0510A}.Debug|x64.Build.0 = Debug|Any CPU
39 | {B6316996-A470-470F-9CF1-475096A0510A}.Debug|x86.ActiveCfg = Debug|Any CPU
40 | {B6316996-A470-470F-9CF1-475096A0510A}.Debug|x86.Build.0 = Debug|Any CPU
41 | {B6316996-A470-470F-9CF1-475096A0510A}.Release|Any CPU.ActiveCfg = Release|Any CPU
42 | {B6316996-A470-470F-9CF1-475096A0510A}.Release|Any CPU.Build.0 = Release|Any CPU
43 | {B6316996-A470-470F-9CF1-475096A0510A}.Release|x64.ActiveCfg = Release|Any CPU
44 | {B6316996-A470-470F-9CF1-475096A0510A}.Release|x64.Build.0 = Release|Any CPU
45 | {B6316996-A470-470F-9CF1-475096A0510A}.Release|x86.ActiveCfg = Release|Any CPU
46 | {B6316996-A470-470F-9CF1-475096A0510A}.Release|x86.Build.0 = Release|Any CPU
47 | EndGlobalSection
48 | EndGlobal
49 |
--------------------------------------------------------------------------------
/samples/aspnetcore/Abstractions/Abstractions.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/samples/aspnetcore/Abstractions/IPlugin.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.Extensions.DependencyInjection;
6 |
7 | namespace Plugin.Abstractions
8 | {
9 | public interface IWebPlugin
10 | {
11 | void Configure(IApplicationBuilder appBuilder);
12 | void ConfigureServices(IServiceCollection services);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/samples/aspnetcore/Abstractions/IPluginLink.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | namespace Plugin.Abstractions
5 | {
6 | public interface IPluginLink
7 | {
8 | string GetHref();
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/samples/aspnetcore/MainWebApp/MainWebApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/samples/aspnetcore/MainWebApp/Pages/Index.cshtml:
--------------------------------------------------------------------------------
1 | @page
2 | @using MainWebApp
3 | @model IndexModel
4 |
5 | @foreach (var link in Model.Links)
6 | {
7 | -
8 | @link
9 |
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/samples/aspnetcore/MainWebApp/Pages/Index.cshtml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using Microsoft.AspNetCore.Mvc.RazorPages;
7 | using Plugin.Abstractions;
8 |
9 | namespace MainWebApp
10 | {
11 | public class IndexModel : PageModel
12 | {
13 | public IndexModel(IEnumerable pluginLinks)
14 | {
15 | Links = pluginLinks.Select(p => p.GetHref()).ToArray();
16 | }
17 |
18 | public string[] Links { get; }
19 |
20 | public void OnGet()
21 | {
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/samples/aspnetcore/MainWebApp/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using Microsoft.AspNetCore;
5 | using Microsoft.AspNetCore.Hosting;
6 |
7 | namespace MainWebApp
8 | {
9 | public class Program
10 | {
11 | public static void Main(string[] args)
12 | {
13 | CreateWebHostBuilder(args).Build().Run();
14 | }
15 |
16 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
17 | WebHost.CreateDefaultBuilder(args)
18 | .UseStartup();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/samples/aspnetcore/MainWebApp/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:35566",
7 | "sslPort": 44362
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "environmentVariables": {
15 | "ASPNETCORE_ENVIRONMENT": "Development"
16 | }
17 | },
18 | "WebAppWithPlugins": {
19 | "commandName": "Project",
20 | "launchBrowser": true,
21 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
22 | "environmentVariables": {
23 | "ASPNETCORE_ENVIRONMENT": "Development"
24 | }
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/samples/aspnetcore/MainWebApp/Startup.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Linq;
8 | using McMaster.NETCore.Plugins;
9 | using Microsoft.AspNetCore.Builder;
10 | using Microsoft.AspNetCore.Hosting;
11 | using Microsoft.Extensions.DependencyInjection;
12 | using Plugin.Abstractions;
13 |
14 | namespace MainWebApp
15 | {
16 | public class Startup
17 | {
18 | private readonly List _plugins = new();
19 |
20 | public Startup()
21 | {
22 | foreach (var pluginDir in Directory.GetDirectories(Path.Combine(AppContext.BaseDirectory, "plugins")))
23 | {
24 | var dirName = Path.GetFileName(pluginDir);
25 | var pluginFile = Path.Combine(pluginDir, dirName + ".dll");
26 | var loader = PluginLoader.CreateFromAssemblyFile(pluginFile,
27 | // this ensures that the plugin resolves to the same version of DependencyInjection
28 | // and ASP.NET Core that the current app uses
29 | sharedTypes: new[]
30 | {
31 | typeof(IApplicationBuilder),
32 | typeof(IWebPlugin),
33 | typeof(IServiceCollection),
34 | });
35 | foreach (var type in loader.LoadDefaultAssembly()
36 | .GetTypes()
37 | .Where(t => typeof(IWebPlugin).IsAssignableFrom(t) && !t.IsAbstract))
38 | {
39 | Console.WriteLine("Found plugin " + type.Name);
40 | var plugin = (IWebPlugin)Activator.CreateInstance(type)!;
41 | _plugins.Add(plugin);
42 | }
43 | }
44 | }
45 |
46 | public void ConfigureServices(IServiceCollection services)
47 | {
48 | services.AddMvc().AddMvcOptions(o => o.EnableEndpointRouting = false);
49 |
50 | foreach (var plugin in _plugins)
51 | {
52 | plugin.ConfigureServices(services);
53 | }
54 | }
55 |
56 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
57 | {
58 | app.UseDeveloperExceptionPage();
59 |
60 | foreach (var plugin in _plugins)
61 | {
62 | plugin.Configure(app);
63 | }
64 |
65 | app.UseMvcWithDefaultRoute();
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/samples/aspnetcore/README.md:
--------------------------------------------------------------------------------
1 | ASP.NET Core Sample
2 | ===================
3 |
4 | This sample contains 4 projects which demonstrate a simple plugin scenario.
5 |
6 | 1. 'Abstractions' defines common interfaces shared by the web application (host) and plugins
7 | 2. 'MainWebApp' is an ASP.NET Core application which scans for a 'plugins' folder in its base directory and attempts to load any plugins it finds
8 | 3. 'WebAppPlugin1' references 'Abstractions' and implements `IWebPlugin`. This plugin has a dependency on [AutoMapper](https://www.nuget.org/packages/AutoMapper/) version 6.
9 | 4. 'WebAppPlugin2' is the same as plugin1, but it uses AutoMapper version 7.
10 |
11 | Normally, in .NET Core applications you cannot reference two different versions of the same assembly.
12 | However, as this sample demonstrates, using .NET Core plugins you can load and use two different versions.
13 |
14 | * http://localhost:5000/plugin/v1 responds with
15 | ```
16 | This plugin uses AutoMapper, Version=6.2.2.0, Culture=neutral, PublicKeyToken=be96cd2c38ef1005
17 | ```
18 |
19 | * http://localhost:5000/plugin/v2 responds with
20 | ```
21 | This plugin uses AutoMapper, Version=7.0.1.0, Culture=neutral, PublicKeyToken=be96cd2c38ef1005
22 | ```
23 |
24 | There are some important types, however, which must share the same identity between the plugins and the host.
25 | To ensure type exchange works between the host and the plugins, the MainWebApp project uses the `sharedTypes`
26 | parameter on `PluginLoader.CreateFromAssemblyFile`.
27 |
28 | ```csharp
29 | var loader = PluginLoader.CreateFromAssemblyFile(
30 | pluginAssembly,
31 | sharedTypes: new[]
32 | {
33 | typeof(IApplicationBuilder),
34 | typeof(IWebPlugin),
35 | typeof(IServiceCollection),
36 | });
37 | ```
38 |
39 | This is important because the plugins in this sample are compiled for ASP.NET Core 2.0 interfaces,
40 | but the MainWebApp uses ASP.NET Core 2.1. If not for this parameter, the plugins would also attempt to use
41 | a private copy of the ASP.NET Core implementations and type exchange between the plugin and the web app
42 | would fail to resolve `IApplicationBuilder` and `IServiceCollection` as the same type.
43 |
--------------------------------------------------------------------------------
/samples/aspnetcore/WebAppPlugin1/WebAppPlugin1.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/samples/aspnetcore/WebAppPlugin1/WebPlugin1.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Plugin.Abstractions;
8 |
9 | namespace Plugin1
10 | {
11 | internal class WebPlugin1 : IWebPlugin, IPluginLink
12 | {
13 | public string GetHref() => "/plugin/v1";
14 |
15 | public void ConfigureServices(IServiceCollection services)
16 | {
17 | services.AddScoped();
18 | }
19 |
20 | public void Configure(IApplicationBuilder appBuilder)
21 | {
22 | appBuilder.Map("/plugin/v1", c =>
23 | {
24 | var autoMapperType = typeof(AutoMapper.IMapper).Assembly;
25 | c.Run(async (ctx) =>
26 | {
27 | await ctx.Response.WriteAsync("This plugin uses " + autoMapperType.GetName().ToString());
28 | });
29 | });
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/samples/aspnetcore/WebAppPlugin2/WebAppPlugin2.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/samples/aspnetcore/WebAppPlugin2/WebPlugin2.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Plugin.Abstractions;
8 |
9 | namespace Plugin2
10 | {
11 | internal class WebPlugin2 : IWebPlugin, IPluginLink
12 | {
13 | public string GetHref() => "/plugin/v2";
14 |
15 | public void ConfigureServices(IServiceCollection services)
16 | {
17 | services.AddScoped();
18 | }
19 |
20 | public void Configure(IApplicationBuilder appBuilder)
21 | {
22 | appBuilder.Map("/plugin/v2", c =>
23 | {
24 | var autoMapperType = typeof(AutoMapper.IMapper).Assembly;
25 | c.Run(async (ctx) =>
26 | {
27 | await ctx.Response.WriteAsync("This plugin uses " + autoMapperType.GetName().ToString());
28 | });
29 | });
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/samples/aspnetcore/aspnetcore.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstractions", "Abstractions\Abstractions.csproj", "{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MainWebApp", "MainWebApp\MainWebApp.csproj", "{781A86B1-C278-44CD-997B-1AA26D6BFB38}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAppPlugin1", "WebAppPlugin1\WebAppPlugin1.csproj", "{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAppPlugin2", "WebAppPlugin2\WebAppPlugin2.csproj", "{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Debug|x64 = Debug|x64
18 | Debug|x86 = Debug|x86
19 | Release|Any CPU = Release|Any CPU
20 | Release|x64 = Release|x64
21 | Release|x86 = Release|x86
22 | EndGlobalSection
23 | GlobalSection(SolutionProperties) = preSolution
24 | HideSolutionNode = FALSE
25 | EndGlobalSection
26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
27 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|x64.ActiveCfg = Debug|Any CPU
30 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|x64.Build.0 = Debug|Any CPU
31 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|x86.ActiveCfg = Debug|Any CPU
32 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|x86.Build.0 = Debug|Any CPU
33 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|Any CPU.Build.0 = Release|Any CPU
35 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|x64.ActiveCfg = Release|Any CPU
36 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|x64.Build.0 = Release|Any CPU
37 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|x86.ActiveCfg = Release|Any CPU
38 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|x86.Build.0 = Release|Any CPU
39 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|Any CPU.Build.0 = Debug|Any CPU
41 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|x64.ActiveCfg = Debug|Any CPU
42 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|x64.Build.0 = Debug|Any CPU
43 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|x86.ActiveCfg = Debug|Any CPU
44 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|x86.Build.0 = Debug|Any CPU
45 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|Any CPU.ActiveCfg = Release|Any CPU
46 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|Any CPU.Build.0 = Release|Any CPU
47 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|x64.ActiveCfg = Release|Any CPU
48 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|x64.Build.0 = Release|Any CPU
49 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|x86.ActiveCfg = Release|Any CPU
50 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|x86.Build.0 = Release|Any CPU
51 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
52 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
53 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|x64.ActiveCfg = Debug|Any CPU
54 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|x64.Build.0 = Debug|Any CPU
55 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|x86.ActiveCfg = Debug|Any CPU
56 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|x86.Build.0 = Debug|Any CPU
57 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
58 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|Any CPU.Build.0 = Release|Any CPU
59 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|x64.ActiveCfg = Release|Any CPU
60 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|x64.Build.0 = Release|Any CPU
61 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|x86.ActiveCfg = Release|Any CPU
62 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|x86.Build.0 = Release|Any CPU
63 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
64 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
65 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|x64.ActiveCfg = Debug|Any CPU
66 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|x64.Build.0 = Debug|Any CPU
67 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|x86.ActiveCfg = Debug|Any CPU
68 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|x86.Build.0 = Debug|Any CPU
69 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
70 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|Any CPU.Build.0 = Release|Any CPU
71 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|x64.ActiveCfg = Release|Any CPU
72 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|x64.Build.0 = Release|Any CPU
73 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|x86.ActiveCfg = Release|Any CPU
74 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|x86.Build.0 = Release|Any CPU
75 | EndGlobalSection
76 | EndGlobal
77 |
--------------------------------------------------------------------------------
/samples/dependency-injection/DI.HostApp/DI.HostApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/samples/dependency-injection/DI.HostApp/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Linq;
8 | using McMaster.NETCore.Plugins;
9 | using Microsoft.Extensions.DependencyInjection;
10 |
11 | namespace DependencyInjection
12 | {
13 | public class Program
14 | {
15 | public static void Main(string[] args)
16 | {
17 | var services = new ServiceCollection();
18 | var loaders = GetPluginLoaders();
19 |
20 | ConfigureServices(services, loaders);
21 |
22 | using var serviceProvider = services.BuildServiceProvider();
23 |
24 | var consumer = serviceProvider.GetRequiredService();
25 | consumer.Consume();
26 | }
27 |
28 | private static List GetPluginLoaders()
29 | {
30 | var loaders = new List();
31 |
32 | // create plugin loaders
33 | var pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins");
34 | foreach (var dir in Directory.GetDirectories(pluginsDir))
35 | {
36 | var dirName = Path.GetFileName(dir);
37 | var pluginDll = Path.Combine(dir, dirName + ".dll");
38 | if (File.Exists(pluginDll))
39 | {
40 | var loader = PluginLoader.CreateFromAssemblyFile(
41 | pluginDll,
42 | sharedTypes: new[] { typeof(IPluginFactory), typeof(IServiceCollection) });
43 | loaders.Add(loader);
44 | }
45 | }
46 |
47 | return loaders;
48 | }
49 |
50 | private static void ConfigureServices(ServiceCollection services, List loaders)
51 | {
52 | // Create an instance of plugin types
53 | foreach (var loader in loaders)
54 | {
55 | foreach (var pluginType in loader
56 | .LoadDefaultAssembly()
57 | .GetTypes()
58 | .Where(t => typeof(IPluginFactory).IsAssignableFrom(t) && !t.IsAbstract))
59 | {
60 | // This assumes the implementation of IPluginFactory has a parameterless constructor
61 | var plugin = Activator.CreateInstance(pluginType) as IPluginFactory;
62 |
63 | plugin?.Configure(services);
64 | }
65 | }
66 | }
67 |
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/samples/dependency-injection/DI.SharedAbstractions/DI.SharedAbstractions.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/samples/dependency-injection/DI.SharedAbstractions/Fruit.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | namespace DependencyInjection
5 | {
6 | public class Fruit
7 | {
8 | public Fruit(string name)
9 | {
10 | Name = name;
11 | }
12 |
13 | public string Name { get; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/samples/dependency-injection/DI.SharedAbstractions/IFruitConsumer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | namespace DependencyInjection
5 | {
6 | public interface IFruitConsumer
7 | {
8 | void Consume();
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/samples/dependency-injection/DI.SharedAbstractions/IFruitProducer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System.Collections.Generic;
5 |
6 | namespace DependencyInjection
7 | {
8 | public interface IFruitProducer
9 | {
10 | IEnumerable Produce();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/samples/dependency-injection/DI.SharedAbstractions/IPluginFactory.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using Microsoft.Extensions.DependencyInjection;
5 |
6 | namespace DependencyInjection
7 | {
8 | public interface IPluginFactory
9 | {
10 | void Configure(IServiceCollection services);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/samples/dependency-injection/MyPlugin1/MyFruitProducer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System.Collections.Generic;
5 | using DependencyInjection;
6 |
7 | namespace MyPlugin1
8 | {
9 | internal class MyFruitProducer : IFruitProducer
10 | {
11 | public IEnumerable Produce()
12 | {
13 | yield return new Fruit("banana");
14 | yield return new Fruit("orange");
15 | yield return new Fruit("strawberry");
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/samples/dependency-injection/MyPlugin1/MyPlugin1.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/samples/dependency-injection/MyPlugin1/PluginConfiguration.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using DependencyInjection;
5 | using Microsoft.Extensions.DependencyInjection;
6 |
7 | namespace MyPlugin1
8 | {
9 | public class PluginConfiguration : IPluginFactory
10 | {
11 | public void Configure(IServiceCollection services)
12 | {
13 | services.AddSingleton();
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/samples/dependency-injection/MyPlugin2/MyFruitConsumer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using DependencyInjection;
7 |
8 | namespace MyPlugin2
9 | {
10 | internal class MyFruitConsumer : IFruitConsumer
11 | {
12 | private readonly IEnumerable _producers;
13 |
14 | public MyFruitConsumer(IEnumerable producers)
15 | {
16 | _producers = producers;
17 | }
18 |
19 | public void Consume()
20 | {
21 | foreach (var producer in _producers)
22 | {
23 | foreach (var fruit in producer.Produce())
24 | {
25 | Console.WriteLine($"Consumed {fruit.Name}");
26 | }
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/samples/dependency-injection/MyPlugin2/MyPlugin2.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/samples/dependency-injection/MyPlugin2/PluginConfiguration.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using DependencyInjection;
5 | using Microsoft.Extensions.DependencyInjection;
6 |
7 | namespace MyPlugin2
8 | {
9 | public class PluginConfiguration : IPluginFactory
10 | {
11 | public void Configure(IServiceCollection services)
12 | {
13 | services.AddSingleton();
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/samples/dependency-injection/README.md:
--------------------------------------------------------------------------------
1 | Dependency Injection Sample
2 | ===========================
3 |
4 | This sample contains 4 projects which demonstrate a plugin scenario which coordinates types between
5 | plugins using a dependency injection container.
6 |
7 | * 'DI.HostApp' is a console application which scans for a 'plugins' folder in its base directory and attempts to load any plugins it finds. It then configures plugins in a dependency injection collection.
8 | * 'DI.SharedAbstractions' which contains an interface shared by plugins and the host.
9 | * 'MyPlugin1' and 'MyPlugin2' implement shared abstractions and register them with the host.
10 |
11 | ## Running the sample
12 |
13 | Open a command line to this folder and run:
14 |
15 | ```
16 | dotnet restore
17 | dotnet run --project DI.HostApp/
18 | ```
19 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/Contracts/Contracts.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Library
5 | net8.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/Contracts/Fruit.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | namespace Contracts
5 | {
6 | public class Fruit
7 | {
8 | public string? Name { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/Contracts/IFruitService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System.Collections.Generic;
5 |
6 | namespace Contracts
7 | {
8 | public interface IFruitService
9 | {
10 | List GetFruits();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/Contracts/IMixerService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | namespace Contracts
5 | {
6 | public interface IMixerService
7 | {
8 | string MixIt();
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/Contracts/IPluginFactory.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using Microsoft.Extensions.DependencyInjection;
5 |
6 | namespace Contracts
7 | {
8 | public interface IPluginFactory
9 | {
10 | void Configure(IServiceCollection services);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/DynamicImplementation.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.28307.757
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Host", "Host\Host.csproj", "{52016A46-23E0-48BB-AB79-CA13BECB55DF}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contracts", "Contracts\Contracts.csproj", "{138FD514-7F88-43A5-A3CD-6084ABA316CD}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceImplementation", "ServiceImplementation\ServiceImplementation.csproj", "{A8104C41-1224-4626-A812-ED561BD35432}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mixer", "Mixer\Mixer.csproj", "{DB9CE54F-D2A1-454A-BF58-19F41D4F3E9B}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {52016A46-23E0-48BB-AB79-CA13BECB55DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {52016A46-23E0-48BB-AB79-CA13BECB55DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {52016A46-23E0-48BB-AB79-CA13BECB55DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {52016A46-23E0-48BB-AB79-CA13BECB55DF}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {138FD514-7F88-43A5-A3CD-6084ABA316CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {138FD514-7F88-43A5-A3CD-6084ABA316CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {138FD514-7F88-43A5-A3CD-6084ABA316CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {138FD514-7F88-43A5-A3CD-6084ABA316CD}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {A8104C41-1224-4626-A812-ED561BD35432}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {A8104C41-1224-4626-A812-ED561BD35432}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {A8104C41-1224-4626-A812-ED561BD35432}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {A8104C41-1224-4626-A812-ED561BD35432}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {DB9CE54F-D2A1-454A-BF58-19F41D4F3E9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {DB9CE54F-D2A1-454A-BF58-19F41D4F3E9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {DB9CE54F-D2A1-454A-BF58-19F41D4F3E9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {DB9CE54F-D2A1-454A-BF58-19F41D4F3E9B}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(ExtensibilityGlobals) = postSolution
41 | SolutionGuid = {98A28D5B-F00C-44F0-A5DA-80A16E551E57}
42 | EndGlobalSection
43 | EndGlobal
44 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/Host/Host.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/Host/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Linq;
8 | using Contracts;
9 | using McMaster.NETCore.Plugins;
10 | using Microsoft.Extensions.DependencyInjection;
11 |
12 | namespace Host
13 | {
14 | internal class Program
15 | {
16 | public static void Main(string[] args)
17 | {
18 | var services = new ServiceCollection();
19 | var loaders = GetPluginLoaders();
20 |
21 | ConfigureServices(services, loaders);
22 |
23 | var serviceProvider = services.BuildServiceProvider();
24 |
25 | var mixer = serviceProvider.GetRequiredService();
26 | mixer.MixIt();
27 | }
28 |
29 | private static List GetPluginLoaders()
30 | {
31 | var loaders = new List();
32 |
33 | // create plugin loaders
34 | var pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins");
35 | foreach (var dir in Directory.GetDirectories(pluginsDir))
36 | {
37 | var dirName = Path.GetFileName(dir);
38 | var pluginDll = Path.Combine(dir, dirName + ".dll");
39 | if (File.Exists(pluginDll))
40 | {
41 | var loader = PluginLoader.CreateFromAssemblyFile(
42 | pluginDll,
43 | sharedTypes: new[] { typeof(IPluginFactory), typeof(IServiceCollection) });
44 | loaders.Add(loader);
45 | }
46 | }
47 |
48 | return loaders;
49 | }
50 |
51 | private static void ConfigureServices(ServiceCollection services, List loaders)
52 | {
53 | // Create an instance of plugin types
54 | foreach (var loader in loaders)
55 | {
56 | foreach (var pluginType in loader
57 | .LoadDefaultAssembly()
58 | .GetTypes()
59 | .Where(t => typeof(IPluginFactory).IsAssignableFrom(t) && !t.IsAbstract))
60 | {
61 | // This assumes the implementation of IPluginFactory has a parameterless constructor
62 | var plugin = Activator.CreateInstance(pluginType) as IPluginFactory;
63 |
64 | plugin?.Configure(services);
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/Mixer/Mixer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | Library
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/Mixer/MixerPlugin.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using Contracts;
5 | using Microsoft.Extensions.DependencyInjection;
6 |
7 | namespace Mixer
8 | {
9 | public class MixerPluginConfiguration : IPluginFactory
10 | {
11 | public void Configure(IServiceCollection services)
12 | {
13 | services.AddSingleton();
14 | services.AddSingleton();
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/Mixer/MixerService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System.Linq;
5 | using Contracts;
6 |
7 | namespace Mixer
8 | {
9 | public class MixerService : IMixerService
10 | {
11 | protected IFruitService fruit;
12 | public MixerService(IFruitService fruit)
13 | {
14 | this.fruit = fruit;
15 | }
16 |
17 | public string MixIt()
18 | {
19 | return string.Join(",", fruit.GetFruits().Select(x => x.Name).ToArray());
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/Mixer/StandardFruiteService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System.Collections.Generic;
5 | using Contracts;
6 |
7 | namespace Mixer
8 | {
9 | public class StandardFruiteService : IFruitService
10 | {
11 | public List GetFruits()
12 | {
13 | return new List()
14 | {
15 | new Fruit { Name="Banana" },
16 | new Fruit { Name="Apple" },
17 | new Fruit { Name="Carrot" },
18 | };
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/README.md:
--------------------------------------------------------------------------------
1 | Dynamic implementation
2 | ===========================
3 |
4 | This sample contains 4 projects which demonstrate a plugin scenario that coordinates types between
5 | plugins using a dependency injection container. In this scenario, one implements a default service definition (Mixer), but adding a plugin the normal behavior is changed.
6 |
7 | This sample can be useful to create a pluggable application that can be extended or altered just by adding new modules.
8 |
9 | * 'Host': console app that contains the sample
10 | * 'Contracts': which contains an interface shared by plugins and the host.
11 | * 'Mixer': this plugin contains the default mixer implementation. It contains the FruitService, which gives 3 fruits and the Mixer service that returns the fruit shake.
12 | * 'ServiceImplementation': this plugin contains an alternative version of the FruitService. By adding this to the plugin set, the default behavior is altered and the shake is composed by only Banana.
13 |
14 | ## Running the sample
15 |
16 | Open a command line to this folder and run, otherwise open the .sln file:
17 |
18 | ```
19 | dotnet restore
20 | dotnet run --project Host/
21 | ```
22 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/ServiceImplementation/OverrideFruiteService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System.Collections.Generic;
5 | using Contracts;
6 |
7 | namespace ServiceImplementation
8 | {
9 | public class OverrideFruiteService : IFruitService
10 | {
11 | public List GetFruits()
12 | {
13 | return new List()
14 | {
15 | new Fruit { Name="Banana" }
16 | };
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/ServiceImplementation/OverridePlugin.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using Contracts;
5 | using Microsoft.Extensions.DependencyInjection;
6 |
7 | namespace ServiceImplementation
8 | {
9 | public class OverridePluginConfiguration : IPluginFactory
10 | {
11 | public void Configure(IServiceCollection services)
12 | {
13 | //this service override the standard one. unload this plugin or comment this to use the basic service
14 | services.AddSingleton();
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/samples/dynamic-implementation/ServiceImplementation/ServiceImplementation.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Library
5 | net8.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/samples/hello-world/HostApp/HostApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/samples/hello-world/HostApp/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Linq;
8 | using HelloWorld;
9 | using McMaster.NETCore.Plugins;
10 |
11 | var loaders = new List();
12 |
13 | // create plugin loaders
14 | var pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins");
15 | foreach (var dir in Directory.GetDirectories(pluginsDir))
16 | {
17 | var dirName = Path.GetFileName(dir);
18 | var pluginDll = Path.Combine(dir, dirName + ".dll");
19 | if (File.Exists(pluginDll))
20 | {
21 | var loader = PluginLoader.CreateFromAssemblyFile(
22 | pluginDll,
23 | sharedTypes: new[] { typeof(IPlugin) });
24 | loaders.Add(loader);
25 | }
26 | }
27 |
28 | // Create an instance of plugin types
29 | foreach (var loader in loaders)
30 | {
31 | foreach (var pluginType in loader
32 | .LoadDefaultAssembly()
33 | .GetTypes()
34 | .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract))
35 | {
36 | // This assumes the implementation of IPlugin has a parameterless constructor
37 | var plugin = Activator.CreateInstance(pluginType) as IPlugin;
38 |
39 | Console.WriteLine($"Created plugin instance '{plugin?.GetName()}'.");
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/samples/hello-world/MyPlugin/MyPlugin.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/samples/hello-world/MyPlugin/MyPlugin1.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using HelloWorld;
5 |
6 | namespace MyPlugin
7 | {
8 | internal class MyPlugin1 : IPlugin
9 | {
10 | public string GetName() => "My plugin v1";
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/samples/hello-world/PluginContract/IPlugin.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | namespace HelloWorld
5 | {
6 | ///
7 | /// This interface is an example of one way to define the interactions between the host and plugins.
8 | /// There is nothing special about the name "IPlugin"; it's used here to illustrate a concept.
9 | /// Look at https://github.com/natemcmaster/DotNetCorePlugins/tree/main/samples for additional examples
10 | /// of ways you could define the interaction between host and plugins.
11 | ///
12 | public interface IPlugin
13 | {
14 | string GetName();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/samples/hello-world/PluginContract/PluginContract.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/samples/hello-world/README.md:
--------------------------------------------------------------------------------
1 | "Hello World" Sample
2 | ====================
3 |
4 | This sample contains 3 projects which demonstrate a simple plugin scenario.
5 |
6 | 1. 'HostApp' is a console application which scans for a 'plugins' folder in its base directory and attempts to load any plugins it finds
7 | 2. 'MyPlugin' which implements an implementation of this plugin
8 | 3. 'PluginContract' which contains an interface shared by plugins and the host.
9 |
10 | ## Running the sample
11 |
12 | Open a command line to this folder and run:
13 |
14 | ```
15 | dotnet restore
16 | dotnet run --project HostApp/
17 | ```
18 |
--------------------------------------------------------------------------------
/samples/hello-world/hello-world.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostApp", "HostApp\HostApp.csproj", "{FAA91115-37CE-4A74-8EC1-C58E15E5D683}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyPlugin", "MyPlugin\MyPlugin.csproj", "{EF895D6F-B174-46AF-936C-F4E3FE2B96E0}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginContract", "PluginContract\PluginContract.csproj", "{5F3E90BD-7D9E-4469-A89A-63B0A8F77465}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Debug|x64 = Debug|x64
16 | Debug|x86 = Debug|x86
17 | Release|Any CPU = Release|Any CPU
18 | Release|x64 = Release|x64
19 | Release|x86 = Release|x86
20 | EndGlobalSection
21 | GlobalSection(SolutionProperties) = preSolution
22 | HideSolutionNode = FALSE
23 | EndGlobalSection
24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
25 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Debug|x64.ActiveCfg = Debug|Any CPU
28 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Debug|x64.Build.0 = Debug|Any CPU
29 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Debug|x86.ActiveCfg = Debug|Any CPU
30 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Debug|x86.Build.0 = Debug|Any CPU
31 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Release|Any CPU.Build.0 = Release|Any CPU
33 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Release|x64.ActiveCfg = Release|Any CPU
34 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Release|x64.Build.0 = Release|Any CPU
35 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Release|x86.ActiveCfg = Release|Any CPU
36 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Release|x86.Build.0 = Release|Any CPU
37 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Debug|x64.ActiveCfg = Debug|Any CPU
40 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Debug|x64.Build.0 = Debug|Any CPU
41 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Debug|x86.ActiveCfg = Debug|Any CPU
42 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Debug|x86.Build.0 = Debug|Any CPU
43 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
44 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Release|Any CPU.Build.0 = Release|Any CPU
45 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Release|x64.ActiveCfg = Release|Any CPU
46 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Release|x64.Build.0 = Release|Any CPU
47 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Release|x86.ActiveCfg = Release|Any CPU
48 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Release|x86.Build.0 = Release|Any CPU
49 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
50 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Debug|Any CPU.Build.0 = Debug|Any CPU
51 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Debug|x64.ActiveCfg = Debug|Any CPU
52 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Debug|x64.Build.0 = Debug|Any CPU
53 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Debug|x86.ActiveCfg = Debug|Any CPU
54 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Debug|x86.Build.0 = Debug|Any CPU
55 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Release|Any CPU.ActiveCfg = Release|Any CPU
56 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Release|Any CPU.Build.0 = Release|Any CPU
57 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Release|x64.ActiveCfg = Release|Any CPU
58 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Release|x64.Build.0 = Release|Any CPU
59 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Release|x86.ActiveCfg = Release|Any CPU
60 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Release|x86.Build.0 = Release|Any CPU
61 | EndGlobalSection
62 | EndGlobal
63 |
--------------------------------------------------------------------------------
/samples/hot-reload/HotReloadApp/HotReloadApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/samples/hot-reload/HotReloadApp/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using McMaster.NETCore.Plugins;
8 |
9 | var pluginPath = args[0];
10 | var loader = PluginLoader.CreateFromAssemblyFile(pluginPath,
11 | config => config.EnableHotReload = true);
12 |
13 | loader.Reloaded += ShowPluginInfo;
14 |
15 | var cts = new CancellationTokenSource();
16 | Console.CancelKeyPress += (_, __) => cts.Cancel();
17 |
18 | // Show info on first load
19 | InvokePlugin(loader);
20 |
21 | await Task.Delay(-1, cts.Token);
22 |
23 | static void ShowPluginInfo(object sender, PluginReloadedEventArgs eventArgs)
24 | {
25 | Console.ForegroundColor = ConsoleColor.Blue;
26 | Console.Write("HotReloadApp: ");
27 | Console.ResetColor();
28 | Console.WriteLine("plugin was reloaded");
29 | InvokePlugin(eventArgs.Loader);
30 | }
31 |
32 | static void InvokePlugin(PluginLoader loader)
33 | {
34 | var assembly = loader.LoadDefaultAssembly();
35 | assembly
36 | .GetType("TimestampedPlugin.InfoDisplayer", throwOnError: true)
37 | !.GetMethod("Print")
38 | !.Invoke(null, null);
39 | }
40 |
--------------------------------------------------------------------------------
/samples/hot-reload/README.md:
--------------------------------------------------------------------------------
1 | # Hot Reload Sample
2 |
3 | This is a minimal sample of how to take advantage of hot reloading support.
4 |
5 | To run this sample, execute the `run.sh` script. This will:
6 |
7 | * Compile the projects once
8 | * Start the HotReloadApp console application
9 | * This creates a single loader with hot reloading enabled
10 | * It subscribes to the `PluginLoader.Reloaded` event to be notified when a new version of the assemblies
11 | are availabled.
12 | * It invokes the new version of the assembly
13 | * Rebuilds the TimestampedPlugin every 5 seconds (until you press CTRL+C to exit)
14 |
--------------------------------------------------------------------------------
/samples/hot-reload/TimestampedPlugin/InfoDisplayer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Linq;
6 | using System.Reflection;
7 | using Microsoft.Data.Sqlite;
8 |
9 | namespace TimestampedPlugin
10 | {
11 | public class InfoDisplayer
12 | {
13 | public static void Print()
14 | {
15 | // Use something from Microsoft.Data.Sqlite to trigger loading of native dependency
16 | var connectionString = new SqliteConnectionStringBuilder
17 | {
18 | DataSource = "HELLO"
19 | };
20 |
21 | var compileTimestamp = typeof(InfoDisplayer)
22 | .Assembly
23 | .GetCustomAttributes()
24 | .First(a => a.Key == "CompileTimestamp")
25 | .Value;
26 | Console.ForegroundColor = ConsoleColor.Green;
27 | Console.Write("TimestampedPlugin: ");
28 | Console.ResetColor();
29 | Console.WriteLine($"this plugin was compiled at {compileTimestamp}. {connectionString.DataSource}!");
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/samples/hot-reload/TimestampedPlugin/TimestampedPlugin.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 | <_Parameter1>CompileTimestamp
10 | <_Parameter2>$([System.DateTime]::Now.ToString('h:mm:ss tt'))
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/samples/hot-reload/hot-reload.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29209.152
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotReloadApp", "HotReloadApp\HotReloadApp.csproj", "{421F2562-390A-45C3-B028-32A0027AD8BD}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TimestampedPlugin", "TimestampedPlugin\TimestampedPlugin.csproj", "{92DCE5FD-1A77-4A77-AE04-2820004A446B}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Debug|x64 = Debug|x64
14 | Debug|x86 = Debug|x86
15 | Release|Any CPU = Release|Any CPU
16 | Release|x64 = Release|x64
17 | Release|x86 = Release|x86
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Debug|x64.ActiveCfg = Debug|Any CPU
26 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Debug|x64.Build.0 = Debug|Any CPU
27 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Debug|x86.ActiveCfg = Debug|Any CPU
28 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Debug|x86.Build.0 = Debug|Any CPU
29 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Release|x64.ActiveCfg = Release|Any CPU
32 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Release|x64.Build.0 = Release|Any CPU
33 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Release|x86.ActiveCfg = Release|Any CPU
34 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Release|x86.Build.0 = Release|Any CPU
35 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Debug|Any CPU.Build.0 = Debug|Any CPU
37 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Debug|x64.ActiveCfg = Debug|Any CPU
38 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Debug|x64.Build.0 = Debug|Any CPU
39 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Debug|x86.ActiveCfg = Debug|Any CPU
40 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Debug|x86.Build.0 = Debug|Any CPU
41 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Release|Any CPU.ActiveCfg = Release|Any CPU
42 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Release|Any CPU.Build.0 = Release|Any CPU
43 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Release|x64.ActiveCfg = Release|Any CPU
44 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Release|x64.Build.0 = Release|Any CPU
45 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Release|x86.ActiveCfg = Release|Any CPU
46 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Release|x86.Build.0 = Release|Any CPU
47 | EndGlobalSection
48 | EndGlobal
49 |
--------------------------------------------------------------------------------
/samples/hot-reload/run.ps1:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env pwsh
2 |
3 | $ErrorActionPreference = 'Stop'
4 |
5 | Push-Location $PSScriptRoot
6 | try {
7 | $publish_dir = "$PSScriptRoot/bin/plugins/TimestampedPlugin/"
8 |
9 | function log {
10 | Write-Host -NoNewline -ForegroundColor Yellow "run.ps1: "
11 | Write-Host $args
12 | }
13 |
14 | function publish {
15 | Write-Host ""
16 | dotnet publish --no-restore TimestampedPlugin/ -o $publish_dir -nologo
17 | Write-Host ""
18 | }
19 |
20 | log "Compiling apps"
21 |
22 | & dotnet build HotReloadApp -nologo -clp:NoSummary
23 | & publish
24 |
25 | log "Use CTRL+C to exit"
26 |
27 | $bg_args = @("run", "--no-build", "--project", "HotReloadApp", "$publish_dir/TimestampedPlugin.dll")
28 | $host_process = Start-Process -NoNewWindow -FilePath dotnet -ArgumentList $bg_args
29 | try {
30 | while ($true) {
31 | Start-Sleep 5
32 | log "Rebuilding plugin..."
33 | publish
34 | }
35 | }
36 | finally {
37 | $host_process.Kill()
38 | }
39 | }
40 | finally {
41 | Pop-Location
42 | }
43 |
--------------------------------------------------------------------------------
/samples/hot-reload/run.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 |
5 | RESET="\033[0m"
6 | YELLOW="\033[0;33m"
7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
8 | pushd $DIR >/dev/null
9 |
10 | publish_dir="$DIR/bin/plugins/TimestampedPlugin/"
11 |
12 | publish() {
13 | echo ""
14 | dotnet publish --no-restore TimestampedPlugin/ -o $publish_dir -nologo
15 | echo ""
16 | }
17 |
18 | echo -e "${YELLOW}run.sh:${RESET} Compiling apps"
19 | dotnet build HotReloadApp -nologo -clp:NoSummary
20 | publish
21 |
22 | trap "kill 0" EXIT
23 |
24 | echo -e "${YELLOW}run.sh:${RESET} Use CTRL+C to exit"
25 |
26 | dotnet run --no-build --project HotReloadApp -- "$publish_dir/TimestampedPlugin.dll" &
27 |
28 | while true
29 | do
30 | sleep 5
31 | echo -e "${YELLOW}run.sh:${RESET} Rebuilding plugin..."
32 | publish
33 | done
34 |
--------------------------------------------------------------------------------
/src/Directory.Build.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/Plugins.Mvc/McMaster.NETCore.Plugins.Mvc.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | library
6 | true
7 | true
8 | Provides API for dynamically loading MVC controllers into an ASP.NET Core web application.
9 |
10 | This package should be used by the host application which needs to load plugins.
11 | See https://github.com/natemcmaster/DotNetCorePlugins/blob/main/README.md for more samples and documentation.
12 |
13 | .NET Core;plugins;aspnetcore
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/Plugins.Mvc/MvcPluginExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System.Reflection;
5 | using McMaster.NETCore.Plugins;
6 | using Microsoft.AspNetCore.Mvc.ApplicationParts;
7 |
8 | namespace Microsoft.Extensions.DependencyInjection
9 | {
10 | ///
11 | /// Extends the MVC builder.
12 | ///
13 | public static class MvcPluginExtensions
14 | {
15 | ///
16 | /// Loads controllers and razor pages from a plugin assembly.
17 | ///
18 | /// This creates a loader with set to true.
19 | /// If you need more control over shared types, use instead.
20 | ///
21 | ///
22 | /// The MVC builder
23 | /// Full path the main .dll file for the plugin.
24 | /// The builder
25 | public static IMvcBuilder AddPluginFromAssemblyFile(this IMvcBuilder mvcBuilder, string assemblyFile)
26 | {
27 | var plugin = PluginLoader.CreateFromAssemblyFile(
28 | assemblyFile, // create a plugin from for the .dll file
29 | config =>
30 | // this ensures that the version of MVC is shared between this app and the plugin
31 | config.PreferSharedTypes = true);
32 |
33 | return mvcBuilder.AddPluginLoader(plugin);
34 | }
35 |
36 | ///
37 | /// Loads controllers and razor pages from a plugin loader.
38 | ///
39 | /// In order for this to work, the PluginLoader instance must be configured to share the types
40 | /// and
41 | /// (comes from Microsoft.AspNetCore.Mvc.Core.dll). The easiest way to ensure that is done correctly
42 | /// is to set to true.
43 | ///
44 | ///
45 | /// The MVC builder
46 | /// An instance of PluginLoader.
47 | /// The builder
48 | public static IMvcBuilder AddPluginLoader(this IMvcBuilder mvcBuilder, PluginLoader pluginLoader)
49 | {
50 | var pluginAssembly = pluginLoader.LoadDefaultAssembly();
51 |
52 | // This loads MVC application parts from plugin assemblies
53 | var partFactory = ApplicationPartFactory.GetApplicationPartFactory(pluginAssembly);
54 | foreach (var part in partFactory.GetApplicationParts(pluginAssembly))
55 | {
56 | mvcBuilder.PartManager.ApplicationParts.Add(part);
57 | }
58 |
59 | // This piece finds and loads related parts, such as MvcAppPlugin1.Views.dll.
60 | var relatedAssembliesAttrs = pluginAssembly.GetCustomAttributes();
61 | foreach (var attr in relatedAssembliesAttrs)
62 | {
63 | var assembly = pluginLoader.LoadAssembly(attr.AssemblyFileName);
64 | partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly);
65 | foreach (var part in partFactory.GetApplicationParts(assembly))
66 | {
67 | mvcBuilder.PartManager.ApplicationParts.Add(part);
68 | }
69 | }
70 |
71 | return mvcBuilder;
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Plugins.Mvc/PublicAPI.Shipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 | Microsoft.Extensions.DependencyInjection.MvcPluginExtensions
3 | static Microsoft.Extensions.DependencyInjection.MvcPluginExtensions.AddPluginFromAssemblyFile(this Microsoft.Extensions.DependencyInjection.IMvcBuilder! mvcBuilder, string! assemblyFile) -> Microsoft.Extensions.DependencyInjection.IMvcBuilder!
4 | static Microsoft.Extensions.DependencyInjection.MvcPluginExtensions.AddPluginLoader(this Microsoft.Extensions.DependencyInjection.IMvcBuilder! mvcBuilder, McMaster.NETCore.Plugins.PluginLoader! pluginLoader) -> Microsoft.Extensions.DependencyInjection.IMvcBuilder!
5 |
--------------------------------------------------------------------------------
/src/Plugins.Mvc/PublicAPI.Unshipped.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natemcmaster/DotNetCorePlugins/a6094157bfd1ace9d6b13df108c488526cfe850f/src/Plugins.Mvc/PublicAPI.Unshipped.txt
--------------------------------------------------------------------------------
/src/Plugins.Mvc/releasenotes.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Breaking change: require .NET >= 8.0
5 |
6 | See also https://nuget.org/packages/McMaster.NETCore.Plugins/$(VersionPrefix) for its release notes.
7 |
8 |
9 | No changes to this library, but updated to use 1.1.0 of Microsoft.NETCore.Plugins.
10 | See https://nuget.org/packages/McMaster.NETCore.Plugins/$(VersionPrefix) for its release notes.
11 |
12 |
13 | Changes:
14 | * Add new API: IMvcBuilder.AddPluginLoader(PluginLoader pluginLoader)
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Plugins/Internal/Debouncer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace McMaster.NETCore.Plugins.Internal
9 | {
10 | internal class Debouncer : IDisposable
11 | {
12 | private readonly CancellationTokenSource _cts = new();
13 | private readonly TimeSpan _waitTime;
14 | private int _counter;
15 |
16 | public Debouncer(TimeSpan waitTime)
17 | {
18 | _waitTime = waitTime;
19 | }
20 |
21 | public void Execute(Action action)
22 | {
23 | var current = Interlocked.Increment(ref _counter);
24 |
25 | Task.Delay(_waitTime).ContinueWith(task =>
26 | {
27 | // Is this the last task that was queued?
28 | if (current == _counter && !_cts.IsCancellationRequested)
29 | {
30 | action();
31 | }
32 |
33 | task.Dispose();
34 | }, _cts.Token);
35 | }
36 |
37 | public void Dispose()
38 | {
39 | _cts.Cancel();
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Plugins/Internal/PlatformInformation.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Diagnostics;
6 | using System.Runtime.InteropServices;
7 |
8 | namespace McMaster.NETCore.Plugins
9 | {
10 | internal class PlatformInformation
11 | {
12 | public static readonly string[] NativeLibraryExtensions;
13 | public static readonly string[] NativeLibraryPrefixes;
14 | public static readonly string[] ManagedAssemblyExtensions = new[]
15 | {
16 | ".dll",
17 | ".ni.dll",
18 | ".exe",
19 | ".ni.exe"
20 | };
21 |
22 | static PlatformInformation()
23 | {
24 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
25 | {
26 | NativeLibraryPrefixes = new[] { "" };
27 | NativeLibraryExtensions = new[] { ".dll" };
28 | }
29 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
30 | {
31 | NativeLibraryPrefixes = new[] { "", "lib", };
32 | NativeLibraryExtensions = new[] { ".dylib" };
33 | }
34 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
35 | {
36 | NativeLibraryPrefixes = new[] { "", "lib" };
37 | NativeLibraryExtensions = new[] { ".so", ".so.1" };
38 | }
39 | else
40 | {
41 | Debug.Fail("Unknown OS type");
42 | NativeLibraryPrefixes = Array.Empty();
43 | NativeLibraryExtensions = Array.Empty();
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Plugins/Internal/RuntimeConfig.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 |
6 | namespace McMaster.NETCore.Plugins
7 | {
8 | internal class RuntimeConfig
9 | {
10 | public RuntimeOptions? RuntimeOptions { get; set; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Plugins/Internal/RuntimeOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | namespace McMaster.NETCore.Plugins
5 | {
6 | internal class RuntimeOptions
7 | {
8 | public string? Tfm { get; set; }
9 |
10 | public string[]? AdditionalProbingPaths { get; set; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Plugins/LibraryModel/ManagedLibrary.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Diagnostics;
6 | using System.IO;
7 | using System.Reflection;
8 |
9 | namespace McMaster.NETCore.Plugins.LibraryModel
10 | {
11 | ///
12 | /// Represents a managed, .NET assembly.
13 | ///
14 | [DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
15 | public class ManagedLibrary
16 | {
17 | private ManagedLibrary(AssemblyName name, string additionalProbingPath, string appLocalPath)
18 | {
19 | Name = name ?? throw new ArgumentNullException(nameof(name));
20 | AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
21 | AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
22 | }
23 |
24 | ///
25 | /// Name of the managed library
26 | ///
27 | public AssemblyName Name { get; private set; }
28 |
29 | ///
30 | /// Contains path to file within an additional probing path root. This is typically a combination
31 | /// of the NuGet package ID (lowercased), version, and path within the package.
32 | ///
33 | /// For example, microsoft.data.sqlite/1.0.0/lib/netstandard1.3/Microsoft.Data.Sqlite.dll
34 | ///
35 | ///
36 | public string AdditionalProbingPath { get; private set; }
37 |
38 | ///
39 | /// Contains path to file within a deployed, framework-dependent application.
40 | ///
41 | /// For most managed libraries, this will be the file name.
42 | /// For example, MyPlugin1.dll.
43 | ///
44 | ///
45 | /// For runtime-specific managed implementations, this may include a sub folder path.
46 | /// For example, runtimes/win/lib/netcoreapp2.0/System.Diagnostics.EventLog.dll
47 | ///
48 | ///
49 | public string AppLocalPath { get; private set; }
50 |
51 | ///
52 | /// Create an instance of from a NuGet package.
53 | ///
54 | /// The name of the package.
55 | /// The version of the package.
56 | /// The path within the NuGet package.
57 | ///
58 | public static ManagedLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
59 | {
60 | // When the asset comes from "lib/$tfm/", Microsoft.NET.Sdk will flatten this during publish based on the most compatible TFM.
61 | // The SDK will not flatten managed libraries found under runtimes/
62 | var appLocalPath = assetPath.StartsWith("lib/")
63 | ? Path.GetFileName(assetPath)
64 | : assetPath;
65 |
66 | return new ManagedLibrary(
67 | new AssemblyName(Path.GetFileNameWithoutExtension(assetPath)),
68 | Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath),
69 | appLocalPath
70 | );
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Plugins/LibraryModel/NativeLibrary.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Diagnostics;
6 | using System.IO;
7 |
8 | namespace McMaster.NETCore.Plugins.LibraryModel
9 | {
10 | ///
11 | /// Represents an unmanaged library, such as `libsqlite3`, which may need to be loaded
12 | /// for P/Invoke to work.
13 | ///
14 | [DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
15 | public class NativeLibrary
16 | {
17 | private NativeLibrary(string name, string appLocalPath, string additionalProbingPath)
18 | {
19 | Name = name ?? throw new ArgumentNullException(nameof(name));
20 | AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
21 | AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
22 | }
23 |
24 | ///
25 | /// Name of the native library. This should match the name of the P/Invoke call.
26 | ///
27 | /// For example, if specifying `[DllImport("sqlite3")]`, should be sqlite3.
28 | /// This may not match the exact file name as loading will attempt variations on the name according
29 | /// to OS convention. On Windows, P/Invoke will attempt to load `sqlite3.dll`. On macOS, it will
30 | /// attempt to find `sqlite3.dylib` and `libsqlite3.dylib`. On Linux, it will attempt to find
31 | /// `sqlite3.so` and `libsqlite3.so`.
32 | ///
33 | ///
34 | public string Name { get; private set; }
35 |
36 | ///
37 | /// Contains path to file within a deployed, framework-dependent application
38 | ///
39 | /// For example, runtimes/linux-x64/native/libsqlite.so
40 | ///
41 | ///
42 | public string AppLocalPath { get; private set; }
43 |
44 | ///
45 | /// Contains path to file within an additional probing path root. This is typically a combination
46 | /// of the NuGet package ID (lowercased), version, and path within the package.
47 | ///
48 | /// For example, sqlite/3.13.3/runtimes/linux-x64/native/libsqlite.so
49 | ///
50 | ///
51 | public string AdditionalProbingPath { get; private set; }
52 |
53 | ///
54 | /// Create an instance of from a NuGet package.
55 | ///
56 | /// The name of the package.
57 | /// The version of the package.
58 | /// The path within the NuGet package.
59 | ///
60 | public static NativeLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
61 | {
62 | return new NativeLibrary(
63 | Path.GetFileNameWithoutExtension(assetPath),
64 | assetPath,
65 | Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath)
66 | );
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Plugins/Loader/RuntimeConfigExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Diagnostics;
6 | using System.IO;
7 | using System.Runtime.InteropServices;
8 | using System.Text.Json;
9 |
10 | namespace McMaster.NETCore.Plugins.Loader
11 | {
12 | ///
13 | /// Extensions for creating a load context using settings from a runtimeconfig.json file
14 | ///
15 | public static class RuntimeConfigExtensions
16 | {
17 | private const string JsonExt = ".json";
18 | private static readonly JsonSerializerOptions s_serializerOptions = new()
19 | {
20 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase
21 | };
22 |
23 | ///
24 | /// Adds additional probing paths to a managed load context using settings found in the runtimeconfig.json
25 | /// and runtimeconfig.dev.json files.
26 | ///
27 | /// The context builder
28 | /// The path to the runtimeconfig.json file
29 | /// Also read runtimeconfig.dev.json file, if present.
30 | /// The error, if one occurs while parsing runtimeconfig.json
31 | /// The builder.
32 | public static AssemblyLoadContextBuilder TryAddAdditionalProbingPathFromRuntimeConfig(
33 | this AssemblyLoadContextBuilder builder,
34 | string runtimeConfigPath,
35 | bool includeDevConfig,
36 | out Exception? error)
37 | {
38 | error = null;
39 | try
40 | {
41 | var config = TryReadConfig(runtimeConfigPath);
42 | if (config == null)
43 | {
44 | return builder;
45 | }
46 |
47 | RuntimeConfig? devConfig = null;
48 | if (includeDevConfig)
49 | {
50 | var configDevPath = runtimeConfigPath.Substring(0, runtimeConfigPath.Length - JsonExt.Length) + ".dev.json";
51 | devConfig = TryReadConfig(configDevPath);
52 | }
53 |
54 | var tfm = config.RuntimeOptions?.Tfm ?? devConfig?.RuntimeOptions?.Tfm;
55 |
56 | if (config.RuntimeOptions != null)
57 | {
58 | AddProbingPaths(builder, config.RuntimeOptions, tfm);
59 | }
60 |
61 | if (devConfig?.RuntimeOptions != null)
62 | {
63 | AddProbingPaths(builder, devConfig.RuntimeOptions, tfm);
64 | }
65 |
66 | if (tfm != null)
67 | {
68 | var dotnet = Process.GetCurrentProcess().MainModule?.FileName;
69 | if (dotnet != null && string.Equals(Path.GetFileNameWithoutExtension(dotnet), "dotnet", StringComparison.OrdinalIgnoreCase))
70 | {
71 | var dotnetHome = Path.GetDirectoryName(dotnet);
72 | if (dotnetHome != null)
73 | {
74 | builder.AddProbingPath(Path.Combine(dotnetHome, "store", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(), tfm));
75 | }
76 | }
77 | }
78 | }
79 | catch (Exception ex)
80 | {
81 | error = ex;
82 | }
83 | return builder;
84 | }
85 |
86 | private static void AddProbingPaths(AssemblyLoadContextBuilder builder, RuntimeOptions options, string? tfm)
87 | {
88 | if (options.AdditionalProbingPaths == null)
89 | {
90 | return;
91 | }
92 |
93 | foreach (var item in options.AdditionalProbingPaths)
94 | {
95 | var path = item;
96 | if (path.Contains("|arch|"))
97 | {
98 | path = path.Replace("|arch|", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant());
99 | }
100 |
101 | if (path.Contains("|tfm|"))
102 | {
103 | if (tfm == null)
104 | {
105 | // We don't have enough information to parse this
106 | continue;
107 | }
108 |
109 | path = path.Replace("|tfm|", tfm);
110 | }
111 |
112 | builder.AddProbingPath(path);
113 | }
114 | }
115 |
116 | private static RuntimeConfig? TryReadConfig(string path)
117 | {
118 | try
119 | {
120 | var file = File.ReadAllBytes(path);
121 | return JsonSerializer.Deserialize(file, s_serializerOptions);
122 | }
123 | catch
124 | {
125 | return null;
126 | }
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/Plugins/McMaster.NETCore.Plugins.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | library
6 | true
7 | true
8 | Provides API for dynamically loading assemblies into a .NET application.
9 |
10 | This package should be used by the host application which needs to load plugins.
11 | See https://github.com/natemcmaster/DotNetCorePlugins/blob/main/README.md for more samples and documentation.
12 |
13 | .NET Core;plugins
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/Plugins/PluginConfig.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Reflection;
8 | using System.Runtime.Loader;
9 |
10 | namespace McMaster.NETCore.Plugins
11 | {
12 | ///
13 | /// Represents the configuration for a .NET Core plugin.
14 | ///
15 | public class PluginConfig
16 | {
17 | ///
18 | /// Initializes a new instance of
19 | ///
20 | /// The full file path to the main assembly for the plugin.
21 | public PluginConfig(string mainAssemblyPath)
22 | {
23 | if (string.IsNullOrEmpty(mainAssemblyPath))
24 | {
25 | throw new ArgumentException("Value must be null or not empty", nameof(mainAssemblyPath));
26 | }
27 |
28 | if (!Path.IsPathRooted(mainAssemblyPath))
29 | {
30 | throw new ArgumentException("Value must be an absolute file path", nameof(mainAssemblyPath));
31 | }
32 |
33 | MainAssemblyPath = mainAssemblyPath;
34 | }
35 |
36 | ///
37 | /// The file path to the main assembly.
38 | ///
39 | public string MainAssemblyPath { get; }
40 |
41 | ///
42 | /// A list of assemblies which should be treated as private.
43 | ///
44 | public ICollection PrivateAssemblies { get; protected set; } = new List();
45 |
46 | ///
47 | /// A list of assemblies which should be unified between the host and the plugin.
48 | ///
49 | ///
50 | /// https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md
51 | ///
52 | public ICollection SharedAssemblies { get; protected set; } = new List();
53 |
54 | ///
55 | /// Attempt to unify all types from a plugin with the host.
56 | ///
57 | /// This does not guarantee types will unify.
58 | ///
59 | ///
60 | /// https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md
61 | ///
62 | ///
63 | public bool PreferSharedTypes { get; set; }
64 |
65 | ///
66 | /// If enabled, will lazy load dependencies of all shared assemblies.
67 | /// Reduces plugin load time at the expense of non-determinism in how transitive dependencies are loaded
68 | /// between the plugin and the host.
69 | ///
70 | /// Please be aware of the danger of using this option:
71 | ///
72 | /// https://github.com/natemcmaster/DotNetCorePlugins/pull/164#issuecomment-751557873
73 | ///
74 | ///
75 | public bool IsLazyLoaded { get; set; } = false;
76 |
77 | ///
78 | /// If set, replaces the default used by the .
79 | /// Use this feature if the of the is not the Runtime's default load context.
80 | /// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) !=
81 | ///
82 | public AssemblyLoadContext DefaultContext { get; set; } = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
83 |
84 | private bool _isUnloadable;
85 |
86 | ///
87 | /// The plugin can be unloaded from memory.
88 | ///
89 | public bool IsUnloadable
90 | {
91 | get => _isUnloadable || EnableHotReload;
92 | set => _isUnloadable = value;
93 | }
94 |
95 | private bool _loadInMemory;
96 |
97 | ///
98 | /// Loads assemblies into memory in order to not lock files.
99 | /// As example use case here would be: no hot reloading but able to
100 | /// replace files and reload manually at later time
101 | ///
102 | public bool LoadInMemory
103 | {
104 | get => _loadInMemory || EnableHotReload;
105 | set => _loadInMemory = value;
106 | }
107 |
108 | ///
109 | /// When any of the loaded files changes on disk, the plugin will be reloaded.
110 | /// Use the event to be notified of changes.
111 | ///
112 | ///
113 | /// It will load assemblies into memory in order to not lock files
114 | ///
115 | ///
116 | public bool EnableHotReload { get; set; }
117 |
118 | ///
119 | /// Specifies the delay to reload a plugin, after file changes have been detected.
120 | /// Default value is 200 milliseconds.
121 | ///
122 | public TimeSpan ReloadDelay { get; set; } = TimeSpan.FromMilliseconds(200);
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/Plugins/PluginReloadedEventHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 |
6 | namespace McMaster.NETCore.Plugins
7 | {
8 | ///
9 | /// Represents the method that will handle the event.
10 | ///
11 | /// The object sending the event
12 | /// Data about the event.
13 | public delegate void PluginReloadedEventHandler(object sender, PluginReloadedEventArgs eventArgs);
14 |
15 | ///
16 | /// Provides data for the event.
17 | ///
18 | public class PluginReloadedEventArgs : EventArgs
19 | {
20 | ///
21 | /// Initializes .
22 | ///
23 | ///
24 | public PluginReloadedEventArgs(PluginLoader loader)
25 | {
26 | Loader = loader;
27 | }
28 |
29 | ///
30 | /// The plugin loader
31 | ///
32 | public PluginLoader Loader { get; }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Plugins/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System.Runtime.CompilerServices;
5 |
6 | [assembly: InternalsVisibleTo("McMaster.NETCore.Plugins.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001001df0eba4297c8ffdf114a13714ad787744619dfb18e29191703f6f782d6a09e4a4cac35b8c768cbbd9ade8197bc0f66ec66fabc9071a206c8060af8b7a332236968d3ee44b90bd2f30d0edcb6150555c6f8d988e48234debaf2d427a08d7c06ba1343411142dc8ac996f7f7dbe0e93d13f17a7624db5400510e6144b0fd683b9")]
7 |
--------------------------------------------------------------------------------
/src/Plugins/PublicAPI.Shipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 | McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary
3 | McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary.AdditionalProbingPath.get -> string!
4 | McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary.AppLocalPath.get -> string!
5 | McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary.Name.get -> System.Reflection.AssemblyName!
6 | McMaster.NETCore.Plugins.LibraryModel.NativeLibrary
7 | McMaster.NETCore.Plugins.LibraryModel.NativeLibrary.AdditionalProbingPath.get -> string!
8 | McMaster.NETCore.Plugins.LibraryModel.NativeLibrary.AppLocalPath.get -> string!
9 | McMaster.NETCore.Plugins.LibraryModel.NativeLibrary.Name.get -> string!
10 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder
11 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.AddManagedLibrary(McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary! library) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
12 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.AddNativeLibrary(McMaster.NETCore.Plugins.LibraryModel.NativeLibrary! library) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
13 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.AddProbingPath(string! path) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
14 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.AddResourceProbingPath(string! path) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
15 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.AssemblyLoadContextBuilder() -> void
16 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.Build() -> System.Runtime.Loader.AssemblyLoadContext!
17 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.EnableUnloading() -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
18 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.IsLazyLoaded(bool isLazyLoaded) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
19 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.PreferDefaultLoadContext(bool preferDefaultLoadContext) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
20 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.PreferDefaultLoadContextAssembly(System.Reflection.AssemblyName! assemblyName) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
21 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.PreferLoadContextAssembly(System.Reflection.AssemblyName! assemblyName) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
22 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.PreloadAssembliesIntoMemory() -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
23 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.SetDefaultContext(System.Runtime.Loader.AssemblyLoadContext! context) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
24 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.SetMainAssemblyPath(string! path) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
25 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.ShadowCopyNativeLibraries() -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
26 | McMaster.NETCore.Plugins.Loader.RuntimeConfigExtensions
27 | McMaster.NETCore.Plugins.PluginConfig
28 | McMaster.NETCore.Plugins.PluginConfig.DefaultContext.get -> System.Runtime.Loader.AssemblyLoadContext!
29 | McMaster.NETCore.Plugins.PluginConfig.DefaultContext.set -> void
30 | McMaster.NETCore.Plugins.PluginConfig.EnableHotReload.get -> bool
31 | McMaster.NETCore.Plugins.PluginConfig.EnableHotReload.set -> void
32 | McMaster.NETCore.Plugins.PluginConfig.IsLazyLoaded.get -> bool
33 | McMaster.NETCore.Plugins.PluginConfig.IsLazyLoaded.set -> void
34 | McMaster.NETCore.Plugins.PluginConfig.IsUnloadable.get -> bool
35 | McMaster.NETCore.Plugins.PluginConfig.IsUnloadable.set -> void
36 | McMaster.NETCore.Plugins.PluginConfig.LoadInMemory.get -> bool
37 | McMaster.NETCore.Plugins.PluginConfig.LoadInMemory.set -> void
38 | McMaster.NETCore.Plugins.PluginConfig.MainAssemblyPath.get -> string!
39 | McMaster.NETCore.Plugins.PluginConfig.PluginConfig(string! mainAssemblyPath) -> void
40 | McMaster.NETCore.Plugins.PluginConfig.PreferSharedTypes.get -> bool
41 | McMaster.NETCore.Plugins.PluginConfig.PreferSharedTypes.set -> void
42 | McMaster.NETCore.Plugins.PluginConfig.PrivateAssemblies.get -> System.Collections.Generic.ICollection!
43 | McMaster.NETCore.Plugins.PluginConfig.PrivateAssemblies.set -> void
44 | McMaster.NETCore.Plugins.PluginConfig.ReloadDelay.get -> System.TimeSpan
45 | McMaster.NETCore.Plugins.PluginConfig.ReloadDelay.set -> void
46 | McMaster.NETCore.Plugins.PluginConfig.SharedAssemblies.get -> System.Collections.Generic.ICollection!
47 | McMaster.NETCore.Plugins.PluginConfig.SharedAssemblies.set -> void
48 | McMaster.NETCore.Plugins.PluginLoader
49 | McMaster.NETCore.Plugins.PluginLoader.Dispose() -> void
50 | McMaster.NETCore.Plugins.PluginLoader.EnterContextualReflection() -> System.Runtime.Loader.AssemblyLoadContext.ContextualReflectionScope
51 | McMaster.NETCore.Plugins.PluginLoader.IsUnloadable.get -> bool
52 | McMaster.NETCore.Plugins.PluginLoader.LoadAssembly(string! assemblyName) -> System.Reflection.Assembly!
53 | McMaster.NETCore.Plugins.PluginLoader.LoadAssembly(System.Reflection.AssemblyName! assemblyName) -> System.Reflection.Assembly!
54 | McMaster.NETCore.Plugins.PluginLoader.LoadAssemblyFromPath(string! assemblyPath) -> System.Reflection.Assembly!
55 | McMaster.NETCore.Plugins.PluginLoader.LoadContext.get -> System.Runtime.Loader.AssemblyLoadContext!
56 | McMaster.NETCore.Plugins.PluginLoader.LoadDefaultAssembly() -> System.Reflection.Assembly!
57 | McMaster.NETCore.Plugins.PluginLoader.PluginLoader(McMaster.NETCore.Plugins.PluginConfig! config) -> void
58 | McMaster.NETCore.Plugins.PluginLoader.Reload() -> void
59 | McMaster.NETCore.Plugins.PluginLoader.Reloaded -> McMaster.NETCore.Plugins.PluginReloadedEventHandler?
60 | McMaster.NETCore.Plugins.PluginReloadedEventArgs
61 | McMaster.NETCore.Plugins.PluginReloadedEventArgs.Loader.get -> McMaster.NETCore.Plugins.PluginLoader!
62 | McMaster.NETCore.Plugins.PluginReloadedEventArgs.PluginReloadedEventArgs(McMaster.NETCore.Plugins.PluginLoader! loader) -> void
63 | McMaster.NETCore.Plugins.PluginReloadedEventHandler
64 | static McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary.CreateFromPackage(string! packageId, string! packageVersion, string! assetPath) -> McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary!
65 | static McMaster.NETCore.Plugins.LibraryModel.NativeLibrary.CreateFromPackage(string! packageId, string! packageVersion, string! assetPath) -> McMaster.NETCore.Plugins.LibraryModel.NativeLibrary!
66 | static McMaster.NETCore.Plugins.Loader.RuntimeConfigExtensions.TryAddAdditionalProbingPathFromRuntimeConfig(this McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! builder, string! runtimeConfigPath, bool includeDevConfig, out System.Exception? error) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
67 | static McMaster.NETCore.Plugins.PluginLoader.CreateFromAssemblyFile(string! assemblyFile) -> McMaster.NETCore.Plugins.PluginLoader!
68 | static McMaster.NETCore.Plugins.PluginLoader.CreateFromAssemblyFile(string! assemblyFile, bool isUnloadable, System.Type![]! sharedTypes) -> McMaster.NETCore.Plugins.PluginLoader!
69 | static McMaster.NETCore.Plugins.PluginLoader.CreateFromAssemblyFile(string! assemblyFile, bool isUnloadable, System.Type![]! sharedTypes, System.Action! configure) -> McMaster.NETCore.Plugins.PluginLoader!
70 | static McMaster.NETCore.Plugins.PluginLoader.CreateFromAssemblyFile(string! assemblyFile, System.Action! configure) -> McMaster.NETCore.Plugins.PluginLoader!
71 | static McMaster.NETCore.Plugins.PluginLoader.CreateFromAssemblyFile(string! assemblyFile, System.Type![]! sharedTypes) -> McMaster.NETCore.Plugins.PluginLoader!
72 | static McMaster.NETCore.Plugins.PluginLoader.CreateFromAssemblyFile(string! assemblyFile, System.Type![]! sharedTypes, System.Action! configure) -> McMaster.NETCore.Plugins.PluginLoader!
73 |
--------------------------------------------------------------------------------
/src/Plugins/PublicAPI.Unshipped.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natemcmaster/DotNetCorePlugins/a6094157bfd1ace9d6b13df108c488526cfe850f/src/Plugins/PublicAPI.Unshipped.txt
--------------------------------------------------------------------------------
/src/Plugins/releasenotes.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Breaking changes:
5 | * Require .NET >= 8.0
6 | * Drop dependencies on .NET Core 2 libraries for manually parsing .deps.json. Use the built-in .NET 8 API for this.
7 |
8 |
9 | Changes:
10 | * @Sewer56 - feature: add option to support lazy loading of transitive dependencies to increase performance (PR #164)
11 | * @Sewer56 - bugfix: search in additional probing paths (PR #172)
12 |
13 |
14 | Changes:
15 | * @KatoStoelen - don't create shadow copy that already exists (PR #147)
16 |
17 |
18 | Changes:
19 | * @bergi9 - add 'LoadInMemory' option so you can manually trigger reloads (alternative to HotReload) (PR #133)
20 |
21 |
22 | Changes:
23 | * @thoemmi - Debounce file system events when using hot reload and add API to control the debounce delay (default = 200 ms) (PR #129)
24 |
25 |
26 | Changes:
27 | * Add an API to enable shadow copying native library dependencies (PR #119)
28 | * Fix issue with native libraries being locked on disk during hot reload (Issue #118)
29 |
30 |
31 | Changes:
32 | * Add an API to enable contextual reflection in .NET Core 3+ (see https://github.com/natemcmaster/DotNetCorePlugins/blob/v1.0.0/README.md#Reflection for details)
33 | * @Sewer56 - Add support for non-default AssemblyLoadContext's (useful for plugins which load more plugins and native .NET Core hosting) (#111)
34 | * Remove API that was made obsolete in 0.3.0
35 |
36 |
37 | Fixes:
38 | * Fix issue preventing hot reload from working on Windows (#108)
39 |
40 |
41 | Fixes:
42 | * @CopaDataPM: Support CoreCLR in native apps (#80)
43 |
44 |
45 | Changes:
46 | * .NET Core 3.0 support
47 | * Support unloading plugins from memory (only .NET Core >3.0)
48 | * Support hot reloading (only .NET Core >3.0)
49 |
50 | Fixes:
51 | * Fix errors in loading the transitive dependencies of shared types
52 |
53 | Breaking changes:
54 | * Support for loading plugin config from an XML file has been dropped. The APIs for PluginLoader.CreateFromConfigFile were removed
55 | in favor of PluginLoader.CreateFromAssemblyFile
56 | * Merged PluginLoaderOptions with the PluginConfig class
57 |
58 |
59 |
60 | Bug fix:
61 | * Fix the MSBuild targets which generate plugin.config to put it into the correct output directory.
62 |
63 |
64 | Bug fix:
65 | * Fix config file generation when using the SDK package
66 |
67 |
68 |
74 |
75 |
76 |
82 |
83 |
84 |
89 |
90 |
91 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/src/StrongName.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natemcmaster/DotNetCorePlugins/a6094157bfd1ace9d6b13df108c488526cfe850f/src/StrongName.snk
--------------------------------------------------------------------------------
/src/common.psm1:
--------------------------------------------------------------------------------
1 | function exec([string]$_cmd) {
2 | write-host -ForegroundColor DarkGray ">>> $_cmd $args"
3 | $ErrorActionPreference = 'Continue'
4 | & $_cmd @args
5 | $ErrorActionPreference = 'Stop'
6 | if ($LASTEXITCODE -ne 0) {
7 | write-error "Failed with exit code $LASTEXITCODE"
8 | exit 1
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/Plugins.Tests/BasicAssemblyLoaderTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Reflection;
6 | using System.Runtime.CompilerServices;
7 | using McMaster.Extensions.Xunit;
8 | using Test.Referenced.Library;
9 | using Xunit;
10 | using Xunit.Abstractions;
11 |
12 | namespace McMaster.NETCore.Plugins.Tests
13 | {
14 | public class BasicAssemblyLoaderTests
15 | {
16 | private readonly ITestOutputHelper output;
17 |
18 | public BasicAssemblyLoaderTests(ITestOutputHelper output)
19 | {
20 | this.output = output;
21 | }
22 |
23 | [Fact]
24 | public void PluginLoaderCanUnload()
25 | {
26 | var path = TestResources.GetTestProjectAssembly("NetCoreApp2App");
27 |
28 | // See https://github.com/dotnet/coreclr/pull/22221
29 |
30 | ExecuteAndUnload(path, out var weakRef);
31 |
32 | // Force a GC collect to ensure unloaded has completed
33 | for (var i = 0; weakRef.IsAlive && (i < 10); i++)
34 | {
35 | GC.Collect();
36 | GC.WaitForPendingFinalizers();
37 | }
38 |
39 | Assert.False(weakRef.IsAlive);
40 | }
41 |
42 | [MethodImpl(MethodImplOptions.NoInlining)] // ensure no local vars are create
43 | private void ExecuteAndUnload(string path, out WeakReference weakRef)
44 | {
45 | var loader = PluginLoader.CreateFromAssemblyFile(path, c => { c.IsUnloadable = true; });
46 | var assembly = loader.LoadDefaultAssembly();
47 |
48 | var method = assembly
49 | .GetType("NetCoreApp2App.Program", throwOnError: true)!
50 | .GetMethod("GetGreeting", BindingFlags.Static | BindingFlags.Public);
51 |
52 | Assert.True(loader.IsUnloadable);
53 | Assert.NotNull(method);
54 | Assert.Equal("Hello world!", method!.Invoke(null, Array.Empty