From 94c299d48956c1796bb409bcf8ed2c9356949b0b Mon Sep 17 00:00:00 2001 From: Vladislav Antonyuk Date: Sat, 11 Jan 2025 01:18:17 +0200 Subject: [PATCH] Modular Monolith --- .NET-Templates.sln | 9 + README.md | 10 +- .../WebApp1/WebApp1.csproj | 2 +- templates/ModularMonolith/.editorconfig | 216 ++++++++++ templates/ModularMonolith/.github/FUNDING.yml | 1 + .../ModularMonolith/.github/dependabot.yml | 39 ++ .../.github/workflows/main_App1_client.yml | 95 ++++ .../.github/workflows/main_App1_webapp.yml | 123 ++++++ templates/ModularMonolith/.gitignore | 408 ++++++++++++++++++ .../.template.config/template.json | 49 +++ templates/ModularMonolith/App1.slnx | 42 ++ .../ModularMonolith/Directory.Build.props | 20 + templates/ModularMonolith/README.md | 1 + .../App1.Common.Application.csproj | 17 + .../ApplicationConfiguration.cs | 25 ++ .../ExceptionHandlingPipelineBehavior.cs | 26 ++ .../RequestLoggingPipelineBehavior.cs | 48 +++ .../Behaviors/ValidationPipelineBehavior.cs | 68 +++ .../EventBus/IEventBus.cs | 6 + .../EventBus/IIntegrationEvent.cs | 8 + .../EventBus/IIntegrationEventHandler.cs | 12 + .../EventBus/IntegrationEvent.cs | 8 + .../EventBus/IntegrationEventHandler.cs | 12 + .../Exceptions/App1Exception.cs | 13 + .../Messaging/DomainEventHandler.cs | 14 + .../Messaging/ICommand.cs | 10 + .../Messaging/ICommandHandler.cs | 9 + .../Messaging/IDomainEventHandler.cs | 13 + .../Messaging/IQuery.cs | 6 + .../Messaging/IQueryHandler.cs | 7 + .../App1.Common.Domain.csproj | 8 + .../Common/App1.Common.Domain/DomainEvent.cs | 12 + .../src/Common/App1.Common.Domain/Entity.cs | 18 + .../src/Common/App1.Common.Domain/Error.cs | 28 ++ .../Common/App1.Common.Domain/ErrorType.cs | 10 + .../Common/App1.Common.Domain/IDomainEvent.cs | 8 + .../src/Common/App1.Common.Domain/Result.cs | 61 +++ .../App1.Common.Domain/ValidationError.cs | 11 + .../App1.Common.Infrastructure.csproj | 24 ++ .../AuthenticationExtensions.cs | 16 + .../ClaimsPrincipalExtensions.cs | 17 + .../AdministratorAuthorizationHandler.cs | 5 + .../AdministratorAuthorizationRequirement.cs | 6 + .../Authorization/AuthorizationExtensions.cs | 40 ++ .../IRoleAuthorizationRequirement.cs | 8 + .../Authorization/RoleAuthorizationHandler.cs | 27 ++ .../EnumExtensions.cs | 39 ++ .../EventBus/EventBus.cs | 13 + .../Inbox/InboxMessage.cs | 14 + .../Inbox/InboxMessageConfiguration.cs | 12 + .../InfrastructureConfiguration.cs | 103 +++++ .../Outbox/InsertOutboxMessagesInterceptor.cs | 45 ++ .../Outbox/OutboxMessage.cs | 14 + .../Outbox/OutboxMessageConfiguration.cs | 14 + .../Serialization/SerializerSettings.cs | 23 + .../App1.Common.Presentation.csproj | 15 + .../Endpoints/EndpointExtensions.cs | 37 ++ .../Endpoints/IEndpoint.cs | 8 + .../Results/ApiResults.cs | 82 ++++ .../Results/ResultExtensions.cs | 18 + .../Abstractions/Data/IUnitOfWork.cs | 6 + .../App1.Modules.Module1s.Application.csproj | 13 + .../AssemblyReference.cs | 8 + .../CreateModule1/CreateModule1Command.cs | 5 + .../CreateModule1CommandHandler.cs | 25 ++ .../CreateModule1CommandValidator.cs | 11 + .../Module1CreatedDomainEventHandler.cs | 28 ++ .../Module1s/CreateModule1/Module1Response.cs | 3 + .../DeleteModule1/DeleteModule1Command.cs | 5 + .../DeleteModule1CommandHandler.cs | 27 ++ .../Module1DeletedDomainEventHandler.cs | 16 + .../GetModule1ById/GetModule1Query.cs | 5 + .../GetModule1ById/GetModule1QueryHandler.cs | 21 + .../GetModule1ById/Module1Response.cs | 3 + .../Module1s/GetModule1s/GetModule1Query.cs | 5 + .../GetModule1s/GetModule1QueryHandler.cs | 22 + .../Module1s/GetModule1s/Module1Response.cs | 3 + ...Module1ProfileUpdatedDomainEventHandler.cs | 17 + .../UpdateModule1/UpdateModule1Comand.cs | 5 + .../UpdateModule1CommandHandler.cs | 25 ++ .../UpdateModule1CommandValidator.cs | 11 + .../Abstractions/BaseTest.cs | 17 + .../Abstractions/TestResultExtensions.cs | 12 + ....Modules.Module1s.ArchitectureTests.csproj | 33 ++ .../Application/ApplicationTests.cs | 217 ++++++++++ .../Domain/DomainTests.cs | 76 ++++ .../Layers/LayerTests.cs | 57 +++ .../Presentation/PresentationTests.cs | 50 +++ .../App1.Modules.Module1s.Domain.csproj | 9 + .../Module1s/IModule1Repository.cs | 10 + .../Module1s/Module1.cs | 34 ++ .../Module1s/Module1CreatedDomainEvent.cs | 8 + .../Module1s/Module1DeletedDomainEvent.cs | 8 + .../Module1s/Module1Errors.cs | 16 + .../Module1s/Module1UpdatedDomainEvent.cs | 8 + ...pp1.Modules.Module1s.Infrastructure.csproj | 23 + .../20250110195626_Module1s.Designer.cs | 66 +++ .../Migrations/20250110195626_Module1s.cs | 58 +++ .../Module1sDbContextModelSnapshot.cs | 63 +++ .../Database/Module1sDbContext.cs | 37 ++ .../Database/Schemas.cs | 6 + .../AzureAdB2CGraphClientConfiguration.cs | 11 + .../Module1s/Module1Configuration.cs | 13 + .../Module1s/Module1Repository.cs | 33 ++ .../Module1sModule.cs | 71 +++ .../Outbox/ConfigureProcessOutboxJob.cs | 24 ++ .../Outbox/OutboxOptions.cs | 8 + .../Outbox/ProcessOutboxJob.cs | 82 ++++ ....Modules.Module1s.IntegrationEvents.csproj | 9 + .../Module1CreatedIntegrationEvent.cs | 11 + .../Module1DeletedIntegrationEvent.cs | 9 + .../Module1UpdatedIntegrationEvent.cs | 11 + .../Abstractions/BaseIntegrationTest.cs | 63 +++ .../Fixtures/MockSchemeProvider.cs | 36 ++ .../Abstractions/Fixtures/TestAuthHandler.cs | 47 ++ .../Fixtures/TestAuthHandlerOptions.cs | 10 + .../Abstractions/IntegrationTestCollection.cs | 4 + .../IntegrationTestWebAppFactory.cs | 48 +++ ...1.Modules.Module1s.IntegrationTests.csproj | 34 ++ .../Module1s/GetModule1ProfileTests.cs | 60 +++ .../Module1s/GetModule1Tests.cs | 38 ++ .../Module1s/RegisterModule1Tests.cs | 66 +++ .../Module1s/UpdateModule1Tests.cs | 59 +++ .../App1.Modules.Module1s.Presentation.csproj | 15 + .../AssemblyReference.cs | 8 + .../Module1s/CreateModule1.cs | 42 ++ .../Module1s/DeleteModule1.cs | 27 ++ .../Module1s/GetModule1.cs | 25 ++ .../Module1s/GetModule1ById.cs | 24 ++ .../Module1s/UpdateModule1Profile.cs | 31 ++ .../Tags.cs | 6 + .../Abstractions/BaseTest.cs | 21 + .../App1.Modules.Module1s.UnitTests.csproj | 31 ++ .../Module1s/Module1Tests.cs | 60 +++ .../Abstractions/Data/IUnitOfWork.cs | 6 + .../App1.Modules.Module2s.Application.csproj | 13 + .../AssemblyReference.cs | 8 + .../CreateModule2/CreateModule2Command.cs | 5 + .../CreateModule2CommandHandler.cs | 25 ++ .../CreateModule2CommandValidator.cs | 11 + .../Module2CreatedDomainEventHandler.cs | 28 ++ .../Module2s/CreateModule2/Module2Response.cs | 3 + .../DeleteModule2/DeleteModule2Command.cs | 5 + .../DeleteModule2CommandHandler.cs | 27 ++ .../Module2DeletedDomainEventHandler.cs | 16 + .../GetModule2ById/GetModule2Query.cs | 5 + .../GetModule2ById/GetModule2QueryHandler.cs | 21 + .../GetModule2ById/Module2Response.cs | 3 + .../Module2s/GetModule2s/GetModule2Query.cs | 5 + .../GetModule2s/GetModule2QueryHandler.cs | 22 + .../Module2s/GetModule2s/Module2Response.cs | 3 + ...Module2ProfileUpdatedDomainEventHandler.cs | 17 + .../UpdateModule2/UpdateModule2Comand.cs | 5 + .../UpdateModule2CommandHandler.cs | 25 ++ .../UpdateModule2CommandValidator.cs | 11 + .../Abstractions/BaseTest.cs | 17 + .../Abstractions/TestResultExtensions.cs | 12 + ....Modules.Module2s.ArchitectureTests.csproj | 33 ++ .../Application/ApplicationTests.cs | 217 ++++++++++ .../Domain/DomainTests.cs | 76 ++++ .../Layers/LayerTests.cs | 57 +++ .../Presentation/PresentationTests.cs | 50 +++ .../App1.Modules.Module2s.Domain.csproj | 9 + .../Module2s/IModule2Repository.cs | 10 + .../Module2s/Module2.cs | 34 ++ .../Module2s/Module2CreatedDomainEvent.cs | 8 + .../Module2s/Module2DeletedDomainEvent.cs | 8 + .../Module2s/Module2Errors.cs | 16 + .../Module2s/Module2UpdatedDomainEvent.cs | 8 + ...pp1.Modules.Module2s.Infrastructure.csproj | 23 + .../20250110195653_Module2s.Designer.cs | 90 ++++ .../Migrations/20250110195653_Module2s.cs | 78 ++++ .../Module2sDbContextModelSnapshot.cs | 87 ++++ .../Database/Module2sDbContext.cs | 41 ++ .../Database/Schemas.cs | 6 + .../Inbox/ConfigureProcessInboxJob.cs | 24 ++ .../Inbox/InboxOptions.cs | 8 + .../Inbox/IntegrationEventConsumer.cs | 27 ++ .../Inbox/ProcessInboxJob.cs | 76 ++++ .../AzureAdB2CGraphClientConfiguration.cs | 11 + .../Module2s/Module2Configuration.cs | 13 + .../Module2s/Module2Repository.cs | 33 ++ .../Module2sModule.cs | 84 ++++ .../Outbox/ConfigureProcessOutboxJob.cs | 24 ++ .../Outbox/OutboxOptions.cs | 8 + .../Outbox/ProcessOutboxJob.cs | 82 ++++ ....Modules.Module2s.IntegrationEvents.csproj | 9 + .../Module2CreatedIntegrationEvent.cs | 11 + .../Module2DeletedIntegrationEvent.cs | 9 + .../Module2UpdatedIntegrationEvent.cs | 11 + .../Abstractions/BaseIntegrationTest.cs | 62 +++ .../Fixtures/MockSchemeProvider.cs | 36 ++ .../Abstractions/Fixtures/TestAuthHandler.cs | 44 ++ .../Fixtures/TestAuthHandlerOptions.cs | 10 + .../Abstractions/IntegrationTestCollection.cs | 4 + .../IntegrationTestWebAppFactory.cs | 47 ++ ...1.Modules.Module2s.IntegrationTests.csproj | 34 ++ .../Module2s/GetModule2ProfileTests.cs | 59 +++ .../Module2s/GetModule2Tests.cs | 34 ++ .../Module2s/RegisterModule2Tests.cs | 72 ++++ .../Module2s/UpdateModule2Tests.cs | 54 +++ .../App1.Modules.Module2s.Presentation.csproj | 15 + .../AssemblyReference.cs | 8 + .../Module2s/CreateModule2.cs | 42 ++ .../Module2s/DeleteModule2.cs | 27 ++ .../Module2s/GetModule2.cs | 25 ++ .../Module2s/GetModule2ById.cs | 24 ++ .../Module2s/UpdateModule2Profile.cs | 31 ++ .../Tags.cs | 6 + .../Abstractions/BaseTest.cs | 21 + .../App1.Modules.Module2s.UnitTests.csproj | 31 ++ .../Module2s/Module2Tests.cs | 60 +++ .../App1.ApiService/App1.ApiService.csproj | 21 + .../Extensions/ConfigurationExtensions.cs | 13 + .../Extensions/MigrationExtensions.cs | 22 + .../BearerSecuritySchemeTransformer.cs | 33 ++ .../Middleware/GlobalExceptionHandler.cs | 27 ++ .../LogContextTraceLoggingMiddleware.cs | 16 + .../Middleware/MiddlewareExtensions.cs | 11 + .../src/Web/App1.ApiService/Program.cs | 65 +++ .../Properties/launchSettings.json | 15 + .../src/Web/App1.ApiService/appsettings.json | 24 ++ .../Web/App1.ApiService/modules.module1s.json | 8 + .../Web/App1.ApiService/modules.module2s.json | 12 + .../src/Web/App1.ApiService/users.http | 12 + .../src/Web/App1.ApiService/web.config | 9 + .../App1.AppHost.Tests.csproj | 36 ++ .../src/Web/App1.AppHost.Tests/WebTests.cs | 31 ++ .../src/Web/App1.AppHost/.gitignore | 1 + .../src/Web/App1.AppHost/App1.AppHost.csproj | 24 ++ .../src/Web/App1.AppHost/Program.cs | 40 ++ .../Properties/launchSettings.json | 17 + .../src/Web/App1.AppHost/appsettings.json | 9 + .../src/Web/App1.AppHost/azure.yaml | 8 + .../src/Web/App1.AppHost/next-steps.md | 74 ++++ .../App1.ServiceDefaults.csproj | 18 + .../Web/App1.ServiceDefaults/Extensions.cs | 103 +++++ .../src/Web/App1.Web/App1.Web.csproj | 23 + .../Pages/Account/SignedOut.cshtml | 10 + .../Pages/Account/SignedOut.cshtml.cs | 14 + .../src/Web/App1.Web/Components/App.razor | 23 + .../Components/App1AuthBaseComponent.cs | 33 ++ .../App1.Web/Components/App1BaseComponent.cs | 20 + .../Components/Layout/App1BaseLayout.cs | 5 + .../Components/Layout/MainLayout.razor | 73 ++++ .../Components/Layout/MainLayout.razor.cs | 50 +++ .../App1.Web/Components/Layout/NavMenu.razor | 25 ++ .../Components/Layout/NavMenu.razor.cs | 15 + .../Components/Layout/NavMenu.razor.css | 7 + .../App1.Web/Components/LoadingControl.razor | 6 + .../Components/LoadingControl.razor.cs | 5 + .../App1.Web/Components/LoginControl.razor | 17 + .../App1.Web/Components/LoginControl.razor.cs | 3 + .../Web/App1.Web/Components/Pages/About.razor | 6 + .../App1.Web/Components/Pages/About.razor.cs | 5 + .../Web/App1.Web/Components/Pages/Error.razor | 25 ++ .../App1.Web/Components/Pages/Error.razor.cs | 18 + .../Web/App1.Web/Components/Pages/Home.razor | 13 + .../App1.Web/Components/Pages/Home.razor.cs | 5 + .../App1.Web/Components/Pages/Privacy.razor | 279 ++++++++++++ .../Components/Pages/Privacy.razor.cs | 5 + .../src/Web/App1.Web/Components/Promo.razor | 48 +++ .../Web/App1.Web/Components/Promo.razor.cs | 14 + .../Web/App1.Web/Components/Promo.razor.css | 4 + .../src/Web/App1.Web/Components/Routes.razor | 6 + .../Web/App1.Web/Components/_Imports.razor | 13 + .../src/Web/App1.Web/Constants.cs | 6 + ...dentityUserAuthenticationMessageHandler.cs | 28 ++ .../src/Web/App1.Web/Program.cs | 39 ++ .../App1.Web/Properties/launchSettings.json | 14 + .../Web/App1.Web/Services/App1ApiClient.cs | 29 ++ .../App1.Web/Services/ServiceExtensions.cs | 73 ++++ .../Web/App1.Web/Services/TaskExtensions.cs | 29 ++ .../Services/User/CurrentUserService.cs | 24 ++ .../Services/User/ICurrentUserService.cs | 6 + .../Web/App1.Web/Services/User/UserInfo.cs | 9 + .../src/Web/App1.Web/appsettings.json | 25 ++ .../Web/App1.Web/i18ntext/Translation.en.json | 3 + .../Web/App1.Web/i18ntext/Translation.uk.json | 3 + .../src/Web/App1.Web/web.config | 9 + .../wwwroot/assets/default-location-pin.png | Bin 0 -> 4084 bytes .../src/Web/App1.Web/wwwroot/assets/logo.svg | 1 + .../wwwroot/assets/user-location-pin.png | Bin 0 -> 14806 bytes .../src/Web/App1.Web/wwwroot/css/site.css | 16 + .../src/Web/App1.Web/wwwroot/favicon.ico | Bin 0 -> 4286 bytes .../src/Web/App1.Web/wwwroot/sitemap.xml | 21 + .../App1.Application.Tests.csproj | 8 +- .../App1.Infrastructure.Business.Tests.csproj | 8 +- .../App1.Infrastructure.Data.Tests.csproj | 8 +- .../App1.Application.Tests.csproj | 8 +- .../App1.Infrastructure.Business.Tests.csproj | 8 +- .../App1.Infrastructure.Data.Tests.csproj | 8 +- .../App1.Application.Tests.csproj | 8 +- .../App1.Infrastructure.Business.Tests.csproj | 8 +- .../App1.Infrastructure.Data.Tests.csproj | 8 +- .../UI/Client/App1.Client/App1.Client.csproj | 6 +- .../App1.Application.Tests.csproj | 8 +- ...nfrastructure.Client.Business.Tests.csproj | 8 +- ...p1.Infrastructure.Client.Data.Tests.csproj | 8 +- ...nfrastructure.WebApp.Business.Tests.csproj | 8 +- ...p1.Infrastructure.WebApp.Data.Tests.csproj | 8 +- .../UI/Client/App1.Client/App1.Client.csproj | 6 +- .../App1.Application.Tests.csproj | 8 +- ...nfrastructure.Client.Business.Tests.csproj | 8 +- ...p1.Infrastructure.Client.Data.Tests.csproj | 8 +- ...nfrastructure.WebApp.Business.Tests.csproj | 8 +- ...p1.Infrastructure.WebApp.Data.Tests.csproj | 8 +- .../src/UI/App1.Client/App1.Client.csproj | 6 +- .../App1.Application.Tests.csproj | 8 +- .../App1.Infrastructure.Business.Tests.csproj | 8 +- .../App1.Infrastructure.Data.Tests.csproj | 8 +- .../src/UI/App1.Client/App1.Client.csproj | 6 +- .../App1.Application.Tests.csproj | 8 +- .../App1.Infrastructure.Business.Tests.csproj | 8 +- .../App1.Infrastructure.Data.Tests.csproj | 8 +- 315 files changed, 8343 insertions(+), 117 deletions(-) create mode 100644 templates/ModularMonolith/.editorconfig create mode 100644 templates/ModularMonolith/.github/FUNDING.yml create mode 100644 templates/ModularMonolith/.github/dependabot.yml create mode 100644 templates/ModularMonolith/.github/workflows/main_App1_client.yml create mode 100644 templates/ModularMonolith/.github/workflows/main_App1_webapp.yml create mode 100644 templates/ModularMonolith/.gitignore create mode 100644 templates/ModularMonolith/.template.config/template.json create mode 100644 templates/ModularMonolith/App1.slnx create mode 100644 templates/ModularMonolith/Directory.Build.props create mode 100644 templates/ModularMonolith/README.md create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/App1.Common.Application.csproj create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/ApplicationConfiguration.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/Behaviors/ExceptionHandlingPipelineBehavior.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/Behaviors/RequestLoggingPipelineBehavior.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/Behaviors/ValidationPipelineBehavior.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IEventBus.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IIntegrationEvent.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IIntegrationEventHandler.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IntegrationEvent.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IntegrationEventHandler.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/Exceptions/App1Exception.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/DomainEventHandler.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/ICommand.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/ICommandHandler.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/IDomainEventHandler.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/IQuery.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/IQueryHandler.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Domain/App1.Common.Domain.csproj create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Domain/DomainEvent.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Domain/Entity.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Domain/Error.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Domain/ErrorType.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Domain/IDomainEvent.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Domain/Result.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Domain/ValidationError.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/App1.Common.Infrastructure.csproj create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authentication/AuthenticationExtensions.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authentication/ClaimsPrincipalExtensions.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/AdministratorAuthorizationHandler.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/AdministratorAuthorizationRequirement.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/AuthorizationExtensions.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/IRoleAuthorizationRequirement.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/RoleAuthorizationHandler.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/EnumExtensions.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/EventBus/EventBus.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Inbox/InboxMessage.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Inbox/InboxMessageConfiguration.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/InfrastructureConfiguration.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Outbox/InsertOutboxMessagesInterceptor.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Outbox/OutboxMessage.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Outbox/OutboxMessageConfiguration.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Serialization/SerializerSettings.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Presentation/App1.Common.Presentation.csproj create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Presentation/Endpoints/EndpointExtensions.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Presentation/Endpoints/IEndpoint.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Presentation/Results/ApiResults.cs create mode 100644 templates/ModularMonolith/src/Common/App1.Common.Presentation/Results/ResultExtensions.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Abstractions/Data/IUnitOfWork.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/App1.Modules.Module1s.Application.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/AssemblyReference.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/CreateModule1Command.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/CreateModule1CommandHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/CreateModule1CommandValidator.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/Module1CreatedDomainEventHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/Module1Response.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/DeleteModule1/DeleteModule1Command.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/DeleteModule1/DeleteModule1CommandHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/DeleteModule1/Module1DeletedDomainEventHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1ById/GetModule1Query.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1ById/GetModule1QueryHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1ById/Module1Response.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1s/GetModule1Query.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1s/GetModule1QueryHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1s/Module1Response.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/Module1ProfileUpdatedDomainEventHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/UpdateModule1Comand.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/UpdateModule1CommandHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/UpdateModule1CommandValidator.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Abstractions/BaseTest.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Abstractions/TestResultExtensions.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/App1.Modules.Module1s.ArchitectureTests.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Application/ApplicationTests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Domain/DomainTests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Layers/LayerTests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Presentation/PresentationTests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/App1.Modules.Module1s.Domain.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/IModule1Repository.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1CreatedDomainEvent.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1DeletedDomainEvent.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1Errors.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1UpdatedDomainEvent.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/App1.Modules.Module1s.Infrastructure.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Migrations/20250110195626_Module1s.Designer.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Migrations/20250110195626_Module1s.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Migrations/Module1sDbContextModelSnapshot.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Module1sDbContext.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Schemas.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1s/AzureAdB2CGraphClientConfiguration.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1s/Module1Configuration.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1s/Module1Repository.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1sModule.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Outbox/ConfigureProcessOutboxJob.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Outbox/OutboxOptions.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Outbox/ProcessOutboxJob.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/App1.Modules.Module1s.IntegrationEvents.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/Module1CreatedIntegrationEvent.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/Module1DeletedIntegrationEvent.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/Module1UpdatedIntegrationEvent.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/BaseIntegrationTest.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/Fixtures/MockSchemeProvider.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/Fixtures/TestAuthHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/Fixtures/TestAuthHandlerOptions.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/IntegrationTestCollection.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/IntegrationTestWebAppFactory.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/App1.Modules.Module1s.IntegrationTests.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/GetModule1ProfileTests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/GetModule1Tests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/RegisterModule1Tests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/UpdateModule1Tests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/App1.Modules.Module1s.Presentation.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/AssemblyReference.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/CreateModule1.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/DeleteModule1.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/GetModule1.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/GetModule1ById.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/UpdateModule1Profile.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Tags.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.UnitTests/Abstractions/BaseTest.cs create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.UnitTests/App1.Modules.Module1s.UnitTests.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.UnitTests/Module1s/Module1Tests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Abstractions/Data/IUnitOfWork.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/App1.Modules.Module2s.Application.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/AssemblyReference.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/CreateModule2Command.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/CreateModule2CommandHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/CreateModule2CommandValidator.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/Module2CreatedDomainEventHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/Module2Response.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/DeleteModule2/DeleteModule2Command.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/DeleteModule2/DeleteModule2CommandHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/DeleteModule2/Module2DeletedDomainEventHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2ById/GetModule2Query.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2ById/GetModule2QueryHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2ById/Module2Response.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2s/GetModule2Query.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2s/GetModule2QueryHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2s/Module2Response.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/Module2ProfileUpdatedDomainEventHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/UpdateModule2Comand.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/UpdateModule2CommandHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/UpdateModule2CommandValidator.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Abstractions/BaseTest.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Abstractions/TestResultExtensions.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/App1.Modules.Module2s.ArchitectureTests.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Application/ApplicationTests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Domain/DomainTests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Layers/LayerTests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Presentation/PresentationTests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/App1.Modules.Module2s.Domain.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/IModule2Repository.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2CreatedDomainEvent.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2DeletedDomainEvent.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2Errors.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2UpdatedDomainEvent.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/App1.Modules.Module2s.Infrastructure.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Migrations/20250110195653_Module2s.Designer.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Migrations/20250110195653_Module2s.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Migrations/Module2sDbContextModelSnapshot.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Module2sDbContext.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Schemas.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/ConfigureProcessInboxJob.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/InboxOptions.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/IntegrationEventConsumer.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/ProcessInboxJob.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2s/AzureAdB2CGraphClientConfiguration.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2s/Module2Configuration.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2s/Module2Repository.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2sModule.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Outbox/ConfigureProcessOutboxJob.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Outbox/OutboxOptions.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Outbox/ProcessOutboxJob.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/App1.Modules.Module2s.IntegrationEvents.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/Module2CreatedIntegrationEvent.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/Module2DeletedIntegrationEvent.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/Module2UpdatedIntegrationEvent.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/BaseIntegrationTest.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/Fixtures/MockSchemeProvider.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/Fixtures/TestAuthHandler.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/Fixtures/TestAuthHandlerOptions.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/IntegrationTestCollection.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/IntegrationTestWebAppFactory.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/App1.Modules.Module2s.IntegrationTests.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/GetModule2ProfileTests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/GetModule2Tests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/RegisterModule2Tests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/UpdateModule2Tests.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/App1.Modules.Module2s.Presentation.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/AssemblyReference.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/CreateModule2.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/DeleteModule2.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/GetModule2.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/GetModule2ById.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/UpdateModule2Profile.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Tags.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.UnitTests/Abstractions/BaseTest.cs create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.UnitTests/App1.Modules.Module2s.UnitTests.csproj create mode 100644 templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.UnitTests/Module2s/Module2Tests.cs create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/App1.ApiService.csproj create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/Extensions/ConfigurationExtensions.cs create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/Extensions/MigrationExtensions.cs create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/Infrastructure/BearerSecuritySchemeTransformer.cs create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/Middleware/GlobalExceptionHandler.cs create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/Middleware/LogContextTraceLoggingMiddleware.cs create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/Middleware/MiddlewareExtensions.cs create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/Program.cs create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/Properties/launchSettings.json create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/appsettings.json create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/modules.module1s.json create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/modules.module2s.json create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/users.http create mode 100644 templates/ModularMonolith/src/Web/App1.ApiService/web.config create mode 100644 templates/ModularMonolith/src/Web/App1.AppHost.Tests/App1.AppHost.Tests.csproj create mode 100644 templates/ModularMonolith/src/Web/App1.AppHost.Tests/WebTests.cs create mode 100644 templates/ModularMonolith/src/Web/App1.AppHost/.gitignore create mode 100644 templates/ModularMonolith/src/Web/App1.AppHost/App1.AppHost.csproj create mode 100644 templates/ModularMonolith/src/Web/App1.AppHost/Program.cs create mode 100644 templates/ModularMonolith/src/Web/App1.AppHost/Properties/launchSettings.json create mode 100644 templates/ModularMonolith/src/Web/App1.AppHost/appsettings.json create mode 100644 templates/ModularMonolith/src/Web/App1.AppHost/azure.yaml create mode 100644 templates/ModularMonolith/src/Web/App1.AppHost/next-steps.md create mode 100644 templates/ModularMonolith/src/Web/App1.ServiceDefaults/App1.ServiceDefaults.csproj create mode 100644 templates/ModularMonolith/src/Web/App1.ServiceDefaults/Extensions.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/App1.Web.csproj create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/App.razor create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/App1AuthBaseComponent.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/App1BaseComponent.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Layout/App1BaseLayout.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Layout/MainLayout.razor create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Layout/MainLayout.razor.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Layout/NavMenu.razor create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Layout/NavMenu.razor.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Layout/NavMenu.razor.css create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/LoadingControl.razor create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/LoadingControl.razor.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/LoginControl.razor create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/LoginControl.razor.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Pages/About.razor create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Pages/About.razor.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Error.razor create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Error.razor.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Home.razor create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Home.razor.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Privacy.razor create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Privacy.razor.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Promo.razor create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Promo.razor.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Promo.razor.css create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/Routes.razor create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Components/_Imports.razor create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Constants.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/MicrosoftIdentityUserAuthenticationMessageHandler.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Program.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Properties/launchSettings.json create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Services/App1ApiClient.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Services/ServiceExtensions.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Services/TaskExtensions.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Services/User/CurrentUserService.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Services/User/ICurrentUserService.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/Services/User/UserInfo.cs create mode 100644 templates/ModularMonolith/src/Web/App1.Web/appsettings.json create mode 100644 templates/ModularMonolith/src/Web/App1.Web/i18ntext/Translation.en.json create mode 100644 templates/ModularMonolith/src/Web/App1.Web/i18ntext/Translation.uk.json create mode 100644 templates/ModularMonolith/src/Web/App1.Web/web.config create mode 100644 templates/ModularMonolith/src/Web/App1.Web/wwwroot/assets/default-location-pin.png create mode 100644 templates/ModularMonolith/src/Web/App1.Web/wwwroot/assets/logo.svg create mode 100644 templates/ModularMonolith/src/Web/App1.Web/wwwroot/assets/user-location-pin.png create mode 100644 templates/ModularMonolith/src/Web/App1.Web/wwwroot/css/site.css create mode 100644 templates/ModularMonolith/src/Web/App1.Web/wwwroot/favicon.ico create mode 100644 templates/ModularMonolith/src/Web/App1.Web/wwwroot/sitemap.xml diff --git a/.NET-Templates.sln b/.NET-Templates.sln index df00e41..911835f 100644 --- a/.NET-Templates.sln +++ b/.NET-Templates.sln @@ -318,6 +318,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BlazorWebAppMicrosoftIdenti EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApp1", "templates\BlazorWebAppMicrosoftIdentityPlatform\WebApp1\WebApp1.csproj", "{A51BB60B-104E-40D9-82BB-76CC66DD9D14}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + validate-build.ps1 = validate-build.ps1 + validate-build.sh = validate-build.sh + validate-packages.ps1 = validate-packages.ps1 + validate-packages.sh = validate-packages.sh + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/README.md b/README.md index 3ad98ac..953083c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ [![NuGet Downloads](https://img.shields.io/nuget/dt/VladislavAntonyuk.DotNetTemplates.svg?style=flat-square)](https://www.nuget.org/packages/VladislavAntonyuk.DotNetTemplates) Table of contents: +* [Modular Monolith](#modular-monolith) * [Blazor WebApp Microsoft Identity Platform](#blazor-webapp-microsoft-identity-platform) * [Onion Architecture Templates](#onion-architecture-templates) + [Onion Architecture Cross-Platform Application](#onion-architecture-cross-platform-application) @@ -43,14 +44,14 @@ Table of contents: [Table of contents generated with markdown-toc]: <> -## Blazor Maui Shared +## Modular Monolith -Creates .NET MAUI application, Blazor WevApp and Blazor WebAssembly projects with shared UI. +Creates a Modular Monolith application. Create solution: ```pwsh -dotnet new blazor-maui-shared -n MyProductName --ApplicationId com.vladislavantonyuk.myapp +dotnet new modular-monolith -n MyProductName --module1 MyModule1Name --module2 MyModule2Name ``` ## Blazor WebApp Microsoft Identity Platform @@ -73,6 +74,7 @@ Create solution: ```pwsh dotnet new onion-app -n MyProductName --entityName MyEntityName +``` ### Onion Architecture Cross-Platform Application Repository @@ -367,7 +369,7 @@ The final application id: `com.vladislavantonyuk.myapp.myapp-TodayExtension`. ## Build ```pwsh -dotnet pack +dotnet pack .\VladislavAntonyukDotnetTemplates.csproj ``` ## Install Templates diff --git a/templates/BlazorWebAppMicrosoftIdentityPlatform/WebApp1/WebApp1.csproj b/templates/BlazorWebAppMicrosoftIdentityPlatform/WebApp1/WebApp1.csproj index 5c0a7f3..061b078 100644 --- a/templates/BlazorWebAppMicrosoftIdentityPlatform/WebApp1/WebApp1.csproj +++ b/templates/BlazorWebAppMicrosoftIdentityPlatform/WebApp1/WebApp1.csproj @@ -8,6 +8,6 @@ - + diff --git a/templates/ModularMonolith/.editorconfig b/templates/ModularMonolith/.editorconfig new file mode 100644 index 0000000..2d2eaf7 --- /dev/null +++ b/templates/ModularMonolith/.editorconfig @@ -0,0 +1,216 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +############################### +# Core EditorConfig Options # +############################### +# All files +[*] +end_of_line = crlf +indent_style = tab +csharp_style_namespace_declarations = file_scoped +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_braces_for_for = required +resharper_braces_for_foreach = required +resharper_braces_for_ifelse = required +resharper_braces_for_while = required +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_redundant_base_qualifier_highlighting = warning +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_xml_indent_style = tab +resharper_xml_use_indent_from_vs = false +resharper_xmldoc_indent_style = tab +resharper_xmldoc_use_indent_from_vs = false +trim_trailing_whitespace = true +csharp_new_line_before_members_in_object_initializers = true +csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_using_directive_Module2ment = inside_namespace:silent +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.lower_camel_case_style_1.capitalization = camel_case +dotnet_naming_style.lower_camel_case_style_1.required_prefix = _ +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# Microsoft .NET properties +csharp_preserve_single_line_blocks = true +dotnet_naming_rule.constants_rule.severity = warning +dotnet_naming_rule.constants_rule.style = upper_camel_case_style +dotnet_naming_rule.constants_rule.symbols = constants_symbols +dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.constants_symbols.applicable_kinds = field +dotnet_naming_symbols.constants_symbols.required_modifiers = const + +# ReSharper properties +resharper_align_linq_query = true +resharper_align_multiline_argument = true +resharper_align_multiline_calls_chain = true +resharper_align_multiline_extends_list = true +resharper_align_multiline_for_stmt = true +resharper_align_multline_type_parameter_constrains = true +resharper_align_multline_type_parameter_list = true +resharper_align_tuple_components = true +resharper_csharp_align_multiline_parameter = false +resharper_csharp_align_multiple_declaration = true +resharper_csharp_indent_style = tab +resharper_csharp_naming_rule.constants = AaBb, aaBb +resharper_csharp_naming_rule.private_constants = AaBb +resharper_csharp_naming_rule.private_static_fields = _ + aaBb +resharper_csharp_naming_rule.private_static_readonly = AaBb +resharper_csharp_stick_comment = false +resharper_csharp_wrap_chained_method_calls = chop_if_long +resharper_csharp_wrap_extends_list_style = chop_if_long +resharper_csharp_wrap_parameters_style = chop_if_long +resharper_enforce_line_ending_style = true +resharper_keep_existing_arrangement = false +resharper_max_initializer_elements_on_line = 0 +resharper_nested_ternary_style = compact +resharper_Module2_attribute_on_same_line = false +resharper_Module2_expr_accessor_on_single_line = true +resharper_Module2_expr_method_on_single_line = true +resharper_Module2_expr_property_on_single_line = true +resharper_Module2_simple_accessor_on_single_line = false +resharper_Module2_simple_anonymousmethod_on_single_line = false +resharper_Module2_simple_embedded_statement_on_same_line = false +resharper_Module2_simple_initializer_on_single_line = false +resharper_use_indent_from_vs = false +resharper_wrap_array_initializer_style = chop_always +resharper_wrap_chained_binary_expressions = chop_if_long +resharper_wrap_object_and_collection_initializer_style = chop_always +# Code files +[*.{cs,csx,vb,vbx}] +charset = utf-8-bom +indent_size = 4 +insert_final_newline = false +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_sort_system_directives_first = true +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_readonly_field = true:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +############################### +# C# Coding Conventions # +############################### +[*.cs] +csharp_indent_case_contents_when_block = true +csharp_indent_case_contents = true +csharp_indent_labels = flush_left +csharp_indent_switch_labels = true +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true +csharp_prefer_braces = true:silent +csharp_prefer_simple_default_expression = true:suggestion +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_around_binary_operators = before_and_after +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_var_elsewhere = true:silent +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:silent +dotnet_analyzer_diagnostic.category-.severity = unset +dotnet_analyzer_diagnostic.severity = unset +dotnet_diagnostic..severity = unset +dotnet_separate_import_directive_groups = false +indent_style = tab +indent_size = 4 +tab_width = 4 + +[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,jsproj,lsproj,njsproj,nuspec,proj,props,proto,StyleCop,targets,tasks,vbproj,xaml,xamlx,xml,xoml,xsd,asax,ascx,aspx,cs,cshtml,css,htm,html,js,json,jsx,master,razor,resjson,resw,resx,skin,ts,tsx,vb}] +indent_size = 4 +indent_style = tab +tab_width = 4 \ No newline at end of file diff --git a/templates/ModularMonolith/.github/FUNDING.yml b/templates/ModularMonolith/.github/FUNDING.yml new file mode 100644 index 0000000..52d9f3e --- /dev/null +++ b/templates/ModularMonolith/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ["https://www.buymeacoffee.com/vlad.antonyuk"] \ No newline at end of file diff --git a/templates/ModularMonolith/.github/dependabot.yml b/templates/ModularMonolith/.github/dependabot.yml new file mode 100644 index 0000000..05cad52 --- /dev/null +++ b/templates/ModularMonolith/.github/dependabot.yml @@ -0,0 +1,39 @@ +version: 2 +updates: +- package-ecosystem: nuget + directory: "/" + schedule: + interval: daily + reviewers: + - "VladislavAntonyuk" + assignees: + - "VladislavAntonyuk" + open-pull-requests-limit: 10 + groups: + Aspire: + patterns: + - "Aspire*" + HotChocolate: + patterns: + - "HotChocolate*" + Microsoft: + patterns: + - "Microsoft*" + MassTransit: + patterns: + - "MassTransit*" + Syncfusion: + patterns: + - "Syncfusion*" + CommunityToolkit: + patterns: + - "CommunityToolkit*" + OpenTelementry: + patterns: + - "OpenTelementry*" + Testcontainers: + patterns: + - "Testcontainers*" + xUnit: + patterns: + - "xunit*" diff --git a/templates/ModularMonolith/.github/workflows/main_App1_client.yml b/templates/ModularMonolith/.github/workflows/main_App1_client.yml new file mode 100644 index 0000000..90b3987 --- /dev/null +++ b/templates/ModularMonolith/.github/workflows/main_App1_client.yml @@ -0,0 +1,95 @@ +name: Build and deploy client + +on: + push: + branches: [ main ] + paths: + - 'src/Client/**' + - '.github/workflows/main_world-explorer_client.yml' + pull_request: + branches: [ main ] + paths: + - 'src/Client/**' + - '.github/workflows/main_world-explorer_client.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + VERSION: 2.0.${{github.run_number}}.0 + +jobs: + buildClient: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest] + #os: [windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.x' + + - name: Install workloads + run: dotnet workload install maui + + - name: Prepare build + run: | + (Get-Content src\Client\Client\Platforms\Windows\Package.appxmanifest).Replace('1.0.0.0', '${{ env.VERSION }}') | Set-Content src\Client\Client\Platforms\Windows\Package.appxmanifest + (Get-Content src\Client\Client\Client.csproj).Replace('1.0', '${{ env.VERSION }}') | Set-Content src\Client\Client\Client.csproj + (Get-Content src\Client\Client\Client.csproj).Replace('1', '${{github.run_number}}') | Set-Content src\Client\Client\Client.csproj + + - name: Build Client (Android) + run: | + dotnet publish src/Client/Client/Client.csproj -f net9.0-android /p:Version="${{ env.VERSION }}" + mkdir output\android + copy ".\src\Client\Client\bin\Release\net9.0-android\publish\com.vladislavantonyuk.worldexplorer.aab" "output\android" + + - name: Build Client (Apple) + run: | + dotnet build src/Client/Client/Client.csproj -f net9.0-ios /p:Version="${{ env.VERSION }}" + dotnet publish src/Client/Client/Client.csproj -f net9.0-maccatalyst /p:Version="${{ env.VERSION }}" + mkdir output\maccatalyst + copy ".\src\Client\Client\bin\Release\net9.0-maccatalyst\**\*.pkg" "output\maccatalyst" + + - name: Build Client (Windows) + run: | + dotnet publish src/Client/Client/Client.csproj -f net9.0-windows10.0.19041.0 /p:Version="${{ env.VERSION }}" + mkdir output\windows + copy ".\src\Client\Client\bin\Release\net9.0-windows10.0.19041.0\win10-x64\AppPackages\**\*.msix" "output\windows" + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: client + path: output + retention-days: 7 + + deployClient: + runs-on: windows-latest + needs: buildClient + if: github.event_name != 'pull_request' + environment: + name: 'Production' + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: client + path: "${{ github.workspace }}/client" + + # - name: Deploy Windows App to Microsoft Store + # uses: isaacrlevin/windows-store-action + # with: + # tenant-id: ${{ secrets.AZURE_AD_TENANT_ID }} + # client-id: ${{ secrets.AZURE_AD_APPLICATION_CLIENT_ID }} + # client-secret: ${{ secrets.AZURE_AD_APPLICATION_SECRET }} + # app-id: ${{ secrets.STORE_APP_ID }} + # package-path: "${{ github.workspace }}/client/windows/WorldExplorer.msix" diff --git a/templates/ModularMonolith/.github/workflows/main_App1_webapp.yml b/templates/ModularMonolith/.github/workflows/main_App1_webapp.yml new file mode 100644 index 0000000..9c594df --- /dev/null +++ b/templates/ModularMonolith/.github/workflows/main_App1_webapp.yml @@ -0,0 +1,123 @@ +name: Build and deploy webapp + +on: + push: + branches: [ main ] + paths: + - 'src/Web/**' + - 'src/Common/**' + - 'src/Modules/**' + - '.github/workflows/main_world-explorer_webapp.yml' + pull_request: + branches: [ main ] + paths: + - 'src/Web/**' + - 'src/Common/**' + - 'src/Modules/**' + - '.github/workflows/main_world-explorer_webapp.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + VERSION: 2.0.${{github.run_number}}.0 + +jobs: + buildWebApp: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.x' + + - name: Publish WebApp + run: dotnet publish src/Web/WorldExplorer.Web/WorldExplorer.Web.csproj -o '${{env.DOTNET_ROOT}}/webapp' /p:Version="${{ env.VERSION }}" + + - name: Publish WebApi + run: dotnet publish src/Web/WorldExplorer.ApiService/WorldExplorer.ApiService.csproj -o '${{env.DOTNET_ROOT}}/webapi' /p:Version="${{ env.VERSION }}" + + - name: Upload artifact for webapp job + uses: actions/upload-artifact@v4 + with: + name: webapp + path: ${{env.DOTNET_ROOT}}/webapp + retention-days: 1 + + - name: Upload artifact for webapi job + uses: actions/upload-artifact@v4 + with: + name: webapi + path: ${{env.DOTNET_ROOT}}/webapi + retention-days: 1 + + deployWebApi: + runs-on: windows-latest + needs: buildWebApp + if: github.event_name != 'pull_request' + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapi.outputs.webapp-url }} + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: webapi + + - uses: cschleiden/replace-tokens@v1 + env: + AAD_B2C_CLAIMS_BASIC_AUTH_USERNAME: ${{ secrets.AAD_B2C_CLAIMS_BASIC_AUTH_USERNAME }} + AAD_B2C_CLAIMS_BASIC_AUTH_PASSWORD: ${{ secrets.AAD_B2C_CLAIMS_BASIC_AUTH_PASSWORD }} + AAD_B2C_CLIENT_SECRET: ${{ secrets.AAD_B2C_CLIENT_SECRET }} + AAD_B2C_GRAPH_CLIENT_SECRET: ${{ secrets.AAD_B2C_GRAPH_CLIENT_SECRET }} + GOOGLE_SEARCH_APIKEY: ${{ secrets.GOOGLE_SEARCH_APIKEY }} + with: + tokenPrefix: '#{' + tokenSuffix: '}#' + files: '["**/appsettings.json", "**/modules.**.json"]' + + - name: Deploy to Azure Web App + id: deploy-to-webapi + uses: azure/webapps-deploy@v3 + with: + app-name: 'world-explorer-api' + slot-name: 'Production' + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_680706DABCBA4243912E52268C835D16 }} + package: . + + deployWebApp: + runs-on: windows-latest + needs: deployWebApi + if: github.event_name != 'pull_request' + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: webapp + + - uses: cschleiden/replace-tokens@v1 + env: + AAD_B2C_CLIENT_SECRET: ${{ secrets.AAD_B2C_CLIENT_SECRET }} + WORLD_EXPLORER_API_CLIENT_SCOPES: ${{ secrets.WORLD_EXPLORER_API_CLIENT_SCOPES }} + with: + tokenPrefix: '#{' + tokenSuffix: '}#' + files: '["**/appsettings.json"]' + + - name: Deploy to Azure Web App + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'world-explorer' + slot-name: 'Production' + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_649309CDF7F74F7B9D0D75A9017C1111 }} + package: . diff --git a/templates/ModularMonolith/.gitignore b/templates/ModularMonolith/.gitignore new file mode 100644 index 0000000..068788e --- /dev/null +++ b/templates/ModularMonolith/.gitignore @@ -0,0 +1,408 @@ +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# content below from: https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +.vscode/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +!**/[Aa]ssets/*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ +*.[Dd]evelopment.json \ No newline at end of file diff --git a/templates/ModularMonolith/.template.config/template.json b/templates/ModularMonolith/.template.config/template.json new file mode 100644 index 0000000..7e5f11e --- /dev/null +++ b/templates/ModularMonolith/.template.config/template.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Vladislav Antonyuk", + "classifications": ["Web","MAUI"], + "description": "Creates Modular Monolith application with Microsoft Identity Platform authentication (Azure Active Directory B2C).", + "name": "Modular Monolith App with Microsoft Identity Platform authentication", + "identity": "VladislavAntonyuk.DotNetTemplates.ModularMonolith", + "tags": { + "language": "C#", + "type": "project" + }, + "shortName": "modular-monolith", + "sourceName": "App1", + "preferNameDirectory":true, + "symbols": { + "module1": { + "description": "Module1 name", + "type": "parameter", + "replaces": "Module1", + "FileRename": "Module1" + }, + "module1Lower": { + "type": "generated", + "generator": "casing", + "parameters": { + "source": "module1", + "toLower": true + }, + "replaces": "module1", + "FileRename": "module1" + }, + "module2": { + "description": "Module2 name", + "type": "parameter", + "replaces": "Module2", + "FileRename": "Module2" + }, + "module2Lower": { + "type": "generated", + "generator": "casing", + "parameters": { + "source": "module2", + "toLower": true + }, + "replaces": "module2", + "FileRename": "module2" + } + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/App1.slnx b/templates/ModularMonolith/App1.slnx new file mode 100644 index 0000000..0d015c1 --- /dev/null +++ b/templates/ModularMonolith/App1.slnx @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/ModularMonolith/Directory.Build.props b/templates/ModularMonolith/Directory.Build.props new file mode 100644 index 0000000..8537180 --- /dev/null +++ b/templates/ModularMonolith/Directory.Build.props @@ -0,0 +1,20 @@ + + + + + + net9.0 + enable + enable + true + true + $(NoWarn);NU1902;NU1903;S101 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/templates/ModularMonolith/README.md b/templates/ModularMonolith/README.md new file mode 100644 index 0000000..af6a92a --- /dev/null +++ b/templates/ModularMonolith/README.md @@ -0,0 +1 @@ +# App1 diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/App1.Common.Application.csproj b/templates/ModularMonolith/src/Common/App1.Common.Application/App1.Common.Application.csproj new file mode 100644 index 0000000..88ca040 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/App1.Common.Application.csproj @@ -0,0 +1,17 @@ + + + + $(NetVersion) + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/ApplicationConfiguration.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/ApplicationConfiguration.cs new file mode 100644 index 0000000..eece06b --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/ApplicationConfiguration.cs @@ -0,0 +1,25 @@ +namespace App1.Common.Application; + +using System.Reflection; +using Behaviors; +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; + +public static class ApplicationConfiguration +{ + public static IServiceCollection AddApplication(this IServiceCollection services, Assembly[] moduleAssemblies) + { + services.AddMediatR(config => + { + config.RegisterServicesFromAssemblies(moduleAssemblies); + + config.AddOpenBehavior(typeof(ExceptionHandlingPipelineBehavior<,>)); + config.AddOpenBehavior(typeof(RequestLoggingPipelineBehavior<,>)); + config.AddOpenBehavior(typeof(ValidationPipelineBehavior<,>)); + }); + + services.AddValidatorsFromAssemblies(moduleAssemblies, includeInternalTypes: true); + + return services; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/Behaviors/ExceptionHandlingPipelineBehavior.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/Behaviors/ExceptionHandlingPipelineBehavior.cs new file mode 100644 index 0000000..2ddc6c1 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/Behaviors/ExceptionHandlingPipelineBehavior.cs @@ -0,0 +1,26 @@ +namespace App1.Common.Application.Behaviors; + +using Exceptions; +using MediatR; +using Microsoft.Extensions.Logging; + +internal sealed class ExceptionHandlingPipelineBehavior( + ILogger> logger) + : IPipelineBehavior where TRequest : class +{ + public async Task Handle(TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + try + { + return await next(); + } + catch (Exception exception) + { + logger.LogError(exception, "Unhandled exception for {RequestName}", typeof(TRequest).Name); + + throw new App1Exception(typeof(TRequest).Name, innerException: exception); + } + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/Behaviors/RequestLoggingPipelineBehavior.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/Behaviors/RequestLoggingPipelineBehavior.cs new file mode 100644 index 0000000..e5b3135 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/Behaviors/RequestLoggingPipelineBehavior.cs @@ -0,0 +1,48 @@ +namespace App1.Common.Application.Behaviors; + +using System.Diagnostics; +using Domain; +using MediatR; +using Microsoft.Extensions.Logging; + +internal sealed class RequestLoggingPipelineBehavior( + ILogger> logger) + : IPipelineBehavior where TRequest : class where TResponse : Result +{ + public async Task Handle(TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + var moduleName = GetModuleName(typeof(TRequest).FullName!); + var requestName = typeof(TRequest).Name; + + Activity.Current?.SetTag("request.module", moduleName); + Activity.Current?.SetTag("request.name", requestName); + + using (logger.BeginScope("Module {ModuleName}", moduleName)) + { + logger.LogInformation("Processing request {RequestName}", requestName); + + var result = await next(); + + if (result.IsSuccess) + { + logger.LogInformation("Completed request {RequestName}", requestName); + } + else + { + using (logger.BeginScope("Error {Error}", result.Error)) + { + logger.LogError("Completed request {RequestName} with error", requestName); + } + } + + return result; + } + } + + private static string GetModuleName(string requestName) + { + return requestName.Split('.')[2]; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/Behaviors/ValidationPipelineBehavior.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/Behaviors/ValidationPipelineBehavior.cs new file mode 100644 index 0000000..6afe29e --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/Behaviors/ValidationPipelineBehavior.cs @@ -0,0 +1,68 @@ +namespace App1.Common.Application.Behaviors; + +using Domain; +using FluentValidation; +using FluentValidation.Results; +using MediatR; +using Messaging; + +internal sealed class ValidationPipelineBehavior(IEnumerable> validators) + : IPipelineBehavior where TRequest : IBaseCommand +{ + public async Task Handle(TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + var validationFailures = await ValidateAsync(request); + + if (validationFailures.Length == 0) + { + return await next(); + } + + if (typeof(TResponse).IsGenericType && typeof(TResponse).GetGenericTypeDefinition() == typeof(Result<>)) + { + var resultType = typeof(TResponse).GetGenericArguments()[0]; + + var failureMethod = typeof(Result<>).MakeGenericType(resultType) + .GetMethod(nameof(Result.ValidationFailure)); + + if (failureMethod is not null) + { + return (TResponse)failureMethod.Invoke(null, [CreateValidationError(validationFailures)])!; + } + } + else if (typeof(TResponse) == typeof(Result)) + { + return (TResponse)(object)Result.Failure(CreateValidationError(validationFailures)); + } + + throw new ValidationException(validationFailures); + } + + private async Task ValidateAsync(TRequest request) + { + if (!validators.Any()) + { + return []; + } + + var context = new ValidationContext(request); + + ValidationResult[] validationResults = await Task.WhenAll( + validators.Select(validator => validator.ValidateAsync(context))); + + ValidationFailure[] validationFailures = validationResults.Where(validationResult => !validationResult.IsValid) + .SelectMany( + validationResult => validationResult.Errors) + .ToArray(); + + return validationFailures; + } + + private static ValidationError CreateValidationError(ValidationFailure[] validationFailures) + { + return new ValidationError(validationFailures.Select(f => Error.Problem(f.ErrorCode, f.ErrorMessage)) + .ToArray()); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IEventBus.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IEventBus.cs new file mode 100644 index 0000000..b7a6030 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IEventBus.cs @@ -0,0 +1,6 @@ +namespace App1.Common.Application.EventBus; + +public interface IEventBus +{ + Task PublishAsync(T integrationEvent, CancellationToken cancellationToken = default) where T : IIntegrationEvent; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IIntegrationEvent.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IIntegrationEvent.cs new file mode 100644 index 0000000..bed15cc --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IIntegrationEvent.cs @@ -0,0 +1,8 @@ +namespace App1.Common.Application.EventBus; + +public interface IIntegrationEvent +{ + Guid Id { get; } + + DateTime OccurredOnUtc { get; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IIntegrationEventHandler.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IIntegrationEventHandler.cs new file mode 100644 index 0000000..a29f159 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IIntegrationEventHandler.cs @@ -0,0 +1,12 @@ +namespace App1.Common.Application.EventBus; + +public interface IIntegrationEventHandler : IIntegrationEventHandler + where TIntegrationEvent : IIntegrationEvent +{ + Task Handle(TIntegrationEvent integrationEvent, CancellationToken cancellationToken = default); +} + +public interface IIntegrationEventHandler +{ + Task Handle(IIntegrationEvent integrationEvent, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IntegrationEvent.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IntegrationEvent.cs new file mode 100644 index 0000000..ea0f370 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IntegrationEvent.cs @@ -0,0 +1,8 @@ +namespace App1.Common.Application.EventBus; + +public abstract class IntegrationEvent(Guid id, DateTime occurredOnUtc) : IIntegrationEvent +{ + public Guid Id { get; init; } = id; + + public DateTime OccurredOnUtc { get; init; } = occurredOnUtc; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IntegrationEventHandler.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IntegrationEventHandler.cs new file mode 100644 index 0000000..375b892 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/EventBus/IntegrationEventHandler.cs @@ -0,0 +1,12 @@ +namespace App1.Common.Application.EventBus; + +public abstract class IntegrationEventHandler : IIntegrationEventHandler + where TIntegrationEvent : IIntegrationEvent +{ + public abstract Task Handle(TIntegrationEvent integrationEvent, CancellationToken cancellationToken = default); + + public Task Handle(IIntegrationEvent integrationEvent, CancellationToken cancellationToken = default) + { + return Handle((TIntegrationEvent)integrationEvent, cancellationToken); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/Exceptions/App1Exception.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/Exceptions/App1Exception.cs new file mode 100644 index 0000000..342fa67 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/Exceptions/App1Exception.cs @@ -0,0 +1,13 @@ +namespace App1.Common.Application.Exceptions; + +using Domain; + +public sealed class App1Exception( + string requestName, + Error? error = default, + Exception? innerException = default) : Exception("Application exception", innerException) +{ + public string RequestName { get; } = requestName; + + public Error? Error { get; } = error; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/DomainEventHandler.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/DomainEventHandler.cs new file mode 100644 index 0000000..b5c9c57 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/DomainEventHandler.cs @@ -0,0 +1,14 @@ +namespace App1.Common.Application.Messaging; + +using Domain; + +public abstract class DomainEventHandler : IDomainEventHandler + where TDomainEvent : IDomainEvent +{ + public abstract Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken = default); + + public Task Handle(IDomainEvent domainEvent, CancellationToken cancellationToken = default) + { + return Handle((TDomainEvent)domainEvent, cancellationToken); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/ICommand.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/ICommand.cs new file mode 100644 index 0000000..73bdc2b --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/ICommand.cs @@ -0,0 +1,10 @@ +namespace App1.Common.Application.Messaging; + +using Domain; +using MediatR; + +public interface ICommand : IRequest, IBaseCommand; + +public interface ICommand : IRequest>, IBaseCommand; + +public interface IBaseCommand; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/ICommandHandler.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/ICommandHandler.cs new file mode 100644 index 0000000..7c55382 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/ICommandHandler.cs @@ -0,0 +1,9 @@ +namespace App1.Common.Application.Messaging; + +using Domain; +using MediatR; + +public interface ICommandHandler : IRequestHandler where TCommand : ICommand; + +public interface ICommandHandler : IRequestHandler> + where TCommand : ICommand; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/IDomainEventHandler.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/IDomainEventHandler.cs new file mode 100644 index 0000000..b63a2e4 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/IDomainEventHandler.cs @@ -0,0 +1,13 @@ +namespace App1.Common.Application.Messaging; + +using Domain; + +public interface IDomainEventHandler : IDomainEventHandler where TDomainEvent : IDomainEvent +{ + Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken = default); +} + +public interface IDomainEventHandler +{ + Task Handle(IDomainEvent domainEvent, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/IQuery.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/IQuery.cs new file mode 100644 index 0000000..1506c40 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/IQuery.cs @@ -0,0 +1,6 @@ +namespace App1.Common.Application.Messaging; + +using Domain; +using MediatR; + +public interface IQuery : IRequest>; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/IQueryHandler.cs b/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/IQueryHandler.cs new file mode 100644 index 0000000..f6be087 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Application/Messaging/IQueryHandler.cs @@ -0,0 +1,7 @@ +namespace App1.Common.Application.Messaging; + +using Domain; +using MediatR; + +public interface IQueryHandler : IRequestHandler> + where TQuery : IQuery; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Domain/App1.Common.Domain.csproj b/templates/ModularMonolith/src/Common/App1.Common.Domain/App1.Common.Domain.csproj new file mode 100644 index 0000000..7e18fd7 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Domain/App1.Common.Domain.csproj @@ -0,0 +1,8 @@ + + + + $(NetVersion) + preview + + + diff --git a/templates/ModularMonolith/src/Common/App1.Common.Domain/DomainEvent.cs b/templates/ModularMonolith/src/Common/App1.Common.Domain/DomainEvent.cs new file mode 100644 index 0000000..adae981 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Domain/DomainEvent.cs @@ -0,0 +1,12 @@ +namespace App1.Common.Domain; + +public abstract class DomainEvent(Guid id, DateTime occurredOnUtc) : IDomainEvent +{ + protected DomainEvent() : this(Guid.CreateVersion7(), DateTime.UtcNow) + { + } + + public Guid Id { get; init; } = id; + + public DateTime OccurredOnUtc { get; init; } = occurredOnUtc; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Domain/Entity.cs b/templates/ModularMonolith/src/Common/App1.Common.Domain/Entity.cs new file mode 100644 index 0000000..c8e9da0 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Domain/Entity.cs @@ -0,0 +1,18 @@ +namespace App1.Common.Domain; + +public abstract class Entity +{ + private readonly List domainEvents = []; + + public IReadOnlyCollection DomainEvents => domainEvents.ToList().AsReadOnly(); + + public void ClearDomainEvents() + { + domainEvents.Clear(); + } + + protected void Raise(IDomainEvent domainEvent) + { + domainEvents.Add(domainEvent); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Domain/Error.cs b/templates/ModularMonolith/src/Common/App1.Common.Domain/Error.cs new file mode 100644 index 0000000..3c07fd0 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Domain/Error.cs @@ -0,0 +1,28 @@ +namespace App1.Common.Domain; + +public record Error(string Code, string Description, ErrorType Type) +{ + public static readonly Error None = new(string.Empty, string.Empty, ErrorType.Failure); + + public static readonly Error NullValue = new("General.Null", "Null value was provided", ErrorType.Failure); + + public static Error Failure(string code, string description) + { + return new Error(code, description, ErrorType.Failure); + } + + public static Error NotFound(string code, string description) + { + return new Error(code, description, ErrorType.NotFound); + } + + public static Error Problem(string code, string description) + { + return new Error(code, description, ErrorType.Problem); + } + + public static Error Conflict(string code, string description) + { + return new Error(code, description, ErrorType.Conflict); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Domain/ErrorType.cs b/templates/ModularMonolith/src/Common/App1.Common.Domain/ErrorType.cs new file mode 100644 index 0000000..ffa9ac2 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Domain/ErrorType.cs @@ -0,0 +1,10 @@ +namespace App1.Common.Domain; + +public enum ErrorType +{ + Failure = 0, + Validation = 1, + Problem = 2, + NotFound = 3, + Conflict = 4 +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Domain/IDomainEvent.cs b/templates/ModularMonolith/src/Common/App1.Common.Domain/IDomainEvent.cs new file mode 100644 index 0000000..df42555 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Domain/IDomainEvent.cs @@ -0,0 +1,8 @@ +namespace App1.Common.Domain; + +public interface IDomainEvent +{ + Guid Id { get; } + + DateTime OccurredOnUtc { get; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Domain/Result.cs b/templates/ModularMonolith/src/Common/App1.Common.Domain/Result.cs new file mode 100644 index 0000000..8bc9e7d --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Domain/Result.cs @@ -0,0 +1,61 @@ +namespace App1.Common.Domain; + +using System.Diagnostics.CodeAnalysis; + +public class Result +{ + public Result(bool isSuccess, Error error) + { + if ((isSuccess && error != Error.None) || (!isSuccess && error == Error.None)) + { + throw new ArgumentException("Invalid error", nameof(error)); + } + + IsSuccess = isSuccess; + Error = error; + } + + public bool IsSuccess { get; } + + public bool IsFailure => !IsSuccess; + + public Error Error { get; } + + public static Result Success() + { + return new Result(true, Error.None); + } + + public static Result Success(TValue value) + { + return new Result(value, true, Error.None); + } + + public static Result Failure(Error error) + { + return new Result(false, error); + } + + public static Result Failure(Error error) + { + return new Result(default, false, error); + } +} + +public class Result(TValue? value, bool isSuccess, Error error) : Result(isSuccess, error) +{ + [NotNull] + public TValue Value => IsSuccess + ? value! + : throw new InvalidOperationException("The value of a failure result can't be accessed."); + + public static implicit operator Result(TValue? value) + { + return value is not null ? Success(value) : Failure(Error.NullValue); + } + + public static Result ValidationFailure(Error error) + { + return new Result(default, false, error); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Domain/ValidationError.cs b/templates/ModularMonolith/src/Common/App1.Common.Domain/ValidationError.cs new file mode 100644 index 0000000..e04cb8e --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Domain/ValidationError.cs @@ -0,0 +1,11 @@ +namespace App1.Common.Domain; + +public sealed record ValidationError(Error[] Errors) : Error("General.Validation", + "One or more validation errors occurred", + ErrorType.Validation) +{ + public static ValidationError FromResults(IEnumerable results) + { + return new ValidationError(results.Where(r => r.IsFailure).Select(r => r.Error).ToArray()); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/App1.Common.Infrastructure.csproj b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/App1.Common.Infrastructure.csproj new file mode 100644 index 0000000..57591aa --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/App1.Common.Infrastructure.csproj @@ -0,0 +1,24 @@ + + + + $(NetVersion) + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authentication/AuthenticationExtensions.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authentication/AuthenticationExtensions.cs new file mode 100644 index 0000000..031c3b1 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authentication/AuthenticationExtensions.cs @@ -0,0 +1,16 @@ +namespace App1.Common.Infrastructure.Authentication; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Web; + +internal static class AuthenticationExtensions +{ + internal static IServiceCollection AddAuthenticationInternal(this IServiceCollection services, + IConfiguration configuration) + { + services.AddMicrosoftIdentityWebApiAuthentication(configuration, Constants.AzureAdB2C); + + return services; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authentication/ClaimsPrincipalExtensions.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authentication/ClaimsPrincipalExtensions.cs new file mode 100644 index 0000000..7eb5cbe --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authentication/ClaimsPrincipalExtensions.cs @@ -0,0 +1,17 @@ +namespace App1.Common.Infrastructure.Authentication; + +using System.Security.Claims; +using Application.Exceptions; +using Microsoft.Identity.Web; + +public static class ClaimsPrincipalExtensions +{ + public static Guid GetModule1Id(this ClaimsPrincipal? principal) + { + var module1Id = principal?.GetObjectId(); + + return Guid.TryParse(module1Id, out var parsedModule1Id) + ? parsedModule1Id + : throw new App1Exception("Module1 identifier is unavailable"); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/AdministratorAuthorizationHandler.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/AdministratorAuthorizationHandler.cs new file mode 100644 index 0000000..16031c3 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/AdministratorAuthorizationHandler.cs @@ -0,0 +1,5 @@ +namespace App1.Common.Infrastructure.Authorization; + +public class AdministratorAuthorizationHandler : RoleAuthorizationHandler +{ +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/AdministratorAuthorizationRequirement.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/AdministratorAuthorizationRequirement.cs new file mode 100644 index 0000000..d1ecf25 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/AdministratorAuthorizationRequirement.cs @@ -0,0 +1,6 @@ +namespace App1.Common.Infrastructure.Authorization; + +public class AdministratorAuthorizationRequirement : IRoleAuthorizationRequirement +{ + public List RequiredRoles => ["Administrator"]; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/AuthorizationExtensions.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/AuthorizationExtensions.cs new file mode 100644 index 0000000..88cd876 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/AuthorizationExtensions.cs @@ -0,0 +1,40 @@ +namespace App1.Common.Infrastructure.Authorization; + +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; + +public static class PolicyConstants +{ + public const string AdministratorPolicy = "IsAdministrator"; +} + +internal static class AuthorizationExtensions +{ + internal static IServiceCollection AddJwtAuthorizationInternal(this IServiceCollection services) + { + services.AddSingleton(); + services.AddAuthorization(options => + { + var administratorOrHigherPolicyBuilder = new AuthorizationPolicyBuilder().AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); + administratorOrHigherPolicyBuilder.Requirements.Add(new AdministratorAuthorizationRequirement()); + options.AddPolicy(PolicyConstants.AdministratorPolicy, administratorOrHigherPolicyBuilder.Build()); + }); + + return services; + } + + internal static IServiceCollection AddOpenIdAuthorizationInternal(this IServiceCollection services) + { + services.AddSingleton(); + services.AddAuthorization(options => + { + var administratorOrHigherPolicyBuilder = new AuthorizationPolicyBuilder().AddAuthenticationSchemes(OpenIdConnectDefaults.AuthenticationScheme); + administratorOrHigherPolicyBuilder.Requirements.Add(new AdministratorAuthorizationRequirement()); + options.AddPolicy(PolicyConstants.AdministratorPolicy, administratorOrHigherPolicyBuilder.Build()); + }); + + return services; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/IRoleAuthorizationRequirement.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/IRoleAuthorizationRequirement.cs new file mode 100644 index 0000000..f6c7402 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/IRoleAuthorizationRequirement.cs @@ -0,0 +1,8 @@ +namespace App1.Common.Infrastructure.Authorization; + +using Microsoft.AspNetCore.Authorization; + +public interface IRoleAuthorizationRequirement : IAuthorizationRequirement +{ + public List RequiredRoles { get; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/RoleAuthorizationHandler.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/RoleAuthorizationHandler.cs new file mode 100644 index 0000000..03a59a6 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Authorization/RoleAuthorizationHandler.cs @@ -0,0 +1,27 @@ +namespace App1.Common.Infrastructure.Authorization; + +using Microsoft.AspNetCore.Authorization; + +public abstract class RoleAuthorizationHandler : AuthorizationHandler where T : IRoleAuthorizationRequirement +{ + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, T requirement) + { + var isAuthenticated = context.User.Identity?.IsAuthenticated ?? false; + if (!isAuthenticated) + { + context.Fail(); + } + + if (context.User.HasClaim(c => c.Type == "extension_Groups" && + requirement.RequiredRoles.TrueForAll(x => c.Value.Split(',').Contains(x)))) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/EnumExtensions.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/EnumExtensions.cs new file mode 100644 index 0000000..d608e48 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/EnumExtensions.cs @@ -0,0 +1,39 @@ +namespace App1.Common.Infrastructure; + +using System.ComponentModel; +using System.Reflection; + +public static class EnumExtensions +{ + public static string GetDescription(this Enum enumValue) + { + return enumValue.GetType() + .GetField(enumValue.ToString()) + ?.GetCustomAttribute() + ?.Description ?? + enumValue.ToString(); + } + + public static T? GetValueFromDescription(this string description) where T : Enum + { + foreach (var field in typeof(T).GetFields()) + { + if (Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute) + { + if (attribute.Description.Equals(description, StringComparison.InvariantCultureIgnoreCase)) + { + return (T?)field.GetValue(null); + } + } + else + { + if (field.Name.Equals(description, StringComparison.InvariantCultureIgnoreCase)) + { + return (T?)field.GetValue(null); + } + } + } + + return default; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/EventBus/EventBus.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/EventBus/EventBus.cs new file mode 100644 index 0000000..b042068 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/EventBus/EventBus.cs @@ -0,0 +1,13 @@ +namespace App1.Common.Infrastructure.EventBus; + +using Application.EventBus; +using MassTransit; + +internal sealed class EventBus(IBus bus) : IEventBus +{ + public async Task PublishAsync(T integrationEvent, CancellationToken cancellationToken = default) + where T : IIntegrationEvent + { + await bus.Publish(integrationEvent, cancellationToken); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Inbox/InboxMessage.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Inbox/InboxMessage.cs new file mode 100644 index 0000000..ff4cabb --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Inbox/InboxMessage.cs @@ -0,0 +1,14 @@ +namespace App1.Common.Infrastructure.Inbox; + +public sealed class InboxMessage +{ + public Guid Id { get; init; } + + public required string Content { get; init; } + + public DateTime OccurredOnUtc { get; init; } + + public DateTime? ProcessedOnUtc { get; init; } + + public string? Error { get; init; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Inbox/InboxMessageConfiguration.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Inbox/InboxMessageConfiguration.cs new file mode 100644 index 0000000..e5d5823 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Inbox/InboxMessageConfiguration.cs @@ -0,0 +1,12 @@ +namespace App1.Common.Infrastructure.Inbox; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +public sealed class InboxMessageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(o => o.Id); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/InfrastructureConfiguration.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/InfrastructureConfiguration.cs new file mode 100644 index 0000000..9348cac --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/InfrastructureConfiguration.cs @@ -0,0 +1,103 @@ +namespace App1.Common.Infrastructure; + +using Application.EventBus; +using Authentication; +using Authorization; +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Outbox; +using Quartz; + +public static class InfrastructureConfiguration +{ + public static IServiceCollection AddAuthZ(this IServiceCollection services) + { + services.AddOpenIdAuthorizationInternal(); + return services; + } + + public static IHostApplicationBuilder AddInfrastructure(this IHostApplicationBuilder builder, + Action[] moduleConfigureConsumers) + { + builder.Services.AddAuthenticationInternal(builder.Configuration); + + builder.Services.AddJwtAuthorizationInternal(); + + builder.Services.TryAddSingleton(); + + builder.Services.TryAddSingleton(); + + builder.Services.AddQuartz(configurator => + { + var scheduler = Guid.NewGuid(); + configurator.SchedulerId = $"default-id-{scheduler}"; + configurator.SchedulerName = $"default-name-{scheduler}"; + }); + + builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true); + +#pragma warning disable EXTEXP0018 + builder.Services.AddHybridCache(); +#pragma warning restore EXTEXP0018 + if (!builder.Environment.IsDevelopment()) + { + builder.AddRedisDistributedCache("cache"); + } + + builder.Services.AddMassTransit(configure => + { + foreach (var configureConsumers in moduleConfigureConsumers) + { + configureConsumers(configure); + } + + configure.SetKebabCaseEndpointNameFormatter(); + + if (builder.Environment.IsDevelopment()) + { + configure.UsingInMemory(static (context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + } + else + { + configure.UsingRabbitMq((context, cfg) => + { + cfg.Host(builder.Configuration.GetConnectionString("servicebus")); + + cfg.ConfigureEndpoints(context); + }); + } + }); + + return builder; + } + + public static IHostApplicationBuilder AddDatabase(this IHostApplicationBuilder builder, + string schema, + Action? dbContextConfigure = null, + Action? sqlServerConfigure = null) where T : DbContext + { + builder.AddSqlServerDbContext("database"); + builder.Services.AddDbContextPool((sp, options) => + { + options.UseSqlServer(builder.Configuration.GetConnectionString("database"), optionsBuilder => + { + optionsBuilder.MigrationsHistoryTable(HistoryRepository.DefaultTableName, schema); + sqlServerConfigure?.Invoke(optionsBuilder); + optionsBuilder.EnableRetryOnFailure(5, TimeSpan.FromSeconds(30), [0]); + }) + .AddInterceptors(sp.GetRequiredService()); + dbContextConfigure?.Invoke(options); + }); + builder.EnrichSqlServerDbContext(); + return builder; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Outbox/InsertOutboxMessagesInterceptor.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Outbox/InsertOutboxMessagesInterceptor.cs new file mode 100644 index 0000000..5d95014 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Outbox/InsertOutboxMessagesInterceptor.cs @@ -0,0 +1,45 @@ +namespace App1.Common.Infrastructure.Outbox; + +using System.Text.Json; +using Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Serialization; + +public sealed class InsertOutboxMessagesInterceptor : SaveChangesInterceptor +{ + public override ValueTask> SavingChangesAsync(DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + if (eventData.Context is not null) + { + InsertOutboxMessages(eventData.Context); + } + + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + private static void InsertOutboxMessages(DbContext context) + { + var outboxMessages = context.ChangeTracker.Entries() + .Select(entry => entry.Entity) + .SelectMany(entity => + { + var domainEvents = entity.DomainEvents; + + entity.ClearDomainEvents(); + + return domainEvents; + }) + .Select(static domainEvent => new OutboxMessage + { + Id = domainEvent.Id, + Content = JsonSerializer.Serialize(domainEvent, SerializerSettings.Instance), + OccurredOnUtc = domainEvent.OccurredOnUtc + }) + .ToList(); + + context.Set().AddRange(outboxMessages); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Outbox/OutboxMessage.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Outbox/OutboxMessage.cs new file mode 100644 index 0000000..b42dd79 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Outbox/OutboxMessage.cs @@ -0,0 +1,14 @@ +namespace App1.Common.Infrastructure.Outbox; + +public sealed class OutboxMessage +{ + public Guid Id { get; init; } + + public required string Content { get; init; } + + public DateTime OccurredOnUtc { get; init; } + + public DateTime? ProcessedOnUtc { get; init; } + + public string? Error { get; init; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Outbox/OutboxMessageConfiguration.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Outbox/OutboxMessageConfiguration.cs new file mode 100644 index 0000000..462ce23 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Outbox/OutboxMessageConfiguration.cs @@ -0,0 +1,14 @@ +namespace App1.Common.Infrastructure.Outbox; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +public sealed class OutboxMessageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(o => o.Id); + + builder.Property(o => o.Content); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Serialization/SerializerSettings.cs b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Serialization/SerializerSettings.cs new file mode 100644 index 0000000..79b9b1b --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Infrastructure/Serialization/SerializerSettings.cs @@ -0,0 +1,23 @@ +namespace App1.Common.Infrastructure.Serialization; + +using System.Text.Json; +using System.Text.Json.Serialization; +using Application.EventBus; +using Domain; + +public static class SerializerSettings +{ + public static readonly JsonSerializerOptions Instance = new(JsonSerializerDefaults.Web) + { + NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, + Converters = { new InterfaceConverter(), new InterfaceConverter() } + }; + + public static void ConfigureJsonSerializerOptionsInstance(IList converters) + { + foreach (var converter in converters) + { + Instance.Converters.Add(converter); + } + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Presentation/App1.Common.Presentation.csproj b/templates/ModularMonolith/src/Common/App1.Common.Presentation/App1.Common.Presentation.csproj new file mode 100644 index 0000000..5032804 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Presentation/App1.Common.Presentation.csproj @@ -0,0 +1,15 @@ + + + + $(NetVersion) + + + + + + + + + + + diff --git a/templates/ModularMonolith/src/Common/App1.Common.Presentation/Endpoints/EndpointExtensions.cs b/templates/ModularMonolith/src/Common/App1.Common.Presentation/Endpoints/EndpointExtensions.cs new file mode 100644 index 0000000..7a2ab11 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Presentation/Endpoints/EndpointExtensions.cs @@ -0,0 +1,37 @@ +namespace App1.Common.Presentation.Endpoints; + +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +public static class EndpointExtensions +{ + public static IServiceCollection AddEndpoints(this IServiceCollection services, params Assembly[] assemblies) + { + var serviceDescriptors = assemblies.SelectMany(a => a.GetTypes()) + .Where(type => type is { IsAbstract: false, IsInterface: false } && + type.IsAssignableTo(typeof(IEndpoint))) + .Select(type => ServiceDescriptor.Transient(typeof(IEndpoint), type)) + .ToArray(); + + services.TryAddEnumerable(serviceDescriptors); + + return services; + } + + public static IApplicationBuilder MapEndpoints(this WebApplication app, RouteGroupBuilder? routeGroupBuilder = null) + { + var endpoints = app.Services.GetRequiredService>(); + + IEndpointRouteBuilder builder = routeGroupBuilder is null ? app : routeGroupBuilder; + + foreach (var endpoint in endpoints) + { + endpoint.MapEndpoint(builder); + } + + return app; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Presentation/Endpoints/IEndpoint.cs b/templates/ModularMonolith/src/Common/App1.Common.Presentation/Endpoints/IEndpoint.cs new file mode 100644 index 0000000..67a0489 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Presentation/Endpoints/IEndpoint.cs @@ -0,0 +1,8 @@ +namespace App1.Common.Presentation.Endpoints; + +using Microsoft.AspNetCore.Routing; + +public interface IEndpoint +{ + void MapEndpoint(IEndpointRouteBuilder app); +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Presentation/Results/ApiResults.cs b/templates/ModularMonolith/src/Common/App1.Common.Presentation/Results/ApiResults.cs new file mode 100644 index 0000000..280bc36 --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Presentation/Results/ApiResults.cs @@ -0,0 +1,82 @@ +namespace App1.Common.Presentation.Results; + +using Domain; +using Microsoft.AspNetCore.Http; + +public static class ApiResults +{ + public static IResult Problem(Result result) + { + if (result.IsSuccess) + { + throw new InvalidOperationException(); + } + + return Results.Problem(title: GetTitle(result.Error), detail: GetDetail(result.Error), + type: GetType(result.Error.Type), statusCode: GetStatusCode(result.Error.Type), + extensions: GetErrors(result)); + + static string GetTitle(Error error) + { + return error.Type switch + { + ErrorType.Validation => error.Code, + ErrorType.Problem => error.Code, + ErrorType.NotFound => error.Code, + ErrorType.Conflict => error.Code, + _ => "Server failure" + }; + } + + static string GetDetail(Error error) + { + return error.Type switch + { + ErrorType.Validation => error.Description, + ErrorType.Problem => error.Description, + ErrorType.NotFound => error.Description, + ErrorType.Conflict => error.Description, + _ => "An unexpected error occurred" + }; + } + + static string GetType(ErrorType errorType) + { + return errorType switch + { + ErrorType.Validation => "https://tools.ietf.org/html/rfc7231#section-6.5.1", + ErrorType.Problem => "https://tools.ietf.org/html/rfc7231#section-6.5.1", + ErrorType.NotFound => "https://tools.ietf.org/html/rfc7231#section-6.5.4", + ErrorType.Conflict => "https://tools.ietf.org/html/rfc7231#section-6.5.8", + _ => "https://tools.ietf.org/html/rfc7231#section-6.6.1" + }; + } + + static int GetStatusCode(ErrorType errorType) + { + return errorType switch + { + ErrorType.Validation => StatusCodes.Status400BadRequest, + ErrorType.Problem => StatusCodes.Status400BadRequest, + ErrorType.NotFound => StatusCodes.Status404NotFound, + ErrorType.Conflict => StatusCodes.Status409Conflict, + _ => StatusCodes.Status500InternalServerError + }; + } + + static Dictionary? GetErrors(Result result) + { + if (result.Error is not ValidationError validationError) + { + return null; + } + + return new Dictionary + { + { + "errors", validationError.Errors + } + }; + } + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Common/App1.Common.Presentation/Results/ResultExtensions.cs b/templates/ModularMonolith/src/Common/App1.Common.Presentation/Results/ResultExtensions.cs new file mode 100644 index 0000000..4fb4eaa --- /dev/null +++ b/templates/ModularMonolith/src/Common/App1.Common.Presentation/Results/ResultExtensions.cs @@ -0,0 +1,18 @@ +namespace App1.Common.Presentation.Results; + +using Domain; + +public static class ResultExtensions +{ + public static TOut Match(this Result result, Func onSuccess, Func onFailure) + { + return result.IsSuccess ? onSuccess() : onFailure(result); + } + + public static TOut Match(this Result result, + Func onSuccess, + Func, TOut> onFailure) + { + return result.IsSuccess ? onSuccess(result.Value) : onFailure(result); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Abstractions/Data/IUnitOfWork.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Abstractions/Data/IUnitOfWork.cs new file mode 100644 index 0000000..6439a52 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Abstractions/Data/IUnitOfWork.cs @@ -0,0 +1,6 @@ +namespace App1.Modules.Module1s.Application.Abstractions.Data; + +public interface IUnitOfWork +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/App1.Modules.Module1s.Application.csproj b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/App1.Modules.Module1s.Application.csproj new file mode 100644 index 0000000..0ebc543 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/App1.Modules.Module1s.Application.csproj @@ -0,0 +1,13 @@ + + + + $(NetVersion) + + + + + + + + + diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/AssemblyReference.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/AssemblyReference.cs new file mode 100644 index 0000000..6172636 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/AssemblyReference.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace App1.Modules.Module1s.Application; + +public static class AssemblyReference +{ + public static readonly Assembly Assembly = typeof(AssemblyReference).Assembly; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/CreateModule1Command.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/CreateModule1Command.cs new file mode 100644 index 0000000..6a1f5f6 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/CreateModule1Command.cs @@ -0,0 +1,5 @@ +using App1.Common.Application.Messaging; + +namespace App1.Modules.Module1s.Application.Module1s.CreateModule1; + +public sealed record CreateModule1Command(Guid ProviderId) : ICommand; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/CreateModule1CommandHandler.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/CreateModule1CommandHandler.cs new file mode 100644 index 0000000..a53e302 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/CreateModule1CommandHandler.cs @@ -0,0 +1,25 @@ +using App1.Common.Application.Messaging; +using App1.Common.Domain; +using App1.Modules.Module1s.Application.Abstractions.Data; +using App1.Modules.Module1s.Domain.Module1s; + +namespace App1.Modules.Module1s.Application.Module1s.CreateModule1; + +internal sealed class CreateModule1CommandHandler( + IModule1Repository module1Repository, + IUnitOfWork unitOfWork) : ICommandHandler +{ + public async Task> Handle(CreateModule1Command request, CancellationToken cancellationToken) + { + var module1 = await module1Repository.GetAsync(request.ProviderId, cancellationToken); + if (module1 is null) + { + module1 = Module1.Create(request.ProviderId); + module1Repository.Insert(module1); + + await unitOfWork.SaveChangesAsync(cancellationToken); + } + + return new Module1Response(module1.Id); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/CreateModule1CommandValidator.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/CreateModule1CommandValidator.cs new file mode 100644 index 0000000..a65ed96 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/CreateModule1CommandValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace App1.Modules.Module1s.Application.Module1s.CreateModule1; + +internal sealed class CreateModule1CommandValidator : AbstractValidator +{ + public CreateModule1CommandValidator() + { + RuleFor(c => c.ProviderId).NotEmpty(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/Module1CreatedDomainEventHandler.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/Module1CreatedDomainEventHandler.cs new file mode 100644 index 0000000..11d7ec1 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/Module1CreatedDomainEventHandler.cs @@ -0,0 +1,28 @@ +using App1.Common.Application.EventBus; +using App1.Common.Application.Exceptions; +using App1.Common.Application.Messaging; +using App1.Modules.Module1s.Application.Module1s.GetModule1ById; +using App1.Modules.Module1s.Domain.Module1s; +using App1.Modules.Module1s.IntegrationEvents; +using MediatR; + +namespace App1.Modules.Module1s.Application.Module1s.CreateModule1; + +internal sealed class Module1CreatedDomainEventHandler(ISender sender, IEventBus bus) + : DomainEventHandler +{ + public override async Task Handle(Module1CreatedDomainEvent domainEvent, + CancellationToken cancellationToken = default) + { + var result = await sender.Send(new GetModule1Query(domainEvent.Module1Id), cancellationToken); + + if (result.IsFailure) + { + throw new App1Exception(nameof(GetModule1Query), result.Error); + } + + await bus.PublishAsync( + new Module1CreatedIntegrationEvent(domainEvent.Id, domainEvent.OccurredOnUtc, result.Value.Id), + cancellationToken); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/Module1Response.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/Module1Response.cs new file mode 100644 index 0000000..168bea1 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/CreateModule1/Module1Response.cs @@ -0,0 +1,3 @@ +namespace App1.Modules.Module1s.Application.Module1s.CreateModule1; + +public sealed record Module1Response(Guid Id); \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/DeleteModule1/DeleteModule1Command.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/DeleteModule1/DeleteModule1Command.cs new file mode 100644 index 0000000..5d67975 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/DeleteModule1/DeleteModule1Command.cs @@ -0,0 +1,5 @@ +using App1.Common.Application.Messaging; + +namespace App1.Modules.Module1s.Application.Module1s.DeleteModule1; + +public sealed record DeleteModule1Command(Guid Module1Id) : ICommand; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/DeleteModule1/DeleteModule1CommandHandler.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/DeleteModule1/DeleteModule1CommandHandler.cs new file mode 100644 index 0000000..feb0d27 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/DeleteModule1/DeleteModule1CommandHandler.cs @@ -0,0 +1,27 @@ +using App1.Common.Application.Messaging; +using App1.Common.Domain; +using App1.Modules.Module1s.Application.Abstractions.Data; +using App1.Modules.Module1s.Domain.Module1s; + +namespace App1.Modules.Module1s.Application.Module1s.DeleteModule1; + +internal sealed class DeleteModule1CommandHandler( + IModule1Repository module1Repository, + IUnitOfWork unitOfWork) : ICommandHandler +{ + public async Task Handle(DeleteModule1Command request, CancellationToken cancellationToken) + { + var module1 = await module1Repository.GetAsync(request.Module1Id, cancellationToken); + + if (module1 is null) + { + return Result.Failure(Module1Errors.NotFound(request.Module1Id)); + } + + module1.Delete(); + module1Repository.Delete(module1); + await unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/DeleteModule1/Module1DeletedDomainEventHandler.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/DeleteModule1/Module1DeletedDomainEventHandler.cs new file mode 100644 index 0000000..220b2df --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/DeleteModule1/Module1DeletedDomainEventHandler.cs @@ -0,0 +1,16 @@ +using App1.Common.Application.EventBus; +using App1.Common.Application.Messaging; +using App1.Modules.Module1s.Domain.Module1s; +using App1.Modules.Module1s.IntegrationEvents; + +namespace App1.Modules.Module1s.Application.Module1s.DeleteModule1; + +internal sealed class Module1DeletedDomainEventHandler(IEventBus eventBus) : DomainEventHandler +{ + public override async Task Handle(Module1DeletedDomainEvent domainEvent, CancellationToken cancellationToken = default) + { + await eventBus.PublishAsync( + new Module1DeletedIntegrationEvent(domainEvent.Id, domainEvent.OccurredOnUtc, domainEvent.Module1Id), + cancellationToken); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1ById/GetModule1Query.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1ById/GetModule1Query.cs new file mode 100644 index 0000000..e884f33 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1ById/GetModule1Query.cs @@ -0,0 +1,5 @@ +using App1.Common.Application.Messaging; + +namespace App1.Modules.Module1s.Application.Module1s.GetModule1ById; + +public sealed record GetModule1Query(Guid Module1Id) : IQuery; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1ById/GetModule1QueryHandler.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1ById/GetModule1QueryHandler.cs new file mode 100644 index 0000000..f1ade17 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1ById/GetModule1QueryHandler.cs @@ -0,0 +1,21 @@ +using App1.Common.Application.Messaging; +using App1.Common.Domain; +using App1.Modules.Module1s.Domain.Module1s; + +namespace App1.Modules.Module1s.Application.Module1s.GetModule1ById; + +internal sealed class GetModule1QueryHandler(IModule1Repository module1Repository) + : IQueryHandler +{ + public async Task> Handle(GetModule1Query request, CancellationToken cancellationToken) + { + var module1 = await module1Repository.GetAsync(request.Module1Id, cancellationToken); + + if (module1 is null) + { + return Result.Failure(Module1Errors.NotFound(request.Module1Id)); + } + + return new Module1Response(module1.Id); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1ById/Module1Response.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1ById/Module1Response.cs new file mode 100644 index 0000000..e5f1c64 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1ById/Module1Response.cs @@ -0,0 +1,3 @@ +namespace App1.Modules.Module1s.Application.Module1s.GetModule1ById; + +public sealed record Module1Response(Guid Id); \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1s/GetModule1Query.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1s/GetModule1Query.cs new file mode 100644 index 0000000..090bb5e --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1s/GetModule1Query.cs @@ -0,0 +1,5 @@ +using App1.Common.Application.Messaging; + +namespace App1.Modules.Module1s.Application.Module1s.GetModule1s; + +public sealed record GetModule1SQuery : IQuery>; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1s/GetModule1QueryHandler.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1s/GetModule1QueryHandler.cs new file mode 100644 index 0000000..930368a --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1s/GetModule1QueryHandler.cs @@ -0,0 +1,22 @@ +using App1.Common.Application.Messaging; +using App1.Common.Domain; +using App1.Modules.Module1s.Domain.Module1s; + +namespace App1.Modules.Module1s.Application.Module1s.GetModule1s; + +internal sealed class GetModule1QueryHandler(IModule1Repository module1Repository) + : IQueryHandler> +{ + public async Task>> Handle(GetModule1SQuery request, CancellationToken cancellationToken) + { + var dbModule1S = await module1Repository.GetAsync(cancellationToken); + + var module1S = new List(); + foreach (var module1 in dbModule1S) + { + module1S.Add(new Module1Response(module1.Id)); + } + + return module1S; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1s/Module1Response.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1s/Module1Response.cs new file mode 100644 index 0000000..4577c00 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/GetModule1s/Module1Response.cs @@ -0,0 +1,3 @@ +namespace App1.Modules.Module1s.Application.Module1s.GetModule1s; + +public sealed record Module1Response(Guid Id); \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/Module1ProfileUpdatedDomainEventHandler.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/Module1ProfileUpdatedDomainEventHandler.cs new file mode 100644 index 0000000..2fbc787 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/Module1ProfileUpdatedDomainEventHandler.cs @@ -0,0 +1,17 @@ +using App1.Common.Application.EventBus; +using App1.Common.Application.Messaging; +using App1.Modules.Module1s.Domain.Module1s; +using App1.Modules.Module1s.IntegrationEvents; + +namespace App1.Modules.Module1s.Application.Module1s.UpdateModule1; + +internal sealed class Module1UpdatedDomainEventHandler(IEventBus eventBus) + : DomainEventHandler +{ + public override async Task Handle(Module1UpdatedDomainEvent domainEvent, + CancellationToken cancellationToken = default) + { + await eventBus.PublishAsync( + new Module1UpdatedIntegrationEvent(domainEvent.Id, domainEvent.OccurredOnUtc, domainEvent.Module1Id), cancellationToken); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/UpdateModule1Comand.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/UpdateModule1Comand.cs new file mode 100644 index 0000000..c44dd2a --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/UpdateModule1Comand.cs @@ -0,0 +1,5 @@ +using App1.Common.Application.Messaging; + +namespace App1.Modules.Module1s.Application.Module1s.UpdateModule1; + +public sealed record UpdateModule1Command(Guid Module1Id) : ICommand; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/UpdateModule1CommandHandler.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/UpdateModule1CommandHandler.cs new file mode 100644 index 0000000..962a7a7 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/UpdateModule1CommandHandler.cs @@ -0,0 +1,25 @@ +using App1.Common.Application.Messaging; +using App1.Common.Domain; +using App1.Modules.Module1s.Application.Abstractions.Data; +using App1.Modules.Module1s.Domain.Module1s; + +namespace App1.Modules.Module1s.Application.Module1s.UpdateModule1; + +internal sealed class UpdateModule1CommandHandler(IModule1Repository module1Repository, IUnitOfWork unitOfWork) + : ICommandHandler +{ + public async Task Handle(UpdateModule1Command request, CancellationToken cancellationToken) + { + var module1 = await module1Repository.GetAsync(request.Module1Id, cancellationToken); + + if (module1 is null) + { + return Result.Failure(Module1Errors.NotFound(request.Module1Id)); + } + + module1.Update(); + await unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/UpdateModule1CommandValidator.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/UpdateModule1CommandValidator.cs new file mode 100644 index 0000000..1904f87 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Application/Module1s/UpdateModule1/UpdateModule1CommandValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace App1.Modules.Module1s.Application.Module1s.UpdateModule1; + +internal sealed class UpdateModule1CommandValidator : AbstractValidator +{ + public UpdateModule1CommandValidator() + { + RuleFor(c => c.Module1Id).NotEmpty(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Abstractions/BaseTest.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Abstractions/BaseTest.cs new file mode 100644 index 0000000..afb9b7e --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Abstractions/BaseTest.cs @@ -0,0 +1,17 @@ +using System.Reflection; +using App1.Modules.Module1s.Application; +using App1.Modules.Module1s.Domain.Module1s; +using App1.Modules.Module1s.Infrastructure; + +namespace App1.Modules.Module1s.ArchitectureTests.Abstractions; + +public abstract class BaseTest +{ + protected static readonly Assembly ApplicationAssembly = typeof(AssemblyReference).Assembly; + + protected static readonly Assembly DomainAssembly = typeof(Module1).Assembly; + + protected static readonly Assembly InfrastructureAssembly = typeof(Module1sModule).Assembly; + + protected static readonly Assembly PresentationAssembly = typeof(Module1s.Presentation.AssemblyReference).Assembly; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Abstractions/TestResultExtensions.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Abstractions/TestResultExtensions.cs new file mode 100644 index 0000000..44fab64 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Abstractions/TestResultExtensions.cs @@ -0,0 +1,12 @@ +using FluentAssertions; +using NetArchTest.Rules; + +namespace App1.Modules.Module1s.ArchitectureTests.Abstractions; + +internal static class TestResultExtensions +{ + internal static void ShouldBeSuccessful(this TestResult testResult) + { + testResult.FailingTypes?.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/App1.Modules.Module1s.ArchitectureTests.csproj b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/App1.Modules.Module1s.ArchitectureTests.csproj new file mode 100644 index 0000000..db4a0c2 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/App1.Modules.Module1s.ArchitectureTests.csproj @@ -0,0 +1,33 @@ + + + + + $(NetVersion) + false + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Application/ApplicationTests.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Application/ApplicationTests.cs new file mode 100644 index 0000000..55763bd --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Application/ApplicationTests.cs @@ -0,0 +1,217 @@ +using App1.Common.Application.Messaging; +using App1.Modules.Module1s.ArchitectureTests.Abstractions; +using FluentValidation; +using NetArchTest.Rules; + +namespace App1.Modules.Module1s.ArchitectureTests.Application; + +public class ApplicationTests : BaseTest +{ + [Fact] + public void Command_Should_BeSealed() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(ICommand)) + .Or() + .ImplementInterface(typeof(ICommand<>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Command_ShouldHave_NameEndingWith_Command() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(ICommand)) + .Or() + .ImplementInterface(typeof(ICommand<>)) + .Should() + .HaveNameEndingWith("Command") + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void CommandHandler_Should_NotBePublic() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(ICommandHandler<>)) + .Or() + .ImplementInterface(typeof(ICommandHandler<,>)) + .Should() + .NotBePublic() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void CommandHandler_Should_BeSealed() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(ICommandHandler<>)) + .Or() + .ImplementInterface(typeof(ICommandHandler<,>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void CommandHandler_ShouldHave_NameEndingWith_CommandHandler() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(ICommandHandler<>)) + .Or() + .ImplementInterface(typeof(ICommandHandler<,>)) + .Should() + .HaveNameEndingWith("CommandHandler") + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Query_Should_BeSealed() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IQuery<>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Query_ShouldHave_NameEndingWith_Query() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IQuery<>)) + .Should() + .HaveNameEndingWith("Query") + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void QueryHandler_Should_NotBePublic() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IQueryHandler<,>)) + .Should() + .NotBePublic() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void QueryHandler_Should_BeSealed() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IQueryHandler<,>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void QueryHandler_ShouldHave_NameEndingWith_QueryHandler() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IQueryHandler<,>)) + .Should() + .HaveNameEndingWith("QueryHandler") + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Validator_Should_NotBePublic() + { + Types.InAssembly(ApplicationAssembly) + .That() + .Inherit(typeof(AbstractValidator<>)) + .Should() + .NotBePublic() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Validator_Should_BeSealed() + { + Types.InAssembly(ApplicationAssembly) + .That() + .Inherit(typeof(AbstractValidator<>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Validator_ShouldHave_NameEndingWith_Validator() + { + Types.InAssembly(ApplicationAssembly) + .That() + .Inherit(typeof(AbstractValidator<>)) + .Should() + .HaveNameEndingWith("Validator") + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void DomainEventHandler_Should_NotBePublic() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IDomainEventHandler<>)) + .Or() + .Inherit(typeof(DomainEventHandler<>)) + .Should() + .NotBePublic() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void DomainEventHandler_Should_BeSealed() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IDomainEventHandler<>)) + .Or() + .Inherit(typeof(DomainEventHandler<>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void DomainEventHandler_ShouldHave_NameEndingWith_DomainEventHandler() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IDomainEventHandler<>)) + .Or() + .Inherit(typeof(DomainEventHandler<>)) + .Should() + .HaveNameEndingWith("DomainEventHandler") + .GetResult() + .ShouldBeSuccessful(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Domain/DomainTests.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Domain/DomainTests.cs new file mode 100644 index 0000000..ad86eb9 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Domain/DomainTests.cs @@ -0,0 +1,76 @@ +using System.Reflection; +using App1.Common.Domain; +using App1.Modules.Module1s.ArchitectureTests.Abstractions; +using FluentAssertions; +using NetArchTest.Rules; + +namespace App1.Modules.Module1s.ArchitectureTests.Domain; + +public class DomainTests : BaseTest +{ + [Fact] + public void DomainEvents_Should_BeSealed() + { + Types.InAssembly(DomainAssembly) + .That() + .ImplementInterface(typeof(IDomainEvent)) + .Or() + .Inherit(typeof(DomainEvent)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void DomainEvent_ShouldHave_DomainEventPostfix() + { + Types.InAssembly(DomainAssembly) + .That() + .ImplementInterface(typeof(IDomainEvent)) + .Or() + .Inherit(typeof(DomainEvent)) + .Should() + .HaveNameEndingWith("DomainEvent") + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Entities_ShouldHave_PrivateParameterlessConstructor() + { + IEnumerable entityTypes = Types.InAssembly(DomainAssembly).That().Inherit(typeof(Entity)).GetTypes(); + + var failingTypes = new List(); + foreach (var entityType in entityTypes) + { + var constructors = entityType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance); + + if (!constructors.Any(c => c.IsPrivate && c.GetParameters().Length == 0)) + { + failingTypes.Add(entityType); + } + } + + failingTypes.Should().BeEmpty(); + } + + [Fact] + public void Entities_ShouldOnlyHave_PrivateConstructors() + { + IEnumerable entityTypes = Types.InAssembly(DomainAssembly).That().Inherit(typeof(Entity)).GetTypes(); + + var failingTypes = new List(); + foreach (var entityType in entityTypes) + { + var constructors = entityType.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + + if (constructors.Any()) + { + failingTypes.Add(entityType); + } + } + + failingTypes.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Layers/LayerTests.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Layers/LayerTests.cs new file mode 100644 index 0000000..47fb06b --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Layers/LayerTests.cs @@ -0,0 +1,57 @@ +using App1.Modules.Module1s.ArchitectureTests.Abstractions; +using NetArchTest.Rules; + +namespace App1.Modules.Module1s.ArchitectureTests.Layers; + +public class LayerTests : BaseTest +{ + [Fact] + public void DomainLayer_ShouldNotHaveDependencyOn_ApplicationLayer() + { + Types.InAssembly(DomainAssembly) + .Should() + .NotHaveDependencyOn(ApplicationAssembly.GetName().Name) + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void DomainLayer_ShouldNotHaveDependencyOn_InfrastructureLayer() + { + Types.InAssembly(DomainAssembly) + .Should() + .NotHaveDependencyOn(InfrastructureAssembly.GetName().Name) + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void ApplicationLayer_ShouldNotHaveDependencyOn_InfrastructureLayer() + { + Types.InAssembly(ApplicationAssembly) + .Should() + .NotHaveDependencyOn(InfrastructureAssembly.GetName().Name) + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void ApplicationLayer_ShouldNotHaveDependencyOn_PresentationLayer() + { + Types.InAssembly(ApplicationAssembly) + .Should() + .NotHaveDependencyOn(PresentationAssembly.GetName().Name) + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void PresentationLayer_ShouldNotHaveDependencyOn_InfrastructureLayer() + { + Types.InAssembly(PresentationAssembly) + .Should() + .NotHaveDependencyOn(InfrastructureAssembly.GetName().Name) + .GetResult() + .ShouldBeSuccessful(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Presentation/PresentationTests.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Presentation/PresentationTests.cs new file mode 100644 index 0000000..be71f83 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.ArchitectureTests/Presentation/PresentationTests.cs @@ -0,0 +1,50 @@ +using App1.Common.Application.EventBus; +using App1.Modules.Module1s.ArchitectureTests.Abstractions; +using NetArchTest.Rules; + +namespace App1.Modules.Module1s.ArchitectureTests.Presentation; + +public class PresentationTests : BaseTest +{ + [Fact] + public void IntegrationEventHandler_Should_NotBePublic() + { + Types.InAssembly(PresentationAssembly) + .That() + .ImplementInterface(typeof(IIntegrationEventHandler<>)) + .Or() + .Inherit(typeof(IntegrationEventHandler<>)) + .Should() + .NotBePublic() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void IntegrationEventHandler_Should_BeSealed() + { + Types.InAssembly(PresentationAssembly) + .That() + .ImplementInterface(typeof(IIntegrationEventHandler<>)) + .Or() + .Inherit(typeof(IntegrationEventHandler<>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void IntegrationEventHandler_ShouldHave_NameEndingWith_DomainEventHandler() + { + Types.InAssembly(PresentationAssembly) + .That() + .ImplementInterface(typeof(IIntegrationEventHandler<>)) + .Or() + .Inherit(typeof(IntegrationEventHandler<>)) + .Should() + .HaveNameEndingWith("IntegrationEventHandler") + .GetResult() + .ShouldBeSuccessful(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/App1.Modules.Module1s.Domain.csproj b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/App1.Modules.Module1s.Domain.csproj new file mode 100644 index 0000000..f88f557 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/App1.Modules.Module1s.Domain.csproj @@ -0,0 +1,9 @@ + + + $(NetVersion) + + + + + + diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/IModule1Repository.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/IModule1Repository.cs new file mode 100644 index 0000000..a373429 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/IModule1Repository.cs @@ -0,0 +1,10 @@ +namespace App1.Modules.Module1s.Domain.Module1s; + +public interface IModule1Repository +{ + Task GetAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetAsync(CancellationToken cancellationToken = default); + + void Insert(Module1 module1); + void Delete(Module1 module1); +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1.cs new file mode 100644 index 0000000..ebce576 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1.cs @@ -0,0 +1,34 @@ +using App1.Common.Domain; + +namespace App1.Modules.Module1s.Domain.Module1s; + +public sealed class Module1 : Entity +{ + private Module1() + { + } + + public Guid Id { get; private init; } + + public static Module1 Create(Guid id) + { + var module1 = new Module1 + { + Id = id, + }; + + module1.Raise(new Module1CreatedDomainEvent(module1.Id)); + + return module1; + } + + public void Update() + { + Raise(new Module1UpdatedDomainEvent(Id)); + } + + public void Delete() + { + Raise(new Module1DeletedDomainEvent(Id)); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1CreatedDomainEvent.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1CreatedDomainEvent.cs new file mode 100644 index 0000000..d97400d --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1CreatedDomainEvent.cs @@ -0,0 +1,8 @@ +using App1.Common.Domain; + +namespace App1.Modules.Module1s.Domain.Module1s; + +public sealed class Module1CreatedDomainEvent(Guid module1Id) : DomainEvent +{ + public Guid Module1Id { get; init; } = module1Id; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1DeletedDomainEvent.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1DeletedDomainEvent.cs new file mode 100644 index 0000000..bb0e358 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1DeletedDomainEvent.cs @@ -0,0 +1,8 @@ +using App1.Common.Domain; + +namespace App1.Modules.Module1s.Domain.Module1s; + +public sealed class Module1DeletedDomainEvent(Guid module1Id) : DomainEvent +{ + public Guid Module1Id { get; init; } = module1Id; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1Errors.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1Errors.cs new file mode 100644 index 0000000..1d689d7 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1Errors.cs @@ -0,0 +1,16 @@ +using App1.Common.Domain; + +namespace App1.Modules.Module1s.Domain.Module1s; + +public static class Module1Errors +{ + public static Error NotFound(Guid module1Id) + { + return Error.NotFound("Module1s.NotFound", $"The Module1 with the identifier {module1Id} not found"); + } + + public static Error NotFound(string identityId) + { + return Error.NotFound("Module1s.NotFound", $"The Module1 with the IDP identifier {identityId} not found"); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1UpdatedDomainEvent.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1UpdatedDomainEvent.cs new file mode 100644 index 0000000..febae96 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Domain/Module1s/Module1UpdatedDomainEvent.cs @@ -0,0 +1,8 @@ +using App1.Common.Domain; + +namespace App1.Modules.Module1s.Domain.Module1s; + +public sealed class Module1UpdatedDomainEvent(Guid module1Id) : DomainEvent +{ + public Guid Module1Id { get; init; } = module1Id; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/App1.Modules.Module1s.Infrastructure.csproj b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/App1.Modules.Module1s.Infrastructure.csproj new file mode 100644 index 0000000..735353d --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/App1.Modules.Module1s.Infrastructure.csproj @@ -0,0 +1,23 @@ + + + $(NetVersion) + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Migrations/20250110195626_Module1s.Designer.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Migrations/20250110195626_Module1s.Designer.cs new file mode 100644 index 0000000..e1e72ef --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Migrations/20250110195626_Module1s.Designer.cs @@ -0,0 +1,66 @@ +// +using System; +using App1.Modules.Module1s.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace App1.Modules.Module1s.Infrastructure.Database.Migrations +{ + [DbContext(typeof(Module1sDbContext))] + [Migration("20250110195626_Module1s")] + partial class Module1s + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("App1.Module1s") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("App1.Common.Infrastructure.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Error") + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOnUtc") + .HasColumnType("datetime2"); + + b.Property("ProcessedOnUtc") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", "App1.Module1s"); + }); + + modelBuilder.Entity("App1.Modules.Module1s.Domain.Module1s.Module1", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.ToTable("Module1s", "App1.Module1s"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Migrations/20250110195626_Module1s.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Migrations/20250110195626_Module1s.cs new file mode 100644 index 0000000..9ffd7be --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Migrations/20250110195626_Module1s.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace App1.Modules.Module1s.Infrastructure.Database.Migrations +{ + /// + public partial class Module1s : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "App1.Module1s"); + + migrationBuilder.CreateTable( + name: "Module1s", + schema: "App1.Module1s", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Module1s", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OutboxMessages", + schema: "App1.Module1s", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Content = table.Column(type: "nvarchar(max)", nullable: false), + OccurredOnUtc = table.Column(type: "datetime2", nullable: false), + ProcessedOnUtc = table.Column(type: "datetime2", nullable: true), + Error = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxMessages", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Module1s", + schema: "App1.Module1s"); + + migrationBuilder.DropTable( + name: "OutboxMessages", + schema: "App1.Module1s"); + } + } +} diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Migrations/Module1sDbContextModelSnapshot.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Migrations/Module1sDbContextModelSnapshot.cs new file mode 100644 index 0000000..f0f0bf9 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Migrations/Module1sDbContextModelSnapshot.cs @@ -0,0 +1,63 @@ +// +using System; +using App1.Modules.Module1s.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace App1.Modules.Module1s.Infrastructure.Database.Migrations +{ + [DbContext(typeof(Module1sDbContext))] + partial class Module1sDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("App1.Module1s") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("App1.Common.Infrastructure.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Error") + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOnUtc") + .HasColumnType("datetime2"); + + b.Property("ProcessedOnUtc") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", "App1.Module1s"); + }); + + modelBuilder.Entity("App1.Modules.Module1s.Domain.Module1s.Module1", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.ToTable("Module1s", "App1.Module1s"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Module1sDbContext.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Module1sDbContext.cs new file mode 100644 index 0000000..28d71d5 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Module1sDbContext.cs @@ -0,0 +1,37 @@ +using App1.Common.Infrastructure.Outbox; +using App1.Modules.Module1s.Application.Abstractions.Data; +using App1.Modules.Module1s.Domain.Module1s; +using App1.Modules.Module1s.Infrastructure.Module1s; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace App1.Modules.Module1s.Infrastructure.Database; + +public sealed class Module1sDbContext(DbContextOptions options) : DbContext(options), IUnitOfWork +{ + internal DbSet Module1s => Set(); + + internal DbSet OutboxMessages => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema(Schemas.Module1s); + + modelBuilder.ApplyConfiguration(new OutboxMessageConfiguration()); + + modelBuilder.ApplyConfiguration(new Module1Configuration()); + } +} + +#if DEBUG +// dotnet ef migrations add "Module1s" -o "Database\Migrations" +public class Module1SDbContextFactory : IDesignTimeDbContextFactory +{ + public Module1sDbContext CreateDbContext(string[] args) + { + return new Module1sDbContext(new DbContextOptionsBuilder() + .UseSqlServer("Host=localhost;Database=App1;Module1name=sa;Password=password") + .Options); + } +} +#endif \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Schemas.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Schemas.cs new file mode 100644 index 0000000..00ac562 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Database/Schemas.cs @@ -0,0 +1,6 @@ +namespace App1.Modules.Module1s.Infrastructure.Database; + +internal static class Schemas +{ + internal const string Module1s = "App1.Module1s"; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1s/AzureAdB2CGraphClientConfiguration.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1s/AzureAdB2CGraphClientConfiguration.cs new file mode 100644 index 0000000..297d361 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1s/AzureAdB2CGraphClientConfiguration.cs @@ -0,0 +1,11 @@ +namespace App1.Modules.Module1s.Infrastructure.Module1s; + +public class AzureAdB2CGraphClientConfiguration +{ + public const string ConfigurationName = "AzureAdB2CGraphClient"; + public string? ClientId { get; set; } + + public string? ClientSecret { get; set; } + public string? TenantId { get; set; } + public string? DefaultApplicationId { get; set; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1s/Module1Configuration.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1s/Module1Configuration.cs new file mode 100644 index 0000000..f35598b --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1s/Module1Configuration.cs @@ -0,0 +1,13 @@ +using App1.Modules.Module1s.Domain.Module1s; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace App1.Modules.Module1s.Infrastructure.Module1s; + +internal sealed class Module1Configuration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(u => u.Id); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1s/Module1Repository.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1s/Module1Repository.cs new file mode 100644 index 0000000..63e1a3c --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1s/Module1Repository.cs @@ -0,0 +1,33 @@ +using App1.Modules.Module1s.Domain.Module1s; +using App1.Modules.Module1s.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace App1.Modules.Module1s.Infrastructure.Module1s; + +internal sealed class Module1Repository(Module1sDbContext context) : IModule1Repository +{ + public async Task GetAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Module1s.FirstOrDefaultAsync(u => u.Id == id, cancellationToken); + } + + public async Task> GetAsync(CancellationToken cancellationToken = default) + { + return await context.Module1s.ToListAsync(cancellationToken); + } + + public void Insert(Module1 module1) + { + context.Module1s.Add(module1); + } + + public void Delete(Module1 module1) + { + context.Module1s.Remove(module1); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken) + { + await context.Module1s.Where(x => x.Id == id).ExecuteDeleteAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1sModule.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1sModule.cs new file mode 100644 index 0000000..5cf1a29 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Module1sModule.cs @@ -0,0 +1,71 @@ +using App1.Common.Application.Messaging; +using App1.Common.Infrastructure; +using App1.Common.Presentation.Endpoints; +using App1.Modules.Module1s.Application.Abstractions.Data; +using App1.Modules.Module1s.Domain.Module1s; +using App1.Modules.Module1s.Infrastructure.Database; +using App1.Modules.Module1s.Infrastructure.Module1s; +using App1.Modules.Module1s.Infrastructure.Outbox; +using App1.Modules.Module1s.Presentation; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace App1.Modules.Module1s.Infrastructure; + +public static class Module1sModule +{ + public static IHostApplicationBuilder AddModule1sModule(this IHostApplicationBuilder builder) + { + builder.Services.AddDomainEventHandlers(); + + builder.AddInfrastructure(); + + builder.Services.AddEndpoints(AssemblyReference.Assembly); + + return builder; + } + + private static void AddInfrastructure(this IHostApplicationBuilder builder) + { + builder.AddDatabase(Schemas.Module1s, optionsBuilder => + { + optionsBuilder.UseAsyncSeeding(async (dbContext, _, cancellationToken) => + { + var module1Guid = Guid.CreateVersion7(); + var module1 = dbContext.Find(module1Guid); + if (module1 != null) + { + return; + } + + dbContext.Add(Module1.Create(module1Guid)); + await dbContext.SaveChangesAsync(cancellationToken); + }); + }); + + builder.Services.AddScoped(); + + builder.Services.AddScoped(sp => sp.GetRequiredService()); + + builder.Services.Configure(builder.Configuration.GetSection("Module1s:Outbox")); + + builder.Services.ConfigureOptions(); + } + + private static void AddDomainEventHandlers(this IServiceCollection services) + { + var domainEventHandlers = Application.AssemblyReference.Assembly.GetTypes() + .Where(t => t.IsAssignableTo(typeof(IDomainEventHandler))); + + foreach (var domainEventHandler in domainEventHandlers) + { + services.AddKeyedScoped(typeof(IDomainEventHandler), GetKey(domainEventHandler), domainEventHandler); + } + + static string GetKey(Type type) + { + const int handlerNameSuffixLength = 7; + return type.Name.AsSpan(..^handlerNameSuffixLength).ToString(); + } + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Outbox/ConfigureProcessOutboxJob.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Outbox/ConfigureProcessOutboxJob.cs new file mode 100644 index 0000000..7234789 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Outbox/ConfigureProcessOutboxJob.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Options; +using Quartz; + +namespace App1.Modules.Module1s.Infrastructure.Outbox; + +internal sealed class ConfigureProcessOutboxJob(IOptions outboxOptions) : IConfigureOptions +{ + public void Configure(QuartzOptions options) + { + var jobName = typeof(ProcessOutboxJob).FullName!; + + options.AddJob(configure => configure.WithIdentity(jobName)) + .AddTrigger(configure => + { + configure.ForJob(jobName) + .WithSimpleSchedule(schedule => + { + schedule + .WithIntervalInSeconds(outboxOptions.Value.IntervalInSeconds) + .RepeatForever(); + }); + }); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Outbox/OutboxOptions.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Outbox/OutboxOptions.cs new file mode 100644 index 0000000..a90ed85 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Outbox/OutboxOptions.cs @@ -0,0 +1,8 @@ +namespace App1.Modules.Module1s.Infrastructure.Outbox; + +internal sealed class OutboxOptions +{ + public int IntervalInSeconds { get; init; } + + public int BatchSize { get; init; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Outbox/ProcessOutboxJob.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Outbox/ProcessOutboxJob.cs new file mode 100644 index 0000000..0150c65 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Infrastructure/Outbox/ProcessOutboxJob.cs @@ -0,0 +1,82 @@ +using System.Text.Json; +using App1.Common.Application.Messaging; +using App1.Common.Domain; +using App1.Common.Infrastructure.Outbox; +using App1.Common.Infrastructure.Serialization; +using App1.Modules.Module1s.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Quartz; + +namespace App1.Modules.Module1s.Infrastructure.Outbox; + +[DisallowConcurrentExecution] +internal sealed class ProcessOutboxJob( + Module1sDbContext dbContext, + IServiceScopeFactory serviceScopeFactory, + TimeProvider dateTimeProvider, + IOptions outboxOptions, + ILogger logger) : IJob +{ + private const string ModuleName = "Module1s"; + + public async Task Execute(IJobExecutionContext context) + { + logger.LogInformation("{Module} - Beginning to process outbox messages", ModuleName); + + var outboxMessages = await GetOutboxMessagesAsync(); + + foreach (var outboxMessage in outboxMessages) + { + Exception? exception = null; + + try + { + var domainEvent = JsonSerializer.Deserialize(outboxMessage.Content, SerializerSettings.Instance)!; + + using var scope = serviceScopeFactory.CreateScope(); + var handler = scope.ServiceProvider.GetKeyedService(domainEvent.GetType().Name); + if (handler is not null) + { + await handler.Handle(domainEvent, context.CancellationToken); + } + } + catch (Exception caughtException) + { + logger.LogError(caughtException, + "{Module} - Exception while processing outbox message {MessageId}", + ModuleName, + outboxMessage.Id); + + exception = caughtException; + } + + await UpdateOutboxMessageAsync(outboxMessage, exception); + } + + logger.LogInformation("{Module} - Completed processing outbox messages", ModuleName); + } + + private async Task> GetOutboxMessagesAsync() + { + var outboxMessages = await dbContext.OutboxMessages + .Where(x => x.ProcessedOnUtc == null) + .OrderBy(x => x.OccurredOnUtc) + .Take(outboxOptions.Value.BatchSize) + .ToListAsync(); + + return outboxMessages; + } + + private async Task UpdateOutboxMessageAsync(OutboxMessage outboxMessage, Exception? exception) + { + var error = exception?.ToString(); + await dbContext.OutboxMessages.Where(x => x.Id == outboxMessage.Id) + .ExecuteUpdateAsync(m => m.SetProperty(p => p.Error, error) + .SetProperty( + p => p.ProcessedOnUtc, + dateTimeProvider.GetUtcNow().UtcDateTime)); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/App1.Modules.Module1s.IntegrationEvents.csproj b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/App1.Modules.Module1s.IntegrationEvents.csproj new file mode 100644 index 0000000..d3c1e40 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/App1.Modules.Module1s.IntegrationEvents.csproj @@ -0,0 +1,9 @@ + + + $(NetVersion) + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/Module1CreatedIntegrationEvent.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/Module1CreatedIntegrationEvent.cs new file mode 100644 index 0000000..8a7f422 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/Module1CreatedIntegrationEvent.cs @@ -0,0 +1,11 @@ +using App1.Common.Application.EventBus; + +namespace App1.Modules.Module1s.IntegrationEvents; + +public sealed class Module1CreatedIntegrationEvent( + Guid id, + DateTime occurredOnUtc, + Guid module1Id) : IntegrationEvent(id, occurredOnUtc) +{ + public Guid Module1Id { get; init; } = module1Id; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/Module1DeletedIntegrationEvent.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/Module1DeletedIntegrationEvent.cs new file mode 100644 index 0000000..d6b9c85 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/Module1DeletedIntegrationEvent.cs @@ -0,0 +1,9 @@ +using App1.Common.Application.EventBus; + +namespace App1.Modules.Module1s.IntegrationEvents; + +public sealed class Module1DeletedIntegrationEvent(Guid id, DateTime occurredOnUtc, Guid module1Id) + : IntegrationEvent(id, occurredOnUtc) +{ + public Guid Module1Id { get; init; } = module1Id; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/Module1UpdatedIntegrationEvent.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/Module1UpdatedIntegrationEvent.cs new file mode 100644 index 0000000..504fece --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationEvents/Module1UpdatedIntegrationEvent.cs @@ -0,0 +1,11 @@ +using App1.Common.Application.EventBus; + +namespace App1.Modules.Module1s.IntegrationEvents; + +public sealed class Module1UpdatedIntegrationEvent( + Guid id, + DateTime occurredOnUtc, + Guid module1Id) : IntegrationEvent(id, occurredOnUtc) +{ + public Guid Module1Id { get; init; } = module1Id; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/BaseIntegrationTest.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/BaseIntegrationTest.cs new file mode 100644 index 0000000..00223f5 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/BaseIntegrationTest.cs @@ -0,0 +1,63 @@ +using App1.Modules.Module1s.Infrastructure.Database; +using App1.Modules.Module1s.IntegrationTests.Abstractions.Fixtures; +using AutoFixture; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace App1.Modules.Module1s.IntegrationTests.Abstractions; + +[Collection(nameof(IntegrationTestCollection))] +public abstract class BaseIntegrationTest : IDisposable +{ + protected static readonly Fixture Faker = new(); + protected readonly Module1sDbContext DbContext; + protected readonly HttpClient HttpClient; + private readonly IServiceScope scope; + protected readonly ISender Sender; + private bool disposedValue; + + protected BaseIntegrationTest(IntegrationTestWebAppFactory factory) + { + scope = factory.Services.CreateScope(); + HttpClient = factory.CreateClient(); + Sender = scope.ServiceProvider.GetRequiredService(); + DbContext = scope.ServiceProvider.GetRequiredService(); + } + + protected void SetAuth(bool enableAuth, bool failPermission = false) + { + var testAuthHandlerOptions = scope.ServiceProvider.GetRequiredService>(); + testAuthHandlerOptions.CurrentValue.FakeSuccessfulAuthentication = enableAuth; + testAuthHandlerOptions.CurrentValue.FailPermission = failPermission; + } + + protected async Task CleanDatabaseAsync() + { + await DbContext.Database.ExecuteSqlRawAsync(""" + DELETE FROM Module1s.inbox_messages; + DELETE FROM Module1s.outbox_messages; + DELETE FROM Module1s.Module1s; + """); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + scope.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/Fixtures/MockSchemeProvider.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/Fixtures/MockSchemeProvider.cs new file mode 100644 index 0000000..1b07851 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/Fixtures/MockSchemeProvider.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; + +namespace App1.Modules.Module1s.IntegrationTests.Abstractions.Fixtures; + +public class MockSchemeProvider : AuthenticationSchemeProvider +{ + private readonly IOptionsMonitor fakeOptions; + + public MockSchemeProvider(IOptions options, IOptionsMonitor fakeOptions) : base(options) + { + this.fakeOptions = fakeOptions; + } + + protected MockSchemeProvider( + IOptions options, + IOptionsMonitor fakeOptions, + IDictionary schemes) : base(options, schemes) + { + this.fakeOptions = fakeOptions; + } + + public override Task GetSchemeAsync(string name) + { + if (fakeOptions.CurrentValue.FakeSuccessfulAuthentication) + { + var scheme = new AuthenticationScheme( + TestAuthHandler.AuthenticationScheme, + TestAuthHandler.AuthenticationScheme, + typeof(TestAuthHandler)); + return Task.FromResult(scheme); + } + + return base.GetSchemeAsync(name); + } +} diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/Fixtures/TestAuthHandler.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/Fixtures/TestAuthHandler.cs new file mode 100644 index 0000000..41629a8 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/Fixtures/TestAuthHandler.cs @@ -0,0 +1,47 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Web; +using Microsoft.IdentityModel.JsonWebTokens; + +namespace App1.Modules.Module1s.IntegrationTests.Abstractions.Fixtures; + +public class TestAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : AuthenticationHandler(options, logger, encoder) +{ + public const string NoPermissionEmail = "NoPermissionModule1@email.com"; + public const string Email = "Module1@email.com"; + private readonly IOptionsMonitor options = options; + + public static string AuthenticationScheme { get; } = JwtBearerDefaults.AuthenticationScheme; + + protected override Task HandleAuthenticateAsync() + { + List claims = + [ + new(ClaimConstants.ObjectId, "19d3b2c7-8714-4851-ac73-95aeecfba3a6"), + new(ClaimConstants.NameIdentifierId, "123123"), + new(JwtRegisteredClaimNames.Iat, GetEpochTimeFromSeconds().ToString()), + options.CurrentValue.FailPermission + ? new(ClaimConstants.PreferredUserName, NoPermissionEmail) + : new(ClaimConstants.PreferredUserName, Email) + ]; + + var identity = new ClaimsIdentity(claims, AuthenticationScheme); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, AuthenticationScheme); + var result = AuthenticateResult.Success(ticket); + return Task.FromResult(result); + } + + public static long GetEpochTimeFromSeconds() + { + return (long)(DateTime.UtcNow - DateTime.UnixEpoch).TotalSeconds; + } +} diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/Fixtures/TestAuthHandlerOptions.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/Fixtures/TestAuthHandlerOptions.cs new file mode 100644 index 0000000..cca036d --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/Fixtures/TestAuthHandlerOptions.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authentication; + +namespace App1.Modules.Module1s.IntegrationTests.Abstractions.Fixtures; + +public class TestAuthHandlerOptions : AuthenticationSchemeOptions +{ + public bool FakeSuccessfulAuthentication { get; set; } = true; + + public bool FailPermission { get; set; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/IntegrationTestCollection.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/IntegrationTestCollection.cs new file mode 100644 index 0000000..914d6a4 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/IntegrationTestCollection.cs @@ -0,0 +1,4 @@ +namespace App1.Modules.Module1s.IntegrationTests.Abstractions; + +[CollectionDefinition(nameof(IntegrationTestCollection))] +public sealed class IntegrationTestCollection : ICollectionFixture; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/IntegrationTestWebAppFactory.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/IntegrationTestWebAppFactory.cs new file mode 100644 index 0000000..1080cda --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Abstractions/IntegrationTestWebAppFactory.cs @@ -0,0 +1,48 @@ +using App1.ApiService; +using App1.ApiService.Extensions; +using App1.Modules.Module1s.IntegrationTests.Abstractions.Fixtures; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Testcontainers.MsSql; +using Testcontainers.Redis; + +namespace App1.Modules.Module1s.IntegrationTests.Abstractions; + +public class IntegrationTestWebAppFactory : WebApplicationFactory, IAsyncLifetime +{ + private readonly MsSqlContainer dbContainer = new MsSqlBuilder() + .WithImage("mcr.microsoft.com/mssql/server:2022-latest") + .Build(); + + private readonly RedisContainer redisContainer = new RedisBuilder().WithImage("redis:latest").Build(); + + public async Task InitializeAsync() + { + await dbContainer.StartAsync(); + await redisContainer.StartAsync(); + } + + public new async Task DisposeAsync() + { + await dbContainer.StopAsync(); + await redisContainer.StopAsync(); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + Environment.SetEnvironmentVariable("ConnectionStrings:database", dbContainer.GetConnectionString()); + Environment.SetEnvironmentVariable("ConnectionStrings:cache", redisContainer.GetConnectionString()); + Environment.SetEnvironmentVariable("ConnectionStrings:ai-llama3-2", "Endpoint=https://localhost;Model=llama3.2"); + + builder.ConfigureServices(services => + { + services.AddTransient(); + services.Configure(options => + { + options.FakeSuccessfulAuthentication = true; + }); + }); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/App1.Modules.Module1s.IntegrationTests.csproj b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/App1.Modules.Module1s.IntegrationTests.csproj new file mode 100644 index 0000000..db2d8fc --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/App1.Modules.Module1s.IntegrationTests.csproj @@ -0,0 +1,34 @@ + + + $(NetVersion) + false + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/GetModule1ProfileTests.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/GetModule1ProfileTests.cs new file mode 100644 index 0000000..597677e --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/GetModule1ProfileTests.cs @@ -0,0 +1,60 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using App1.Modules.Module1s.Application.Module1s.CreateModule1; +using App1.Modules.Module1s.IntegrationTests.Abstractions; +using FluentAssertions; +using Microsoft.AspNetCore.Authentication.JwtBearer; + +namespace App1.Modules.Module1s.IntegrationTests.Module1s; + +public class GetModule1ByIdTests(IntegrationTestWebAppFactory factory) : BaseIntegrationTest(factory) +{ + [Fact] + public async Task Should_ReturnUnauthorized_WhenAccessTokenNotProvided() + { + SetAuth(false); + + // Act + HttpResponseMessage response = await HttpClient.GetAsync("Module1s/3c3bdb4b-327b-49a9-a13e-0b565526b8a1"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Should_ReturnOk_WhenModule1Exists() + { + SetAuth(true); + + // Arrange + string accessToken = await RegisterModule1AndGetAccessTokenAsync(); + HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + JwtBearerDefaults.AuthenticationScheme, + accessToken); + + // Act + HttpResponseMessage response = await HttpClient.GetAsync("Module1s/3c3bdb4b-327b-49a9-a13e-0b565526b8a1"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + Module1Response? module1 = await response.Content.ReadFromJsonAsync(); + module1.Should().NotBeNull(); + } + + private async Task RegisterModule1AndGetAccessTokenAsync() + { + var request = new Presentation.Module1s.CreateModule1.Request + { + ObjectId = Guid.Parse("19d3b2c7-8714-4851-ac73-95aeecfba3a6") + }; + + var response = await HttpClient.PostAsJsonAsync("Module1s", request); + response.EnsureSuccessStatusCode(); + + string accessToken = string.Empty; + + return accessToken; + } +} diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/GetModule1Tests.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/GetModule1Tests.cs new file mode 100644 index 0000000..25fe267 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/GetModule1Tests.cs @@ -0,0 +1,38 @@ +using App1.Modules.Module1s.Application.Module1s.CreateModule1; +using App1.Modules.Module1s.Application.Module1s.GetModule1ById; +using App1.Modules.Module1s.Domain.Module1s; +using App1.Modules.Module1s.IntegrationTests.Abstractions; +using FluentAssertions; + +namespace App1.Modules.Module1s.IntegrationTests.Module1s; + +public class GetModule1Tests(IntegrationTestWebAppFactory factory) : BaseIntegrationTest(factory) +{ + [Fact] + public async Task Should_ReturnError_WhenModule1DoesNotExist() + { + // Arrange + var module1Id = Guid.NewGuid(); + + // Act + var module1Result = await Sender.Send(new GetModule1Query(module1Id)); + + // Assert + module1Result.Error.Should().Be(Module1Errors.NotFound(module1Id)); + } + + [Fact] + public async Task Should_ReturnModule1_WhenModule1Exists() + { + // Arrange + var result = await Sender.Send(new CreateModule1Command(Guid.Parse("19d3b2c7-8714-4851-ac73-95aeecfba3a6"))); + var module1Id = result.Value.Id; + + // Act + var module1Result = await Sender.Send(new GetModule1Query(module1Id)); + + // Assert + module1Result.IsSuccess.Should().BeTrue(); + module1Result.Value.Should().NotBeNull(); + } +} diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/RegisterModule1Tests.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/RegisterModule1Tests.cs new file mode 100644 index 0000000..3f1cd7c --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/RegisterModule1Tests.cs @@ -0,0 +1,66 @@ +using System.Net; +using System.Net.Http.Json; +using App1.Modules.Module1s.IntegrationTests.Abstractions; +using App1.Modules.Module1s.Presentation.Module1s; +using FluentAssertions; + +namespace App1.Modules.Module1s.IntegrationTests.Module1s; + +public class RegisterModule1Tests(IntegrationTestWebAppFactory factory) : BaseIntegrationTest(factory) +{ + public static readonly TheoryData InvalidRequests = ["19d3b2c7-8714-4851-ac73-95aeecfba3a6"]; + + + [Theory] + [MemberData(nameof(InvalidRequests))] + public async Task Should_ReturnUnauthorized_WhenRequestIsNotValid(string objectId) + { + SetAuth(true); + + // Arrange + var request = new CreateModule1.Request + { + ObjectId = Guid.Parse(objectId) + }; + + // Act + HttpResponseMessage response = await HttpClient.PostAsJsonAsync("Module1s", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + + [Fact] + public async Task Should_ReturnBadRequest_WhenRequestIsNull() + { + SetAuth(true); + + // Arrange + CreateModule1.Request? request = null; + + // Act + HttpResponseMessage response = await HttpClient.PostAsJsonAsync("Module1s", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task Should_ReturnOk_WhenRequestIsValid() + { + SetAuth(true); + + // Arrange + var request = new CreateModule1.Request + { + ObjectId = Guid.Parse("19d3b2c7-8714-4851-ac73-95aeecfba3a6") + }; + + // Act + HttpResponseMessage response = await HttpClient.PostAsJsonAsync("Module1s", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + } +} diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/UpdateModule1Tests.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/UpdateModule1Tests.cs new file mode 100644 index 0000000..405562f --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.IntegrationTests/Module1s/UpdateModule1Tests.cs @@ -0,0 +1,59 @@ +using App1.Common.Domain; +using App1.Modules.Module1s.Application.Module1s.CreateModule1; +using App1.Modules.Module1s.Application.Module1s.UpdateModule1; +using App1.Modules.Module1s.Domain.Module1s; +using App1.Modules.Module1s.IntegrationTests.Abstractions; +using AutoFixture; +using FluentAssertions; + +namespace App1.Modules.Module1s.IntegrationTests.Module1s; + +public class UpdateModule1Tests(IntegrationTestWebAppFactory factory) : BaseIntegrationTest(factory) +{ + public static readonly TheoryData InvalidCommands = + [ + new UpdateModule1Command(Guid.Empty) + ]; + + [Theory] + [MemberData(nameof(InvalidCommands))] + public async Task Should_ReturnError_WhenCommandIsNotValid(UpdateModule1Command command) + { + // Act + Result result = await Sender.Send(command); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Type.Should().Be(ErrorType.Validation); + } + + [Fact] + public async Task Should_ReturnError_WhenModule1DoesNotExist() + { + // Arrange + var module1Id = Guid.NewGuid(); + + // Act + Result updateResult = await Sender.Send( + new UpdateModule1Command(module1Id)); + + // Assert + updateResult.Error.Should().Be(Module1Errors.NotFound(module1Id)); + } + + [Fact] + public async Task Should_ReturnSuccess_WhenModule1Exists() + { + // Arrange + var result = await Sender.Send(new CreateModule1Command(Guid.Parse("19d3b2c7-8714-4851-ac73-95aeecfba3a6"))); + + Guid module1Id = result.Value.Id; + + // Act + Result updateResult = await Sender.Send( + new UpdateModule1Command(module1Id)); + + // Assert + updateResult.IsSuccess.Should().BeTrue(); + } +} diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/App1.Modules.Module1s.Presentation.csproj b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/App1.Modules.Module1s.Presentation.csproj new file mode 100644 index 0000000..ef590af --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/App1.Modules.Module1s.Presentation.csproj @@ -0,0 +1,15 @@ + + + $(NetVersion) + + + + + + + + + + + + diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/AssemblyReference.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/AssemblyReference.cs new file mode 100644 index 0000000..138e78f --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/AssemblyReference.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace App1.Modules.Module1s.Presentation; + +public static class AssemblyReference +{ + public static readonly Assembly Assembly = typeof(AssemblyReference).Assembly; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/CreateModule1.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/CreateModule1.cs new file mode 100644 index 0000000..12e4d08 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/CreateModule1.cs @@ -0,0 +1,42 @@ +using App1.Common.Presentation.Endpoints; +using App1.Modules.Module1s.Application.Module1s.CreateModule1; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace App1.Modules.Module1s.Presentation.Module1s; + +internal sealed class CreateModule1 : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("module1s", async (Request? request, ISender sender) => + { + if (request is null) + { + return Results.BadRequest( + new ResponseContent( "There was a problem with your request.")); + } + + var result = await sender.Send(new CreateModule1Command(request.ObjectId)); + if (result.IsSuccess) + { + return Results.Ok(new ResponseContent()); + } + + return Results.BadRequest(new ResponseContent(result.Error.Description)); + }) + .WithTags(Tags.Module1s); + } + + internal sealed class Request + { + public required Guid ObjectId { get; init; } + } + + internal class ResponseContent(string? userMessage = null) + { + public string? UserMessage { get; set; } = userMessage; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/DeleteModule1.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/DeleteModule1.cs new file mode 100644 index 0000000..d3377e3 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/DeleteModule1.cs @@ -0,0 +1,27 @@ +using System.Security.Claims; +using App1.Common.Infrastructure.Authentication; +using App1.Common.Infrastructure.Authorization; +using App1.Common.Presentation.Endpoints; +using App1.Common.Presentation.Results; +using App1.Modules.Module1s.Application.Module1s.DeleteModule1; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace App1.Modules.Module1s.Presentation.Module1s; + +internal sealed class DeleteModule1 : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapDelete("Module1s/{id:guid}", async (Guid id, ISender sender) => + { + var result = await sender.Send(new DeleteModule1Command(id)); + + return result.Match(Results.NoContent, ApiResults.Problem); + }) + .RequireAuthorization(PolicyConstants.AdministratorPolicy) + .WithTags(Tags.Module1s); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/GetModule1.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/GetModule1.cs new file mode 100644 index 0000000..e4a70c8 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/GetModule1.cs @@ -0,0 +1,25 @@ +using App1.Common.Infrastructure.Authorization; +using App1.Common.Presentation.Endpoints; +using App1.Common.Presentation.Results; +using App1.Modules.Module1s.Application.Module1s.GetModule1s; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace App1.Modules.Module1s.Presentation.Module1s; + +internal sealed class GetModule1S : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("Module1s", async (ISender sender) => + { + var result = await sender.Send(new GetModule1SQuery()); + + return result.Match(Results.Ok, ApiResults.Problem); + }) + .RequireAuthorization(PolicyConstants.AdministratorPolicy) + .WithTags(Tags.Module1s); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/GetModule1ById.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/GetModule1ById.cs new file mode 100644 index 0000000..8b65f60 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/GetModule1ById.cs @@ -0,0 +1,24 @@ +using App1.Common.Presentation.Endpoints; +using App1.Common.Presentation.Results; +using App1.Modules.Module1s.Application.Module1s.GetModule1ById; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace App1.Modules.Module1s.Presentation.Module1s; + +internal sealed class GetModule1ById : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("Module1s/{id:guid}", async (Guid id, ISender sender) => + { + var result = await sender.Send(new GetModule1Query(id)); + + return result.Match(Results.Ok, ApiResults.Problem); + }) + .RequireAuthorization() + .WithTags(Tags.Module1s); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/UpdateModule1Profile.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/UpdateModule1Profile.cs new file mode 100644 index 0000000..ec90546 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Module1s/UpdateModule1Profile.cs @@ -0,0 +1,31 @@ +using System.Security.Claims; +using App1.Common.Infrastructure.Authentication; +using App1.Common.Presentation.Endpoints; +using App1.Common.Presentation.Results; +using App1.Modules.Module1s.Application.Module1s.UpdateModule1; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace App1.Modules.Module1s.Presentation.Module1s; + +internal sealed class UpdateModule1Profile : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPut("Module2s/{id:guid}", async (Guid id, Request request, ISender sender) => + { + var result = await sender.Send(new UpdateModule1Command(id)); + + return result.Match(Results.NoContent, ApiResults.Problem); + }) + .RequireAuthorization() + .WithTags(Tags.Module1s); + } + + internal sealed class Request + { + public bool Prop1 { get; set; } + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Tags.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Tags.cs new file mode 100644 index 0000000..bf1736f --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.Presentation/Tags.cs @@ -0,0 +1,6 @@ +namespace App1.Modules.Module1s.Presentation; + +internal static class Tags +{ + internal const string Module1s = "Module1s"; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.UnitTests/Abstractions/BaseTest.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.UnitTests/Abstractions/BaseTest.cs new file mode 100644 index 0000000..fcde143 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.UnitTests/Abstractions/BaseTest.cs @@ -0,0 +1,21 @@ +using App1.Common.Domain; +using AutoFixture; + +namespace App1.Modules.Module1s.UnitTests.Abstractions; + +public abstract class BaseTest +{ + protected static readonly Fixture Faker = new(); + + public static T AssertDomainEventWasPublished(Entity entity) where T : IDomainEvent + { + var domainEvent = entity.DomainEvents.OfType().SingleOrDefault(); + + if (domainEvent is null) + { + throw new Exception($"{typeof(T).Name} was not published"); + } + + return domainEvent; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.UnitTests/App1.Modules.Module1s.UnitTests.csproj b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.UnitTests/App1.Modules.Module1s.UnitTests.csproj new file mode 100644 index 0000000..108cb51 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.UnitTests/App1.Modules.Module1s.UnitTests.csproj @@ -0,0 +1,31 @@ + + + $(NetVersion) + false + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.UnitTests/Module1s/Module1Tests.cs b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.UnitTests/Module1s/Module1Tests.cs new file mode 100644 index 0000000..e23976f --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module1s/App1.Modules.Module1s.UnitTests/Module1s/Module1Tests.cs @@ -0,0 +1,60 @@ +using App1.Modules.Module1s.Domain.Module1s; +using App1.Modules.Module1s.UnitTests.Abstractions; +using FluentAssertions; + +namespace App1.Modules.Module1s.UnitTests.Module1s; + +public class Module1Tests : BaseTest +{ + [Fact] + public void Create_ShouldReturnModule1() + { + // Act + var module1 = Module1.Create(Guid.NewGuid()); + + // Assert + module1.Should().NotBeNull(); + } + + [Fact] + public void Create_ShouldRaiseDomainEvent_WhenModule1Created() + { + // Act + var module1 = Module1.Create(Guid.NewGuid()); + + // Assert + var domainEvent = AssertDomainEventWasPublished(module1); + + domainEvent.Module1Id.Should().Be(module1.Id); + } + + [Fact] + public void Update_ShouldRaiseDomainEvent_WhenModule1Updated() + { + // Arrange + var module1 = Module1.Create(Guid.NewGuid()); + + // Act + module1.Update(); + + // Assert + var domainEvent = AssertDomainEventWasPublished(module1); + + domainEvent.Module1Id.Should().Be(module1.Id); + } + + [Fact] + public void Update_ShouldNotRaiseDomainEvent_WhenModule1NotUpdated() + { + // Arrange + var module1 = Module1.Create(Guid.NewGuid()); + + module1.ClearDomainEvents(); + + // Act + module1.Update(); + + // Assert + module1.DomainEvents.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Abstractions/Data/IUnitOfWork.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Abstractions/Data/IUnitOfWork.cs new file mode 100644 index 0000000..48b184d --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Abstractions/Data/IUnitOfWork.cs @@ -0,0 +1,6 @@ +namespace App1.Modules.Module2s.Application.Abstractions.Data; + +public interface IUnitOfWork +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/App1.Modules.Module2s.Application.csproj b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/App1.Modules.Module2s.Application.csproj new file mode 100644 index 0000000..c4cf355 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/App1.Modules.Module2s.Application.csproj @@ -0,0 +1,13 @@ + + + + $(NetVersion) + + + + + + + + + diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/AssemblyReference.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/AssemblyReference.cs new file mode 100644 index 0000000..07dbd56 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/AssemblyReference.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace App1.Modules.Module2s.Application; + +public static class AssemblyReference +{ + public static readonly Assembly Assembly = typeof(AssemblyReference).Assembly; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/CreateModule2Command.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/CreateModule2Command.cs new file mode 100644 index 0000000..3726cf7 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/CreateModule2Command.cs @@ -0,0 +1,5 @@ +using App1.Common.Application.Messaging; + +namespace App1.Modules.Module2s.Application.Module2s.CreateModule2; + +public sealed record CreateModule2Command(Guid ProviderId) : ICommand; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/CreateModule2CommandHandler.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/CreateModule2CommandHandler.cs new file mode 100644 index 0000000..f1a8790 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/CreateModule2CommandHandler.cs @@ -0,0 +1,25 @@ +using App1.Common.Application.Messaging; +using App1.Common.Domain; +using App1.Modules.Module2s.Application.Abstractions.Data; +using App1.Modules.Module2s.Domain.Module2s; + +namespace App1.Modules.Module2s.Application.Module2s.CreateModule2; + +internal sealed class CreateModule2CommandHandler( + IModule2Repository module2Repository, + IUnitOfWork unitOfWork) : ICommandHandler +{ + public async Task> Handle(CreateModule2Command request, CancellationToken cancellationToken) + { + var module2 = await module2Repository.GetAsync(request.ProviderId, cancellationToken); + if (module2 is null) + { + module2 = Module2.Create(request.ProviderId); + module2Repository.Insert(module2); + + await unitOfWork.SaveChangesAsync(cancellationToken); + } + + return new Module2Response(module2.Id); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/CreateModule2CommandValidator.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/CreateModule2CommandValidator.cs new file mode 100644 index 0000000..503a7d5 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/CreateModule2CommandValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace App1.Modules.Module2s.Application.Module2s.CreateModule2; + +internal sealed class CreateModule2CommandValidator : AbstractValidator +{ + public CreateModule2CommandValidator() + { + RuleFor(c => c.ProviderId).NotEmpty(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/Module2CreatedDomainEventHandler.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/Module2CreatedDomainEventHandler.cs new file mode 100644 index 0000000..da137a0 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/Module2CreatedDomainEventHandler.cs @@ -0,0 +1,28 @@ +using App1.Common.Application.EventBus; +using App1.Common.Application.Exceptions; +using App1.Common.Application.Messaging; +using App1.Modules.Module2s.Application.Module2s.GetModule2ById; +using App1.Modules.Module2s.Domain.Module2s; +using App1.Modules.Module2s.IntegrationEvents; +using MediatR; + +namespace App1.Modules.Module2s.Application.Module2s.CreateModule2; + +internal sealed class Module2CreatedDomainEventHandler(ISender sender, IEventBus bus) + : DomainEventHandler +{ + public override async Task Handle(Module2CreatedDomainEvent domainEvent, + CancellationToken cancellationToken = default) + { + var result = await sender.Send(new GetModule2Query(domainEvent.Module2Id), cancellationToken); + + if (result.IsFailure) + { + throw new App1Exception(nameof(GetModule2Query), result.Error); + } + + await bus.PublishAsync( + new Module2CreatedIntegrationEvent(domainEvent.Id, domainEvent.OccurredOnUtc, result.Value.Id), + cancellationToken); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/Module2Response.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/Module2Response.cs new file mode 100644 index 0000000..b159f3d --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/CreateModule2/Module2Response.cs @@ -0,0 +1,3 @@ +namespace App1.Modules.Module2s.Application.Module2s.CreateModule2; + +public sealed record Module2Response(Guid Id); \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/DeleteModule2/DeleteModule2Command.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/DeleteModule2/DeleteModule2Command.cs new file mode 100644 index 0000000..ff3d6b1 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/DeleteModule2/DeleteModule2Command.cs @@ -0,0 +1,5 @@ +using App1.Common.Application.Messaging; + +namespace App1.Modules.Module2s.Application.Module2s.DeleteModule2; + +public sealed record DeleteModule2Command(Guid Module2Id) : ICommand; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/DeleteModule2/DeleteModule2CommandHandler.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/DeleteModule2/DeleteModule2CommandHandler.cs new file mode 100644 index 0000000..2933f13 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/DeleteModule2/DeleteModule2CommandHandler.cs @@ -0,0 +1,27 @@ +using App1.Common.Application.Messaging; +using App1.Common.Domain; +using App1.Modules.Module2s.Application.Abstractions.Data; +using App1.Modules.Module2s.Domain.Module2s; + +namespace App1.Modules.Module2s.Application.Module2s.DeleteModule2; + +internal sealed class DeleteModule2CommandHandler( + IModule2Repository module2Repository, + IUnitOfWork unitOfWork) : ICommandHandler +{ + public async Task Handle(DeleteModule2Command request, CancellationToken cancellationToken) + { + var module2 = await module2Repository.GetAsync(request.Module2Id, cancellationToken); + + if (module2 is null) + { + return Result.Failure(Module2Errors.NotFound(request.Module2Id)); + } + + module2.Delete(); + module2Repository.Delete(module2); + await unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/DeleteModule2/Module2DeletedDomainEventHandler.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/DeleteModule2/Module2DeletedDomainEventHandler.cs new file mode 100644 index 0000000..ad9bfce --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/DeleteModule2/Module2DeletedDomainEventHandler.cs @@ -0,0 +1,16 @@ +using App1.Common.Application.EventBus; +using App1.Common.Application.Messaging; +using App1.Modules.Module2s.Domain.Module2s; +using App1.Modules.Module2s.IntegrationEvents; + +namespace App1.Modules.Module2s.Application.Module2s.DeleteModule2; + +internal sealed class Module2DeletedDomainEventHandler(IEventBus eventBus) : DomainEventHandler +{ + public override async Task Handle(Module2DeletedDomainEvent domainEvent, CancellationToken cancellationToken = default) + { + await eventBus.PublishAsync( + new Module2DeletedIntegrationEvent(domainEvent.Id, domainEvent.OccurredOnUtc, domainEvent.Module2Id), + cancellationToken); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2ById/GetModule2Query.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2ById/GetModule2Query.cs new file mode 100644 index 0000000..bc6043b --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2ById/GetModule2Query.cs @@ -0,0 +1,5 @@ +using App1.Common.Application.Messaging; + +namespace App1.Modules.Module2s.Application.Module2s.GetModule2ById; + +public sealed record GetModule2Query(Guid Module2Id) : IQuery; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2ById/GetModule2QueryHandler.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2ById/GetModule2QueryHandler.cs new file mode 100644 index 0000000..0f3b683 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2ById/GetModule2QueryHandler.cs @@ -0,0 +1,21 @@ +using App1.Common.Application.Messaging; +using App1.Common.Domain; +using App1.Modules.Module2s.Domain.Module2s; + +namespace App1.Modules.Module2s.Application.Module2s.GetModule2ById; + +internal sealed class GetModule2QueryHandler(IModule2Repository module2Repository) + : IQueryHandler +{ + public async Task> Handle(GetModule2Query request, CancellationToken cancellationToken) + { + var module2 = await module2Repository.GetAsync(request.Module2Id, cancellationToken); + + if (module2 is null) + { + return Result.Failure(Module2Errors.NotFound(request.Module2Id)); + } + + return new Module2Response(module2.Id); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2ById/Module2Response.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2ById/Module2Response.cs new file mode 100644 index 0000000..e49fa16 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2ById/Module2Response.cs @@ -0,0 +1,3 @@ +namespace App1.Modules.Module2s.Application.Module2s.GetModule2ById; + +public sealed record Module2Response(Guid Id); \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2s/GetModule2Query.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2s/GetModule2Query.cs new file mode 100644 index 0000000..2b5c686 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2s/GetModule2Query.cs @@ -0,0 +1,5 @@ +using App1.Common.Application.Messaging; + +namespace App1.Modules.Module2s.Application.Module2s.GetModule2s; + +public sealed record GetModule2SQuery : IQuery>; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2s/GetModule2QueryHandler.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2s/GetModule2QueryHandler.cs new file mode 100644 index 0000000..68836ee --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2s/GetModule2QueryHandler.cs @@ -0,0 +1,22 @@ +using App1.Common.Application.Messaging; +using App1.Common.Domain; +using App1.Modules.Module2s.Domain.Module2s; + +namespace App1.Modules.Module2s.Application.Module2s.GetModule2s; + +internal sealed class GetModule2QueryHandler(IModule2Repository module2Repository) + : IQueryHandler> +{ + public async Task>> Handle(GetModule2SQuery request, CancellationToken cancellationToken) + { + var dbModule2S = await module2Repository.GetAsync(cancellationToken); + + var module2S = new List(); + foreach (var module2 in dbModule2S) + { + module2S.Add(new Module2Response(module2.Id)); + } + + return module2S; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2s/Module2Response.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2s/Module2Response.cs new file mode 100644 index 0000000..2243719 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/GetModule2s/Module2Response.cs @@ -0,0 +1,3 @@ +namespace App1.Modules.Module2s.Application.Module2s.GetModule2s; + +public sealed record Module2Response(Guid Id); \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/Module2ProfileUpdatedDomainEventHandler.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/Module2ProfileUpdatedDomainEventHandler.cs new file mode 100644 index 0000000..1b28501 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/Module2ProfileUpdatedDomainEventHandler.cs @@ -0,0 +1,17 @@ +using App1.Common.Application.EventBus; +using App1.Common.Application.Messaging; +using App1.Modules.Module2s.Domain.Module2s; +using App1.Modules.Module2s.IntegrationEvents; + +namespace App1.Modules.Module2s.Application.Module2s.UpdateModule2; + +internal sealed class Module2UpdatedDomainEventHandler(IEventBus eventBus) + : DomainEventHandler +{ + public override async Task Handle(Module2UpdatedDomainEvent domainEvent, + CancellationToken cancellationToken = default) + { + await eventBus.PublishAsync( + new Module2UpdatedIntegrationEvent(domainEvent.Id, domainEvent.OccurredOnUtc, domainEvent.Module2Id), cancellationToken); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/UpdateModule2Comand.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/UpdateModule2Comand.cs new file mode 100644 index 0000000..1156de9 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/UpdateModule2Comand.cs @@ -0,0 +1,5 @@ +using App1.Common.Application.Messaging; + +namespace App1.Modules.Module2s.Application.Module2s.UpdateModule2; + +public sealed record UpdateModule2Command(Guid Module2Id) : ICommand; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/UpdateModule2CommandHandler.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/UpdateModule2CommandHandler.cs new file mode 100644 index 0000000..01236b6 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/UpdateModule2CommandHandler.cs @@ -0,0 +1,25 @@ +using App1.Common.Application.Messaging; +using App1.Common.Domain; +using App1.Modules.Module2s.Application.Abstractions.Data; +using App1.Modules.Module2s.Domain.Module2s; + +namespace App1.Modules.Module2s.Application.Module2s.UpdateModule2; + +internal sealed class UpdateModule2CommandHandler(IModule2Repository module2Repository, IUnitOfWork unitOfWork) + : ICommandHandler +{ + public async Task Handle(UpdateModule2Command request, CancellationToken cancellationToken) + { + var module2 = await module2Repository.GetAsync(request.Module2Id, cancellationToken); + + if (module2 is null) + { + return Result.Failure(Module2Errors.NotFound(request.Module2Id)); + } + + module2.Update(); + await unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/UpdateModule2CommandValidator.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/UpdateModule2CommandValidator.cs new file mode 100644 index 0000000..fc65c4d --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Application/Module2s/UpdateModule2/UpdateModule2CommandValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace App1.Modules.Module2s.Application.Module2s.UpdateModule2; + +internal sealed class UpdateModule2CommandValidator : AbstractValidator +{ + public UpdateModule2CommandValidator() + { + RuleFor(c => c.Module2Id).NotEmpty(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Abstractions/BaseTest.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Abstractions/BaseTest.cs new file mode 100644 index 0000000..28cb976 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Abstractions/BaseTest.cs @@ -0,0 +1,17 @@ +using System.Reflection; +using App1.Modules.Module2s.Application; +using App1.Modules.Module2s.Domain.Module2s; +using App1.Modules.Module2s.Infrastructure; + +namespace App1.Modules.Module2s.ArchitectureTests.Abstractions; + +public abstract class BaseTest +{ + protected static readonly Assembly ApplicationAssembly = typeof(AssemblyReference).Assembly; + + protected static readonly Assembly DomainAssembly = typeof(Module2).Assembly; + + protected static readonly Assembly InfrastructureAssembly = typeof(Module2SModule).Assembly; + + protected static readonly Assembly PresentationAssembly = typeof(Module2s.Presentation.AssemblyReference).Assembly; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Abstractions/TestResultExtensions.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Abstractions/TestResultExtensions.cs new file mode 100644 index 0000000..771cddc --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Abstractions/TestResultExtensions.cs @@ -0,0 +1,12 @@ +using FluentAssertions; +using NetArchTest.Rules; + +namespace App1.Modules.Module2s.ArchitectureTests.Abstractions; + +internal static class TestResultExtensions +{ + internal static void ShouldBeSuccessful(this TestResult testResult) + { + testResult.FailingTypes?.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/App1.Modules.Module2s.ArchitectureTests.csproj b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/App1.Modules.Module2s.ArchitectureTests.csproj new file mode 100644 index 0000000..53030f1 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/App1.Modules.Module2s.ArchitectureTests.csproj @@ -0,0 +1,33 @@ + + + + + $(NetVersion) + false + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Application/ApplicationTests.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Application/ApplicationTests.cs new file mode 100644 index 0000000..b0c26b7 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Application/ApplicationTests.cs @@ -0,0 +1,217 @@ +using App1.Common.Application.Messaging; +using App1.Modules.Module2s.ArchitectureTests.Abstractions; +using FluentValidation; +using NetArchTest.Rules; + +namespace App1.Modules.Module2s.ArchitectureTests.Application; + +public class ApplicationTests : BaseTest +{ + [Fact] + public void Command_Should_BeSealed() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(ICommand)) + .Or() + .ImplementInterface(typeof(ICommand<>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Command_ShouldHave_NameEndingWith_Command() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(ICommand)) + .Or() + .ImplementInterface(typeof(ICommand<>)) + .Should() + .HaveNameEndingWith("Command") + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void CommandHandler_Should_NotBePublic() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(ICommandHandler<>)) + .Or() + .ImplementInterface(typeof(ICommandHandler<,>)) + .Should() + .NotBePublic() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void CommandHandler_Should_BeSealed() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(ICommandHandler<>)) + .Or() + .ImplementInterface(typeof(ICommandHandler<,>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void CommandHandler_ShouldHave_NameEndingWith_CommandHandler() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(ICommandHandler<>)) + .Or() + .ImplementInterface(typeof(ICommandHandler<,>)) + .Should() + .HaveNameEndingWith("CommandHandler") + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Query_Should_BeSealed() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IQuery<>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Query_ShouldHave_NameEndingWith_Query() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IQuery<>)) + .Should() + .HaveNameEndingWith("Query") + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void QueryHandler_Should_NotBePublic() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IQueryHandler<,>)) + .Should() + .NotBePublic() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void QueryHandler_Should_BeSealed() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IQueryHandler<,>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void QueryHandler_ShouldHave_NameEndingWith_QueryHandler() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IQueryHandler<,>)) + .Should() + .HaveNameEndingWith("QueryHandler") + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Validator_Should_NotBePublic() + { + Types.InAssembly(ApplicationAssembly) + .That() + .Inherit(typeof(AbstractValidator<>)) + .Should() + .NotBePublic() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Validator_Should_BeSealed() + { + Types.InAssembly(ApplicationAssembly) + .That() + .Inherit(typeof(AbstractValidator<>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Validator_ShouldHave_NameEndingWith_Validator() + { + Types.InAssembly(ApplicationAssembly) + .That() + .Inherit(typeof(AbstractValidator<>)) + .Should() + .HaveNameEndingWith("Validator") + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void DomainEventHandler_Should_NotBePublic() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IDomainEventHandler<>)) + .Or() + .Inherit(typeof(DomainEventHandler<>)) + .Should() + .NotBePublic() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void DomainEventHandler_Should_BeSealed() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IDomainEventHandler<>)) + .Or() + .Inherit(typeof(DomainEventHandler<>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void DomainEventHandler_ShouldHave_NameEndingWith_DomainEventHandler() + { + Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IDomainEventHandler<>)) + .Or() + .Inherit(typeof(DomainEventHandler<>)) + .Should() + .HaveNameEndingWith("DomainEventHandler") + .GetResult() + .ShouldBeSuccessful(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Domain/DomainTests.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Domain/DomainTests.cs new file mode 100644 index 0000000..8151145 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Domain/DomainTests.cs @@ -0,0 +1,76 @@ +using System.Reflection; +using App1.Common.Domain; +using App1.Modules.Module2s.ArchitectureTests.Abstractions; +using FluentAssertions; +using NetArchTest.Rules; + +namespace App1.Modules.Module2s.ArchitectureTests.Domain; + +public class DomainTests : BaseTest +{ + [Fact] + public void DomainEvents_Should_BeSealed() + { + Types.InAssembly(DomainAssembly) + .That() + .ImplementInterface(typeof(IDomainEvent)) + .Or() + .Inherit(typeof(DomainEvent)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void DomainEvent_ShouldHave_DomainEventPostfix() + { + Types.InAssembly(DomainAssembly) + .That() + .ImplementInterface(typeof(IDomainEvent)) + .Or() + .Inherit(typeof(DomainEvent)) + .Should() + .HaveNameEndingWith("DomainEvent") + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void Entities_ShouldHave_PrivateParameterlessConstructor() + { + IEnumerable entityTypes = Types.InAssembly(DomainAssembly).That().Inherit(typeof(Entity)).GetTypes(); + + var failingTypes = new List(); + foreach (var entityType in entityTypes) + { + var constructors = entityType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance); + + if (!constructors.Any(c => c.IsPrivate && c.GetParameters().Length == 0)) + { + failingTypes.Add(entityType); + } + } + + failingTypes.Should().BeEmpty(); + } + + [Fact] + public void Entities_ShouldOnlyHave_PrivateConstructors() + { + IEnumerable entityTypes = Types.InAssembly(DomainAssembly).That().Inherit(typeof(Entity)).GetTypes(); + + var failingTypes = new List(); + foreach (var entityType in entityTypes) + { + var constructors = entityType.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + + if (constructors.Any()) + { + failingTypes.Add(entityType); + } + } + + failingTypes.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Layers/LayerTests.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Layers/LayerTests.cs new file mode 100644 index 0000000..4050507 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Layers/LayerTests.cs @@ -0,0 +1,57 @@ +using App1.Modules.Module2s.ArchitectureTests.Abstractions; +using NetArchTest.Rules; + +namespace App1.Modules.Module2s.ArchitectureTests.Layers; + +public class LayerTests : BaseTest +{ + [Fact] + public void DomainLayer_ShouldNotHaveDependencyOn_ApplicationLayer() + { + Types.InAssembly(DomainAssembly) + .Should() + .NotHaveDependencyOn(ApplicationAssembly.GetName().Name) + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void DomainLayer_ShouldNotHaveDependencyOn_InfrastructureLayer() + { + Types.InAssembly(DomainAssembly) + .Should() + .NotHaveDependencyOn(InfrastructureAssembly.GetName().Name) + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void ApplicationLayer_ShouldNotHaveDependencyOn_InfrastructureLayer() + { + Types.InAssembly(ApplicationAssembly) + .Should() + .NotHaveDependencyOn(InfrastructureAssembly.GetName().Name) + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void ApplicationLayer_ShouldNotHaveDependencyOn_PresentationLayer() + { + Types.InAssembly(ApplicationAssembly) + .Should() + .NotHaveDependencyOn(PresentationAssembly.GetName().Name) + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void PresentationLayer_ShouldNotHaveDependencyOn_InfrastructureLayer() + { + Types.InAssembly(PresentationAssembly) + .Should() + .NotHaveDependencyOn(InfrastructureAssembly.GetName().Name) + .GetResult() + .ShouldBeSuccessful(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Presentation/PresentationTests.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Presentation/PresentationTests.cs new file mode 100644 index 0000000..fe77248 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.ArchitectureTests/Presentation/PresentationTests.cs @@ -0,0 +1,50 @@ +using App1.Common.Application.EventBus; +using App1.Modules.Module2s.ArchitectureTests.Abstractions; +using NetArchTest.Rules; + +namespace App1.Modules.Module2s.ArchitectureTests.Presentation; + +public class PresentationTests : BaseTest +{ + [Fact] + public void IntegrationEventHandler_Should_NotBePublic() + { + Types.InAssembly(PresentationAssembly) + .That() + .ImplementInterface(typeof(IIntegrationEventHandler<>)) + .Or() + .Inherit(typeof(IntegrationEventHandler<>)) + .Should() + .NotBePublic() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void IntegrationEventHandler_Should_BeSealed() + { + Types.InAssembly(PresentationAssembly) + .That() + .ImplementInterface(typeof(IIntegrationEventHandler<>)) + .Or() + .Inherit(typeof(IntegrationEventHandler<>)) + .Should() + .BeSealed() + .GetResult() + .ShouldBeSuccessful(); + } + + [Fact] + public void IntegrationEventHandler_ShouldHave_NameEndingWith_DomainEventHandler() + { + Types.InAssembly(PresentationAssembly) + .That() + .ImplementInterface(typeof(IIntegrationEventHandler<>)) + .Or() + .Inherit(typeof(IntegrationEventHandler<>)) + .Should() + .HaveNameEndingWith("IntegrationEventHandler") + .GetResult() + .ShouldBeSuccessful(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/App1.Modules.Module2s.Domain.csproj b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/App1.Modules.Module2s.Domain.csproj new file mode 100644 index 0000000..f88f557 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/App1.Modules.Module2s.Domain.csproj @@ -0,0 +1,9 @@ + + + $(NetVersion) + + + + + + diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/IModule2Repository.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/IModule2Repository.cs new file mode 100644 index 0000000..9c2bc80 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/IModule2Repository.cs @@ -0,0 +1,10 @@ +namespace App1.Modules.Module2s.Domain.Module2s; + +public interface IModule2Repository +{ + Task GetAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetAsync(CancellationToken cancellationToken = default); + + void Insert(Module2 module2); + void Delete(Module2 module2); +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2.cs new file mode 100644 index 0000000..596361f --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2.cs @@ -0,0 +1,34 @@ +using App1.Common.Domain; + +namespace App1.Modules.Module2s.Domain.Module2s; + +public sealed class Module2 : Entity +{ + private Module2() + { + } + + public Guid Id { get; private init; } + + public static Module2 Create(Guid id) + { + var module2 = new Module2 + { + Id = id, + }; + + module2.Raise(new Module2CreatedDomainEvent(module2.Id)); + + return module2; + } + + public void Update() + { + Raise(new Module2UpdatedDomainEvent(Id)); + } + + public void Delete() + { + Raise(new Module2DeletedDomainEvent(Id)); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2CreatedDomainEvent.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2CreatedDomainEvent.cs new file mode 100644 index 0000000..b9881f9 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2CreatedDomainEvent.cs @@ -0,0 +1,8 @@ +using App1.Common.Domain; + +namespace App1.Modules.Module2s.Domain.Module2s; + +public sealed class Module2CreatedDomainEvent(Guid module2Id) : DomainEvent +{ + public Guid Module2Id { get; init; } = module2Id; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2DeletedDomainEvent.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2DeletedDomainEvent.cs new file mode 100644 index 0000000..44d9f7a --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2DeletedDomainEvent.cs @@ -0,0 +1,8 @@ +using App1.Common.Domain; + +namespace App1.Modules.Module2s.Domain.Module2s; + +public sealed class Module2DeletedDomainEvent(Guid module2Id) : DomainEvent +{ + public Guid Module2Id { get; init; } = module2Id; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2Errors.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2Errors.cs new file mode 100644 index 0000000..cf548e6 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2Errors.cs @@ -0,0 +1,16 @@ +using App1.Common.Domain; + +namespace App1.Modules.Module2s.Domain.Module2s; + +public static class Module2Errors +{ + public static Error NotFound(Guid module2Id) + { + return Error.NotFound("Module2s.NotFound", $"The Module2 with the identifier {module2Id} not found"); + } + + public static Error NotFound(string identityId) + { + return Error.NotFound("Module2s.NotFound", $"The Module2 with the IDP identifier {identityId} not found"); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2UpdatedDomainEvent.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2UpdatedDomainEvent.cs new file mode 100644 index 0000000..74670f2 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Domain/Module2s/Module2UpdatedDomainEvent.cs @@ -0,0 +1,8 @@ +using App1.Common.Domain; + +namespace App1.Modules.Module2s.Domain.Module2s; + +public sealed class Module2UpdatedDomainEvent(Guid module2Id) : DomainEvent +{ + public Guid Module2Id { get; init; } = module2Id; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/App1.Modules.Module2s.Infrastructure.csproj b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/App1.Modules.Module2s.Infrastructure.csproj new file mode 100644 index 0000000..5f930f7 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/App1.Modules.Module2s.Infrastructure.csproj @@ -0,0 +1,23 @@ + + + $(NetVersion) + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Migrations/20250110195653_Module2s.Designer.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Migrations/20250110195653_Module2s.Designer.cs new file mode 100644 index 0000000..d5237d0 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Migrations/20250110195653_Module2s.Designer.cs @@ -0,0 +1,90 @@ +// +using System; +using App1.Modules.Module2s.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace App1.Modules.Module2s.Infrastructure.Database.Migrations +{ + [DbContext(typeof(Module2sDbContext))] + [Migration("20250110195653_Module2s")] + partial class Module2s + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("App1.Module2s") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("App1.Common.Infrastructure.Inbox.InboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Error") + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOnUtc") + .HasColumnType("datetime2"); + + b.Property("ProcessedOnUtc") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("InboxMessages", "App1.Module2s"); + }); + + modelBuilder.Entity("App1.Common.Infrastructure.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Error") + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOnUtc") + .HasColumnType("datetime2"); + + b.Property("ProcessedOnUtc") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", "App1.Module2s"); + }); + + modelBuilder.Entity("App1.Modules.Module2s.Domain.Module2s.Module2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.ToTable("Module2S", "App1.Module2s"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Migrations/20250110195653_Module2s.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Migrations/20250110195653_Module2s.cs new file mode 100644 index 0000000..0746a08 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Migrations/20250110195653_Module2s.cs @@ -0,0 +1,78 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace App1.Modules.Module2s.Infrastructure.Database.Migrations +{ + /// + public partial class Module2s : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "App1.Module2s"); + + migrationBuilder.CreateTable( + name: "InboxMessages", + schema: "App1.Module2s", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Content = table.Column(type: "nvarchar(max)", nullable: false), + OccurredOnUtc = table.Column(type: "datetime2", nullable: false), + ProcessedOnUtc = table.Column(type: "datetime2", nullable: true), + Error = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_InboxMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Module2S", + schema: "App1.Module2s", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Module2S", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OutboxMessages", + schema: "App1.Module2s", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Content = table.Column(type: "nvarchar(max)", nullable: false), + OccurredOnUtc = table.Column(type: "datetime2", nullable: false), + ProcessedOnUtc = table.Column(type: "datetime2", nullable: true), + Error = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxMessages", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "InboxMessages", + schema: "App1.Module2s"); + + migrationBuilder.DropTable( + name: "Module2S", + schema: "App1.Module2s"); + + migrationBuilder.DropTable( + name: "OutboxMessages", + schema: "App1.Module2s"); + } + } +} diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Migrations/Module2sDbContextModelSnapshot.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Migrations/Module2sDbContextModelSnapshot.cs new file mode 100644 index 0000000..6e12f61 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Migrations/Module2sDbContextModelSnapshot.cs @@ -0,0 +1,87 @@ +// +using System; +using App1.Modules.Module2s.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace App1.Modules.Module2s.Infrastructure.Database.Migrations +{ + [DbContext(typeof(Module2sDbContext))] + partial class Module2sDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("App1.Module2s") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("App1.Common.Infrastructure.Inbox.InboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Error") + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOnUtc") + .HasColumnType("datetime2"); + + b.Property("ProcessedOnUtc") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("InboxMessages", "App1.Module2s"); + }); + + modelBuilder.Entity("App1.Common.Infrastructure.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Error") + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOnUtc") + .HasColumnType("datetime2"); + + b.Property("ProcessedOnUtc") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", "App1.Module2s"); + }); + + modelBuilder.Entity("App1.Modules.Module2s.Domain.Module2s.Module2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.ToTable("Module2S", "App1.Module2s"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Module2sDbContext.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Module2sDbContext.cs new file mode 100644 index 0000000..fc898b9 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Module2sDbContext.cs @@ -0,0 +1,41 @@ +using App1.Common.Infrastructure.Inbox; +using App1.Common.Infrastructure.Outbox; +using App1.Modules.Module2s.Application.Abstractions.Data; +using App1.Modules.Module2s.Domain.Module2s; +using App1.Modules.Module2s.Infrastructure.Module2s; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace App1.Modules.Module2s.Infrastructure.Database; + +public sealed class Module2sDbContext(DbContextOptions options) : DbContext(options), IUnitOfWork +{ + internal DbSet Module2S => Set(); + internal DbSet OutboxMessages => Set(); + + internal DbSet InboxMessages => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema(Schemas.Module2S); + + modelBuilder.ApplyConfiguration(new OutboxMessageConfiguration()); + + modelBuilder.ApplyConfiguration(new InboxMessageConfiguration()); + + modelBuilder.ApplyConfiguration(new Module2Configuration()); + } +} + +#if DEBUG +// dotnet ef migrations add "Module2s" -o "Database\Migrations" +public class Module2SDbContextFactory : IDesignTimeDbContextFactory +{ + public Module2sDbContext CreateDbContext(string[] args) + { + return new Module2sDbContext(new DbContextOptionsBuilder() + .UseSqlServer("Host=localhost;Database=App1;Module2name=sa;Password=password") + .Options); + } +} +#endif \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Schemas.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Schemas.cs new file mode 100644 index 0000000..33bac17 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Database/Schemas.cs @@ -0,0 +1,6 @@ +namespace App1.Modules.Module2s.Infrastructure.Database; + +internal static class Schemas +{ + internal const string Module2S = "App1.Module2s"; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/ConfigureProcessInboxJob.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/ConfigureProcessInboxJob.cs new file mode 100644 index 0000000..0953c71 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/ConfigureProcessInboxJob.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Options; +using Quartz; + +namespace App1.Modules.Module2s.Infrastructure.Inbox; + +internal sealed class ConfigureProcessInboxJob(IOptions inboxOptions) : IConfigureOptions +{ + public void Configure(QuartzOptions options) + { + var jobName = typeof(ProcessInboxJob).FullName!; + + options.AddJob(configure => configure.WithIdentity(jobName)) + .AddTrigger(configure => + { + configure.ForJob(jobName) + .WithSimpleSchedule(schedule => + { + schedule + .WithIntervalInSeconds(inboxOptions.Value.IntervalInSeconds) + .RepeatForever(); + }); + }); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/InboxOptions.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/InboxOptions.cs new file mode 100644 index 0000000..ebf9142 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/InboxOptions.cs @@ -0,0 +1,8 @@ +namespace App1.Modules.Module2s.Infrastructure.Inbox; + +internal sealed class InboxOptions +{ + public int IntervalInSeconds { get; init; } + + public int BatchSize { get; init; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/IntegrationEventConsumer.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/IntegrationEventConsumer.cs new file mode 100644 index 0000000..f069f20 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/IntegrationEventConsumer.cs @@ -0,0 +1,27 @@ +using System.Text.Json; +using App1.Common.Application.EventBus; +using App1.Common.Infrastructure.Inbox; +using App1.Common.Infrastructure.Serialization; +using App1.Modules.Module2s.Infrastructure.Database; +using MassTransit; + +namespace App1.Modules.Module2s.Infrastructure.Inbox; + +internal sealed class IntegrationEventConsumer(Module2sDbContext dbContext) + : IConsumer where TIntegrationEvent : IntegrationEvent +{ + public async Task Consume(ConsumeContext context) + { + var integrationEvent = context.Message; + + var inboxMessage = new InboxMessage + { + Id = integrationEvent.Id, + Content = JsonSerializer.Serialize(integrationEvent, SerializerSettings.Instance), + OccurredOnUtc = integrationEvent.OccurredOnUtc + }; + + dbContext.InboxMessages.Add(inboxMessage); + await dbContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/ProcessInboxJob.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/ProcessInboxJob.cs new file mode 100644 index 0000000..f784bad --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Inbox/ProcessInboxJob.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using App1.Common.Application.EventBus; +using App1.Common.Infrastructure.Inbox; +using App1.Common.Infrastructure.Serialization; +using App1.Modules.Module2s.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Quartz; + +namespace App1.Modules.Module2s.Infrastructure.Inbox; + +[DisallowConcurrentExecution] +internal sealed class ProcessInboxJob( + Module2sDbContext dbContext, + IServiceScopeFactory serviceScopeFactory, + TimeProvider timeProvider, + IOptions inboxOptions, + ILogger logger) : IJob +{ + private const string ModuleName = "Travellers"; + + public async Task Execute(IJobExecutionContext context) + { + logger.LogInformation("{Module} - Beginning to process inbox messages", ModuleName); + + var inboxMessages = await GetInboxMessagesAsync(); + foreach (var inboxMessage in inboxMessages) + { + Exception? exception = null; + + try + { + var integrationEvent = JsonSerializer.Deserialize(inboxMessage.Content, SerializerSettings.Instance)!; + + using var scope = serviceScopeFactory.CreateScope(); + var handler = scope.ServiceProvider.GetRequiredKeyedService(integrationEvent.GetType().Name); + await handler.Handle(integrationEvent, context.CancellationToken); + } + catch (Exception caughtException) + { + logger.LogError(caughtException, + "{Module} - Exception while processing inbox message {MessageId}", + ModuleName, + inboxMessage.Id); + + exception = caughtException; + } + + await UpdateInboxMessageAsync(inboxMessage, exception); + } + + logger.LogInformation("{Module} - Completed processing inbox messages", ModuleName); + } + + private async Task> GetInboxMessagesAsync() + { + var inboxMessages = await dbContext.InboxMessages + .Where(x => x.ProcessedOnUtc == null) + .OrderBy(x => x.OccurredOnUtc) + .Take(inboxOptions.Value.BatchSize) + .ToListAsync(); + + return inboxMessages; + } + + private async Task UpdateInboxMessageAsync(InboxMessage inboxMessage, Exception? exception) + { + var message = exception?.Message ?? null; + await dbContext.InboxMessages.Where(x => x.Id == inboxMessage.Id) + .ExecuteUpdateAsync( + m => m.SetProperty(p => p.ProcessedOnUtc, timeProvider.GetUtcNow().UtcDateTime) + .SetProperty(p => p.Error, message)); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2s/AzureAdB2CGraphClientConfiguration.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2s/AzureAdB2CGraphClientConfiguration.cs new file mode 100644 index 0000000..7189c35 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2s/AzureAdB2CGraphClientConfiguration.cs @@ -0,0 +1,11 @@ +namespace App1.Modules.Module2s.Infrastructure.Module2s; + +public class AzureAdB2CGraphClientConfiguration +{ + public const string ConfigurationName = "AzureAdB2CGraphClient"; + public string? ClientId { get; set; } + + public string? ClientSecret { get; set; } + public string? TenantId { get; set; } + public string? DefaultApplicationId { get; set; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2s/Module2Configuration.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2s/Module2Configuration.cs new file mode 100644 index 0000000..e3497c3 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2s/Module2Configuration.cs @@ -0,0 +1,13 @@ +using App1.Modules.Module2s.Domain.Module2s; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace App1.Modules.Module2s.Infrastructure.Module2s; + +internal sealed class Module2Configuration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(u => u.Id); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2s/Module2Repository.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2s/Module2Repository.cs new file mode 100644 index 0000000..dc0e660 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2s/Module2Repository.cs @@ -0,0 +1,33 @@ +using App1.Modules.Module2s.Domain.Module2s; +using App1.Modules.Module2s.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace App1.Modules.Module2s.Infrastructure.Module2s; + +internal sealed class Module2Repository(Module2sDbContext context) : IModule2Repository +{ + public async Task GetAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Module2S.FirstOrDefaultAsync(u => u.Id == id, cancellationToken); + } + + public async Task> GetAsync(CancellationToken cancellationToken = default) + { + return await context.Module2S.ToListAsync(cancellationToken); + } + + public void Insert(Module2 module2) + { + context.Module2S.Add(module2); + } + + public void Delete(Module2 module2) + { + context.Module2S.Remove(module2); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken) + { + await context.Module2S.Where(x => x.Id == id).ExecuteDeleteAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2sModule.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2sModule.cs new file mode 100644 index 0000000..8b44449 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Module2sModule.cs @@ -0,0 +1,84 @@ +using App1.Common.Application.Messaging; +using App1.Common.Infrastructure; +using App1.Common.Presentation.Endpoints; +using App1.Modules.Module2s.Application.Abstractions.Data; +using App1.Modules.Module2s.Domain.Module2s; +using App1.Modules.Module2s.Infrastructure.Database; +using App1.Modules.Module2s.Infrastructure.Inbox; +using App1.Modules.Module2s.Infrastructure.Module2s; +using App1.Modules.Module2s.Infrastructure.Outbox; +using App1.Modules.Module2s.Presentation; +using MassTransit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Schemas = App1.Modules.Module2s.Infrastructure.Database.Schemas; + +namespace App1.Modules.Module2s.Infrastructure; + +public static class Module2sModule +{ + public static IHostApplicationBuilder AddModule2sModule(this IHostApplicationBuilder builder) + { + builder.Services.AddDomainEventHandlers(); + + builder.AddInfrastructure(); + + builder.Services.AddEndpoints(AssemblyReference.Assembly); + + return builder; + } + + public static void ConfigureConsumers(IRegistrationConfigurator registrationConfigurator) + { + registrationConfigurator.AddConsumers(typeof(Module2sModule).Assembly); + } + + private static void AddInfrastructure(this IHostApplicationBuilder builder) + { + builder.AddDatabase(Schemas.Module2S, optionsBuilder => + { + optionsBuilder.UseAsyncSeeding(async (dbContext, _, cancellationToken) => + { + var module2Guid = Guid.CreateVersion7(); + var module2 = dbContext.Find(module2Guid); + if (module2 != null) + { + return; + } + + dbContext.Add(Module2.Create(module2Guid)); + await dbContext.SaveChangesAsync(cancellationToken); + }); + }); + + builder.Services.AddScoped(); + + builder.Services.AddScoped(sp => sp.GetRequiredService()); + + builder.Services.Configure(builder.Configuration.GetSection("Module2s:Outbox")); + + builder.Services.Configure(builder.Configuration.GetSection("Module2s:Inbox")); + + builder.Services.ConfigureOptions(); + + + builder.Services.ConfigureOptions(); + } + + private static void AddDomainEventHandlers(this IServiceCollection services) + { + var domainEventHandlers = Application.AssemblyReference.Assembly.GetTypes() + .Where(t => t.IsAssignableTo(typeof(IDomainEventHandler))); + + foreach (var domainEventHandler in domainEventHandlers) + { + services.AddKeyedScoped(typeof(IDomainEventHandler), GetKey(domainEventHandler), domainEventHandler); + } + + static string GetKey(Type type) + { + const int handlerNameSuffixLength = 7; + return type.Name.AsSpan(..^handlerNameSuffixLength).ToString(); + } + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Outbox/ConfigureProcessOutboxJob.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Outbox/ConfigureProcessOutboxJob.cs new file mode 100644 index 0000000..c05442e --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Outbox/ConfigureProcessOutboxJob.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Options; +using Quartz; + +namespace App1.Modules.Module2s.Infrastructure.Outbox; + +internal sealed class ConfigureProcessOutboxJob(IOptions outboxOptions) : IConfigureOptions +{ + public void Configure(QuartzOptions options) + { + var jobName = typeof(ProcessOutboxJob).FullName!; + + options.AddJob(configure => configure.WithIdentity(jobName)) + .AddTrigger(configure => + { + configure.ForJob(jobName) + .WithSimpleSchedule(schedule => + { + schedule + .WithIntervalInSeconds(outboxOptions.Value.IntervalInSeconds) + .RepeatForever(); + }); + }); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Outbox/OutboxOptions.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Outbox/OutboxOptions.cs new file mode 100644 index 0000000..c8966a6 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Outbox/OutboxOptions.cs @@ -0,0 +1,8 @@ +namespace App1.Modules.Module2s.Infrastructure.Outbox; + +internal sealed class OutboxOptions +{ + public int IntervalInSeconds { get; init; } + + public int BatchSize { get; init; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Outbox/ProcessOutboxJob.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Outbox/ProcessOutboxJob.cs new file mode 100644 index 0000000..0404b6f --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Infrastructure/Outbox/ProcessOutboxJob.cs @@ -0,0 +1,82 @@ +using System.Text.Json; +using App1.Common.Application.Messaging; +using App1.Common.Domain; +using App1.Common.Infrastructure.Outbox; +using App1.Common.Infrastructure.Serialization; +using App1.Modules.Module2s.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Quartz; + +namespace App1.Modules.Module2s.Infrastructure.Outbox; + +[DisallowConcurrentExecution] +internal sealed class ProcessOutboxJob( + Module2sDbContext dbContext, + IServiceScopeFactory serviceScopeFactory, + TimeProvider dateTimeProvider, + IOptions outboxOptions, + ILogger logger) : IJob +{ + private const string ModuleName = "Module2s"; + + public async Task Execute(IJobExecutionContext context) + { + logger.LogInformation("{Module} - Beginning to process outbox messages", ModuleName); + + var outboxMessages = await GetOutboxMessagesAsync(); + + foreach (var outboxMessage in outboxMessages) + { + Exception? exception = null; + + try + { + var domainEvent = JsonSerializer.Deserialize(outboxMessage.Content, SerializerSettings.Instance)!; + + using var scope = serviceScopeFactory.CreateScope(); + var handler = scope.ServiceProvider.GetKeyedService(domainEvent.GetType().Name); + if (handler is not null) + { + await handler.Handle(domainEvent, context.CancellationToken); + } + } + catch (Exception caughtException) + { + logger.LogError(caughtException, + "{Module} - Exception while processing outbox message {MessageId}", + ModuleName, + outboxMessage.Id); + + exception = caughtException; + } + + await UpdateOutboxMessageAsync(outboxMessage, exception); + } + + logger.LogInformation("{Module} - Completed processing outbox messages", ModuleName); + } + + private async Task> GetOutboxMessagesAsync() + { + var outboxMessages = await dbContext.OutboxMessages + .Where(x => x.ProcessedOnUtc == null) + .OrderBy(x => x.OccurredOnUtc) + .Take(outboxOptions.Value.BatchSize) + .ToListAsync(); + + return outboxMessages; + } + + private async Task UpdateOutboxMessageAsync(OutboxMessage outboxMessage, Exception? exception) + { + var error = exception?.ToString(); + await dbContext.OutboxMessages.Where(x => x.Id == outboxMessage.Id) + .ExecuteUpdateAsync(m => m.SetProperty(p => p.Error, error) + .SetProperty( + p => p.ProcessedOnUtc, + dateTimeProvider.GetUtcNow().UtcDateTime)); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/App1.Modules.Module2s.IntegrationEvents.csproj b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/App1.Modules.Module2s.IntegrationEvents.csproj new file mode 100644 index 0000000..d3c1e40 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/App1.Modules.Module2s.IntegrationEvents.csproj @@ -0,0 +1,9 @@ + + + $(NetVersion) + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/Module2CreatedIntegrationEvent.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/Module2CreatedIntegrationEvent.cs new file mode 100644 index 0000000..ad87f23 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/Module2CreatedIntegrationEvent.cs @@ -0,0 +1,11 @@ +using App1.Common.Application.EventBus; + +namespace App1.Modules.Module2s.IntegrationEvents; + +public sealed class Module2CreatedIntegrationEvent( + Guid id, + DateTime occurredOnUtc, + Guid module2Id) : IntegrationEvent(id, occurredOnUtc) +{ + public Guid Module2Id { get; init; } = module2Id; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/Module2DeletedIntegrationEvent.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/Module2DeletedIntegrationEvent.cs new file mode 100644 index 0000000..20a27e2 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/Module2DeletedIntegrationEvent.cs @@ -0,0 +1,9 @@ +using App1.Common.Application.EventBus; + +namespace App1.Modules.Module2s.IntegrationEvents; + +public sealed class Module2DeletedIntegrationEvent(Guid id, DateTime occurredOnUtc, Guid module2Id) + : IntegrationEvent(id, occurredOnUtc) +{ + public Guid Module2Id { get; init; } = module2Id; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/Module2UpdatedIntegrationEvent.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/Module2UpdatedIntegrationEvent.cs new file mode 100644 index 0000000..cfd284b --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationEvents/Module2UpdatedIntegrationEvent.cs @@ -0,0 +1,11 @@ +using App1.Common.Application.EventBus; + +namespace App1.Modules.Module2s.IntegrationEvents; + +public sealed class Module2UpdatedIntegrationEvent( + Guid id, + DateTime occurredOnUtc, + Guid module2Id) : IntegrationEvent(id, occurredOnUtc) +{ + public Guid Module2Id { get; init; } = module2Id; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/BaseIntegrationTest.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/BaseIntegrationTest.cs new file mode 100644 index 0000000..15ff438 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/BaseIntegrationTest.cs @@ -0,0 +1,62 @@ +using App1.Modules.Module2s.IntegrationTests.Abstractions.Fixtures; +using AutoFixture; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace App1.Modules.Module2s.IntegrationTests.Abstractions; + +[Collection(nameof(IntegrationTestCollection))] +public abstract class BaseIntegrationTest : IDisposable +{ + protected static readonly Fixture Faker = new(); + protected readonly Module2DbContext DbContext; + protected readonly HttpClient HttpClient; + private readonly IServiceScope scope; + protected readonly ISender Sender; + private bool disposedValue; + + protected BaseIntegrationTest(IntegrationTestWebAppFactory factory) + { + scope = factory.Services.CreateScope(); + HttpClient = factory.CreateClient(); + Sender = scope.ServiceProvider.GetRequiredService(); + DbContext = scope.ServiceProvider.GetRequiredService(); + } + + protected void SetAuth(bool enableAuth, bool failPermission = false) + { + var testAuthHandlerOptions = scope.ServiceProvider.GetRequiredService>(); + testAuthHandlerOptions.CurrentValue.FakeSuccessfulAuthentication = enableAuth; + testAuthHandlerOptions.CurrentValue.FailPermission = failPermission; + } + + protected async Task CleanDatabaseAsync() + { + await DbContext.Database.ExecuteSqlRawAsync(""" + DELETE FROM Module2s.inbox_message_consumers; + DELETE FROM Module2s.inbox_messages; + DELETE FROM Module2s.outbox_message_consumers; + DELETE FROM Module2s.outbox_messages; + DELETE FROM Module2s.Module2s; + """); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + scope.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/Fixtures/MockSchemeProvider.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/Fixtures/MockSchemeProvider.cs new file mode 100644 index 0000000..5e595e0 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/Fixtures/MockSchemeProvider.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; + +namespace App1.Modules.Module2s.IntegrationTests.Abstractions.Fixtures; + +public class MockSchemeProvider : AuthenticationSchemeProvider +{ + private readonly IOptionsMonitor fakeOptions; + + public MockSchemeProvider(IOptions options, IOptionsMonitor fakeOptions) : base(options) + { + this.fakeOptions = fakeOptions; + } + + protected MockSchemeProvider( + IOptions options, + IOptionsMonitor fakeOptions, + IDictionary schemes) : base(options, schemes) + { + this.fakeOptions = fakeOptions; + } + + public override Task GetSchemeAsync(string name) + { + if (fakeOptions.CurrentValue.FakeSuccessfulAuthentication) + { + var scheme = new AuthenticationScheme( + TestAuthHandler.AuthenticationScheme, + TestAuthHandler.AuthenticationScheme, + typeof(TestAuthHandler)); + return Task.FromResult(scheme); + } + + return base.GetSchemeAsync(name); + } +} diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/Fixtures/TestAuthHandler.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/Fixtures/TestAuthHandler.cs new file mode 100644 index 0000000..297fc98 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/Fixtures/TestAuthHandler.cs @@ -0,0 +1,44 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace App1.Modules.Module2s.IntegrationTests.Abstractions.Fixtures; + +public class TestAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : AuthenticationHandler(options, logger, encoder) +{ + public const string NoPermissionEmail = "NoPermissionModule2@email.com"; + public const string Email = "Module2@email.com"; + private readonly IOptionsMonitor options = options; + + public static string AuthenticationScheme { get; } = JwtBearerDefaults.AuthenticationScheme; + + protected override Task HandleAuthenticateAsync() + { + List claims = + [ + new(ClaimConstants.ObjectId, "19d3b2c7-8714-4851-ac73-95aeecfba3a6"), + new(ClaimConstants.NameIdentifierId, "123123"), + new(JwtRegisteredClaimNames.Iat, GetEpochTimeFromSeconds().ToString()), + options.CurrentValue.FailPermission + ? new(ClaimConstants.PreferredModule2Name, NoPermissionEmail) + : new(ClaimConstants.PreferredModule2Name, Email) + ]; + + var identity = new ClaimsIdentity(claims, AuthenticationScheme); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, AuthenticationScheme); + var result = AuthenticateResult.Success(ticket); + return Task.FromResult(result); + } + + public static long GetEpochTimeFromSeconds() + { + return (long)(DateTime.UtcNow - DateTime.UnixEpoch).TotalSeconds; + } +} diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/Fixtures/TestAuthHandlerOptions.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/Fixtures/TestAuthHandlerOptions.cs new file mode 100644 index 0000000..8146bef --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/Fixtures/TestAuthHandlerOptions.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authentication; + +namespace App1.Modules.Module2s.IntegrationTests.Abstractions.Fixtures; + +public class TestAuthHandlerOptions : AuthenticationSchemeOptions +{ + public bool FakeSuccessfulAuthentication { get; set; } = true; + + public bool FailPermission { get; set; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/IntegrationTestCollection.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/IntegrationTestCollection.cs new file mode 100644 index 0000000..d35812e --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/IntegrationTestCollection.cs @@ -0,0 +1,4 @@ +namespace App1.Modules.Module2s.IntegrationTests.Abstractions; + +[CollectionDefinition(nameof(IntegrationTestCollection))] +public sealed class IntegrationTestCollection : ICollectionFixture; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/IntegrationTestWebAppFactory.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/IntegrationTestWebAppFactory.cs new file mode 100644 index 0000000..04c3174 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Abstractions/IntegrationTestWebAppFactory.cs @@ -0,0 +1,47 @@ +using App1.ApiService; +using App1.Modules.Module2s.IntegrationTests.Abstractions.Fixtures; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Testcontainers.MsSql; +using Testcontainers.Redis; + +namespace App1.Modules.Module2s.IntegrationTests.Abstractions; + +public class IntegrationTestWebAppFactory : WebApplicationFactory, IAsyncLifetime +{ + private readonly MsSqlContainer dbContainer = new MsSqlBuilder() + .WithImage("mcr.microsoft.com/mssql/server:2022-latest") + .Build(); + + private readonly RedisContainer redisContainer = new RedisBuilder().WithImage("redis:latest").Build(); + + public async Task InitializeAsync() + { + await dbContainer.StartAsync(); + await redisContainer.StartAsync(); + } + + public new async Task DisposeAsync() + { + await dbContainer.StopAsync(); + await redisContainer.StopAsync(); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + Environment.SetEnvironmentVariable("ConnectionStrings:database", dbContainer.GetConnectionString()); + Environment.SetEnvironmentVariable("ConnectionStrings:cache", redisContainer.GetConnectionString()); + Environment.SetEnvironmentVariable("ConnectionStrings:ai-llama3-2", "Endpoint=https://localhost;Model=llama3.2"); + + builder.ConfigureServices(services => + { + services.AddTransient(); + services.Configure(options => + { + options.FakeSuccessfulAuthentication = true; + }); + }); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/App1.Modules.Module2s.IntegrationTests.csproj b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/App1.Modules.Module2s.IntegrationTests.csproj new file mode 100644 index 0000000..e20457f --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/App1.Modules.Module2s.IntegrationTests.csproj @@ -0,0 +1,34 @@ + + + $(NetVersion) + false + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/GetModule2ProfileTests.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/GetModule2ProfileTests.cs new file mode 100644 index 0000000..bd7353d --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/GetModule2ProfileTests.cs @@ -0,0 +1,59 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using App1.Modules.Module2s.IntegrationTests.Abstractions; +using FluentAssertions; + +namespace App1.Modules.Module2s.IntegrationTests.Module2s; + +public class GetModule2ProfileTests(IntegrationTestWebAppFactory factory) : BaseIntegrationTest(factory) +{ + [Fact] + public async Task Should_ReturnUnauthorized_WhenAccessTokenNotProvided() + { + SetAuth(false); + + // Act + HttpResponseMessage response = await HttpClient.GetAsync("Module2s/profile"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Should_ReturnOk_WhenModule2Exists() + { + SetAuth(true); + + // Arrange + string accessToken = await RegisterModule2AndGetAccessTokenAsync(); + HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + JwtBearerDefaults.AuthenticationScheme, + accessToken); + + // Act + HttpResponseMessage response = await HttpClient.GetAsync("Module2s/profile"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + Module2Response? module2 = await response.Content.ReadFromJsonAsync(); + module2.Should().NotBeNull(); + } + + private async Task RegisterModule2AndGetAccessTokenAsync() + { + var request = new RegisterModule2.Request + { + ClientId = "3c3bdb4b-327b-49a9-a13e-0b565526b8a1", + ObjectId = Guid.Parse("19d3b2c7-8714-4851-ac73-95aeecfba3a6") + }; + + var response = await HttpClient.PostAsJsonAsync("Module2s/register", request); + response.EnsureSuccessStatusCode(); + + string accessToken = string.Empty; + + return accessToken; + } +} diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/GetModule2Tests.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/GetModule2Tests.cs new file mode 100644 index 0000000..60ced3d --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/GetModule2Tests.cs @@ -0,0 +1,34 @@ +using App1.Modules.Module2s.IntegrationTests.Abstractions; + +namespace App1.Modules.Module2s.IntegrationTests.Module2s; + +public class GetModule2Tests(IntegrationTestWebAppFactory factory) : BaseIntegrationTest(factory) +{ + [Fact] + public async Task Should_ReturnError_WhenModule2DoesNotExist() + { + // Arrange + var module2Id = Guid.NewGuid(); + + // Act + var module2Result = await Sender.Send(new GetModule2Query(module2Id)); + + // Assert + module2Result.Error.Should().Be(Module2Errors.NotFound(module2Id)); + } + + [Fact] + public async Task Should_ReturnModule2_WhenModule2Exists() + { + // Arrange + var result = await Sender.Send(new RegisterModule2Command(Guid.Parse("19d3b2c7-8714-4851-ac73-95aeecfba3a6"))); + var module2Id = result.Value.Id; + + // Act + var module2Result = await Sender.Send(new GetModule2Query(module2Id)); + + // Assert + module2Result.IsSuccess.Should().BeTrue(); + module2Result.Value.Should().NotBeNull(); + } +} diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/RegisterModule2Tests.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/RegisterModule2Tests.cs new file mode 100644 index 0000000..5e0ad05 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/RegisterModule2Tests.cs @@ -0,0 +1,72 @@ +using System.Net; +using System.Net.Http.Json; +using App1.Modules.Module2s.IntegrationTests.Abstractions; +using FluentAssertions; + +namespace App1.Modules.Module2s.IntegrationTests.Module2s; + +public class RegisterModule2Tests(IntegrationTestWebAppFactory factory) : BaseIntegrationTest(factory) +{ + public static readonly TheoryData InvalidRequests = new() + { + {"", "19d3b2c7-8714-4851-ac73-95aeecfba3a6"} + }; + + + [Theory] + [MemberData(nameof(InvalidRequests))] + public async Task Should_ReturnUnauthorized_WhenRequestIsNotValid( + string clientId, + string objectId) + { + SetAuth(true); + + // Arrange + var request = new RegisterModule2.Request + { + ClientId = clientId, + ObjectId = Guid.Parse(objectId) + }; + + // Act + HttpResponseMessage response = await HttpClient.PostAsJsonAsync("Module2s/register", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + + [Fact] + public async Task Should_ReturnBadRequest_WhenRequestIsNull() + { + SetAuth(true); + + // Arrange + RegisterModule2.Request? request = null; + + // Act + HttpResponseMessage response = await HttpClient.PostAsJsonAsync("Module2s/register", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task Should_ReturnOk_WhenRequestIsValid() + { + SetAuth(true); + + // Arrange + var request = new RegisterModule2.Request + { + ClientId = "3c3bdb4b-327b-49a9-a13e-0b565526b8a1", + ObjectId = Guid.Parse("19d3b2c7-8714-4851-ac73-95aeecfba3a6") + }; + + // Act + HttpResponseMessage response = await HttpClient.PostAsJsonAsync("Module2s/register", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + } +} diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/UpdateModule2Tests.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/UpdateModule2Tests.cs new file mode 100644 index 0000000..eb8c896 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.IntegrationTests/Module2s/UpdateModule2Tests.cs @@ -0,0 +1,54 @@ +using App1.Modules.Module2s.IntegrationTests.Abstractions; +using AutoFixture; + +namespace App1.Modules.Module2s.IntegrationTests.Module2s; + +public class UpdateModule2Tests(IntegrationTestWebAppFactory factory) : BaseIntegrationTest(factory) +{ + public static readonly TheoryData InvalidCommands = + [ + new UpdateModule2Command(Guid.Empty, Faker.Create()) + ]; + + [Theory] + [MemberData(nameof(InvalidCommands))] + public async Task Should_ReturnError_WhenCommandIsNotValid(UpdateModule2Command command) + { + // Act + Result result = await Sender.Send(command); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Type.Should().Be(ErrorType.Validation); + } + + [Fact] + public async Task Should_ReturnError_WhenModule2DoesNotExist() + { + // Arrange + var module2Id = Guid.NewGuid(); + + // Act + Result updateResult = await Sender.Send( + new UpdateModule2Command(module2Id, Faker.Create())); + + // Assert + updateResult.Error.Should().Be(Module2Errors.NotFound(module2Id)); + } + + [Fact] + public async Task Should_ReturnSuccess_WhenModule2Exists() + { + // Arrange + var result = await Sender.Send(new RegisterModule2Command(Guid.Parse("19d3b2c7-8714-4851-ac73-95aeecfba3a6"))); + + Guid module2Id = result.Value.Id; + + // Act + Result updateResult = await Sender.Send( + new UpdateModule2Command(module2Id, Faker.Create())); + + // Assert + updateResult.IsSuccess.Should().BeTrue(); + } +} diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/App1.Modules.Module2s.Presentation.csproj b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/App1.Modules.Module2s.Presentation.csproj new file mode 100644 index 0000000..276d687 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/App1.Modules.Module2s.Presentation.csproj @@ -0,0 +1,15 @@ + + + $(NetVersion) + + + + + + + + + + + + diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/AssemblyReference.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/AssemblyReference.cs new file mode 100644 index 0000000..93bde9f --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/AssemblyReference.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace App1.Modules.Module2s.Presentation; + +public static class AssemblyReference +{ + public static readonly Assembly Assembly = typeof(AssemblyReference).Assembly; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/CreateModule2.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/CreateModule2.cs new file mode 100644 index 0000000..03d5676 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/CreateModule2.cs @@ -0,0 +1,42 @@ +using App1.Common.Presentation.Endpoints; +using App1.Modules.Module2s.Application.Module2s.CreateModule2; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace App1.Modules.Module2s.Presentation.Module2s; + +internal sealed class CreateModule2 : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("module2s", async (Request? request, ISender sender) => + { + if (request is null) + { + return Results.BadRequest( + new ResponseContent( "There was a problem with your request.")); + } + + var result = await sender.Send(new CreateModule2Command(request.ObjectId)); + if (result.IsSuccess) + { + return Results.Ok(new ResponseContent()); + } + + return Results.BadRequest(new ResponseContent(result.Error.Description)); + }) + .WithTags(Tags.Module2s); + } + + internal sealed class Request + { + public required Guid ObjectId { get; init; } + } + + internal class ResponseContent(string? userMessage = null) + { + public string? UserMessage { get; set; } = userMessage; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/DeleteModule2.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/DeleteModule2.cs new file mode 100644 index 0000000..bfe0173 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/DeleteModule2.cs @@ -0,0 +1,27 @@ +using System.Security.Claims; +using App1.Common.Infrastructure.Authentication; +using App1.Common.Infrastructure.Authorization; +using App1.Common.Presentation.Endpoints; +using App1.Common.Presentation.Results; +using App1.Modules.Module2s.Application.Module2s.DeleteModule2; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace App1.Modules.Module2s.Presentation.Module2s; + +internal sealed class DeleteModule2 : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapDelete("Module2s/{id:guid}", async (Guid id, ISender sender) => + { + var result = await sender.Send(new DeleteModule2Command(id)); + + return result.Match(Results.NoContent, ApiResults.Problem); + }) + .RequireAuthorization() + .WithTags(Tags.Module2s); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/GetModule2.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/GetModule2.cs new file mode 100644 index 0000000..1c8b771 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/GetModule2.cs @@ -0,0 +1,25 @@ +using App1.Common.Infrastructure.Authorization; +using App1.Common.Presentation.Endpoints; +using App1.Common.Presentation.Results; +using App1.Modules.Module2s.Application.Module2s.GetModule2s; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace App1.Modules.Module2s.Presentation.Module2s; + +internal sealed class GetModule2S : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("Module2s", async (ISender sender) => + { + var result = await sender.Send(new GetModule2SQuery()); + + return result.Match(Results.Ok, ApiResults.Problem); + }) + .RequireAuthorization(PolicyConstants.AdministratorPolicy) + .WithTags(Tags.Module2s); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/GetModule2ById.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/GetModule2ById.cs new file mode 100644 index 0000000..5283666 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/GetModule2ById.cs @@ -0,0 +1,24 @@ +using App1.Common.Presentation.Endpoints; +using App1.Common.Presentation.Results; +using App1.Modules.Module2s.Application.Module2s.GetModule2ById; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace App1.Modules.Module2s.Presentation.Module2s; + +internal sealed class GetModule2ById : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("Module2s/{id:guid}", async (Guid id, ISender sender) => + { + var result = await sender.Send(new GetModule2Query(id)); + + return result.Match(Results.Ok, ApiResults.Problem); + }) + .RequireAuthorization() + .WithTags(Tags.Module2s); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/UpdateModule2Profile.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/UpdateModule2Profile.cs new file mode 100644 index 0000000..955e64e --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Module2s/UpdateModule2Profile.cs @@ -0,0 +1,31 @@ +using System.Security.Claims; +using App1.Common.Infrastructure.Authentication; +using App1.Common.Presentation.Endpoints; +using App1.Common.Presentation.Results; +using App1.Modules.Module2s.Application.Module2s.UpdateModule2; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace App1.Modules.Module2s.Presentation.Module2s; + +internal sealed class UpdateModule2Profile : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPut("Module2s/{id:guid}", async (Guid id, Request request, ISender sender) => + { + var result = await sender.Send(new UpdateModule2Command(id)); + + return result.Match(Results.NoContent, ApiResults.Problem); + }) + .RequireAuthorization() + .WithTags(Tags.Module2s); + } + + internal sealed class Request + { + public bool Prop1 { get; set; } + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Tags.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Tags.cs new file mode 100644 index 0000000..09b433c --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.Presentation/Tags.cs @@ -0,0 +1,6 @@ +namespace App1.Modules.Module2s.Presentation; + +internal static class Tags +{ + internal const string Module2s = "Module2s"; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.UnitTests/Abstractions/BaseTest.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.UnitTests/Abstractions/BaseTest.cs new file mode 100644 index 0000000..c16940b --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.UnitTests/Abstractions/BaseTest.cs @@ -0,0 +1,21 @@ +using App1.Common.Domain; +using AutoFixture; + +namespace App1.Modules.Module2s.UnitTests.Abstractions; + +public abstract class BaseTest +{ + protected static readonly Fixture Faker = new(); + + public static T AssertDomainEventWasPublished(Entity entity) where T : IDomainEvent + { + var domainEvent = entity.DomainEvents.OfType().SingleOrDefault(); + + if (domainEvent is null) + { + throw new Exception($"{typeof(T).Name} was not published"); + } + + return domainEvent; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.UnitTests/App1.Modules.Module2s.UnitTests.csproj b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.UnitTests/App1.Modules.Module2s.UnitTests.csproj new file mode 100644 index 0000000..c22981d --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.UnitTests/App1.Modules.Module2s.UnitTests.csproj @@ -0,0 +1,31 @@ + + + $(NetVersion) + false + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.UnitTests/Module2s/Module2Tests.cs b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.UnitTests/Module2s/Module2Tests.cs new file mode 100644 index 0000000..9370e21 --- /dev/null +++ b/templates/ModularMonolith/src/Modules/Module2s/App1.Modules.Module2s.UnitTests/Module2s/Module2Tests.cs @@ -0,0 +1,60 @@ +using App1.Modules.Module2s.Domain.Module2s; +using App1.Modules.Module2s.UnitTests.Abstractions; +using FluentAssertions; + +namespace App1.Modules.Module2s.UnitTests.Module2s; + +public class Module2Tests : BaseTest +{ + [Fact] + public void Create_ShouldReturnModule2() + { + // Act + var module2 = Module2.Create(Guid.NewGuid()); + + // Assert + module2.Should().NotBeNull(); + } + + [Fact] + public void Create_ShouldRaiseDomainEvent_WhenModule2Created() + { + // Act + var module2 = Module2.Create(Guid.NewGuid()); + + // Assert + var domainEvent = AssertDomainEventWasPublished(module2); + + domainEvent.Module2Id.Should().Be(module2.Id); + } + + [Fact] + public void Update_ShouldRaiseDomainEvent_WhenModule2Updated() + { + // Arrange + var module2 = Module2.Create(Guid.NewGuid()); + + // Act + module2.Update(); + + // Assert + var domainEvent = AssertDomainEventWasPublished(module2); + + domainEvent.Module2Id.Should().Be(module2.Id); + } + + [Fact] + public void Update_ShouldNotRaiseDomainEvent_WhenModule2NotUpdated() + { + // Arrange + var module2 = Module2.Create(Guid.NewGuid()); + + module2.ClearDomainEvents(); + + // Act + module2.Update(); + + // Assert + module2.DomainEvents.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/App1.ApiService.csproj b/templates/ModularMonolith/src/Web/App1.ApiService/App1.ApiService.csproj new file mode 100644 index 0000000..a1f39ad --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/App1.ApiService.csproj @@ -0,0 +1,21 @@ + + + + $(NetVersion) + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/Extensions/ConfigurationExtensions.cs b/templates/ModularMonolith/src/Web/App1.ApiService/Extensions/ConfigurationExtensions.cs new file mode 100644 index 0000000..f504b84 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,13 @@ +namespace App1.ApiService.Extensions; + +internal static class ConfigurationExtensions +{ + internal static void AddModuleConfiguration(this IConfigurationBuilder configurationBuilder, string[] modules) + { + foreach (var module in modules) + { + configurationBuilder.AddJsonFile($"modules.{module}.json", false, true); + configurationBuilder.AddJsonFile($"modules.{module}.Development.json", true, true); + } + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/Extensions/MigrationExtensions.cs b/templates/ModularMonolith/src/Web/App1.ApiService/Extensions/MigrationExtensions.cs new file mode 100644 index 0000000..46a9f11 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/Extensions/MigrationExtensions.cs @@ -0,0 +1,22 @@ +using App1.Modules.Module1s.Infrastructure.Database; +using App1.Modules.Module2s.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace App1.ApiService.Extensions; + +public static class MigrationExtensions +{ + public static async Task ApplyMigrations(this IApplicationBuilder app) + { + using var scope = app.ApplicationServices.CreateScope(); + + await ApplyMigration(scope); + await ApplyMigration(scope); + } + + private static async Task ApplyMigration(IServiceScope scope) where TDbContext : DbContext + { + await using var context = scope.ServiceProvider.GetRequiredService(); + await context.Database.MigrateAsync(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/Infrastructure/BearerSecuritySchemeTransformer.cs b/templates/ModularMonolith/src/Web/App1.ApiService/Infrastructure/BearerSecuritySchemeTransformer.cs new file mode 100644 index 0000000..38432d3 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/Infrastructure/BearerSecuritySchemeTransformer.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; + +namespace App1.ApiService.Infrastructure; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi.Models; + +internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) + : IOpenApiDocumentTransformer +{ + public async Task TransformAsync(OpenApiDocument document, + OpenApiDocumentTransformerContext context, + CancellationToken cancellationToken) + { + var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync(); + if (authenticationSchemes.Any(authScheme => authScheme.Name == JwtBearerDefaults.AuthenticationScheme)) + { + var requirements = new Dictionary + { + [JwtBearerDefaults.AuthenticationScheme] = new() + { + Type = SecuritySchemeType.Http, + Scheme = JwtBearerDefaults.AuthenticationScheme, + In = ParameterLocation.Header, + BearerFormat = "Json Web Token" + } + }; + document.Components ??= new OpenApiComponents(); + document.Components.SecuritySchemes = requirements; + } + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/Middleware/GlobalExceptionHandler.cs b/templates/ModularMonolith/src/Web/App1.ApiService/Middleware/GlobalExceptionHandler.cs new file mode 100644 index 0000000..de06ed7 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/Middleware/GlobalExceptionHandler.cs @@ -0,0 +1,27 @@ +namespace App1.ApiService.Middleware; + +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; + +internal sealed class GlobalExceptionHandler(ILogger logger) : IExceptionHandler +{ + public async ValueTask TryHandleAsync(HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken) + { + logger.LogError(exception, "Unhandled exception occurred"); + + var problemDetails = new ProblemDetails + { + Status = StatusCodes.Status500InternalServerError, + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1", + Title = "Server failure" + }; + + httpContext.Response.StatusCode = problemDetails.Status.Value; + + await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken); + + return true; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/Middleware/LogContextTraceLoggingMiddleware.cs b/templates/ModularMonolith/src/Web/App1.ApiService/Middleware/LogContextTraceLoggingMiddleware.cs new file mode 100644 index 0000000..89b650c --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/Middleware/LogContextTraceLoggingMiddleware.cs @@ -0,0 +1,16 @@ +namespace App1.ApiService.Middleware; + +using System.Diagnostics; + +internal sealed class LogContextTraceLoggingMiddleware(RequestDelegate next) +{ + public Task Invoke(HttpContext context, ILogger logger) + { + var traceId = Activity.Current?.TraceId.ToString(); + + using (logger.BeginScope("TraceId {traceId}", traceId)) + { + return next.Invoke(context); + } + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/Middleware/MiddlewareExtensions.cs b/templates/ModularMonolith/src/Web/App1.ApiService/Middleware/MiddlewareExtensions.cs new file mode 100644 index 0000000..c048502 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/Middleware/MiddlewareExtensions.cs @@ -0,0 +1,11 @@ +namespace App1.ApiService.Middleware; + +internal static class MiddlewareExtensions +{ + internal static IApplicationBuilder UseLogContext(this IApplicationBuilder app) + { + app.UseMiddleware(); + + return app; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/Program.cs b/templates/ModularMonolith/src/Web/App1.ApiService/Program.cs new file mode 100644 index 0000000..b3538c4 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/Program.cs @@ -0,0 +1,65 @@ +using System.Reflection; +using App1.ApiService.Extensions; +using App1.ApiService.Infrastructure; +using App1.Common.Application; +using App1.Common.Infrastructure; +using App1.Common.Presentation.Endpoints; +using App1.Modules.Module1s.Infrastructure; +using App1.Modules.Module2s.Infrastructure; +using App1.ServiceDefaults; +using Scalar.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add service defaults & Aspire components. +builder.AddServiceDefaults(); + +builder.Services.AddProblemDetails(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddOpenApi(options => +{ + options.AddDocumentTransformer(); +}); + +Assembly[] moduleApplicationAssemblies = +[ + App1.Modules.Module1s.Application.AssemblyReference.Assembly, + App1.Modules.Module2s.Application.AssemblyReference.Assembly +]; + +builder.Services.AddApplication(moduleApplicationAssemblies); + +builder.AddInfrastructure([ + App1.Modules.Module2s.Infrastructure.Module2sModule.ConfigureConsumers +]); + +builder.Configuration.AddModuleConfiguration(["module1s", "module2s"]); + +builder.AddModule1sModule(); +builder.AddModule2sModule(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +app.UseExceptionHandler(); + +if (app.Environment.IsDevelopment()) +{ + await app.ApplyMigrations(); + app.MapOpenApi(); + app.MapScalarApiReference(); +} + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapEndpoints(); +app.MapDefaultEndpoints(); + +await app.RunAsync(); + +public partial class Program +{ + protected Program() { } +}; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/Properties/launchSettings.json b/templates/ModularMonolith/src/Web/App1.ApiService/Properties/launchSettings.json new file mode 100644 index 0000000..a41f246 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "scalar/v1", + "applicationUrl": "https://localhost:5002", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/appsettings.json b/templates/ModularMonolith/src/Web/App1.ApiService/appsettings.json new file mode 100644 index 0000000..fce6bdf --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/appsettings.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "AzureAdB2C": { + "Instance": "https://b2clogin.com", + "TenantId": "", + "ClientId": "1", + "CallbackPath": "/signin-oidc", + "Domain": "onmicrosoft.com", + "SignedOutCallbackPath": "/signout", + "SignUpSignInPolicyId": "B2C_1_App1_SIGNUP_SIGNIN", + "ClientSecret": "#{AAD_B2C_CLIENT_SECRET}#", + "AllowWebApiToBeAuthorizedByACL": true, + "TokenValidationParameters": { + "ValidateAuthority": false + } + }, + "OTEL_EXPORTER_OTLP_ENDPOINT": "" +} diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/modules.module1s.json b/templates/ModularMonolith/src/Web/App1.ApiService/modules.module1s.json new file mode 100644 index 0000000..100f89f --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/modules.module1s.json @@ -0,0 +1,8 @@ +{ + "Module1s": { + "Outbox": { + "IntervalInSeconds": 60, + "BatchSize": 50 + } + } +} diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/modules.module2s.json b/templates/ModularMonolith/src/Web/App1.ApiService/modules.module2s.json new file mode 100644 index 0000000..dba43d0 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/modules.module2s.json @@ -0,0 +1,12 @@ +{ + "Module2s": { + "Outbox": { + "IntervalInSeconds": 60, + "BatchSize": 50 + }, + "Inbox": { + "IntervalInSeconds": 60, + "BatchSize": 50 + } + } +} diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/users.http b/templates/ModularMonolith/src/Web/App1.ApiService/users.http new file mode 100644 index 0000000..cabd131 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/users.http @@ -0,0 +1,12 @@ +# For more info on HTTP files go to https://aka.ms/vs/httpfile +@host=https://localhost:5002 + +POST {{host}}/Module1s/register +Content-Type: application/json +Authorization: Basic JUpfRnFGYWNoUUwtc3FGbmRMSjJuX25EWV9ZLSQjdXg6cDdqTTUrI1lzeUBzRT1UWFhfcjR5K3BzYWFhTCpra2c= + +{ + "client_id":"123", + "objectId":"19d3b2c7-8714-4851-ac73-95aeecfba3a6" +} + diff --git a/templates/ModularMonolith/src/Web/App1.ApiService/web.config b/templates/ModularMonolith/src/Web/App1.ApiService/web.config new file mode 100644 index 0000000..b792b29 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ApiService/web.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.AppHost.Tests/App1.AppHost.Tests.csproj b/templates/ModularMonolith/src/Web/App1.AppHost.Tests/App1.AppHost.Tests.csproj new file mode 100644 index 0000000..9b366ef --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.AppHost.Tests/App1.AppHost.Tests.csproj @@ -0,0 +1,36 @@ + + + + $(NetVersion) + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/templates/ModularMonolith/src/Web/App1.AppHost.Tests/WebTests.cs b/templates/ModularMonolith/src/Web/App1.AppHost.Tests/WebTests.cs new file mode 100644 index 0000000..fbeb751 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.AppHost.Tests/WebTests.cs @@ -0,0 +1,31 @@ +using Projects; + +namespace App1.AppHost.Tests; + +public class WebTests +{ + [Fact] + public async Task GetWebResourceRootReturnsOkStatusCode() + { + // Arrange + var appHost = await DistributedApplicationTestingBuilder.CreateAsync(); + appHost.Services.ConfigureHttpClientDefaults(clientBuilder => + { + clientBuilder.AddStandardResilienceHandler(); + }); + // To output logs to the xUnit.net ITestOutputHelper, consider adding a package from https://www.nuget.org/packages?q=xunit+logging + + await using var app = await appHost.BuildAsync(); + var resourceNotificationService = app.Services.GetRequiredService(); + await app.StartAsync(); + + // Act + var httpClient = app.CreateHttpClient("webfrontend"); + await resourceNotificationService.WaitForResourceAsync("webfrontend", KnownResourceStates.Running) + .WaitAsync(TimeSpan.FromSeconds(30)); + var response = await httpClient.GetAsync("/"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.AppHost/.gitignore b/templates/ModularMonolith/src/Web/App1.AppHost/.gitignore new file mode 100644 index 0000000..8e84380 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.AppHost/.gitignore @@ -0,0 +1 @@ +.azure diff --git a/templates/ModularMonolith/src/Web/App1.AppHost/App1.AppHost.csproj b/templates/ModularMonolith/src/Web/App1.AppHost/App1.AppHost.csproj new file mode 100644 index 0000000..35c9419 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.AppHost/App1.AppHost.csproj @@ -0,0 +1,24 @@ + + + + + + Exe + $(NetVersion) + true + 647960e2-f059-4696-9a0e-1008352f1dfc + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.AppHost/Program.cs b/templates/ModularMonolith/src/Web/App1.AppHost/Program.cs new file mode 100644 index 0000000..07e3db2 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.AppHost/Program.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Hosting; +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); + +var cache = builder.AddRedis("cache") + .WithRedisInsight(s => s.WithLifetime(ContainerLifetime.Persistent)) + .WithLifetime(ContainerLifetime.Persistent) + .WithDataVolume("App1-cache"); + +var sqlServer = builder.AddSqlServer("sqlserver") + .WithLifetime(ContainerLifetime.Persistent) + .WithDataVolume("App1-database"); + +var database = sqlServer.AddDatabase("database", "App1"); + +var apiService = builder.AddProject("apiservice") + .WithReference(database) + .WaitFor(database) + .WithReference(cache) + .WaitFor(cache); + +builder.AddProject("webfrontend") + .WithExternalHttpEndpoints() + .WithReference(apiService) + .WaitFor(apiService) + .WithReference(cache) + .WaitFor(cache); + +if (!builder.Environment.IsDevelopment()) +{ + var serviceBus = builder.AddRabbitMQ("servicebus") + .WithManagementPlugin() + .WithLifetime(ContainerLifetime.Persistent) + .WithDataVolume("App1-servicebus"); + + apiService.WithReference(serviceBus); +} + +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.AppHost/Properties/launchSettings.json b/templates/ModularMonolith/src/Web/App1.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..155d04f --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.AppHost/Properties/launchSettings.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17097", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21038", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22254" + } + } + } +} diff --git a/templates/ModularMonolith/src/Web/App1.AppHost/appsettings.json b/templates/ModularMonolith/src/Web/App1.AppHost/appsettings.json new file mode 100644 index 0000000..2d7f748 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/templates/ModularMonolith/src/Web/App1.AppHost/azure.yaml b/templates/ModularMonolith/src/Web/App1.AppHost/azure.yaml new file mode 100644 index 0000000..ba3656e --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.AppHost/azure.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubModule1content.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: App1.AppHost +services: + app: + language: dotnet + project: .\App1.AppHost.csproj + host: containerapp diff --git a/templates/ModularMonolith/src/Web/App1.AppHost/next-steps.md b/templates/ModularMonolith/src/Web/App1.AppHost/next-steps.md new file mode 100644 index 0000000..4a55854 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.AppHost/next-steps.md @@ -0,0 +1,74 @@ +# Next Steps after `azd init` + +## Table of Contents + +1. [Next Steps](#next-steps) +2. [What was added](#what-was-added) +3. [Billing](#billing) +4. [Troubleshooting](#troubleshooting) + +## Next Steps + +### Provision infrastructure and deploy application code + +Run `azd up` to provision your infrastructure and deploy to Azure in one step (or run `azd provision` then `azd deploy` to accomplish the tasks separately). Visit the service endpoints listed to see your application up-and-running! + +To troubleshoot any issues, see [troubleshooting](#troubleshooting). + +### Configure CI/CD pipeline + +1. Create a workflow pipeline file locally. The following starters are available: + - [Deploy with GitHub Actions](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.github/workflows/azure-dev.yml) + - [Deploy with Azure Pipelines](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.azdo/pipelines/azure-dev.yml) +2. Run `azd pipeline config -e ` to configure the deployment pipeline to connect securely to Azure. An environment name is specified here to configure the pipeline with a different environment for isolation purposes. Run `azd env list` and `azd env set` to reselect the default environment after this step. + +## What was added + +### Infrastructure configuration + +To describe the infrastructure and application, an `azure.yaml` was added with the following directory structure: + +```yaml +- azure.yaml # azd project configuration +``` + +This file contains a single service, which references your project's App Host. When needed, `azd` generates the required infrastructure as code in memory and uses it. + +If you would like to see or modify the infrastructure that `azd` uses, run `azd infra synth` to persist it to disk. + +If you do this, some additional directories will be created: + +```yaml +- infra/ # Infrastructure as Code (bicep) files + - main.bicep # main deployment module + - resources.bicep # resources shared across your application's services +``` + +In addition, for each project resource referenced by your app host, a `containerApp.tmpl.yaml` file will be created in a directory named `manifests` next the project file. This file contains the infrastructure as code for running the project on Azure Container Apps. + +*Note*: Once you have synthesized your infrastructure to disk, changes made to your App Host will not be reflected in the infrastructure. You can re-generate the infrastructure by running `azd infra synth` again. It will prompt you before overwriting files. You can pass `--force` to force `azd infra synth` to overwrite the files without prompting. + +*Note*: `azd infra synth` is currently an alpha feature and must be explicitly enabled by running `azd config set alpha.infraSynth on`. You only need to do this once. + +## Billing + +Visit the *Cost Management + Billing* page in Azure Portal to track current spend. For more information about how you're billed, and how you can monitor the costs incurred in your Azure subscriptions, visit [billing overview](https://learn.microsoft.com/azure/developer/intro/azure-developer-billing). + +## Troubleshooting + +Q: I visited the service endpoint listed, and I'm seeing a blank page, a generic welcome page, or an error page. + +A: Your service may have failed to start, or it may be missing some configuration settings. To investigate further: + +1. Run `azd show`. Click on the link under "View in Azure Portal" to open the resource group in Azure Portal. +2. Navigate to the specific Container App service that is failing to deploy. +3. Click on the failing revision under "Revisions with Issues". +4. Review "Status details" for more information about the type of failure. +5. Observe the log outputs from Console log stream and System log stream to identify any errors. +6. If logs are written to disk, use *Console* in the navigation to connect to a shell within the running container. + +For more troubleshooting information, visit [Container Apps troubleshooting](https://learn.microsoft.com/azure/container-apps/troubleshooting). + +### Additional information + +For additional information about setting up your `azd` project, visit our official [docs](https://learn.microsoft.com/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-convert). diff --git a/templates/ModularMonolith/src/Web/App1.ServiceDefaults/App1.ServiceDefaults.csproj b/templates/ModularMonolith/src/Web/App1.ServiceDefaults/App1.ServiceDefaults.csproj new file mode 100644 index 0000000..19e3043 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ServiceDefaults/App1.ServiceDefaults.csproj @@ -0,0 +1,18 @@ + + + + $(NetVersion) + true + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.ServiceDefaults/Extensions.cs b/templates/ModularMonolith/src/Web/App1.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..58f2363 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.ServiceDefaults/Extensions.cs @@ -0,0 +1,103 @@ +namespace App1.ServiceDefaults; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation().AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/App1.Web.csproj b/templates/ModularMonolith/src/Web/App1.Web/App1.Web.csproj new file mode 100644 index 0000000..14081cb --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/App1.Web.csproj @@ -0,0 +1,23 @@ + + + + $(NetVersion) + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml b/templates/ModularMonolith/src/Web/App1.Web/Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml new file mode 100644 index 0000000..f6fcc2c --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml @@ -0,0 +1,10 @@ +@page +@model App1.Web.Areas.MicrosoftIdentity.Pages.Account.SignedOutModel +@{ + ViewData["Title"] = "Signed out"; +} + +

@ViewData["Title"]

+

+ You have successfully signed out. +

\ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml.cs b/templates/ModularMonolith/src/Web/App1.Web/Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml.cs new file mode 100644 index 0000000..6503167 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml.cs @@ -0,0 +1,14 @@ +namespace App1.Web.Areas.MicrosoftIdentity.Pages.Account; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +[AllowAnonymous] +public class SignedOutModel : PageModel +{ + public IActionResult OnGet() + { + return LocalRedirect("~/"); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/App.razor b/templates/ModularMonolith/src/Web/App1.Web/Components/App.razor new file mode 100644 index 0000000..d8ecc59 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/App.razor @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/App1AuthBaseComponent.cs b/templates/ModularMonolith/src/Web/App1.Web/Components/App1AuthBaseComponent.cs new file mode 100644 index 0000000..aa052d7 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/App1AuthBaseComponent.cs @@ -0,0 +1,33 @@ +using App1.Web.Services.User; + +namespace App1.Web.Components; + +using Microsoft.AspNetCore.Components; + +public abstract class App1AuthBaseComponent : App1BaseComponent +{ + protected UserInfo CurrentUser { get; private set; } = null!; + + [Inject] + public required ICurrentUserService CurrentUserService { get; set; } + + [Inject] + public required NavigationManager NavigationManager { get; set; } + + protected override async Task OnInitializedAsync() + { + CurrentUser = CurrentUserService.GetCurrentUser(); + if (string.IsNullOrEmpty(CurrentUser.ProviderId)) + { + Logout(); + } + + await I18NText.SetCurrentLanguageAsync("ua"); + await base.OnInitializedAsync(); + } + + public void Logout() + { + NavigationManager.NavigateTo("MicrosoftIdentity/Account/SignOut", true); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/App1BaseComponent.cs b/templates/ModularMonolith/src/Web/App1.Web/Components/App1BaseComponent.cs new file mode 100644 index 0000000..44a8405 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/App1BaseComponent.cs @@ -0,0 +1,20 @@ +namespace App1.Web.Components; + +using I18nText; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Toolbelt.Blazor.I18nText; + +public abstract class App1BaseComponent : MudComponentBase +{ + protected Translation Translation = new(); + + [Inject] + public required I18nText I18NText { get; set; } + + protected override async Task OnInitializedAsync() + { + Translation = await I18NText.GetTextTableAsync(this); + await base.OnInitializedAsync(); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/App1BaseLayout.cs b/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/App1BaseLayout.cs new file mode 100644 index 0000000..26cb7c1 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/App1BaseLayout.cs @@ -0,0 +1,5 @@ +namespace App1.Web.Components.Layout; + +using Microsoft.AspNetCore.Components; + +public abstract class App1BaseLayout : LayoutComponentBase; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/MainLayout.razor b/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..03eeebd --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/MainLayout.razor @@ -0,0 +1,73 @@ +@inherits App1BaseLayout + + + + + + + + + + + + @Constants.ProductName + + + + + + + @Constants.ProductName + + + @Constants.ProductName + + + + + + + + + + + + + + + + + + + + + @Body + + + Error has occurred + @exception.Message + Go To The Homepage + + + + + + + + + + +
+ An unhandled error has occurred. + Reload + 🗙 +
\ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/MainLayout.razor.cs b/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/MainLayout.razor.cs new file mode 100644 index 0000000..eb7c0f0 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/MainLayout.razor.cs @@ -0,0 +1,50 @@ +namespace App1.Web.Components.Layout; + +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +public partial class MainLayout : App1BaseLayout +{ + private bool drawerOpen = true; + private ErrorBoundary? errorBoundary; + private bool isDarkMode = true; + private MudThemeProvider? mudThemeProvider; + private bool rightToLeft; + + [Inject] + public required NavigationManager NavigationManager { get; set; } + + private void DrawerToggle() + { + drawerOpen = !drawerOpen; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (firstRender && mudThemeProvider is not null) + { + var isDarkSystemPreference = await mudThemeProvider.GetSystemPreference(); + await OnSystemPreferenceChanged(isDarkSystemPreference); + await mudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged); + } + } + + private Task OnSystemPreferenceChanged(bool isDarkSystemPreference) + { + if (isDarkMode != isDarkSystemPreference) + { + isDarkMode = isDarkSystemPreference; + StateHasChanged(); + } + + return Task.CompletedTask; + } + + private void ResetError() + { + errorBoundary?.Recover(); + NavigationManager.NavigateTo("/"); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/NavMenu.razor b/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/NavMenu.razor new file mode 100644 index 0000000..3013d4c --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/NavMenu.razor @@ -0,0 +1,25 @@ +@using System.Reflection +@inherits App1BaseComponent + + + + + Translation.MenuModule1s + + + + + Admin + + + +
+
+ Vladislav Antonyuk +
+
@frameworkName
+
+ © @DateTime.UtcNow.Year +
+
@Translation.Version: @Assembly.GetEntryAssembly()?.GetName().Version
+
\ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/NavMenu.razor.cs b/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/NavMenu.razor.cs new file mode 100644 index 0000000..3e5bd31 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/NavMenu.razor.cs @@ -0,0 +1,15 @@ +namespace App1.Web.Components.Layout; + +using System.Reflection; +using System.Runtime.Versioning; + +public partial class NavMenu : App1BaseComponent +{ + private string? frameworkName = string.Empty; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + frameworkName = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/NavMenu.razor.css b/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/NavMenu.razor.css new file mode 100644 index 0000000..24ad714 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Layout/NavMenu.razor.css @@ -0,0 +1,7 @@ +.author { + bottom: 0; + left: 0; + position: absolute; + right: 0; + text-align: center; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/LoadingControl.razor b/templates/ModularMonolith/src/Web/App1.Web/Components/LoadingControl.razor new file mode 100644 index 0000000..8b42c18 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/LoadingControl.razor @@ -0,0 +1,6 @@ +@inherits App1BaseComponent + + + + Translation.Loading + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/LoadingControl.razor.cs b/templates/ModularMonolith/src/Web/App1.Web/Components/LoadingControl.razor.cs new file mode 100644 index 0000000..3e4547c --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/LoadingControl.razor.cs @@ -0,0 +1,5 @@ +namespace App1.Web.Components; + +public partial class LoadingControl : App1BaseComponent +{ +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/LoginControl.razor b/templates/ModularMonolith/src/Web/App1.Web/Components/LoginControl.razor new file mode 100644 index 0000000..09a7e67 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/LoginControl.razor @@ -0,0 +1,17 @@ +@inherits App1AuthBaseComponent + + + + + + + @CurrentUser.Name + + + Translation.Profile + + + Translation.Logout + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/LoginControl.razor.cs b/templates/ModularMonolith/src/Web/App1.Web/Components/LoginControl.razor.cs new file mode 100644 index 0000000..f83244b --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/LoginControl.razor.cs @@ -0,0 +1,3 @@ +namespace App1.Web.Components; + +public partial class LoginControl : App1AuthBaseComponent; \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/About.razor b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/About.razor new file mode 100644 index 0000000..f971186 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/About.razor @@ -0,0 +1,6 @@ +@inherits App1BaseComponent +@page "/about" + +@Constants.ProductName + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/About.razor.cs b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/About.razor.cs new file mode 100644 index 0000000..13845a1 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/About.razor.cs @@ -0,0 +1,5 @@ +namespace App1.Web.Components.Pages; + +public partial class About : App1BaseComponent +{ +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Error.razor b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Error.razor new file mode 100644 index 0000000..f37bc2b --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Error.razor @@ -0,0 +1,25 @@ +@page "/Error" +@inherits App1BaseComponent + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end Module1s. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

\ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Error.razor.cs b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Error.razor.cs new file mode 100644 index 0000000..d763dd4 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Error.razor.cs @@ -0,0 +1,18 @@ +namespace App1.Web.Components.Pages; + +using System.Diagnostics; +using Microsoft.AspNetCore.Components; + +public partial class Error : App1BaseComponent +{ + [CascadingParameter] + public HttpContext? HttpContext { get; set; } + + public string? RequestId { get; set; } + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() + { + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Home.razor b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Home.razor new file mode 100644 index 0000000..4ade067 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Home.razor @@ -0,0 +1,13 @@ +@inherits App1BaseComponent +@page "/" + +@Constants.ProductName + + + + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Home.razor.cs b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Home.razor.cs new file mode 100644 index 0000000..6568dec --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Home.razor.cs @@ -0,0 +1,5 @@ +namespace App1.Web.Components.Pages; + +public partial class Home : App1BaseComponent +{ +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Privacy.razor b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Privacy.razor new file mode 100644 index 0000000..5d4e85e --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Privacy.razor @@ -0,0 +1,279 @@ +@inherits App1BaseComponent + +@page "/privacy" + +@Constants.ProductName + +Privacy Policy + + Vladislav Antonyuk built the @Constants.ProductName app as + an Open Source app. This SERVICE is provided by + Vladislav Antonyuk at no cost and is intended for use as + is. + + + This page is used to inform visitors regarding my + policies with the collection, use, and disclosure of Personal + Information if anyone decided to use my Service. + + + If you choose to use my Service, then you agree to + the collection and use of information in relation to this + policy. The Personal Information that I collect is + used for providing and improving the Service. I will not use or share your information with + anyone except as described in this Privacy Policy. + + + The terms used in this Privacy Policy have the same meanings + as in our Terms and Conditions, which are accessible at + @Constants.ProductName unless otherwise defined in this Privacy Policy. + + + +Information Collection and Use + + For a better experience, while using our Service, I + may require you to provide us with certain personally + identifiable information. The information that + I request will be retained on your device and is not collected by me in any way. + + + + The app does use third-party services that may collect + information used to identify you. + + + Link to the privacy policy of third-party service providers used + by the app + + + + Google Play Services + + + + + +Log Data + + I want to inform you that whenever you + use my Service, in a case of an error in the app + I collect data and information (through third-party + products) on your phone called Log Data. This Log Data may + include information such as your device Internet Protocol + (“IP”) address, device name, operating system version, the + configuration of the app when utilizing my Service, + the time and date of your use of the Service, and other + statistics. + + + +Cookies + + Cookies are files with a small amount of data that are + commonly used as anonymous unique identifiers. These are sent + to your browser from the websites that you visit and are + stored on your device's internal memory. + + + This Service does not use these “cookies” explicitly. However, + the app may use third-party code and libraries that use + “cookies” to collect information and improve their services. + You have the option to either accept or refuse these cookies + and know when a cookie is being sent to your device. If you + choose to refuse our cookies, you may not be able to use some + portions of this Service. + + + +Service Providers + + I may employ third-party companies and + individuals due to the following reasons: + + + To facilitate our Service; + To provide the Service on our behalf; + To perform Service-related services; or + To assist us in analyzing how our Service is used. + + + I want to inform Module1s of this Service + that these third parties have access to their Personal + Information. The reason is to perform the tasks assigned to + them on our behalf. However, they are obligated not to + disclose or use the information for any other purpose. + + + +Security + + I value your trust in providing us your + Personal Information, thus we are striving to use commercially + acceptable means of protecting it. But remember that no method + of transmission over the internet, or method of electronic + storage is 100% secure and reliable, and I cannot + guarantee its absolute security. + + + +Links to Other Sites + + This Service may contain links to other sites. If you click on + a third-party link, you will be directed to that site. Note + that these external sites are not operated by me. + Therefore, I strongly advise you to review the + Privacy Policy of these websites. I have + no control over and assume no responsibility for the content, + privacy policies, or practices of any third-party sites or + services. + +Children’s Privacy + + + These Services do not address anyone under the age of 13. + I do not knowingly collect personally + identifiable information from children under 13 years of age. In the case + I discover that a child under 13 has provided + me with personal information, I immediately + delete this from our servers. If you are a parent or guardian + and you are aware that your child has provided us with + personal information, please contact me so that + I will be able to do the necessary actions. + + + + +Changes to This Privacy Policy + + I may update our Privacy Policy from + time to time. Thus, you are advised to review this page + periodically for any changes. I will + notify you of any changes by posting the new Privacy Policy on + this page. + +This policy is effective as of 2023-08-01 + + +@*https://app-privacy-policy-generator.firebaseapp.com*@ + + +Terms & Conditions + + By downloading or using the app, these terms will + automatically apply to you – you should make sure therefore + that you read them carefully before using the app. You’re not + allowed to copy or modify the app, any part of the app, or + our trademarks in any way. You’re not allowed to attempt to + extract the source code of the app, and you also shouldn’t try + to translate the app into other languages or make derivative + versions. The app itself, and all the trademarks, copyright, + database rights, and other intellectual property rights related + to it, still belong to Vladislav Antonyuk. + + + Vladislav Antonyuk is committed to ensuring that the app is + as useful and efficient as possible. For that reason, we + reserve the right to make changes to the app or to charge for + its services, at any time and for any reason. We will never + charge you for the app or its services without making it very + clear to you exactly what you’re paying for. + + + The @Constants.ProductName app stores and processes personal data that + you have provided to us, to provide my + Service. It’s your responsibility to keep your phone and + access to the app secure. We therefore recommend that you do + not jailbreak or root your phone, which is the process of + removing software restrictions and limitations imposed by the + official operating system of your device. It could make your + phone vulnerable to malware/viruses/malicious programs, + compromise your phone’s security features and it could mean + that the @Constants.ProductName app won’t work properly or at all. + + + + The app does use third-party services that declare their + Terms and Conditions. + + + Link to Terms and Conditions of third-party service + providers used by the app + + + + Google Play Services + + + + + You should be aware that there are certain things that + Vladislav Antonyuk will not take responsibility for. Certain + functions of the app will require the app to have an active + internet connection. The connection can be Wi-Fi or provided + by your mobile network provider, but Vladislav Antonyuk + cannot take responsibility for the app not working at full + functionality if you don’t have access to Wi-Fi, and you don’t + have any of your data allowance left. + + + If you’re using the app outside of an area with Wi-Fi, you + should remember that the terms of the agreement with your + mobile network provider will still apply. As a result, you may + be charged by your mobile provider for the cost of data for + the duration of the connection while accessing the app, or + other third-party charges. In using the app, you’re accepting + responsibility for any such charges, including roaming data + charges if you use the app outside of your home territory + (i.e. region or country) without turning off data roaming. If + you are not the bill payer for the device on which you’re + using the app, please be aware that we assume that you have + received permission from the bill payer for using the app. + + + Along the same lines, Vladislav Antonyuk cannot always take + responsibility for the way you use the app i.e. You need to + make sure that your device stays charged – if it runs out of + battery and you can’t turn it on to avail the Service, + Vladislav Antonyuk cannot accept responsibility. + + + With respect to Vladislav Antonyuk’s responsibility for your + use of the app, when you’re using the app, it’s important to + bear in mind that although we endeavor to ensure that it is + updated and correct at all times, we do rely on third parties + to provide information to us so that we can make it available + to you. Vladislav Antonyuk accepts no liability for any + loss, direct or indirect, you experience as a result of + relying wholly on this functionality of the app. + + + At some point, we may wish to update the app. The app is + currently available on Android & iOS – the requirements for the + both systems(and for any additional systems we + decide to extend the availability of the app to) may change, + and you’ll need to download the updates if you want to keep + using the app. Vladislav Antonyuk does not promise that it + will always update the app so that it is relevant to you + and/or works with the Android & iOS version that you have + installed on your device. However, you promise to always + accept updates to the application when offered to you, We may + also wish to stop providing the app, and may terminate use of + it at any time without giving notice of termination to you. + Unless we tell you otherwise, upon any termination, (a) the + rights and licenses granted to you in these terms will end; + (b) you must stop using the app, and (if needed) delete it + from your device. + + +Changes to This Terms and Conditions + + I may update our Terms and Conditions + from time to time. Thus, you are advised to review this page + periodically for any changes. I will + notify you of any changes by posting the new Terms and + Conditions on this page. + + + These terms and conditions are effective as of 2023-08-01 + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Privacy.razor.cs b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Privacy.razor.cs new file mode 100644 index 0000000..2c24066 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Pages/Privacy.razor.cs @@ -0,0 +1,5 @@ +namespace App1.Web.Components.Pages; + +public partial class Privacy : App1BaseComponent +{ +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Promo.razor b/templates/ModularMonolith/src/Web/App1.Web/Components/Promo.razor new file mode 100644 index 0000000..41df107 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Promo.razor @@ -0,0 +1,48 @@ +@inherits App1BaseComponent + + + + + @Constants.ProductName + Translation.Promo + + + + + + + + + + + + + + + + + + + + Translation.Register + + + + + + + + Translation.PromoShortDescription + + + + + + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Promo.razor.cs b/templates/ModularMonolith/src/Web/App1.Web/Components/Promo.razor.cs new file mode 100644 index 0000000..572b3d7 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Promo.razor.cs @@ -0,0 +1,14 @@ +namespace App1.Web.Components; + +using Microsoft.AspNetCore.Components; + +public partial class Promo : App1BaseComponent +{ + [Inject] + public required NavigationManager NavigationManager { get; set; } + + private void SignIn() + { + NavigationManager.NavigateTo("MicrosoftIdentity/Account/SignIn", true); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Promo.razor.css b/templates/ModularMonolith/src/Web/App1.Web/Components/Promo.razor.css new file mode 100644 index 0000000..58e7ed3 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Promo.razor.css @@ -0,0 +1,4 @@ +.video { + min-height: 40vh; + width: 100%; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/Routes.razor b/templates/ModularMonolith/src/Web/App1.Web/Components/Routes.razor new file mode 100644 index 0000000..b68b82e --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Components/_Imports.razor b/templates/ModularMonolith/src/Web/App1.Web/Components/_Imports.razor new file mode 100644 index 0000000..2a6f5ab --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Components/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using App1.Web +@using App1.Web.Components +@using App1.Web.Components.Layout +@using MudBlazor \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Constants.cs b/templates/ModularMonolith/src/Web/App1.Web/Constants.cs new file mode 100644 index 0000000..d9eedf2 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Constants.cs @@ -0,0 +1,6 @@ +namespace App1.Web; + +public static class Constants +{ + public const string ProductName = "App1"; +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/MicrosoftIdentityUserAuthenticationMessageHandler.cs b/templates/ModularMonolith/src/Web/App1.Web/MicrosoftIdentityUserAuthenticationMessageHandler.cs new file mode 100644 index 0000000..7e87f76 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/MicrosoftIdentityUserAuthenticationMessageHandler.cs @@ -0,0 +1,28 @@ +namespace App1.Web; + +using System.Net.Http.Headers; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Web; + +public class MicrosoftIdentityModule1AuthenticationMessageHandler( + ITokenAcquisition tokenAcquisition, + IOptionsMonitor namedMessageHandlerOptions, + string? serviceName = null) + : MicrosoftIdentityAuthenticationBaseMessageHandler(tokenAcquisition, namedMessageHandlerOptions, serviceName) +{ + private readonly IOptionsMonitor namedMessageHandlerOptions = namedMessageHandlerOptions; + + /// + protected override async Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + var authResult = await TokenAcquisition + .GetAccessTokenForUserAsync(namedMessageHandlerOptions.CurrentValue.GetScopes()) + .ConfigureAwait(false); + + request.Headers.Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, authResult); + + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Program.cs b/templates/ModularMonolith/src/Web/App1.Web/Program.cs new file mode 100644 index 0000000..b352e56 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Program.cs @@ -0,0 +1,39 @@ +using App1.ServiceDefaults; +using App1.Web.Components; +using App1.Web.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddRedisOutputCache("cache"); +builder.AddRedisDistributedCache("cache"); + +builder.Services.AddBlazor(); +builder.Services.AddAuth(builder.Configuration); +builder.Services.AddModule1Services(builder.Configuration); +builder.Services.AddApp1Services(builder.Configuration); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.MapStaticAssets(); +app.UseAntiforgery(); + +app.UseOutputCache(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); +app.MapRazorComponents().AddInteractiveServerRenderMode(); + +app.MapDefaultEndpoints(); + +await app.RunAsync(); \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Properties/launchSettings.json b/templates/ModularMonolith/src/Web/App1.Web/Properties/launchSettings.json new file mode 100644 index 0000000..d7c96e0 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:5001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/templates/ModularMonolith/src/Web/App1.Web/Services/App1ApiClient.cs b/templates/ModularMonolith/src/Web/App1.Web/Services/App1ApiClient.cs new file mode 100644 index 0000000..b4646a8 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Services/App1ApiClient.cs @@ -0,0 +1,29 @@ +using App1.Modules.Module1s.Application.Module1s.GetModule1s; +using App1.Modules.Module2s.Application.Module2s.GetModule2ById; + +namespace App1.Web.Services; + +using Microsoft.Identity.Web; + +public class App1ApiClient(HttpClient httpClient, MicrosoftIdentityConsentAndConditionalAccessHandler handler) +{ + public async Task> GetModule1s(CancellationToken cancellationToken) + { + return await httpClient.GetFromJsonAsync>("Module1s", cancellationToken).Safe([], handler.HandleException) ?? []; + } + + public async Task DeleteModule1(Guid module1Id, CancellationToken cancellationToken) + { + // admin delete + await httpClient.DeleteAsync($"Module1s/{module1Id}", cancellationToken).Safe(handler.HandleException); + } + + public Task UpdateModule1(Guid id, CancellationToken cancellationToken) + { + var request = new + { + + }; + return httpClient.PutAsJsonAsync($"Module1s/{id}", request, cancellationToken).Safe(handler.HandleException); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Services/ServiceExtensions.cs b/templates/ModularMonolith/src/Web/App1.Web/Services/ServiceExtensions.cs new file mode 100644 index 0000000..68f6189 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Services/ServiceExtensions.cs @@ -0,0 +1,73 @@ +using App1.Web.Services.User; + +namespace App1.Web.Services; + +using Common.Infrastructure; +using Microsoft.AspNetCore.Localization; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.UI; +using MudBlazor.Services; +using Toolbelt.Blazor.Extensions.DependencyInjection; +using Toolbelt.Blazor.I18nText; +using Constants = Microsoft.Identity.Web.Constants; +using MicrosoftIdentityModule1AuthenticationMessageHandler = MicrosoftIdentityModule1AuthenticationMessageHandler; + +public static class ServiceExtensions +{ + public static void AddBlazor(this IServiceCollection services) + { + services.AddRazorPages(); + services.AddControllersWithViews().AddMicrosoftIdentityUI(); + services.AddMudServices(); + services.AddTranslations(); + services.AddLogging(); + services.AddRazorComponents().AddInteractiveServerComponents().AddMicrosoftIdentityConsentHandler(); + } + + public static void AddAuth(this IServiceCollection services, IConfiguration configuration) + { + var scopes = configuration.GetRequiredSection("App1ApiClient:Scopes").Get()?.Split(' '); + services.AddMicrosoftIdentityWebAppAuthentication(configuration, Constants.AzureAdB2C) + .EnableTokenAcquisitionToCallDownstreamApi(scopes) + .AddDistributedTokenCaches(); + + services.AddCascadingAuthenticationState(); + services.AddAuthZ(); + } + + public static void AddModule1Services(this IServiceCollection services, IConfiguration configuration) + { + services.AddHttpContextAccessor(); + services.AddScoped(); + } + + public static void AddApp1Services(this IServiceCollection services, IConfiguration configuration) + { + var baseUrl = configuration.GetRequiredSection("App1ApiClient:BaseUrl").Get(); + services.AddOptions() + .Bind(configuration.GetSection("App1ApiClient")); + services.AddTransient(); + services.AddHttpClient(httpClient => + { + httpClient.BaseAddress = baseUrl; + }) + .AddHttpMessageHandler(); + + services.AddApp1TravellersClient() + .ConfigureHttpClient( + client => client.BaseAddress = new Uri($"{baseUrl}graphql"), + builder => builder.AddHttpMessageHandler()); + } + + private static void AddTranslations(this IServiceCollection services) + { + services.AddI18nText(options => options.PersistenceLevel = PersistanceLevel.Cookie); + services.Configure(options => + { + string[] supportedCultures = ["en-US"]; + options.DefaultRequestCulture = new RequestCulture(supportedCultures[0]); + options.AddSupportedCultures(supportedCultures); + options.AddSupportedUICultures(supportedCultures); + }); + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Services/TaskExtensions.cs b/templates/ModularMonolith/src/Web/App1.Web/Services/TaskExtensions.cs new file mode 100644 index 0000000..aad275a --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Services/TaskExtensions.cs @@ -0,0 +1,29 @@ +namespace App1.Web.Services; + +public static class TaskExtensions +{ + public static async Task Safe(this Task task, T defaultValue, Action onError) + { + try + { + return await task; + } + catch (Exception e) + { + onError(e); + return defaultValue; + } + } + + public static async Task Safe(this Task task, Action onError) + { + try + { + await task; + } + catch (Exception e) + { + onError(e); + } + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Services/User/CurrentUserService.cs b/templates/ModularMonolith/src/Web/App1.Web/Services/User/CurrentUserService.cs new file mode 100644 index 0000000..1aeb55b --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Services/User/CurrentUserService.cs @@ -0,0 +1,24 @@ +using System.Security.Claims; +using Microsoft.Identity.Web; + +namespace App1.Web.Services.User; + +public class CurrentUserService(IHttpContextAccessor httpContextAccessor) : ICurrentUserService +{ + public UserInfo GetCurrentUser() + { + var module1 = httpContextAccessor.HttpContext?.User; + if (module1 is null) + { + return new UserInfo(); + } + + return new UserInfo + { + ProviderId = module1.GetObjectId(), + Name = module1.GetDisplayName(), + Email = module1.FindFirstValue("emails"), + IsNew = Convert.ToBoolean(module1.FindFirstValue("newUser")) + }; + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Services/User/ICurrentUserService.cs b/templates/ModularMonolith/src/Web/App1.Web/Services/User/ICurrentUserService.cs new file mode 100644 index 0000000..06657e0 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Services/User/ICurrentUserService.cs @@ -0,0 +1,6 @@ +namespace App1.Web.Services.User; + +public interface ICurrentUserService +{ + UserInfo GetCurrentUser(); +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/Services/User/UserInfo.cs b/templates/ModularMonolith/src/Web/App1.Web/Services/User/UserInfo.cs new file mode 100644 index 0000000..f816454 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/Services/User/UserInfo.cs @@ -0,0 +1,9 @@ +namespace App1.Web.Services.User; + +public class UserInfo +{ + public string? ProviderId { get; init; } + public string? Name { get; init; } + public string? Email { get; init; } + public bool IsNew { get; init; } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/appsettings.json b/templates/ModularMonolith/src/Web/App1.Web/appsettings.json new file mode 100644 index 0000000..1c9a68c --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/appsettings.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "AzureAdB2C": { + "Instance": "https://b2clogin.com", + "TenantId": "", + "ClientId": "1", + "CallbackPath": "/signin-oidc", + "Domain": "onmicrosoft.com", + "SignedOutCallbackPath": "/signout", + "SignUpSignInPolicyId": "B2C_1_App1_SIGNUP_SIGNIN", + "ClientSecret": "#{AAD_B2C_CLIENT_SECRET}#", + "AllowWebApiToBeAuthorizedByACL": true, + "ValidateAuthority": false + }, + "App1ApiClient": { + "BaseUrl": "https://apiservice", + "Scopes": "#{API_CLIENT_SCOPES}#" + } +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/i18ntext/Translation.en.json b/templates/ModularMonolith/src/Web/App1.Web/i18ntext/Translation.en.json new file mode 100644 index 0000000..d24e5f2 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/i18ntext/Translation.en.json @@ -0,0 +1,3 @@ +{ + "Version": "Version" +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/i18ntext/Translation.uk.json b/templates/ModularMonolith/src/Web/App1.Web/i18ntext/Translation.uk.json new file mode 100644 index 0000000..9fa2761 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/i18ntext/Translation.uk.json @@ -0,0 +1,3 @@ +{ + "Version": "Версія" +} \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/web.config b/templates/ModularMonolith/src/Web/App1.Web/web.config new file mode 100644 index 0000000..b792b29 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/web.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/wwwroot/assets/default-location-pin.png b/templates/ModularMonolith/src/Web/App1.Web/wwwroot/assets/default-location-pin.png new file mode 100644 index 0000000000000000000000000000000000000000..97e1bc7e05c9c317dfea20e1cd15eba761af0b22 GIT binary patch literal 4084 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 Qyov)iXD_Flj;z#w0d|VIXaE2J literal 0 HcmV?d00001 diff --git a/templates/ModularMonolith/src/Web/App1.Web/wwwroot/assets/logo.svg b/templates/ModularMonolith/src/Web/App1.Web/wwwroot/assets/logo.svg new file mode 100644 index 0000000..e5ffdc3 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/wwwroot/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/ModularMonolith/src/Web/App1.Web/wwwroot/assets/user-location-pin.png b/templates/ModularMonolith/src/Web/App1.Web/wwwroot/assets/user-location-pin.png new file mode 100644 index 0000000000000000000000000000000000000000..4eaa995fabbd3dd60c2fdc1507802283324e1217 GIT binary patch literal 14806 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 Y(YOpo%m3!zc}pk-8CB_eN%OG(0bdA`&j0`b literal 0 HcmV?d00001 diff --git a/templates/ModularMonolith/src/Web/App1.Web/wwwroot/css/site.css b/templates/ModularMonolith/src/Web/App1.Web/wwwroot/css/site.css new file mode 100644 index 0000000..d5c2419 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/wwwroot/css/site.css @@ -0,0 +1,16 @@ +.leaflet-container .leaflet-marker-pane img { + max-height: 50px !important; + max-width: 50px !important; +} + +.h-100 { + height: 100%; +} + +.side-image { + padding: 5px 10px; +} + +.components-reconnect-overlay p { + color: black; +} diff --git a/templates/ModularMonolith/src/Web/App1.Web/wwwroot/favicon.ico b/templates/ModularMonolith/src/Web/App1.Web/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1d10625bd72afe1720cbba316dde2f7dbdadb272 GIT binary patch literal 4286 zcmeHKd2m(L9X>z@i?Wq;=+u#=wWZMMbf#m0B2qy`KoF}18Ni}2&?+E=>?DC~Kp-Ry zK_Cf4i3*e@L6JqorUnAqMr29IN|Kj^yd=Dr<=yx0zJ2}O`^Y1ajKcrk%?s( zgLH*xMnc3ok`PGUF{ypGZ=HP>I;Sd{5&1qneyoTE%*&tb6{ojONl#<4x_c<;kVCra zG9uuT&!br=h0jO`oFSld06!Fp8AkZQbiTFlX$j!tG;i^Il z_Ynjm>?)mQ9Ir>Sn$FT5c-=pE`2>@(j7etVB2EhCGxtr!=56Cq+nS9Q(}Yt4uGJhx z@s&kr=~zX0{#cOcE!yp1!ast#HXU_tAF5k4R5!cn%*haq(0oBj9VDp9-64ClZJ%rD zkIGno*(0vOo2$lR@sg3GM=o42Nr3ddn=yOt&vEG3Y|`8EyC2(n+c9BAqfI;qN4ty~ zT2S30qg~9VxocHXk6orc<&)uXA0_=G13s{@yJAwJ7-*Ld;Z|oOnmkt^8PT?HBqO58D<=>2Dx2=Xm(<-% zQnQ9o4i{N*W$opagAuv@_n5bEJgVAS&_?=i+ZByh7W@HatqtJK48jk#V@^Vty{1#i z0cRY8*UW-Xk0aY>Kr*B5*d|mE^w^oA9&r!8tZ!nnzI7til1{%rL!coX$4{@o^qHga zW%*^a6Mpi}NX&TQ7r0zo0-yOV;jg6_-v&}lnPC6o*Q?vT<3c$BmNzG0Rz zk$f+jvGJA;ezAP80rgx7UjO@a%zE)xcx~wvOqldAw(g9kTu|ZGis98WQPq4E`JbM^ zrEAr=R^`F*QwNZqH47Dv<#vwvWAx4$%&>A^OB(Le?#6(seZwR?M|l^sqbr^*{S~qq zkAGoHdI;eTu(AJ2zQ5K`gWI&TORIoW_To(8IecC2K&4Z}H&wMb|G&?1vS0@;7Kc*} zdOM)IZ38o`p4a0?@mhR9e@(x}6g)#b6O(S`v~7FJt;W_Q)ZU85=9HHRdmx^iG8Frd zX2MNz&`SB(Yyb_sh7XUWqQudJuWo#ay?HMo`|up(=Kl@(pCrQNNurZlHPNys<*FH0 zM>Tl_NdVt49aMK#5PocIS50wwR%6xLKVr_yBPa)hFlq8&Oqm*lvzN}IgYa*YSon>S zfxI&Z@ZZv6967rVX}RN&`rbHfe|Ic)=Ddkpt`igk>q#Q122gC75!ZP=B}mY>2k@c^Z2V&AwgYU>7tPXt{1y?Sh@cLI20|uNbaIxeH(z54Z zYxXaZvTqEwW&aw98NbDmj}O4nTn4X}h<53H!j1%QM0~=D`}^}sc29h=homR|jySf} zsy!|4l~}N76yXmF;7^%43~R#YA!5T4#3!#rqpabU42n?$XU?BQLdtZ6Mn8_Ixbet6 zn2zf=+^DW^MpOGm;)KJt{RLxvtc1l(PqAp6QVzVui1*&0c+o2BmSRolpRxQOFCsKz8D5z`2J;t;KuOI$ zTx}>vT4n;uU2PP9gxv|^9H8d5iE5`#I*X{hN%)l;fwWy8PVyE#&7~KP|cZUtMdFekCgP{}$2~&wfnyq{h z^aMEF;aK?EXgo3DVeHMBhR-gf;6h0;&K2dL*;hgQc7!q{@Oo=V>ON*V!2~^KnP`T1tQsV!!FXrS(|C2^NF;5UfV!rY zD6L$DPJSJ6Po&Ln7Uz*&;qrY9p6)#|@q)EhGDA&4U!Tj#sYATVmfj%m=@Zm6CP+E` zL_KCX#Z0SJ69hGcbXrZaiZCs1x_kez??86F^w&&_6J_!r*-sIS=q#_YJ;+D(uA_l2 z#rOH}@*c+fPWKh{_-Vvhg~YF#zmEBN*3VD=dvF(DD8x5iXA#V>TAvZKnp1W>!fE%X z_qUJ6drvXZ2%&pHN)XlNWyIqJ#CIOz2-C*2?&225kuB`HNq29;Ehk@Z=G3^~xVWM| zozmWKZ~MUCoVSo^n88Hlp94iLZlb71uN1YYbWx8yC>l{mL_I2pe7u3~xpO%+=~0L0 z{XWh7&IiW1-vtFm%MqQD{zo1H6!x?JZG_2K zoX*(OGGn7ihIst)P(5Q^(vRA`i9Js9AF|e1YiXZR)?U`0h-Xx+3B!!*LdE|9u-7&V literal 0 HcmV?d00001 diff --git a/templates/ModularMonolith/src/Web/App1.Web/wwwroot/sitemap.xml b/templates/ModularMonolith/src/Web/App1.Web/wwwroot/sitemap.xml new file mode 100644 index 0000000..3598974 --- /dev/null +++ b/templates/ModularMonolith/src/Web/App1.Web/wwwroot/sitemap.xml @@ -0,0 +1,21 @@ + + + https://App1.azurewebsites.net + 2023-11-30T16:35:12+00:00 + weekly + 1.0 + + + https://App1.azurewebsites.net/about + 2023-11-30T16:35:12+00:00 + weekly + 1.0 + + + https://App1.azurewebsites.net/privacy + 2023-11-30T16:35:12+00:00 + weekly + 1.0 + + \ No newline at end of file diff --git a/templates/OnionArchitectureBlazor/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj b/templates/OnionArchitectureBlazor/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj index 7256f75..d62c337 100644 --- a/templates/OnionArchitectureBlazor/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj +++ b/templates/OnionArchitectureBlazor/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureBlazor/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj b/templates/OnionArchitectureBlazor/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj index cdaad23..c768e41 100644 --- a/templates/OnionArchitectureBlazor/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj +++ b/templates/OnionArchitectureBlazor/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureBlazor/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj b/templates/OnionArchitectureBlazor/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj index c63c332..5a2a0da 100644 --- a/templates/OnionArchitectureBlazor/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj +++ b/templates/OnionArchitectureBlazor/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureBlazorRepository/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj b/templates/OnionArchitectureBlazorRepository/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj index 7256f75..d62c337 100644 --- a/templates/OnionArchitectureBlazorRepository/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj +++ b/templates/OnionArchitectureBlazorRepository/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureBlazorRepository/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj b/templates/OnionArchitectureBlazorRepository/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj index cdaad23..c768e41 100644 --- a/templates/OnionArchitectureBlazorRepository/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj +++ b/templates/OnionArchitectureBlazorRepository/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureBlazorRepository/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj b/templates/OnionArchitectureBlazorRepository/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj index c63c332..5a2a0da 100644 --- a/templates/OnionArchitectureBlazorRepository/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj +++ b/templates/OnionArchitectureBlazorRepository/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureBlazorWebAssembly/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj b/templates/OnionArchitectureBlazorWebAssembly/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj index 7256f75..d62c337 100644 --- a/templates/OnionArchitectureBlazorWebAssembly/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj +++ b/templates/OnionArchitectureBlazorWebAssembly/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureBlazorWebAssembly/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj b/templates/OnionArchitectureBlazorWebAssembly/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj index cdaad23..c768e41 100644 --- a/templates/OnionArchitectureBlazorWebAssembly/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj +++ b/templates/OnionArchitectureBlazorWebAssembly/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureBlazorWebAssembly/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj b/templates/OnionArchitectureBlazorWebAssembly/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj index c63c332..5a2a0da 100644 --- a/templates/OnionArchitectureBlazorWebAssembly/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj +++ b/templates/OnionArchitectureBlazorWebAssembly/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureCrossPlatformApplication/src/UI/Client/App1.Client/App1.Client.csproj b/templates/OnionArchitectureCrossPlatformApplication/src/UI/Client/App1.Client/App1.Client.csproj index 780d19d..bbff024 100644 --- a/templates/OnionArchitectureCrossPlatformApplication/src/UI/Client/App1.Client/App1.Client.csproj +++ b/templates/OnionArchitectureCrossPlatformApplication/src/UI/Client/App1.Client/App1.Client.csproj @@ -47,9 +47,9 @@ - - - + + + diff --git a/templates/OnionArchitectureCrossPlatformApplication/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj b/templates/OnionArchitectureCrossPlatformApplication/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj index 7256f75..d62c337 100644 --- a/templates/OnionArchitectureCrossPlatformApplication/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj +++ b/templates/OnionArchitectureCrossPlatformApplication/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/Client/App1.Infrastructure.Client.Business.Tests/App1.Infrastructure.Client.Business.Tests.csproj b/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/Client/App1.Infrastructure.Client.Business.Tests/App1.Infrastructure.Client.Business.Tests.csproj index 202e198..c042f95 100644 --- a/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/Client/App1.Infrastructure.Client.Business.Tests/App1.Infrastructure.Client.Business.Tests.csproj +++ b/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/Client/App1.Infrastructure.Client.Business.Tests/App1.Infrastructure.Client.Business.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/Client/App1.Infrastructure.Client.Data.Tests/App1.Infrastructure.Client.Data.Tests.csproj b/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/Client/App1.Infrastructure.Client.Data.Tests/App1.Infrastructure.Client.Data.Tests.csproj index 0d57640..ca4e44a 100644 --- a/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/Client/App1.Infrastructure.Client.Data.Tests/App1.Infrastructure.Client.Data.Tests.csproj +++ b/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/Client/App1.Infrastructure.Client.Data.Tests/App1.Infrastructure.Client.Data.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Business.Tests/App1.Infrastructure.WebApp.Business.Tests.csproj b/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Business.Tests/App1.Infrastructure.WebApp.Business.Tests.csproj index 52165a0..8352975 100644 --- a/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Business.Tests/App1.Infrastructure.WebApp.Business.Tests.csproj +++ b/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Business.Tests/App1.Infrastructure.WebApp.Business.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Data.Tests/App1.Infrastructure.WebApp.Data.Tests.csproj b/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Data.Tests/App1.Infrastructure.WebApp.Data.Tests.csproj index 5a65746..d204ebe 100644 --- a/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Data.Tests/App1.Infrastructure.WebApp.Data.Tests.csproj +++ b/templates/OnionArchitectureCrossPlatformApplication/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Data.Tests/App1.Infrastructure.WebApp.Data.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureCrossPlatformApplicationRepository/src/UI/Client/App1.Client/App1.Client.csproj b/templates/OnionArchitectureCrossPlatformApplicationRepository/src/UI/Client/App1.Client/App1.Client.csproj index 780d19d..bbff024 100644 --- a/templates/OnionArchitectureCrossPlatformApplicationRepository/src/UI/Client/App1.Client/App1.Client.csproj +++ b/templates/OnionArchitectureCrossPlatformApplicationRepository/src/UI/Client/App1.Client/App1.Client.csproj @@ -47,9 +47,9 @@ - - - + + + diff --git a/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj b/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj index 7256f75..d62c337 100644 --- a/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj +++ b/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/Client/App1.Infrastructure.Client.Business.Tests/App1.Infrastructure.Client.Business.Tests.csproj b/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/Client/App1.Infrastructure.Client.Business.Tests/App1.Infrastructure.Client.Business.Tests.csproj index 202e198..c042f95 100644 --- a/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/Client/App1.Infrastructure.Client.Business.Tests/App1.Infrastructure.Client.Business.Tests.csproj +++ b/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/Client/App1.Infrastructure.Client.Business.Tests/App1.Infrastructure.Client.Business.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/Client/App1.Infrastructure.Client.Data.Tests/App1.Infrastructure.Client.Data.Tests.csproj b/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/Client/App1.Infrastructure.Client.Data.Tests/App1.Infrastructure.Client.Data.Tests.csproj index 0d57640..ca4e44a 100644 --- a/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/Client/App1.Infrastructure.Client.Data.Tests/App1.Infrastructure.Client.Data.Tests.csproj +++ b/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/Client/App1.Infrastructure.Client.Data.Tests/App1.Infrastructure.Client.Data.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Business.Tests/App1.Infrastructure.WebApp.Business.Tests.csproj b/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Business.Tests/App1.Infrastructure.WebApp.Business.Tests.csproj index 52165a0..8352975 100644 --- a/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Business.Tests/App1.Infrastructure.WebApp.Business.Tests.csproj +++ b/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Business.Tests/App1.Infrastructure.WebApp.Business.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Data.Tests/App1.Infrastructure.WebApp.Data.Tests.csproj b/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Data.Tests/App1.Infrastructure.WebApp.Data.Tests.csproj index 5a65746..d204ebe 100644 --- a/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Data.Tests/App1.Infrastructure.WebApp.Data.Tests.csproj +++ b/templates/OnionArchitectureCrossPlatformApplicationRepository/tests/Infrastructure/WebApp/App1.Infrastructure.WebApp.Data.Tests/App1.Infrastructure.WebApp.Data.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureDotNetMaui/src/UI/App1.Client/App1.Client.csproj b/templates/OnionArchitectureDotNetMaui/src/UI/App1.Client/App1.Client.csproj index 790868c..84c1fd1 100644 --- a/templates/OnionArchitectureDotNetMaui/src/UI/App1.Client/App1.Client.csproj +++ b/templates/OnionArchitectureDotNetMaui/src/UI/App1.Client/App1.Client.csproj @@ -46,9 +46,9 @@ - - - + + + diff --git a/templates/OnionArchitectureDotNetMaui/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj b/templates/OnionArchitectureDotNetMaui/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj index 7256f75..d62c337 100644 --- a/templates/OnionArchitectureDotNetMaui/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj +++ b/templates/OnionArchitectureDotNetMaui/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureDotNetMaui/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj b/templates/OnionArchitectureDotNetMaui/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj index 256b0a9..ee88aa7 100644 --- a/templates/OnionArchitectureDotNetMaui/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj +++ b/templates/OnionArchitectureDotNetMaui/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureDotNetMaui/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj b/templates/OnionArchitectureDotNetMaui/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj index 3db7ce7..e18e8e9 100644 --- a/templates/OnionArchitectureDotNetMaui/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj +++ b/templates/OnionArchitectureDotNetMaui/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureDotNetMauiRepository/src/UI/App1.Client/App1.Client.csproj b/templates/OnionArchitectureDotNetMauiRepository/src/UI/App1.Client/App1.Client.csproj index 790868c..84c1fd1 100644 --- a/templates/OnionArchitectureDotNetMauiRepository/src/UI/App1.Client/App1.Client.csproj +++ b/templates/OnionArchitectureDotNetMauiRepository/src/UI/App1.Client/App1.Client.csproj @@ -46,9 +46,9 @@ - - - + + + diff --git a/templates/OnionArchitectureDotNetMauiRepository/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj b/templates/OnionArchitectureDotNetMauiRepository/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj index 7256f75..d62c337 100644 --- a/templates/OnionArchitectureDotNetMauiRepository/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj +++ b/templates/OnionArchitectureDotNetMauiRepository/tests/Application/App1.Application.Tests/App1.Application.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureDotNetMauiRepository/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj b/templates/OnionArchitectureDotNetMauiRepository/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj index 256b0a9..ee88aa7 100644 --- a/templates/OnionArchitectureDotNetMauiRepository/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj +++ b/templates/OnionArchitectureDotNetMauiRepository/tests/Infrastructure/App1.Infrastructure.Business.Tests/App1.Infrastructure.Business.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/OnionArchitectureDotNetMauiRepository/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj b/templates/OnionArchitectureDotNetMauiRepository/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj index 3db7ce7..e18e8e9 100644 --- a/templates/OnionArchitectureDotNetMauiRepository/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj +++ b/templates/OnionArchitectureDotNetMauiRepository/tests/Infrastructure/App1.Infrastructure.Data.Tests/App1.Infrastructure.Data.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all