Add sample showing how to use plugins in an ASP.NET Core application

This commit is contained in:
Nate McMaster 2018-07-23 07:58:38 -07:00
parent 4cc6170b74
commit e5e88bbefb
No known key found for this signature in database
GPG Key ID: 157F9066DEF38BD0
20 changed files with 455 additions and 1 deletions

View File

@ -8,6 +8,7 @@
<Copyright>Copyright © Nate McMaster</Copyright>
<NeutralLanguage>en-US</NeutralLanguage>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepoRoot>$(MSBuildThisFileDirectory)</RepoRoot>
<PackageLicenseUrl>https://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/natemcmaster/DotNetCorePlugins</PackageProjectUrl>
<RepositoryUrl>https://github.com/natemcmaster/DotNetCorePlugins.git</RepositoryUrl>

View File

@ -7,3 +7,8 @@ simplify the usage of those lower-level APIs.
## Getting started
This project requires [.NET Core 2.0](https://aka.ms/dotnet-download) or higher.
## Usage
See example projects in [samples/](./samples/) for more detailed, example usage.

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.0.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,12 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
namespace Plugin.Abstractions
{
public interface IWebPlugin
{
void Configure(IApplicationBuilder appBuilder);
void ConfigureServices(IServiceCollection services);
}
}

View File

@ -0,0 +1,7 @@
namespace Plugin.Abstractions
{
public interface IPluginLink
{
string GetHref();
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\Abstractions\Abstractions.csproj" />
<!--
Required for the samples in this repo only.
Normally, you should replace this with a PackageReference to McMaster.Extensions.Plugins.
-->
<ProjectReference Include="$(RepoRoot)src\Plugins\McMaster.Extensions.Plugins.csproj" />
</ItemGroup>
<Target Name="PublishPlugins"
BeforeTargets="Build">
<ItemGroup>
<PluginProject Include="..\WebAppPlugin1\WebAppPlugin1.csproj" />
<PluginProject Include="..\WebAppPlugin2\WebAppPlugin2.csproj" />
</ItemGroup>
<MSBuild Projects="@(PluginProject)"
Targets="Publish"
Properties="PublishDir=$(TargetDir)plugins\%(FileName)\" />
</Target>
</Project>

View File

@ -0,0 +1,11 @@
@page
@using MainWebApp
@model IndexModel
<ul>
@foreach (var link in Model.Links)
{
<li>
<a href="@link">@link</a>
</li>
}
</ul>

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Plugin.Abstractions;
namespace MainWebApp
{
public class IndexModel : PageModel
{
public IndexModel(IEnumerable<IPluginLink> pluginLinks)
{
Links = pluginLinks.Select(p => p.GetHref()).ToArray();
}
public string[] Links { get; }
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace MainWebApp
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}

View File

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:35566",
"sslPort": 44362
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"WebAppWithPlugins": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
using McMaster.Extensions.Plugins;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Plugin.Abstractions;
namespace MainWebApp
{
public class Startup
{
private List<IWebPlugin> _plugins = new List<IWebPlugin>();
public Startup()
{
foreach (var pluginFile in Directory.GetFiles(AppContext.BaseDirectory, "plugin.config", SearchOption.AllDirectories))
{
var loader = PluginLoader.CreateFromConfigFile(pluginFile,
// this ensures that the plugin resolves to the same version of DependencyInjection
// and ASP.NET Core that the current app uses
sharedTypes: new[]
{
typeof(IApplicationBuilder),
typeof(IWebPlugin),
typeof(IServiceCollection),
});
foreach (var type in loader.LoadDefaultAssembly()
.GetTypes()
.Where(t => typeof(IWebPlugin).IsAssignableFrom(t) && !t.IsAbstract))
{
Console.WriteLine("Found plugin " + type.Name);
var plugin = (IWebPlugin)Activator.CreateInstance(type);
_plugins.Add(plugin);
}
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
foreach (var plugin in _plugins)
{
plugin.ConfigureServices(services);
}
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
foreach (var plugin in _plugins)
{
plugin.Configure(app);
}
app.UseMvcWithDefaultRoute();
}
}
}

View File

@ -0,0 +1,41 @@
ASP.NET Core Sample
===================
This sample contains 4 projects which demonstrate a simple plugin scenario.
1. 'Abstractions' defines common interfaces shared by the web application (host) and plugins
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
3. 'WebAppPlugin1' references 'Abstractions' and implements `IWebPlugin`. This plugin has a dependency on [AutoMapper](https://www.nuget.org/packages/AutoMapper/) version 6.
4. 'WebAppPlugin2' is the same as plugin1, but it uses AutoMapper version 7.
Normally, in .NET Core applications you cannot reference two different versions of the same assembly.
However, as this sample demonstrates, using .NET Core plugins you can load and use two different versions.
* http://localhost:5000/plugin/v1 responds with
```
This plugin uses AutoMapper, Version=6.2.2.0, Culture=neutral, PublicKeyToken=be96cd2c38ef1005
```
* http://localhost:5000/plugin/v2 responds with
```
This plugin uses AutoMapper, Version=7.0.1.0, Culture=neutral, PublicKeyToken=be96cd2c38ef1005
```
There are some important types, however, which much share the same identity between the plugins and the host.
To ensure type exchnage works between the host and the plugins, the MainWebApp project uses the `sharedTypes`
parameter on `PluginLoader.CreateFromConfigFile`.
```csharp
var loader = PluginLoader.CreateFromConfigFile(pluginFile,
sharedTypes: new[]
{
typeof(IApplicationBuilder),
typeof(IWebPlugin),
typeof(IServiceCollection),
});
```
This is important because the plugins in this sample are compiled for ASP.NET Core 2.0 interfaces,
but the MainWebApp uses ASP.NET Core 2.1. If not for this parameter, the plugins would also attempt to use
a private copy of the ASP.NET Core implementations and type exchange between the plugin and the web app
would fail to resolve `IApplicationBuilder` and `IServiceCollection` as the same type.

View File

@ -0,0 +1,9 @@
<Project>
<!--
Required for the samples in this repo only.
Normally, you should replace this with a PackageReference to McMaster.Extensions.Plugins.Sdk.
-->
<Import Project="$(RepoRoot)src\Plugins.Sdk\sdk\Sdk.targets" />
</Project>

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="6.2.2" />
<ProjectReference Include="..\Abstractions\Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,30 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Plugin.Abstractions;
namespace Plugin1
{
internal class WebPlugin1 : IWebPlugin, IPluginLink
{
public string GetHref() => "/plugin/v1";
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IPluginLink, WebPlugin1>();
}
public void Configure(IApplicationBuilder appBuilder)
{
appBuilder.Map("/plugin/v1", c =>
{
var autoMapperType = typeof(AutoMapper.IMapper).Assembly;
c.Run(async (ctx) =>
{
await ctx.Response.WriteAsync("This plugin uses " + autoMapperType.GetName().ToString());
});
});
}
}
}

View File

@ -0,0 +1,9 @@
<Project>
<!--
Required for the samples in this repo only.
Normally, you should replace this with a PackageReference to McMaster.Extensions.Plugins.Sdk.
-->
<Import Project="$(RepoRoot)src\Plugins.Sdk\sdk\Sdk.targets" />
</Project>

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="7.0.1" />
<ProjectReference Include="..\Abstractions\Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,30 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Plugin.Abstractions;
namespace Plugin2
{
internal class WebPlugin2 : IWebPlugin, IPluginLink
{
public string GetHref() => "/plugin/v2";
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IPluginLink, WebPlugin2>();
}
public void Configure(IApplicationBuilder appBuilder)
{
appBuilder.Map("/plugin/v2", c =>
{
var autoMapperType = typeof(AutoMapper.IMapper).Assembly;
c.Run(async (ctx) =>
{
await ctx.Response.WriteAsync("This plugin uses " + autoMapperType.GetName().ToString());
});
});
}
}
}

View File

@ -0,0 +1,76 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstractions", "Abstractions\Abstractions.csproj", "{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MainWebApp", "MainWebApp\MainWebApp.csproj", "{781A86B1-C278-44CD-997B-1AA26D6BFB38}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAppPlugin1", "WebAppPlugin1\WebAppPlugin1.csproj", "{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAppPlugin2", "WebAppPlugin2\WebAppPlugin2.csproj", "{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|x64.ActiveCfg = Debug|Any CPU
{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|x64.Build.0 = Debug|Any CPU
{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|x86.ActiveCfg = Debug|Any CPU
{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|x86.Build.0 = Debug|Any CPU
{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|Any CPU.Build.0 = Release|Any CPU
{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|x64.ActiveCfg = Release|Any CPU
{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|x64.Build.0 = Release|Any CPU
{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|x86.ActiveCfg = Release|Any CPU
{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|x86.Build.0 = Release|Any CPU
{781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|Any CPU.Build.0 = Debug|Any CPU
{781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|x64.ActiveCfg = Debug|Any CPU
{781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|x64.Build.0 = Debug|Any CPU
{781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|x86.ActiveCfg = Debug|Any CPU
{781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|x86.Build.0 = Debug|Any CPU
{781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|Any CPU.Build.0 = Release|Any CPU
{781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|x64.ActiveCfg = Release|Any CPU
{781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|x64.Build.0 = Release|Any CPU
{781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|x86.ActiveCfg = Release|Any CPU
{781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|x86.Build.0 = Release|Any CPU
{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|x64.ActiveCfg = Debug|Any CPU
{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|x64.Build.0 = Debug|Any CPU
{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|x86.ActiveCfg = Debug|Any CPU
{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|x86.Build.0 = Debug|Any CPU
{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|Any CPU.Build.0 = Release|Any CPU
{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|x64.ActiveCfg = Release|Any CPU
{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|x64.Build.0 = Release|Any CPU
{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|x86.ActiveCfg = Release|Any CPU
{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|x86.Build.0 = Release|Any CPU
{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|x64.ActiveCfg = Debug|Any CPU
{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|x64.Build.0 = Debug|Any CPU
{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|x86.ActiveCfg = Debug|Any CPU
{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|x86.Build.0 = Debug|Any CPU
{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|Any CPU.Build.0 = Release|Any CPU
{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|x64.ActiveCfg = Release|Any CPU
{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|x64.Build.0 = Release|Any CPU
{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|x86.ActiveCfg = Release|Any CPU
{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -1,9 +1,17 @@
<Project>
<PropertyGroup>
<PluginConfigFile>$(OutputPath)plugin.config</PluginConfigFile>
<PluginConfigFile>$(TargetDir)plugin.config</PluginConfigFile>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<ItemGroup>
<Content Include="$(PluginConfigFile)">
<Visible>false</Visible>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<Target Name="GeneratePluginConfig"
BeforeTargets="Build"
Inputs="$(MSBuildAllProjects);$(TargetPath)"
@ -20,5 +28,9 @@
</PropertyGroup>
<WriteLinesToFile Lines="$(PluginConfigContent)" Overwrite="true" File="$(PluginConfigFile)" />
<ItemGroup>
<FileWrites Include="$(PluginConfigFile)" />
</ItemGroup>
</Target>
</Project>