Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/dotnetcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ jobs:
dotnet build ./samples/Plugins/PersonPlugin/Example.PersonPlugin.csproj -c Release
dotnet build ./samples/Plugins/JsonPlugin/Example.JsonPlugin.csproj -c Release
dotnet build ./samples/Plugins/OldJsonPlugin/Example.OldJsonPlugin.csproj -c Release
dotnet build ./samples/Plugins/DependencyInjectionPlugin/Example.DependencyInjectionPlugin.csproj -c Release
dotnet test ./samples/Test/Example.Test.csproj -c Release
14 changes: 10 additions & 4 deletions CPlugin.Net.sln
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.HostWebApi", "sampl
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.SharedEntities", "samples\SharedEntities\Example.SharedEntities.csproj", "{F66A1430-3F32-4E25-8966-54D502D216DE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.DependencyInjectionPlugin", "samples\Plugins\DependencyInjectionPlugin\Example.DependencyInjectionPlugin.csproj", "{28065D77-B890-47DE-B695-04E388176925}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.JsonPlugin", "samples\Plugins\JsonPlugin\Example.JsonPlugin.csproj", "{C5B8EF73-7DB5-441F-AE38-0988751A896B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.OldJsonPlugin", "samples\Plugins\OldJsonPlugin\Example.OldJsonPlugin.csproj", "{1ADE3B86-00EF-4976-8B67-09B360B149FA}"
Expand Down Expand Up @@ -98,6 +100,10 @@ Global
{18534944-583B-4924-AC5B-E0655FD92AAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18534944-583B-4924-AC5B-E0655FD92AAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18534944-583B-4924-AC5B-E0655FD92AAC}.Release|Any CPU.Build.0 = Release|Any CPU
{1E64908D-DC48-4B83-BB25-CF36821EB37F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E64908D-DC48-4B83-BB25-CF36821EB37F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E64908D-DC48-4B83-BB25-CF36821EB37F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E64908D-DC48-4B83-BB25-CF36821EB37F}.Release|Any CPU.Build.0 = Release|Any CPU
{0F27C776-F284-4C94-86C9-0FF089245E13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F27C776-F284-4C94-86C9-0FF089245E13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F27C776-F284-4C94-86C9-0FF089245E13}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -122,10 +128,10 @@ Global
{0BCD3305-F0D5-43E6-B879-EEF0827558A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0BCD3305-F0D5-43E6-B879-EEF0827558A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0BCD3305-F0D5-43E6-B879-EEF0827558A8}.Release|Any CPU.Build.0 = Release|Any CPU
{1E64908D-DC48-4B83-BB25-CF36821EB37F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E64908D-DC48-4B83-BB25-CF36821EB37F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E64908D-DC48-4B83-BB25-CF36821EB37F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E64908D-DC48-4B83-BB25-CF36821EB37F}.Release|Any CPU.Build.0 = Release|Any CPU
{28065D77-B890-47DE-B695-04E388176925}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28065D77-B890-47DE-B695-04E388176925}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28065D77-B890-47DE-B695-04E388176925}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28065D77-B890-47DE-B695-04E388176925}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
6 changes: 6 additions & 0 deletions samples/Contracts/ITestService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Example.Contracts;

public interface ITestService
{
string Execute();
}
10 changes: 10 additions & 0 deletions samples/HostApplications/WebApi/Controllers/ServiceController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Example.HostWebApi.Controllers;

[ApiController]
[Route("[controller]")]
public class ServiceController
{
[HttpGet]
public ActionResult<string> Get(IEnumerable<ITestService> services)
=> services.First().Execute();
}
2 changes: 2 additions & 0 deletions samples/HostApplications/WebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(assembly));
}

builder.Services.AddSubtypesOf<ITestService>(ServiceLifetime.Transient);

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
Expand Down
4 changes: 3 additions & 1 deletion samples/HostApplications/WebApi/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
}
},
"AllowedHosts": "*",
"ServiceName": "TestService",
"Plugins": [
"Example.AppointmentPlugin.dll",
"Example.PersonPlugin.dll"
"Example.PersonPlugin.dll",
"Example.DependencyInjectionPlugin.dll"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<OutDir>$(WebApiProjectDir)</OutDir>
<OutputType>Library</OutputType>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
</PropertyGroup>

</Project>
3 changes: 3 additions & 0 deletions samples/Plugins/DependencyInjectionPlugin/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
global using Example.Contracts;
global using CPlugin.Net;
global using Example.DependencyInjectionPlugin;
23 changes: 23 additions & 0 deletions samples/Plugins/DependencyInjectionPlugin/TestService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[assembly: Plugin(typeof(TestService))]

namespace Example.DependencyInjectionPlugin;

public class TestService : ITestService
{
private readonly ILogger<TestService> _logger;
private readonly IConfiguration _configuration;

public TestService(
ILogger<TestService> logger,
IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}

public string Execute()
{
_logger.LogInformation("TestService");
return _configuration["ServiceName"];
}
}
19 changes: 19 additions & 0 deletions samples/Test/WebApi/Get.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,23 @@ public async Task Get_WhenWeatherForecastAreObtained_ShouldReturnsHttpStatusCode
result.IsSuccess.Should().BeTrue();
result.Data.Should().HaveCount(expectedWeatherForecast);
}

[Test]
public async Task Get_WhenServiceNameIsObtained_ShouldReturnsHttpStatusCodeOk()
{
// Arrange
using var factory = new WebApplicationFactory<Program>();
var client = factory.CreateClient();
var expectedServiceName = "TestService";

// Act
var httpResponse = await client.GetAsync("/Service");
var result = await httpResponse
.Content
.ReadAsStringAsync();

// Asserts
httpResponse.StatusCode.Should().Be(HttpStatusCode.OK);
result.Should().Be(expectedServiceName);
}
}
1 change: 1 addition & 0 deletions src/Core/CPlugin.Net.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>

<ItemGroup>
Expand Down
76 changes: 76 additions & 0 deletions src/Core/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;

namespace CPlugin.Net;

/// <summary>
/// Extension methods for adding services to an <see cref="IServiceCollection"/>.
/// </summary>
public static class CPluginServiceCollectionExtensions
{
/// <summary>
/// Adds the subtypes that implement the contract specified by <typeparamref name="TSupertype"/>
/// to the service collection, using the assemblies loaded by <see cref="PluginLoader"/>.
/// </summary>
/// <typeparam name="TSupertype">
/// The type of contract (base type) shared between the host application and the plugins.
/// </typeparam>
/// <param name="services">
/// The <see cref="IServiceCollection"/> to add the service to.
/// </param>
/// <param name="serviceLifetime">
/// Specifies the lifetime of the services to be added to the service collection.
/// </param>
/// <remarks>
/// This method uses the <see cref="PluginAttribute"/> type to add the implementations of the contract
/// to the service collection, so plugins must use it.
/// </remarks>
/// <returns>
/// A reference to this instance after the operation has completed.
/// </returns>
public static IServiceCollection AddSubtypesOf<TSupertype>(
this IServiceCollection services,
ServiceLifetime serviceLifetime) where TSupertype : class
=> services.AddSubtypesOf<TSupertype>(PluginLoader.Assemblies, serviceLifetime);

// This method is only to be used for testing.
// This way you don't have to depend on the plugin loader when testing.
internal static IServiceCollection AddSubtypesOf<TSupertype>(
this IServiceCollection services,
IEnumerable<Assembly> assemblies,
ServiceLifetime serviceLifetime) where TSupertype : class
{
if (assemblies is null)
throw new ArgumentNullException(nameof(assemblies));

foreach (Assembly assembly in assemblies)
{
var pluginAttributes = assembly.GetCustomAttributes<PluginAttribute>();
foreach (PluginAttribute pluginAttribute in pluginAttributes)
{
Type implementationType = pluginAttribute.PluginType;
if (typeof(TSupertype).IsAssignableFrom(implementationType))
{
services.AddService(
serviceType: typeof(TSupertype),
implementationType,
serviceLifetime);
}
}
}

return services;
}

private static IServiceCollection AddService(
this IServiceCollection services,
Type serviceType,
Type implementationType,
ServiceLifetime serviceLifetime) => serviceLifetime switch
{
ServiceLifetime.Singleton => services.AddSingleton(serviceType, implementationType),
ServiceLifetime.Transient => services.AddTransient(serviceType, implementationType),
ServiceLifetime.Scoped => services.AddScoped(serviceType, implementationType),
_ => throw new NotSupportedException($"Lifetime '{serviceLifetime}' is not supported.")
};
}
9 changes: 3 additions & 6 deletions src/Core/TypeFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ public static class TypeFinder
/// or if no assembly uses <see cref="PluginAttribute"/>.
/// <para>This method never returns <c>null</c>.</para>
/// </returns>
/// <exception cref="ArgumentNullException">
/// <c>assemblies</c> is <c>null</c>.
/// </exception>
public static IEnumerable<TSupertype> FindSubtypesOf<TSupertype>() where TSupertype : class
=> FindSubtypesOf<TSupertype>(PluginLoader.Assemblies);

Expand All @@ -55,9 +52,9 @@ private static IEnumerable<TSupertype> GetSubtypesOf<TSupertype>(IEnumerable<Ass
var pluginAttributes = assembly.GetCustomAttributes<PluginAttribute>();
foreach (PluginAttribute pluginAttribute in pluginAttributes)
{
Type type = pluginAttribute.PluginType;
if (typeof(TSupertype).IsAssignableFrom(type))
yield return (TSupertype)Activator.CreateInstance(type);
Type implementationType = pluginAttribute.PluginType;
if (typeof(TSupertype).IsAssignableFrom(implementationType))
yield return (TSupertype)Activator.CreateInstance(implementationType);
}
}
}
Expand Down
Loading