Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for dependency injection via constructor #33

Merged
merged 10 commits into from
Mar 20, 2024
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
Loading