diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 37e573c..9d68440 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -20,6 +20,7 @@ jobs: Solution_Path: Slim.sln Test_Project_Path: Slim.Tests\Slim.Tests.csproj Source_Project_Path: Slim\Slim.csproj + Integration_Project_Path: Slim.Integration\Slim.Integration.csproj Actions_Allow_Unsecure_Commands: true steps: @@ -44,8 +45,11 @@ jobs: - name: Build Slim project run: dotnet build Slim -c $env:Configuration - - name: Package + - name: Package Slim run: dotnet pack -c Release -o . $env:Source_Project_Path + - name: Package Slim.Integration + run: dotnet pack -c Release -o . $env:Integration_Project_Path + - name: Publish run: dotnet nuget push *.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d1f8b7c..1050fe5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,6 +21,7 @@ jobs: env: Solution_Path: Slim.sln Test_Project_Path: Slim.Tests\Slim.Tests.csproj + Integration_Test_Project_Path: Slim.Integration.Tests\Slim.Integration.Tests.csproj Source_Project_Path: Slim\Slim.csproj Actions_Allow_Unsecure_Commands: true @@ -38,8 +39,11 @@ jobs: - name: Setup MSBuild.exe uses: microsoft/setup-msbuild@v1.0.1 - - name: Execute Unit Tests + - name: Execute Slim Unit Tests run: dotnet test $env:Test_Project_Path + + - name: Execute Slim.Integration Unit Tests + run: dotnet test $env:Integration_Test_Project_Path - name: Restore Project run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier diff --git a/Slim.Integration.Tests/Models/ITestService.cs b/Slim.Integration.Tests/Models/ITestService.cs new file mode 100644 index 0000000..f322c45 --- /dev/null +++ b/Slim.Integration.Tests/Models/ITestService.cs @@ -0,0 +1,5 @@ +namespace Slim.Integration.Tests.Models; + +public interface ITestService +{ +} diff --git a/Slim.Integration.Tests/Models/TestService.cs b/Slim.Integration.Tests/Models/TestService.cs new file mode 100644 index 0000000..0b075cd --- /dev/null +++ b/Slim.Integration.Tests/Models/TestService.cs @@ -0,0 +1,5 @@ +namespace Slim.Integration.Tests.Models; + +public sealed class TestService : ITestService +{ +} diff --git a/Slim.Integration.Tests/ServiceCollectionExtensionsTests.cs b/Slim.Integration.Tests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..dbbdd25 --- /dev/null +++ b/Slim.Integration.Tests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,145 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Slim.Integration.ServiceCollection; +using Slim.Integration.Tests.Models; + +namespace Slim.Integration.Tests; + +[TestClass] +public sealed class ServiceCollectionExtensionsTests +{ + [TestMethod] + public void BuildSlimServiceProvider_ReturnsServiceProvider() + { + var serviceCollection = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); + var serviceProvider = serviceCollection.BuildSlimServiceProvider(); + + serviceProvider.Should().BeAssignableTo(); + } + + [TestMethod] + public void BuildSlimServiceProvider_WithServices_ReturnsProviderWithServices() + { + var serviceCollection = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); + ServiceCollectionServiceExtensions.AddScoped(serviceCollection); + var serviceProvider = serviceCollection.BuildSlimServiceProvider(); + + var testService = serviceProvider.GetRequiredService(); + + testService.Should().BeOfType(); + } + + [TestMethod] + public void BuildSlimServiceProvider_WithSingletonInstance_ReturnsSingletonInstance() + { + var testService = new TestService(); + + var serviceCollection = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); + ServiceCollectionServiceExtensions.AddSingleton(serviceCollection, testService); + var serviceProvider = serviceCollection.BuildSlimServiceProvider(); + + var returnedService = serviceProvider.GetRequiredService(); + + returnedService.Should().Be(testService); + } + + [TestMethod] + public void BuildSlimServiceProvider_WithSingletonFactory_ReturnsSingletonInstance() + { + var testService = new TestService(); + + var serviceCollection = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); + ServiceCollectionServiceExtensions.AddSingleton(serviceCollection, sp => testService); + var serviceProvider = serviceCollection.BuildSlimServiceProvider(); + + var returnedService = serviceProvider.GetRequiredService(); + + returnedService.Should().Be(testService); + } + + [TestMethod] + public void BuildSlimServiceProvider_WithScopedFactory_ReturnsScopedInstance() + { + var serviceCollection = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); + ServiceCollectionServiceExtensions.AddScoped(serviceCollection, sp => new TestService()); + var serviceProvider = serviceCollection.BuildSlimServiceProvider(); + var scopedServiceProvider = serviceProvider.CreateScope().ServiceProvider; + + var returnedService1 = serviceProvider.GetRequiredService(); + var returnedService11 = serviceProvider.GetRequiredService(); + var returnedService2 = scopedServiceProvider.GetRequiredService(); + var returnedService21 = scopedServiceProvider.GetRequiredService(); + + returnedService1.Should().NotBe(returnedService2); + returnedService11.Should().Be(returnedService1); + returnedService21.Should().Be(returnedService2); + } + + [TestMethod] + public void BuildSlimServiceProvider_WithScopedImplementation_ReturnsScopedInstance() + { + var serviceCollection = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); + ServiceCollectionServiceExtensions.AddScoped(serviceCollection); + var serviceProvider = serviceCollection.BuildSlimServiceProvider(); + var scopedServiceProvider = serviceProvider.CreateScope().ServiceProvider; + + var returnedService1 = serviceProvider.GetRequiredService(); + var returnedService11 = serviceProvider.GetRequiredService(); + var returnedService2 = scopedServiceProvider.GetRequiredService(); + var returnedService21 = scopedServiceProvider.GetRequiredService(); + + returnedService1.Should().NotBe(returnedService2); + returnedService11.Should().Be(returnedService1); + returnedService21.Should().Be(returnedService2); + } + + [TestMethod] + public void BuildSlimServiceProvider_WithTransientFactory_ReturnsTransientInstance() + { + var serviceCollection = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); + ServiceCollectionServiceExtensions.AddTransient(serviceCollection, sp => new TestService()); + var serviceProvider = serviceCollection.BuildSlimServiceProvider(); + var scopedServiceProvider = serviceProvider.CreateScope().ServiceProvider; + + var returnedService1 = serviceProvider.GetRequiredService(); + var returnedService11 = serviceProvider.GetRequiredService(); + var returnedService2 = scopedServiceProvider.GetRequiredService(); + var returnedService21 = scopedServiceProvider.GetRequiredService(); + + returnedService1.Should().NotBe(returnedService2); + returnedService11.Should().NotBe(returnedService1); + returnedService21.Should().NotBe(returnedService2); + } + + [TestMethod] + public void BuildSlimServiceProvider_WithTransientImplementation_ReturnsTransientInstance() + { + var serviceCollection = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); + ServiceCollectionServiceExtensions.AddTransient(serviceCollection); + var serviceProvider = serviceCollection.BuildSlimServiceProvider(); + var scopedServiceProvider = serviceProvider.CreateScope().ServiceProvider; + + var returnedService1 = serviceProvider.GetRequiredService(); + var returnedService11 = serviceProvider.GetRequiredService(); + var returnedService2 = scopedServiceProvider.GetRequiredService(); + var returnedService21 = scopedServiceProvider.GetRequiredService(); + + returnedService1.Should().NotBe(returnedService2); + returnedService11.Should().NotBe(returnedService1); + returnedService21.Should().NotBe(returnedService2); + } + + [TestMethod] + public void BuildSlimServiceProvider_CreateScope_ReturnsScopedServiceProvider() + { + var serviceCollection = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); + var serviceProvider = serviceCollection.BuildSlimServiceProvider(); + var scopedServiceProvider = serviceProvider.CreateScope().ServiceProvider; + + serviceProvider.Should().NotBe(scopedServiceProvider); + scopedServiceProvider.Should().NotBeNull(); + scopedServiceProvider.Should().BeOfType(); + serviceProvider.Should().BeOfType(); + scopedServiceProvider.As().ParentServiceManager.Should().Be(serviceProvider); + } +} diff --git a/Slim.Integration.Tests/Slim.Integration.Tests.csproj b/Slim.Integration.Tests/Slim.Integration.Tests.csproj new file mode 100644 index 0000000..080fa4b --- /dev/null +++ b/Slim.Integration.Tests/Slim.Integration.Tests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + diff --git a/Slim.Integration.Tests/SlimServiceContainerTests.cs b/Slim.Integration.Tests/SlimServiceContainerTests.cs new file mode 100644 index 0000000..56b5bdd --- /dev/null +++ b/Slim.Integration.Tests/SlimServiceContainerTests.cs @@ -0,0 +1,82 @@ +using System.ComponentModel.Design; +using FluentAssertions; +using Slim.Integration.ServiceContainer; +using Slim.Integration.Tests.Models; + +namespace Slim.Integration.Tests; + +[TestClass] +public class SlimServiceContainerTests +{ + private readonly SlimServiceContainer slimServiceContainer = new(); + + [TestMethod] + public void AddService_GetService_ReturnsExpectedService() + { + var testService = new TestService(); + + this.slimServiceContainer.AddService(typeof(ITestService), testService); + var retrievedService = this.slimServiceContainer.GetService(typeof(ITestService)); + + retrievedService.Should().BeAssignableTo(); + retrievedService.Should().Be(testService); + } + + [TestMethod] + public void AddServiceWithPromotion_GetService_ReturnsServiceFromParent() + { + var testService = new TestService(); + var parent = new ServiceManager() { AllowScopedManagerModifications = true }; + var child = parent.CreateScope().As(); + var container = new SlimServiceContainer(child); + + container.AddService(typeof(ITestService), testService, true); + var retrievedService = parent.GetService(); + + retrievedService.Should().BeAssignableTo(); + retrievedService.Should().Be(testService); + } + + [TestMethod] + public void AddServiceWithCallback_GetService_ReturnsExpectedService() + { + var testService = new TestService(); + + this.slimServiceContainer.AddService(typeof(ITestService), new ServiceCreatorCallback((sc, type) => testService)); + var retrievedService = this.slimServiceContainer.GetService(typeof(ITestService)); + + retrievedService.Should().BeAssignableTo(); + retrievedService.Should().Be(testService); + } + + [TestMethod] + public void AddServiceWithCallbackWithPromotion_GetService_ReturnsServiceFromParent() + { + var testService = new TestService(); + var parent = new ServiceManager() { AllowScopedManagerModifications = true }; + var child = parent.CreateScope().As(); + var container = new SlimServiceContainer(child); + + container.AddService(typeof(ITestService), new ServiceCreatorCallback((sc, type) => testService), true); + var retrievedService = parent.GetService(); + + retrievedService.Should().BeAssignableTo(); + retrievedService.Should().Be(testService); + } + + [TestMethod] + public void RemoveService_NotSupported() + { + var action = () => this.slimServiceContainer.RemoveService(typeof(ITestService)); + + action.Should().Throw(); + } + + [TestMethod] + public void RemoveServiceWithPromotion_NotSupported() + { + var action = () => this.slimServiceContainer.RemoveService(typeof(ITestService), true); + + action.Should().Throw(); + } +} diff --git a/Slim.Integration.Tests/Usings.cs b/Slim.Integration.Tests/Usings.cs new file mode 100644 index 0000000..ab67c7e --- /dev/null +++ b/Slim.Integration.Tests/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/Slim.Integration/ServiceCollection/ServiceCollectionExtensions.cs b/Slim.Integration/ServiceCollection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..87513f3 --- /dev/null +++ b/Slim.Integration/ServiceCollection/ServiceCollectionExtensions.cs @@ -0,0 +1,85 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Slim.Integration.ServiceCollection; +public static class ServiceCollectionExtensions +{ + public static System.IServiceProvider BuildSlimServiceProvider(this Microsoft.Extensions.DependencyInjection.ServiceCollection serviceCollection) + { + var serviceManager = new ServiceManager(); + foreach(var serviceDescriptor in serviceCollection) + { + switch (serviceDescriptor.Lifetime) + { + case ServiceLifetime.Singleton: + RegisterSingleton(serviceManager, serviceDescriptor); + break; + case ServiceLifetime.Scoped: + RegisterScoped(serviceManager, serviceDescriptor); + break; + case ServiceLifetime.Transient: + RegisterTransient(serviceManager, serviceDescriptor); + break; + default: + throw new InvalidOperationException($"Unexpected service lifetime {serviceDescriptor.Lifetime}."); + } + } + + serviceManager.RegisterScoped(); + return serviceManager; + } + + private static void RegisterSingleton(ServiceManager serviceManager, ServiceDescriptor serviceDescriptor) + { + if (serviceDescriptor.ImplementationInstance is null && + serviceDescriptor.ImplementationFactory is not null) + { + serviceManager.RegisterSingleton(serviceDescriptor.ServiceType, sp => serviceDescriptor.ImplementationFactory(sp)); + } + else if (serviceDescriptor.ImplementationFactory is null && + serviceDescriptor.ImplementationInstance is not null) + { + serviceManager.RegisterSingleton(serviceDescriptor.ServiceType, sp => serviceDescriptor.ImplementationInstance); + } + else + { + serviceManager.RegisterSingleton(serviceDescriptor.ServiceType, serviceDescriptor.ImplementationType); + } + } + + private static void RegisterScoped(ServiceManager serviceManager, ServiceDescriptor serviceDescriptor) + { + if (serviceDescriptor.ImplementationInstance is null && + serviceDescriptor.ImplementationFactory is not null) + { + serviceManager.RegisterScoped(serviceDescriptor.ServiceType, sp => serviceDescriptor.ImplementationFactory(sp)); + } + else if (serviceDescriptor.ImplementationFactory is null && + serviceDescriptor.ImplementationInstance is not null) + { + serviceManager.RegisterScoped(serviceDescriptor.ServiceType, sp => serviceDescriptor.ImplementationInstance); + } + else + { + serviceManager.RegisterScoped(serviceDescriptor.ServiceType, serviceDescriptor.ImplementationType); + } + } + + private static void RegisterTransient(ServiceManager serviceManager, ServiceDescriptor serviceDescriptor) + { + if (serviceDescriptor.ImplementationInstance is null && + serviceDescriptor.ImplementationFactory is not null) + { + serviceManager.RegisterTransient(serviceDescriptor.ServiceType, sp => serviceDescriptor.ImplementationFactory(sp)); + } + else if (serviceDescriptor.ImplementationFactory is null && + serviceDescriptor.ImplementationInstance is not null) + { + serviceManager.RegisterTransient(serviceDescriptor.ServiceType, sp => serviceDescriptor.ImplementationInstance); + } + else + { + serviceManager.RegisterTransient(serviceDescriptor.ServiceType, serviceDescriptor.ImplementationType); + } + } +} diff --git a/Slim.Integration/ServiceCollection/ServiceScope.cs b/Slim.Integration/ServiceCollection/ServiceScope.cs new file mode 100644 index 0000000..9d23783 --- /dev/null +++ b/Slim.Integration/ServiceCollection/ServiceScope.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Slim.Integration.ServiceCollection; + +public sealed class ServiceScope : IServiceScope +{ + public System.IServiceProvider ServiceProvider { get; set; } + + public void Dispose() + { + if (this.ServiceProvider is IDisposable disposable) + { + disposable.Dispose(); + } + } +} diff --git a/Slim.Integration/ServiceCollection/ServiceScopeFactory.cs b/Slim.Integration/ServiceCollection/ServiceScopeFactory.cs new file mode 100644 index 0000000..20d5970 --- /dev/null +++ b/Slim.Integration/ServiceCollection/ServiceScopeFactory.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Slim.Integration.ServiceCollection; + +public sealed class ServiceScopeFactory : IServiceScopeFactory +{ + private readonly IServiceManager serviceManager; + + public ServiceScopeFactory( + IServiceManager serviceManager) + { + this.serviceManager = serviceManager; + } + + public IServiceScope CreateScope() + { + IServiceProvider scopedServiceManager = this.serviceManager.CreateScope(); + return new ServiceScope { ServiceProvider = scopedServiceManager }; + } +} diff --git a/Slim.Integration/ServiceContainer/SlimServiceContainer.cs b/Slim.Integration/ServiceContainer/SlimServiceContainer.cs new file mode 100644 index 0000000..4e7f0c3 --- /dev/null +++ b/Slim.Integration/ServiceContainer/SlimServiceContainer.cs @@ -0,0 +1,76 @@ +using System; +using System.ComponentModel.Design; + +namespace Slim.Integration.ServiceContainer; + +public class SlimServiceContainer : IServiceContainer +{ + private readonly IServiceManager serviceManager; + + public SlimServiceContainer() + { + this.serviceManager = new ServiceManager(); + } + + public SlimServiceContainer(IServiceManager serviceManager) + { + this.serviceManager = serviceManager; + } + + public void AddService(Type serviceType, ServiceCreatorCallback callback) + { + this.serviceManager.RegisterSingleton(serviceType, sp => callback(this, serviceType)); + } + + public void AddService(Type serviceType, ServiceCreatorCallback callback, bool promote) + { + var manager = this.serviceManager; + if (promote is false) + { + this.AddService(serviceType, callback); + return; + } + + while (manager is not null) + { + manager.RegisterSingleton(serviceType, sp => callback(this, serviceType)); + manager = manager.ParentServiceManager; + } + } + + public void AddService(Type serviceType, object serviceInstance) + { + this.serviceManager.RegisterSingleton(serviceType, sp => serviceInstance); + } + + public void AddService(Type serviceType, object serviceInstance, bool promote) + { + var manager = this.serviceManager; + if (promote is false) + { + this.AddService(serviceType, serviceInstance); + return; + } + + while (manager is not null) + { + manager.RegisterSingleton(serviceType, sp => serviceInstance); + manager = manager.ParentServiceManager; + } + } + + public object GetService(Type serviceType) + { + return this.serviceManager.GetService(serviceType); + } + + public void RemoveService(Type serviceType) + { + throw new NotSupportedException("Removing one service is not currently supported"); + } + + public void RemoveService(Type serviceType, bool promote) + { + throw new NotSupportedException("Removing one service is not currently supported"); + } +} diff --git a/Slim.Integration/Slim.Integration.csproj b/Slim.Integration/Slim.Integration.csproj index a3089af..8dd671d 100644 --- a/Slim.Integration/Slim.Integration.csproj +++ b/Slim.Integration/Slim.Integration.csproj @@ -19,7 +19,15 @@ - + + + + + + True + + + diff --git a/Slim.sln b/Slim.sln index 5f272ba..370a0c5 100644 --- a/Slim.sln +++ b/Slim.sln @@ -16,13 +16,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pipelines", "Pipelines", "{ .github\workflows\ci.yaml = .github\workflows\ci.yaml EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Slim.Integration", "Slim.Integration\Slim.Integration.csproj", "{13975492-5299-4589-91C1-C33E4FCEDD72}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Slim.Integration", "Slim.Integration\Slim.Integration.csproj", "{13975492-5299-4589-91C1-C33E4FCEDD72}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D20E2AFD-6BE0-4CCC-AD87-D2961CD1763B}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Slim.Integration.Tests", "Slim.Integration.Tests\Slim.Integration.Tests.csproj", "{5B839C6E-7C50-4E39-A010-83CEABA3F1C9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,6 +43,10 @@ Global {13975492-5299-4589-91C1-C33E4FCEDD72}.Debug|Any CPU.Build.0 = Debug|Any CPU {13975492-5299-4589-91C1-C33E4FCEDD72}.Release|Any CPU.ActiveCfg = Release|Any CPU {13975492-5299-4589-91C1-C33E4FCEDD72}.Release|Any CPU.Build.0 = Release|Any CPU + {5B839C6E-7C50-4E39-A010-83CEABA3F1C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B839C6E-7C50-4E39-A010-83CEABA3F1C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B839C6E-7C50-4E39-A010-83CEABA3F1C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B839C6E-7C50-4E39-A010-83CEABA3F1C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE