Skip to content

Commit

Permalink
Merge pull request #33 from MrDave1999/feat/issue-32
Browse files Browse the repository at this point in the history
feat: Add support for dependency injection via constructor
  • Loading branch information
MrDave1999 authored Mar 20, 2024
2 parents 1a5135f + b7e9179 commit 4984b03
Show file tree
Hide file tree
Showing 15 changed files with 337 additions and 12 deletions.
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

0 comments on commit 4984b03

Please sign in to comment.