diff --git a/src/ApiHost1/Module.cs b/src/ApiHost1/Module.cs new file mode 100644 index 00000000..17387fac --- /dev/null +++ b/src/ApiHost1/Module.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using Infrastructure.WebApi.Common; + +namespace ApiHost1; + +public class Module : ISubDomainModule +{ + public Action MinimalApiRegistrationFunction + { + get { return app => app.RegisterRoutes(); } + } + + public Action RegisterServicesFunction + { + get { return (configuration, services) => { }; } + } + + public Assembly ApiAssembly => typeof(Program).Assembly; +} \ No newline at end of file diff --git a/src/ApiHost1/Program.cs b/src/ApiHost1/Program.cs index 9f9db7fe..1d723913 100644 --- a/src/ApiHost1/Program.cs +++ b/src/ApiHost1/Program.cs @@ -1,29 +1,30 @@ -using ApiHost1; +using CarsApi; using CarsApplication; +using Infrastructure.WebApi.Common; using JetBrains.Annotations; -using MinimalApiRegistration = CarsApi.MinimalApiRegistration; + +//TODO: Add the modules of each API here +var modules = new SubDomainModules(); +modules.Register(new Module()); +modules.Register(new ApiHost1.Module()); var builder = WebApplication.CreateBuilder(args); builder.Services.AddMediatR(configuration => { - //TODO: need to add API assembly of each module - configuration.RegisterServicesFromAssemblies(typeof(ApiHost1.MinimalApiRegistration).Assembly, - typeof(MinimalApiRegistration).Assembly); + configuration.RegisterServicesFromAssemblies(modules.HandlerAssemblies.ToArray()); }); builder.Services.AddScoped(); -//TODO: need register dependencies of each module +modules.RegisterServices(builder.Configuration, builder.Services); var app = builder.Build(); -//TODO: need to add validation -//TODO: need to add swaggerUI - +//TODO: need to add validation (https://www.youtube.com/watch?v=XKN0084p7WQ) +//TODO: need to add authentication/authorization (https://www.youtube.com/watch?v=XKN0084p7WQ) +//TODO: need to add swaggerUI (https://www.youtube.com/watch?v=XKN0084p7WQ) -//TODO: need to call the registration function the minimal API endpoints of each module -app.RegisterRoutes(); -MinimalApiRegistration.RegisterRoutes(app); +modules.ConfigureHost(app); app.Run(); diff --git a/src/CarsApi/Module.cs b/src/CarsApi/Module.cs new file mode 100644 index 00000000..4c7fc991 --- /dev/null +++ b/src/CarsApi/Module.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using Infrastructure.WebApi.Common; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace CarsApi; + +public class Module : ISubDomainModule +{ + public Action MinimalApiRegistrationFunction + { + get { return app => app.RegisterRoutes(); } + } + + public Action RegisterServicesFunction + { + get { return (configuration, services) => { }; } + } + + public Assembly ApiAssembly => typeof(CarsApi).Assembly; +} \ No newline at end of file diff --git a/src/Infrastructure.WebApi.Common.UnitTests/GlobalUsings.cs b/src/Infrastructure.WebApi.Common.UnitTests/GlobalUsings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/src/Infrastructure.WebApi.Common.UnitTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/Infrastructure.WebApi.Common.UnitTests/Infrastructure.WebApi.Common.UnitTests.csproj b/src/Infrastructure.WebApi.Common.UnitTests/Infrastructure.WebApi.Common.UnitTests.csproj new file mode 100644 index 00000000..15914e1e --- /dev/null +++ b/src/Infrastructure.WebApi.Common.UnitTests/Infrastructure.WebApi.Common.UnitTests.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + true + + + + + + + + + + + + diff --git a/src/Infrastructure.WebApi.Common.UnitTests/SubDomainModulesSpec.cs b/src/Infrastructure.WebApi.Common.UnitTests/SubDomainModulesSpec.cs new file mode 100644 index 00000000..820b275e --- /dev/null +++ b/src/Infrastructure.WebApi.Common.UnitTests/SubDomainModulesSpec.cs @@ -0,0 +1,114 @@ +using System.Reflection; +using FluentAssertions; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Infrastructure.WebApi.Common.UnitTests; + +[Trait("Category", "Unit")] +public class SubDomainModulesSpec +{ + private readonly SubDomainModules _modules; + + public SubDomainModulesSpec() + { + _modules = new SubDomainModules(); + } + + [Fact] + public void WhenRegisterAndNullModule_ThenThrows() + { + _modules + .Invoking(x => x.Register(null!)) + .Should().Throw(); + } + + [Fact] + public void WhenRegisterAndNullApiAssembly_ThenThrows() + { + _modules + .Invoking(x => x.Register(new TestModule { ApiAssembly = null! })) + .Should().Throw(); + } + + [Fact] + public void WhenRegisterAndNullMinimalApiRegistrationFunction_ThenThrows() + { + _modules + .Invoking(x => x.Register(new TestModule + { ApiAssembly = typeof(SubDomainModulesSpec).Assembly, RegisterServicesFunction = (_, _) => { } })) + .Should().Throw(); + } + + [Fact] + public void WhenRegisterAndNullRegisterServicesFunction_ThenRegisters() + { + _modules.Register(new TestModule + { + ApiAssembly = typeof(SubDomainModulesSpec).Assembly, MinimalApiRegistrationFunction = _ => { }, + RegisterServicesFunction = null! + }); + + _modules.HandlerAssemblies.Should().ContainSingle(); + } + + [Fact] + public void WhenRegisterServicesAndNoModules_ThenAppliedToAllModules() + { + var configuration = new ConfigurationManager(); + var services = new ServiceCollection(); + + _modules.RegisterServices(configuration, services); + } + + [Fact] + public void WhenRegisterServices_ThenAppliedToAllModules() + { + var configuration = new ConfigurationManager(); + var services = new ServiceCollection(); + var wasCalled = false; + _modules.Register(new TestModule + { + ApiAssembly = typeof(SubDomainModulesSpec).Assembly, + MinimalApiRegistrationFunction = _ => { }, + RegisterServicesFunction = (_, _) => { wasCalled = true; } + }); + + _modules.RegisterServices(configuration, services); + + wasCalled.Should().BeTrue(); + } + + [Fact] + public void WhenConfigureHostAndNoModules_ThenAppliedToAllModules() + { + var app = WebApplication.Create(); + + _modules.ConfigureHost(app); + } + + [Fact] + public void WhenConfigureHost_ThenAppliedToAllModules() + { + var app = WebApplication.Create(); + var wasCalled = false; + _modules.Register(new TestModule + { + ApiAssembly = typeof(SubDomainModulesSpec).Assembly, + MinimalApiRegistrationFunction = _ => { wasCalled = true; }, + RegisterServicesFunction = (_, _) => { } + }); + + _modules.ConfigureHost(app); + + wasCalled.Should().BeTrue(); + } +} + +public class TestModule : ISubDomainModule +{ + public Assembly? ApiAssembly { get; init; } + public Action? MinimalApiRegistrationFunction { get; init; } + public Action? RegisterServicesFunction { get; init; } +} \ No newline at end of file diff --git a/src/Infrastructure.WebApi.Common/SubDomainModules.cs b/src/Infrastructure.WebApi.Common/SubDomainModules.cs new file mode 100644 index 00000000..529c2946 --- /dev/null +++ b/src/Infrastructure.WebApi.Common/SubDomainModules.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Infrastructure.WebApi.Common; + +public class SubDomainModules +{ + private readonly List _handlerAssemblies; + private readonly List> _minimalApiRegistrationFunctions; + private readonly List> _serviceCollectionFunctions; + + public SubDomainModules() + { + _handlerAssemblies = new List(); + _minimalApiRegistrationFunctions = new List>(); + _serviceCollectionFunctions = new List>(); + } + + public IReadOnlyList HandlerAssemblies => _handlerAssemblies; + + public void Register(ISubDomainModule module) + { + ArgumentNullException.ThrowIfNull(module); + ArgumentNullException.ThrowIfNull(module.ApiAssembly, nameof(module.ApiAssembly)); + ArgumentNullException.ThrowIfNull(module.MinimalApiRegistrationFunction, + nameof(module.MinimalApiRegistrationFunction)); + + _handlerAssemblies.Add(module.ApiAssembly); + _minimalApiRegistrationFunctions.Add(module.MinimalApiRegistrationFunction); + if (module.RegisterServicesFunction is not null) + { + _serviceCollectionFunctions.Add(module.RegisterServicesFunction); + } + } + + public void RegisterServices(ConfigurationManager configuration, IServiceCollection serviceCollection) + { + _serviceCollectionFunctions.ForEach(func => func(configuration, serviceCollection)); + } + + public void ConfigureHost(WebApplication app) + { + _minimalApiRegistrationFunctions.ForEach(func => func(app)); + } +} + +public interface ISubDomainModule +{ + public Assembly ApiAssembly { get; } + + public Action MinimalApiRegistrationFunction { get; } + public Action? RegisterServicesFunction { get; } +} \ No newline at end of file diff --git a/src/SaaStack.sln b/src/SaaStack.sln index 3c425e02..61881022 100644 --- a/src/SaaStack.sln +++ b/src/SaaStack.sln @@ -71,6 +71,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTesting.WebApi.C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.WebApi.Generators", "Infrastructure.WebApi.Generators\Infrastructure.WebApi.Generators.csproj", "{7AB39FD6-660F-4400-9955-B92684378492}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{19ADDB2F-B589-49EF-9BDA-BD9908057D60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.WebApi.Common.UnitTests", "Infrastructure.WebApi.Common.UnitTests\Infrastructure.WebApi.Common.UnitTests.csproj", "{4CF7C7E2-C95D-4440-9ECF-5D1CE2A46D7A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -150,6 +154,12 @@ Global {7AB39FD6-660F-4400-9955-B92684378492}.Release|Any CPU.Build.0 = Release|Any CPU {7AB39FD6-660F-4400-9955-B92684378492}.ReleaseForDeploy|Any CPU.ActiveCfg = ReleaseForDeploy|Any CPU {7AB39FD6-660F-4400-9955-B92684378492}.ReleaseForDeploy|Any CPU.Build.0 = ReleaseForDeploy|Any CPU + {4CF7C7E2-C95D-4440-9ECF-5D1CE2A46D7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4CF7C7E2-C95D-4440-9ECF-5D1CE2A46D7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4CF7C7E2-C95D-4440-9ECF-5D1CE2A46D7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4CF7C7E2-C95D-4440-9ECF-5D1CE2A46D7A}.Release|Any CPU.Build.0 = Release|Any CPU + {4CF7C7E2-C95D-4440-9ECF-5D1CE2A46D7A}.ReleaseForDeploy|Any CPU.ActiveCfg = ReleaseForDeploy|Any CPU + {4CF7C7E2-C95D-4440-9ECF-5D1CE2A46D7A}.ReleaseForDeploy|Any CPU.Build.0 = ReleaseForDeploy|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {F5C77A86-38AF-40E4-82FC-617E624B2754} = {508E7DA4-4DF2-4201-955D-CCF70C41AD05} @@ -179,5 +189,7 @@ Global {23FF9513-1B26-41F4-A7FE-1D8A9F0808AE} = {BA1AEAEC-68CD-4855-A8CB-0DC2070B6A8C} {A7CA7AD7-70CA-43F0-BE73-75A01342D571} = {5838EE94-374F-4A6F-A231-1BC1C87985F4} {7AB39FD6-660F-4400-9955-B92684378492} = {B68592DF-E8E8-452A-A46F-5C8ECB178FDF} + {19ADDB2F-B589-49EF-9BDA-BD9908057D60} = {B68592DF-E8E8-452A-A46F-5C8ECB178FDF} + {4CF7C7E2-C95D-4440-9ECF-5D1CE2A46D7A} = {19ADDB2F-B589-49EF-9BDA-BD9908057D60} EndGlobalSection EndGlobal