From d1f322301bd69083043e49cd72502fdd36302cc2 Mon Sep 17 00:00:00 2001 From: kgrzybek Date: Mon, 1 Jan 2024 21:03:40 +0100 Subject: [PATCH 1/6] refactor: extract Registrations WIP --- .../UserAccess/UserRegistrationsController.cs | 15 +- .../StronglyTypedIdValueConverterSelector.cs | 2 +- src/CompanyName.MyMeetings.sln | 69 ++++++ .../CompanyName.MyMeetings.Database.sqlproj | 8 +- .../Scripts/ClearDatabase.sql | 6 +- .../Scripts/CreateStructure.sql | 66 ++++- .../1_0_0_0/0001_initial_structure.sql | 19 +- .../Scripts/SeedDatabase.sql | 4 +- .../Structure/Security/Schemas.sql | 3 + .../Tables/UserRegistrations.sql | 4 +- .../Views/v_UserRegistrations.sql | 4 +- ....Modules.Administration.Application.csproj | 1 + ...ewUserRegisteredIntegrationEventHandler.cs | 2 +- .../EventsBus/EventsBusStartup.cs | 2 +- ...etings.Modules.Meetings.Application.csproj | 1 + ...ewUserRegisteredIntegrationEventHandler.cs | 2 +- .../EventsBus/EventsBusStartup.cs | 2 +- ...etings.Modules.Payments.Application.csproj | 1 + ...ewUserRegisteredIntegrationEventHandler.cs | 2 +- .../AggregateStore/SubscriptionCode.cs | 3 + .../EventsBus/EventsBusStartup.cs | 2 +- ...s.Modules.Registrations.Application.csproj | 5 + .../Configuration/Commands/ICommandHandler.cs | 16 ++ .../Commands/ICommandsScheduler.cs | 11 + .../Commands/InternalCommandBase.cs | 29 +++ .../Configuration/Queries/IQueryHandler.cs | 11 + .../Application/Contracts/CommandBase.cs | 32 +++ .../Application/Contracts/CustomClaimTypes.cs | 9 + .../Application/Contracts/ICommand.cs | 14 ++ .../Application/Contracts/IQuery.cs | 8 + .../Contracts/IRecurringCommand.cs | 6 + .../Contracts/IRegistrationsModule.cs | 11 + .../Application/Contracts/QueryBase.cs | 17 ++ .../Application/Contracts/Roles.cs | 8 + .../ConfirmUserRegistrationCommand.cs | 4 +- .../ConfirmUserRegistrationCommandHandler.cs | 44 ++-- .../UserRegistrationConfirmedHandler.cs | 27 +++ .../GetUserRegistrationQuery.cs | 4 +- .../GetUserRegistrationQueryHandler.cs | 6 +- .../UserRegistrationDto.cs | 2 +- ...gisteredEnqueueEmailConfirmationHandler.cs | 6 +- .../NewUserRegisteredNotification.cs | 4 +- .../NewUserRegisteredPublishEventHandler.cs | 4 +- .../RegisterNewUser/PasswordManager.cs | 82 +++++++ .../RegisterNewUser/RegisterNewUserCommand.cs | 4 +- .../RegisterNewUserCommandHandler.cs | 7 +- ...serRegistrationConfirmationEmailCommand.cs | 6 +- ...strationConfirmationEmailCommandHandler.cs | 4 +- .../UserRegistrations/UsersCounter.cs | 4 +- ...etings.Modules.Registrations.Domain.csproj | 1 + .../Events/NewUserRegisteredDomainEvent.cs | 2 +- .../UserRegistrationConfirmedDomainEvent.cs | 2 +- .../UserRegistrationExpiredDomainEvent.cs | 2 +- .../IUserRegistrationRepository.cs | 2 +- .../Domain/UserRegistrations/IUsersCounter.cs | 2 +- ...eatedWhenRegistrationIsNotConfirmedRule.cs | 2 +- .../Rules/UserLoginMustBeUniqueRule.cs | 2 +- ...ionCannotBeConfirmedAfterExpirationRule.cs | 2 +- ...rationCannotBeConfirmedMoreThanOnceRule.cs | 2 +- ...strationCannotBeExpiredMoreThanOnceRule.cs | 2 +- .../UserRegistrations/UserRegistration.cs | 21 +- .../UserRegistrations/UserRegistrationId.cs | 2 +- .../UserRegistrationStatus.cs | 2 +- ...odules.Registrations.Infrastructure.csproj | 9 + .../Configuration/AllConstructorFinder.cs | 21 ++ .../Configuration/Assemblies.cs | 10 + .../Configuration/Commands/ICommandHandler.cs | 16 ++ .../Commands/ICommandsScheduler.cs | 11 + .../Commands/InternalCommandBase.cs | 29 +++ .../DataAccess/DataAccessModule.cs | 52 ++++ .../Configuration/Domain/DomainModule.cs | 6 +- .../Configuration/Email/EmailModule.cs | 35 +++ .../EventsBus/EventsBusModule.cs | 29 +++ .../EventsBus/EventsBusStartup.cs | 30 +++ .../IntegrationEventGenericHandler.cs | 39 +++ .../Configuration/Logging/LoggingModule.cs | 22 ++ .../Configuration/Mediation/MediatorModule.cs | 93 +++++++ .../Processing/CommandsExecutor.cs | 27 +++ .../Processing/IRecurringCommand.cs | 6 + .../Processing/Inbox/InboxMessageDto.cs | 11 + .../Processing/Inbox/ProcessInboxCommand.cs | 8 + .../Inbox/ProcessInboxCommandHandler.cs | 67 +++++ .../Processing/Inbox/ProcessInboxJob.cs | 13 + .../InternalCommands/CommandsScheduler.cs | 57 +++++ .../ProcessInternalCommandsCommand.cs | 8 + .../ProcessInternalCommandsCommandHandler.cs | 88 +++++++ .../ProcessInternalCommandsJob.cs | 13 + .../LoggingCommandHandlerDecorator.cs | 91 +++++++ ...oggingCommandHandlerWithResultDecorator.cs | 88 +++++++ .../Processing/Outbox/OutboxMessageDto.cs | 11 + .../Processing/Outbox/OutboxModule.cs | 60 +++++ .../Processing/Outbox/ProcessOutboxCommand.cs | 8 + .../Outbox/ProcessOutboxCommandHandler.cs | 89 +++++++ .../Processing/Outbox/ProcessOutboxJob.cs | 13 + .../Processing/ProcessingModule.cs | 69 ++++++ .../UnitOfWorkCommandHandlerDecorator.cs | 42 ++++ ...OfWorkCommandHandlerWithResultDecorator.cs | 44 ++++ .../ValidationCommandHandlerDecorator.cs | 38 +++ ...dationCommandHandlerWithResultDecorator.cs | 39 +++ .../Configuration/Quartz/QuartzModule.cs | 14 ++ .../Configuration/Quartz/QuartzStartup.cs | 112 +++++++++ .../Quartz/SerilogLogProvider.cs | 68 ++++++ .../RegistrationsCompositionRoot.cs | 19 ++ .../Configuration/RegistrationsStartup.cs | 86 +++++++ ...UserRegistrationEntityTypeConfiguration.cs | 6 +- .../UserRegistrationRepository.cs | 25 ++ .../InternalCommandEntityTypeConfiguration.cs | 17 ++ .../Infrastructure/Outbox/OutboxAccessor.cs | 24 ++ .../OutboxMessageEntityTypeConfiguration.cs | 17 ++ .../Infrastructure/RegistrationsContext.cs | 35 +++ .../Infrastructure/RegistrationsModule.cs | 31 +++ .../Registrations/IntegrationEvents/Class1.cs | 5 + ...les.Registrations.IntegrationEvents.csproj | 1 + .../NewUserRegisteredIntegrationEvent.cs | 2 +- .../ArchTests/Application/ApplicationTests.cs | 200 +++++++++++++++ ...ngs.Modules.Registrations.ArchTests.csproj | 1 + .../Tests/ArchTests/Domain/DomainTests.cs | 228 ++++++++++++++++++ .../Tests/ArchTests/Module/LayersTests.cs | 43 ++++ .../Tests/ArchTests/SeedWork/TestBase.cs | 44 ++++ .../Tests/IntegrationTests/AssemblyInfo.cs | 11 + ...ules.Registrations.IntegrationTests.csproj | 1 + .../SeedWork/ExecutionContextMock.cs | 23 ++ .../SeedWork/OutboxMessagesHelper.cs | 35 +++ .../IntegrationTests/SeedWork/TestBase.cs | 80 ++++++ .../ConfirmUserRegistrationTests.cs | 31 +++ ...dUserRegistrationConfirmationEmailTests.cs | 10 +- .../UserRegistrationSampleData.cs | 2 +- .../UserRegistrationTests.cs | 12 +- ...ules.Registrations.Domain.UnitTests.csproj | 1 + .../SeedWork/DomainEventsTestHelper.cs | 48 ++++ .../Tests/UnitTests/SeedWork/TestBase.cs | 32 +++ .../UserRegistrationTests.cs | 93 ++++--- .../{ => Authenticate}/PasswordManager.cs | 2 +- .../UserRegistrationConfirmedHandler.cs | 31 --- .../AddAdminUserCommandHandler.cs | 1 + src/Modules/UserAccess/Domain/Users/User.cs | 5 +- .../Configuration/UserAccessStartup.cs | 9 - .../UserRegistrationRepository.cs | 25 -- .../Infrastructure/UserAccessContext.cs | 5 - .../SeedWork/OutboxMessagesHelper.cs | 4 +- .../ConfirmUserRegistrationTests.cs | 31 --- .../IntegrationTests/Users/CreateUserTests.cs | 60 ++--- src/Tests/SUT/Helpers/UsersFactory.cs | 13 +- src/Tests/SUT/SeedWork/TestBase.cs | 25 ++ src/Tests/SUT/TestCases/CreateMeeting.cs | 4 +- 145 files changed, 3185 insertions(+), 328 deletions(-) rename src/Database/CompanyName.MyMeetings.Database/Structure/{users => registrations}/Tables/UserRegistrations.sql (74%) rename src/Database/CompanyName.MyMeetings.Database/Structure/{users => registrations}/Views/v_UserRegistrations.sql (67%) create mode 100644 src/Modules/Registrations/Application/CompanyName.MyMeetings.Modules.Registrations.Application.csproj create mode 100644 src/Modules/Registrations/Application/Configuration/Commands/ICommandHandler.cs create mode 100644 src/Modules/Registrations/Application/Configuration/Commands/ICommandsScheduler.cs create mode 100644 src/Modules/Registrations/Application/Configuration/Commands/InternalCommandBase.cs create mode 100644 src/Modules/Registrations/Application/Configuration/Queries/IQueryHandler.cs create mode 100644 src/Modules/Registrations/Application/Contracts/CommandBase.cs create mode 100644 src/Modules/Registrations/Application/Contracts/CustomClaimTypes.cs create mode 100644 src/Modules/Registrations/Application/Contracts/ICommand.cs create mode 100644 src/Modules/Registrations/Application/Contracts/IQuery.cs create mode 100644 src/Modules/Registrations/Application/Contracts/IRecurringCommand.cs create mode 100644 src/Modules/Registrations/Application/Contracts/IRegistrationsModule.cs create mode 100644 src/Modules/Registrations/Application/Contracts/QueryBase.cs create mode 100644 src/Modules/Registrations/Application/Contracts/Roles.cs rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommand.cs (59%) rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommandHandler.cs (71%) create mode 100644 src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedHandler.cs rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQuery.cs (60%) rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQueryHandler.cs (86%) rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/GetUserRegistration/UserRegistrationDto.cs (76%) rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredEnqueueEmailConfirmationHandler.cs (72%) rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredNotification.cs (67%) rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredPublishEventHandler.cs (85%) create mode 100644 src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/PasswordManager.cs rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommand.cs (80%) rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommandHandler.cs (77%) rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommand.cs (69%) rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommandHandler.cs (83%) rename src/Modules/{UserAccess => Registrations}/Application/UserRegistrations/UsersCounter.cs (84%) create mode 100644 src/Modules/Registrations/Domain/CompanyName.MyMeetings.Modules.Registrations.Domain.csproj rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/Events/NewUserRegisteredDomainEvent.cs (92%) rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/Events/UserRegistrationConfirmedDomainEvent.cs (80%) rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/Events/UserRegistrationExpiredDomainEvent.cs (80%) rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/IUserRegistrationRepository.cs (71%) rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/IUsersCounter.cs (53%) rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/Rules/UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule.cs (88%) rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/Rules/UserLoginMustBeUniqueRule.cs (86%) rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedAfterExpirationRule.cs (88%) rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedMoreThanOnceRule.cs (87%) rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/Rules/UserRegistrationCannotBeExpiredMoreThanOnceRule.cs (87%) rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/UserRegistration.cs (79%) rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/UserRegistrationId.cs (73%) rename src/Modules/{UserAccess => Registrations}/Domain/UserRegistrations/UserRegistrationStatus.cs (88%) create mode 100644 src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/AllConstructorFinder.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Assemblies.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Commands/ICommandHandler.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Commands/ICommandsScheduler.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Commands/InternalCommandBase.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/DataAccess/DataAccessModule.cs rename src/Modules/{UserAccess => Registrations}/Infrastructure/Configuration/Domain/DomainModule.cs (53%) create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Email/EmailModule.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/EventsBus/EventsBusModule.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Logging/LoggingModule.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Mediation/MediatorModule.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/CommandsExecutor.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/IRecurringCommand.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/InboxMessageDto.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommand.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxJob.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommand.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsJob.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/OutboxMessageDto.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/OutboxModule.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommand.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxJob.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/ProcessingModule.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerDecorator.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerWithResultDecorator.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/ValidationCommandHandlerDecorator.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Processing/ValidationCommandHandlerWithResultDecorator.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Quartz/QuartzModule.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Quartz/QuartzStartup.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/Quartz/SerilogLogProvider.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/RegistrationsCompositionRoot.cs create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs rename src/Modules/{UserAccess => Registrations}/Infrastructure/Domain/UserRegistrations/UserRegistrationEntityTypeConfiguration.cs (82%) create mode 100644 src/Modules/Registrations/Infrastructure/Domain/UserRegistrations/UserRegistrationRepository.cs create mode 100644 src/Modules/Registrations/Infrastructure/InternalCommands/InternalCommandEntityTypeConfiguration.cs create mode 100644 src/Modules/Registrations/Infrastructure/Outbox/OutboxAccessor.cs create mode 100644 src/Modules/Registrations/Infrastructure/Outbox/OutboxMessageEntityTypeConfiguration.cs create mode 100644 src/Modules/Registrations/Infrastructure/RegistrationsContext.cs create mode 100644 src/Modules/Registrations/Infrastructure/RegistrationsModule.cs create mode 100644 src/Modules/Registrations/IntegrationEvents/Class1.cs create mode 100644 src/Modules/Registrations/IntegrationEvents/CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents.csproj rename src/Modules/{UserAccess => Registrations}/IntegrationEvents/NewUserRegisteredIntegrationEvent.cs (91%) create mode 100644 src/Modules/Registrations/Tests/ArchTests/Application/ApplicationTests.cs create mode 100644 src/Modules/Registrations/Tests/ArchTests/CompanyName.MyMeetings.Modules.Registrations.ArchTests.csproj create mode 100644 src/Modules/Registrations/Tests/ArchTests/Domain/DomainTests.cs create mode 100644 src/Modules/Registrations/Tests/ArchTests/Module/LayersTests.cs create mode 100644 src/Modules/Registrations/Tests/ArchTests/SeedWork/TestBase.cs create mode 100644 src/Modules/Registrations/Tests/IntegrationTests/AssemblyInfo.cs create mode 100644 src/Modules/Registrations/Tests/IntegrationTests/CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.csproj create mode 100644 src/Modules/Registrations/Tests/IntegrationTests/SeedWork/ExecutionContextMock.cs create mode 100644 src/Modules/Registrations/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs create mode 100644 src/Modules/Registrations/Tests/IntegrationTests/SeedWork/TestBase.cs create mode 100644 src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/ConfirmUserRegistrationTests.cs rename src/Modules/{UserAccess => Registrations}/Tests/IntegrationTests/UserRegistrations/SendUserRegistrationConfirmationEmailTests.cs (69%) rename src/Modules/{UserAccess => Registrations}/Tests/IntegrationTests/UserRegistrations/UserRegistrationSampleData.cs (76%) rename src/Modules/{UserAccess => Registrations}/Tests/IntegrationTests/UserRegistrations/UserRegistrationTests.cs (69%) create mode 100644 src/Modules/Registrations/Tests/UnitTests/CompanyName.MyMeetings.Modules.Registrations.Domain.UnitTests.csproj create mode 100644 src/Modules/Registrations/Tests/UnitTests/SeedWork/DomainEventsTestHelper.cs create mode 100644 src/Modules/Registrations/Tests/UnitTests/SeedWork/TestBase.cs rename src/Modules/{UserAccess => Registrations}/Tests/UnitTests/UserRegistrations/UserRegistrationTests.cs (71%) rename src/Modules/UserAccess/Application/Authentication/{ => Authenticate}/PasswordManager.cs (98%) delete mode 100644 src/Modules/UserAccess/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedHandler.cs delete mode 100644 src/Modules/UserAccess/Infrastructure/Domain/UserRegistrations/UserRegistrationRepository.cs delete mode 100644 src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/ConfirmUserRegistrationTests.cs diff --git a/src/API/CompanyName.MyMeetings.API/Modules/UserAccess/UserRegistrationsController.cs b/src/API/CompanyName.MyMeetings.API/Modules/UserAccess/UserRegistrationsController.cs index 01d1411c9..5b05df11c 100644 --- a/src/API/CompanyName.MyMeetings.API/Modules/UserAccess/UserRegistrationsController.cs +++ b/src/API/CompanyName.MyMeetings.API/Modules/UserAccess/UserRegistrationsController.cs @@ -1,7 +1,8 @@ using CompanyName.MyMeetings.API.Configuration.Authorization; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.ConfirmUserRegistration; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -11,11 +12,11 @@ namespace CompanyName.MyMeetings.API.Modules.UserAccess [ApiController] public class UserRegistrationsController : ControllerBase { - private readonly IUserAccessModule _userAccessModule; + private readonly IRegistrationsModule _registrationsModule; - public UserRegistrationsController(IUserAccessModule userAccessModule) + public UserRegistrationsController(IRegistrationsModule registrationsModule) { - _userAccessModule = userAccessModule; + _registrationsModule = registrationsModule; } [NoPermissionRequired] @@ -24,7 +25,7 @@ public UserRegistrationsController(IUserAccessModule userAccessModule) [ProducesResponseType(StatusCodes.Status200OK)] public async Task RegisterNewUser(RegisterNewUserRequest request) { - await _userAccessModule.ExecuteCommandAsync(new RegisterNewUserCommand( + await _registrationsModule.ExecuteCommandAsync(new RegisterNewUserCommand( request.Login, request.Password, request.Email, @@ -41,7 +42,7 @@ await _userAccessModule.ExecuteCommandAsync(new RegisterNewUserCommand( [ProducesResponseType(StatusCodes.Status200OK)] public async Task ConfirmRegistration(Guid userRegistrationId) { - await _userAccessModule.ExecuteCommandAsync(new ConfirmUserRegistrationCommand(userRegistrationId)); + await _registrationsModule.ExecuteCommandAsync(new ConfirmUserRegistrationCommand(userRegistrationId)); return Ok(); } diff --git a/src/BuildingBlocks/Infrastructure/StronglyTypedIdValueConverterSelector.cs b/src/BuildingBlocks/Infrastructure/StronglyTypedIdValueConverterSelector.cs index bd06a9221..70b4bbd18 100644 --- a/src/BuildingBlocks/Infrastructure/StronglyTypedIdValueConverterSelector.cs +++ b/src/BuildingBlocks/Infrastructure/StronglyTypedIdValueConverterSelector.cs @@ -5,7 +5,7 @@ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure { /// - /// Based on https://andrewlock.net/strongly-typed-ids-in-ef-core-using-strongly-typed-entity-ids-to-avoid-primitive-obsession-part-4/ + /// Based on https://andrewlock.net/strongly-typed-ids-in-ef-core-using-strongly-typed-entity-ids-to-avoid-primitive-obsession-part-4/. /// public class StronglyTypedIdValueConverterSelector : ValueConverterSelector { diff --git a/src/CompanyName.MyMeetings.sln b/src/CompanyName.MyMeetings.sln index 5e68c9248..0266c55cc 100644 --- a/src/CompanyName.MyMeetings.sln +++ b/src/CompanyName.MyMeetings.sln @@ -136,6 +136,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RequestExamples", "RequestE EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.SUT", "Tests\SUT\CompanyName.MyMeetings.SUT.csproj", "{1853847F-9988-43A1-B3E1-DDBE4B2F3365}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Registrations", "Registrations", "{8F0598A5-2F0C-4FA6-82F6-938F1830ADB7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyName.MyMeetings.Modules.Registrations.Application", "Modules\Registrations\Application\CompanyName.MyMeetings.Modules.Registrations.Application.csproj", "{3D5E4893-E48A-4553-B036-724A8F809656}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyName.MyMeetings.Modules.Registrations.Domain", "Modules\Registrations\Domain\CompanyName.MyMeetings.Modules.Registrations.Domain.csproj", "{98CE491C-8A52-4FC9-87BD-36FE63CB37E6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyName.MyMeetings.Modules.Registrations.Infrastructure", "Modules\Registrations\Infrastructure\CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj", "{5F24D649-5684-458E-8C67-CCEC85E271A0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{646E463D-F0E2-4BA4-9B5E-434ABE26EC07}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyName.MyMeetings.Modules.Registrations.ArchTests", "Modules\Registrations\Tests\ArchTests\CompanyName.MyMeetings.Modules.Registrations.ArchTests.csproj", "{96639493-5D2D-4F61-B399-600673D6912D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests", "Modules\Registrations\Tests\IntegrationTests\CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.csproj", "{9AB969B5-4215-4ACF-8D48-EC0A6F35BC46}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyName.MyMeetings.Modules.Registrations.Domain.UnitTests", "Modules\Registrations\Tests\UnitTests\CompanyName.MyMeetings.Modules.Registrations.Domain.UnitTests.csproj", "{0535D1F2-FA8B-4093-9987-7533F8D07605}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents", "Modules\Registrations\IntegrationEvents\CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents.csproj", "{2E71D2B2-516D-4B0D-8DE6-B9F3105B9C95}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -385,6 +403,48 @@ Global {1853847F-9988-43A1-B3E1-DDBE4B2F3365}.Production|Any CPU.Build.0 = Debug|Any CPU {1853847F-9988-43A1-B3E1-DDBE4B2F3365}.Release|Any CPU.ActiveCfg = Release|Any CPU {1853847F-9988-43A1-B3E1-DDBE4B2F3365}.Release|Any CPU.Build.0 = Release|Any CPU + {3D5E4893-E48A-4553-B036-724A8F809656}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D5E4893-E48A-4553-B036-724A8F809656}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D5E4893-E48A-4553-B036-724A8F809656}.Production|Any CPU.ActiveCfg = Production|Any CPU + {3D5E4893-E48A-4553-B036-724A8F809656}.Production|Any CPU.Build.0 = Production|Any CPU + {3D5E4893-E48A-4553-B036-724A8F809656}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D5E4893-E48A-4553-B036-724A8F809656}.Release|Any CPU.Build.0 = Release|Any CPU + {98CE491C-8A52-4FC9-87BD-36FE63CB37E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98CE491C-8A52-4FC9-87BD-36FE63CB37E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98CE491C-8A52-4FC9-87BD-36FE63CB37E6}.Production|Any CPU.ActiveCfg = Production|Any CPU + {98CE491C-8A52-4FC9-87BD-36FE63CB37E6}.Production|Any CPU.Build.0 = Production|Any CPU + {98CE491C-8A52-4FC9-87BD-36FE63CB37E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98CE491C-8A52-4FC9-87BD-36FE63CB37E6}.Release|Any CPU.Build.0 = Release|Any CPU + {5F24D649-5684-458E-8C67-CCEC85E271A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F24D649-5684-458E-8C67-CCEC85E271A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F24D649-5684-458E-8C67-CCEC85E271A0}.Production|Any CPU.ActiveCfg = Production|Any CPU + {5F24D649-5684-458E-8C67-CCEC85E271A0}.Production|Any CPU.Build.0 = Production|Any CPU + {5F24D649-5684-458E-8C67-CCEC85E271A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F24D649-5684-458E-8C67-CCEC85E271A0}.Release|Any CPU.Build.0 = Release|Any CPU + {96639493-5D2D-4F61-B399-600673D6912D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96639493-5D2D-4F61-B399-600673D6912D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96639493-5D2D-4F61-B399-600673D6912D}.Production|Any CPU.ActiveCfg = Production|Any CPU + {96639493-5D2D-4F61-B399-600673D6912D}.Production|Any CPU.Build.0 = Production|Any CPU + {96639493-5D2D-4F61-B399-600673D6912D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96639493-5D2D-4F61-B399-600673D6912D}.Release|Any CPU.Build.0 = Release|Any CPU + {9AB969B5-4215-4ACF-8D48-EC0A6F35BC46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AB969B5-4215-4ACF-8D48-EC0A6F35BC46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AB969B5-4215-4ACF-8D48-EC0A6F35BC46}.Production|Any CPU.ActiveCfg = Production|Any CPU + {9AB969B5-4215-4ACF-8D48-EC0A6F35BC46}.Production|Any CPU.Build.0 = Production|Any CPU + {9AB969B5-4215-4ACF-8D48-EC0A6F35BC46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AB969B5-4215-4ACF-8D48-EC0A6F35BC46}.Release|Any CPU.Build.0 = Release|Any CPU + {0535D1F2-FA8B-4093-9987-7533F8D07605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0535D1F2-FA8B-4093-9987-7533F8D07605}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0535D1F2-FA8B-4093-9987-7533F8D07605}.Production|Any CPU.ActiveCfg = Production|Any CPU + {0535D1F2-FA8B-4093-9987-7533F8D07605}.Production|Any CPU.Build.0 = Production|Any CPU + {0535D1F2-FA8B-4093-9987-7533F8D07605}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0535D1F2-FA8B-4093-9987-7533F8D07605}.Release|Any CPU.Build.0 = Release|Any CPU + {2E71D2B2-516D-4B0D-8DE6-B9F3105B9C95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E71D2B2-516D-4B0D-8DE6-B9F3105B9C95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E71D2B2-516D-4B0D-8DE6-B9F3105B9C95}.Production|Any CPU.ActiveCfg = Production|Any CPU + {2E71D2B2-516D-4B0D-8DE6-B9F3105B9C95}.Production|Any CPU.Build.0 = Production|Any CPU + {2E71D2B2-516D-4B0D-8DE6-B9F3105B9C95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E71D2B2-516D-4B0D-8DE6-B9F3105B9C95}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -440,6 +500,15 @@ Global {165E76B9-DB0C-49B7-B3DC-52DFBEA55A79} = {C733D087-7051-4E35-BCDB-081252A108E5} {00B904C6-D29A-4F26-B7AD-116C701DB73F} = {BC9DDFD1-FB81-4996-812A-68BEBCA33A97} {1853847F-9988-43A1-B3E1-DDBE4B2F3365} = {8B08A9EE-CE27-4CC3-ACB3-3BD9628E5479} + {8F0598A5-2F0C-4FA6-82F6-938F1830ADB7} = {BCE1EE3C-ADB1-48CC-9FD1-C7324D886964} + {3D5E4893-E48A-4553-B036-724A8F809656} = {8F0598A5-2F0C-4FA6-82F6-938F1830ADB7} + {98CE491C-8A52-4FC9-87BD-36FE63CB37E6} = {8F0598A5-2F0C-4FA6-82F6-938F1830ADB7} + {5F24D649-5684-458E-8C67-CCEC85E271A0} = {8F0598A5-2F0C-4FA6-82F6-938F1830ADB7} + {646E463D-F0E2-4BA4-9B5E-434ABE26EC07} = {8F0598A5-2F0C-4FA6-82F6-938F1830ADB7} + {96639493-5D2D-4F61-B399-600673D6912D} = {646E463D-F0E2-4BA4-9B5E-434ABE26EC07} + {9AB969B5-4215-4ACF-8D48-EC0A6F35BC46} = {646E463D-F0E2-4BA4-9B5E-434ABE26EC07} + {0535D1F2-FA8B-4093-9987-7533F8D07605} = {646E463D-F0E2-4BA4-9B5E-434ABE26EC07} + {2E71D2B2-516D-4B0D-8DE6-B9F3105B9C95} = {8F0598A5-2F0C-4FA6-82F6-938F1830ADB7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6B94C21A-AA6D-4D82-963E-C69C0353B938} diff --git a/src/Database/CompanyName.MyMeetings.Database/CompanyName.MyMeetings.Database.sqlproj b/src/Database/CompanyName.MyMeetings.Database/CompanyName.MyMeetings.Database.sqlproj index 350ad2505..de55659c8 100644 --- a/src/Database/CompanyName.MyMeetings.Database/CompanyName.MyMeetings.Database.sqlproj +++ b/src/Database/CompanyName.MyMeetings.Database/CompanyName.MyMeetings.Database.sqlproj @@ -69,7 +69,6 @@ - @@ -89,6 +88,9 @@ + + + @@ -139,17 +141,17 @@ + + - - diff --git a/src/Database/CompanyName.MyMeetings.Database/Scripts/ClearDatabase.sql b/src/Database/CompanyName.MyMeetings.Database/Scripts/ClearDatabase.sql index 70a87f9e1..bf81acd09 100644 --- a/src/Database/CompanyName.MyMeetings.Database/Scripts/ClearDatabase.sql +++ b/src/Database/CompanyName.MyMeetings.Database/Scripts/ClearDatabase.sql @@ -66,12 +66,12 @@ DELETE FROM [users].[InternalCommands] DELETE FROM [users].[OutboxMessages] -DELETE FROM [users].[UserRegistrations] - DELETE FROM [users].[Users] DELETE FROM [users].[RolesToPermissions] DELETE FROM [users].[UserRoles] -DELETE FROM [users].[Permissions] \ No newline at end of file +DELETE FROM [users].[Permissions] + +DELETE FROM [registrations].[UserRegistrations] \ No newline at end of file diff --git a/src/Database/CompanyName.MyMeetings.Database/Scripts/CreateStructure.sql b/src/Database/CompanyName.MyMeetings.Database/Scripts/CreateStructure.sql index a28aaeb1a..09b30353b 100644 --- a/src/Database/CompanyName.MyMeetings.Database/Scripts/CreateStructure.sql +++ b/src/Database/CompanyName.MyMeetings.Database/Scripts/CreateStructure.sql @@ -35,6 +35,16 @@ CREATE SCHEMA [users] GO +PRINT N'Creating [registrations]...'; + + +GO +CREATE SCHEMA [registrations] + AUTHORIZATION [dbo]; + + +GO + PRINT N'Creating [payments].[NewStreamMessages]...'; @@ -611,6 +621,19 @@ CREATE TABLE [users].[InboxMessages] ( CONSTRAINT [PK_users_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); +PRINT N'Creating [registrations].[InboxMessages]...'; + + +GO +CREATE TABLE [registrations].[InboxMessages] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OccurredOn] DATETIME2 (7) NOT NULL, + [Type] VARCHAR (255) NOT NULL, + [Data] VARCHAR (MAX) NOT NULL, + [ProcessedDate] DATETIME2 (7) NULL, + CONSTRAINT [PK_registrations_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) + ); + GO PRINT N'Creating [users].[UserRoles]...'; @@ -624,11 +647,11 @@ CREATE TABLE [users].[UserRoles] ( GO -PRINT N'Creating [users].[UserRegistrations]...'; +PRINT N'Creating [registrations].[UserRegistrations]...'; GO -CREATE TABLE [users].[UserRegistrations] ( +CREATE TABLE [registrations].[UserRegistrations] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, @@ -639,7 +662,7 @@ CREATE TABLE [users].[UserRegistrations] ( [StatusCode] VARCHAR (50) NOT NULL, [RegisterDate] DATETIME NOT NULL, [ConfirmedDate] DATETIME NULL, - CONSTRAINT [PK_users_UserRegistrations_Id] PRIMARY KEY CLUSTERED ([Id] ASC) + CONSTRAINT [PK_registrations_UserRegistrations_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); @@ -703,6 +726,24 @@ CREATE TABLE [users].[InternalCommands] ( GO + +PRINT N'Creating [registrations].[InternalCommands]...'; + + +GO +CREATE TABLE [registrations].[InternalCommands] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [EnqueueDate] DATETIME2 (7) NOT NULL, + [Type] VARCHAR (255) NOT NULL, + [Data] VARCHAR (MAX) NOT NULL, + [ProcessedDate] DATETIME2 (7) NULL, + [Error] NVARCHAR (MAX) NULL, + CONSTRAINT [PK_registrations_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) + ); + + +GO + PRINT N'Creating [users].[OutboxMessages]...'; @@ -716,6 +757,19 @@ CREATE TABLE [users].[OutboxMessages] ( CONSTRAINT [PK_users_OutboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); +PRINT N'Creating [registrations].[OutboxMessages]...'; + + +GO +CREATE TABLE [registrations].[OutboxMessages] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OccurredOn] DATETIME2 (7) NOT NULL, + [Type] VARCHAR (255) NOT NULL, + [Data] VARCHAR (MAX) NOT NULL, + [ProcessedDate] DATETIME2 (7) NULL, + CONSTRAINT [PK_users_OutboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) + ); + GO PRINT N'Creating [payments].[DF_payments_Streams_Version]...'; @@ -882,7 +936,7 @@ SELECT [User].[Name] FROM [users].[Users] AS [User] GO -PRINT N'Creating [users].[v_UserRegistrations]...'; +PRINT N'Creating [registrations].[v_UserRegistrations]...'; GO @@ -896,9 +950,9 @@ SELECT [UserRegistration].[LastName], [UserRegistration].[Name], [UserRegistration].[StatusCode] -FROM [users].[UserRegistrations] AS [UserRegistration] +FROM [registrations].[UserRegistrations] AS [UserRegistration] GO -PRINT N'Creating [users].[v_UserPermissions]...'; +PRINT N'Creating [registrations].[v_UserPermissions]...'; GO diff --git a/src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0001_initial_structure.sql b/src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0001_initial_structure.sql index 9b817eb51..562db7b92 100644 --- a/src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0001_initial_structure.sql +++ b/src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0001_initial_structure.sql @@ -34,6 +34,15 @@ CREATE SCHEMA [users] AUTHORIZATION [dbo]; +GO +PRINT N'Creating [registrations]...'; + + +GO +CREATE SCHEMA [registrations] + AUTHORIZATION [dbo]; + + GO PRINT N'Creating [payments].[NewStreamMessages]...'; @@ -637,11 +646,11 @@ CREATE TABLE [users].[UserRoles] ( GO -PRINT N'Creating [users].[UserRegistrations]...'; +PRINT N'Creating [registrations].[UserRegistrations]...'; GO -CREATE TABLE [users].[UserRegistrations] ( +CREATE TABLE [registrations].[UserRegistrations] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, @@ -652,7 +661,7 @@ CREATE TABLE [users].[UserRegistrations] ( [StatusCode] VARCHAR (50) NOT NULL, [RegisterDate] DATETIME NOT NULL, [ConfirmedDate] DATETIME NULL, - CONSTRAINT [PK_users_UserRegistrations_Id] PRIMARY KEY CLUSTERED ([Id] ASC) + CONSTRAINT [PK_registrations_UserRegistrations_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); @@ -869,7 +878,7 @@ SELECT [User].[Name] FROM [users].[Users] AS [User] GO -PRINT N'Creating [users].[v_UserRegistrations]...'; +PRINT N'Creating [registrations].[v_UserRegistrations]...'; GO @@ -883,7 +892,7 @@ SELECT [UserRegistration].[LastName], [UserRegistration].[Name], [UserRegistration].[StatusCode] -FROM [users].[UserRegistrations] AS [UserRegistration] +FROM [registrations].[UserRegistrations] AS [UserRegistration] GO PRINT N'Creating [users].[v_UserPermissions]...'; diff --git a/src/Database/CompanyName.MyMeetings.Database/Scripts/SeedDatabase.sql b/src/Database/CompanyName.MyMeetings.Database/Scripts/SeedDatabase.sql index bc7cf1ba7..ea57a6bae 100644 --- a/src/Database/CompanyName.MyMeetings.Database/Scripts/SeedDatabase.sql +++ b/src/Database/CompanyName.MyMeetings.Database/Scripts/SeedDatabase.sql @@ -1,5 +1,5 @@ -- Add Test Member -INSERT INTO users.UserRegistrations VALUES +INSERT INTO registrations.UserRegistrations VALUES ( '2EBFECFC-ED13-43B8-B516-6AC89D51C510', 'testMember@mail.com', @@ -49,7 +49,7 @@ INSERT INTO users.UserRoles VALUES ('2EBFECFC-ED13-43B8-B516-6AC89D51C510', 'Member') -- Add Test Administrator -INSERT INTO users.UserRegistrations VALUES +INSERT INTO registrations.UserRegistrations VALUES ( '4065630E-4A4C-4F01-9142-0BACF6B8C64D', 'testAdmin@mail.com', diff --git a/src/Database/CompanyName.MyMeetings.Database/Structure/Security/Schemas.sql b/src/Database/CompanyName.MyMeetings.Database/Structure/Security/Schemas.sql index 18d3cd7f8..6f8668bcb 100644 --- a/src/Database/CompanyName.MyMeetings.Database/Structure/Security/Schemas.sql +++ b/src/Database/CompanyName.MyMeetings.Database/Structure/Security/Schemas.sql @@ -10,5 +10,8 @@ GO CREATE SCHEMA users AUTHORIZATION dbo GO +CREATE SCHEMA registrations AUTHORIZATION dbo +GO + CREATE SCHEMA payments AUTHORIZATION dbo GO \ No newline at end of file diff --git a/src/Database/CompanyName.MyMeetings.Database/Structure/users/Tables/UserRegistrations.sql b/src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Tables/UserRegistrations.sql similarity index 74% rename from src/Database/CompanyName.MyMeetings.Database/Structure/users/Tables/UserRegistrations.sql rename to src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Tables/UserRegistrations.sql index 0276e53fa..ce66d494c 100644 --- a/src/Database/CompanyName.MyMeetings.Database/Structure/users/Tables/UserRegistrations.sql +++ b/src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Tables/UserRegistrations.sql @@ -1,4 +1,4 @@ -CREATE TABLE [users].[UserRegistrations] +CREATE TABLE [registrations].[UserRegistrations] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR(100) NOT NULL, @@ -10,6 +10,6 @@ [StatusCode] VARCHAR(50) NOT NULL, [RegisterDate] DATETIME NOT NULL, [ConfirmedDate] DATETIME NULL, - CONSTRAINT [PK_users_UserRegistrations_Id] PRIMARY KEY ([Id] ASC) + CONSTRAINT [PK_registrations_UserRegistrations_Id] PRIMARY KEY ([Id] ASC) ) GO \ No newline at end of file diff --git a/src/Database/CompanyName.MyMeetings.Database/Structure/users/Views/v_UserRegistrations.sql b/src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Views/v_UserRegistrations.sql similarity index 67% rename from src/Database/CompanyName.MyMeetings.Database/Structure/users/Views/v_UserRegistrations.sql rename to src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Views/v_UserRegistrations.sql index 0d17d8965..68898d161 100644 --- a/src/Database/CompanyName.MyMeetings.Database/Structure/users/Views/v_UserRegistrations.sql +++ b/src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Views/v_UserRegistrations.sql @@ -1,4 +1,4 @@ -CREATE VIEW [users].[v_UserRegistrations] +CREATE VIEW [registrations].[v_UserRegistrations] AS SELECT [UserRegistration].[Id], @@ -8,5 +8,5 @@ SELECT [UserRegistration].[LastName], [UserRegistration].[Name], [UserRegistration].[StatusCode] -FROM [users].[UserRegistrations] AS [UserRegistration] +FROM [registrations].[UserRegistrations] AS [UserRegistration] GO \ No newline at end of file diff --git a/src/Modules/Administration/Application/CompanyName.MyMeetings.Modules.Administration.Application.csproj b/src/Modules/Administration/Application/CompanyName.MyMeetings.Modules.Administration.Application.csproj index 8f337a235..39e559ede 100644 --- a/src/Modules/Administration/Application/CompanyName.MyMeetings.Modules.Administration.Application.csproj +++ b/src/Modules/Administration/Application/CompanyName.MyMeetings.Modules.Administration.Application.csproj @@ -1,6 +1,7 @@  + diff --git a/src/Modules/Administration/Application/Members/NewUserRegisteredIntegrationEventHandler.cs b/src/Modules/Administration/Application/Members/NewUserRegisteredIntegrationEventHandler.cs index 4455f317c..2608a66c2 100644 --- a/src/Modules/Administration/Application/Members/NewUserRegisteredIntegrationEventHandler.cs +++ b/src/Modules/Administration/Application/Members/NewUserRegisteredIntegrationEventHandler.cs @@ -1,6 +1,6 @@ using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Application.Members.CreateMember; -using CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents; +using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Administration.Application.Members diff --git a/src/Modules/Administration/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs b/src/Modules/Administration/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs index 7b18b98cc..aa15cf428 100644 --- a/src/Modules/Administration/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs +++ b/src/Modules/Administration/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs @@ -1,7 +1,7 @@ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents; -using CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents; +using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using Serilog; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.EventsBus diff --git a/src/Modules/Meetings/Application/CompanyName.MyMeetings.Modules.Meetings.Application.csproj b/src/Modules/Meetings/Application/CompanyName.MyMeetings.Modules.Meetings.Application.csproj index 717ad07d6..c7ae8e6bb 100644 --- a/src/Modules/Meetings/Application/CompanyName.MyMeetings.Modules.Meetings.Application.csproj +++ b/src/Modules/Meetings/Application/CompanyName.MyMeetings.Modules.Meetings.Application.csproj @@ -2,6 +2,7 @@ + diff --git a/src/Modules/Meetings/Application/Members/CreateMember/NewUserRegisteredIntegrationEventHandler.cs b/src/Modules/Meetings/Application/Members/CreateMember/NewUserRegisteredIntegrationEventHandler.cs index 8b55061c2..d9e214a69 100644 --- a/src/Modules/Meetings/Application/Members/CreateMember/NewUserRegisteredIntegrationEventHandler.cs +++ b/src/Modules/Meetings/Application/Members/CreateMember/NewUserRegisteredIntegrationEventHandler.cs @@ -1,5 +1,5 @@ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; -using CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents; +using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Members.CreateMember diff --git a/src/Modules/Meetings/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs b/src/Modules/Meetings/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs index ce0691773..7908adb12 100644 --- a/src/Modules/Meetings/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs +++ b/src/Modules/Meetings/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs @@ -2,7 +2,7 @@ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Administration.IntegrationEvents.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Payments.IntegrationEvents; -using CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents; +using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using Serilog; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.EventsBus diff --git a/src/Modules/Payments/Application/CompanyName.MyMeetings.Modules.Payments.Application.csproj b/src/Modules/Payments/Application/CompanyName.MyMeetings.Modules.Payments.Application.csproj index aa2b1916f..337bec1cd 100644 --- a/src/Modules/Payments/Application/CompanyName.MyMeetings.Modules.Payments.Application.csproj +++ b/src/Modules/Payments/Application/CompanyName.MyMeetings.Modules.Payments.Application.csproj @@ -2,6 +2,7 @@ + diff --git a/src/Modules/Payments/Application/Payers/CreatePayer/NewUserRegisteredIntegrationEventHandler.cs b/src/Modules/Payments/Application/Payers/CreatePayer/NewUserRegisteredIntegrationEventHandler.cs index bd1a6df0a..0b4eea572 100644 --- a/src/Modules/Payments/Application/Payers/CreatePayer/NewUserRegisteredIntegrationEventHandler.cs +++ b/src/Modules/Payments/Application/Payers/CreatePayer/NewUserRegisteredIntegrationEventHandler.cs @@ -1,5 +1,5 @@ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; -using CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents; +using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.Payers.CreatePayer diff --git a/src/Modules/Payments/Infrastructure/AggregateStore/SubscriptionCode.cs b/src/Modules/Payments/Infrastructure/AggregateStore/SubscriptionCode.cs index d8d2a8198..be3f90ca6 100644 --- a/src/Modules/Payments/Infrastructure/AggregateStore/SubscriptionCode.cs +++ b/src/Modules/Payments/Infrastructure/AggregateStore/SubscriptionCode.cs @@ -2,6 +2,9 @@ { public enum SubscriptionCode { + /// + /// All. + /// All } } \ No newline at end of file diff --git a/src/Modules/Payments/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs b/src/Modules/Payments/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs index 5c448de6d..c09d646e5 100644 --- a/src/Modules/Payments/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs +++ b/src/Modules/Payments/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs @@ -2,7 +2,7 @@ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Administration.IntegrationEvents.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents; -using CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents; +using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using Serilog; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.EventsBus diff --git a/src/Modules/Registrations/Application/CompanyName.MyMeetings.Modules.Registrations.Application.csproj b/src/Modules/Registrations/Application/CompanyName.MyMeetings.Modules.Registrations.Application.csproj new file mode 100644 index 000000000..b95ffafd9 --- /dev/null +++ b/src/Modules/Registrations/Application/CompanyName.MyMeetings.Modules.Registrations.Application.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Modules/Registrations/Application/Configuration/Commands/ICommandHandler.cs b/src/Modules/Registrations/Application/Configuration/Commands/ICommandHandler.cs new file mode 100644 index 000000000..37397f205 --- /dev/null +++ b/src/Modules/Registrations/Application/Configuration/Commands/ICommandHandler.cs @@ -0,0 +1,16 @@ +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using MediatR; + +namespace CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands +{ + public interface ICommandHandler : IRequestHandler + where TCommand : ICommand + { + } + + public interface ICommandHandler : + IRequestHandler + where TCommand : ICommand + { + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/Configuration/Commands/ICommandsScheduler.cs b/src/Modules/Registrations/Application/Configuration/Commands/ICommandsScheduler.cs new file mode 100644 index 000000000..1d947e876 --- /dev/null +++ b/src/Modules/Registrations/Application/Configuration/Commands/ICommandsScheduler.cs @@ -0,0 +1,11 @@ +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; + +namespace CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands +{ + public interface ICommandsScheduler + { + Task EnqueueAsync(ICommand command); + + Task EnqueueAsync(ICommand command); + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/Configuration/Commands/InternalCommandBase.cs b/src/Modules/Registrations/Application/Configuration/Commands/InternalCommandBase.cs new file mode 100644 index 000000000..32efe22ff --- /dev/null +++ b/src/Modules/Registrations/Application/Configuration/Commands/InternalCommandBase.cs @@ -0,0 +1,29 @@ +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; + +namespace CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands +{ + public abstract class InternalCommandBase : ICommand + { + protected InternalCommandBase(Guid id) + { + Id = id; + } + + public Guid Id { get; } + } + + public abstract class InternalCommandBase : ICommand + { + protected InternalCommandBase() + { + Id = Guid.NewGuid(); + } + + protected InternalCommandBase(Guid id) + { + Id = id; + } + + public Guid Id { get; } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/Configuration/Queries/IQueryHandler.cs b/src/Modules/Registrations/Application/Configuration/Queries/IQueryHandler.cs new file mode 100644 index 000000000..bca01087c --- /dev/null +++ b/src/Modules/Registrations/Application/Configuration/Queries/IQueryHandler.cs @@ -0,0 +1,11 @@ +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using MediatR; + +namespace CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Queries +{ + public interface IQueryHandler : + IRequestHandler + where TQuery : IQuery + { + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/Contracts/CommandBase.cs b/src/Modules/Registrations/Application/Contracts/CommandBase.cs new file mode 100644 index 000000000..5ecb15285 --- /dev/null +++ b/src/Modules/Registrations/Application/Contracts/CommandBase.cs @@ -0,0 +1,32 @@ +namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts +{ + public abstract class CommandBase : ICommand + { + public Guid Id { get; } + + protected CommandBase() + { + Id = Guid.NewGuid(); + } + + protected CommandBase(Guid id) + { + Id = id; + } + } + + public abstract class CommandBase : ICommand + { + protected CommandBase() + { + Id = Guid.NewGuid(); + } + + protected CommandBase(Guid id) + { + Id = id; + } + + public Guid Id { get; } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/Contracts/CustomClaimTypes.cs b/src/Modules/Registrations/Application/Contracts/CustomClaimTypes.cs new file mode 100644 index 000000000..364c44f25 --- /dev/null +++ b/src/Modules/Registrations/Application/Contracts/CustomClaimTypes.cs @@ -0,0 +1,9 @@ +namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts +{ + public class CustomClaimTypes + { + public const string Roles = "roles"; + public const string Email = "email"; + public const string Name = "name"; + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/Contracts/ICommand.cs b/src/Modules/Registrations/Application/Contracts/ICommand.cs new file mode 100644 index 000000000..26491c355 --- /dev/null +++ b/src/Modules/Registrations/Application/Contracts/ICommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts +{ + public interface ICommand : IRequest + { + Guid Id { get; } + } + + public interface ICommand : IRequest + { + Guid Id { get; } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/Contracts/IQuery.cs b/src/Modules/Registrations/Application/Contracts/IQuery.cs new file mode 100644 index 000000000..cc468265e --- /dev/null +++ b/src/Modules/Registrations/Application/Contracts/IQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts +{ + public interface IQuery : IRequest + { + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/Contracts/IRecurringCommand.cs b/src/Modules/Registrations/Application/Contracts/IRecurringCommand.cs new file mode 100644 index 000000000..bb2be0ddd --- /dev/null +++ b/src/Modules/Registrations/Application/Contracts/IRecurringCommand.cs @@ -0,0 +1,6 @@ +namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts +{ + public interface IRecurringCommand + { + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/Contracts/IRegistrationsModule.cs b/src/Modules/Registrations/Application/Contracts/IRegistrationsModule.cs new file mode 100644 index 000000000..5325e30c6 --- /dev/null +++ b/src/Modules/Registrations/Application/Contracts/IRegistrationsModule.cs @@ -0,0 +1,11 @@ +namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts +{ + public interface IRegistrationsModule + { + Task ExecuteCommandAsync(ICommand command); + + Task ExecuteCommandAsync(ICommand command); + + Task ExecuteQueryAsync(IQuery query); + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/Contracts/QueryBase.cs b/src/Modules/Registrations/Application/Contracts/QueryBase.cs new file mode 100644 index 000000000..956866620 --- /dev/null +++ b/src/Modules/Registrations/Application/Contracts/QueryBase.cs @@ -0,0 +1,17 @@ +namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts +{ + public abstract class QueryBase : IQuery + { + public Guid Id { get; } + + protected QueryBase() + { + Id = Guid.NewGuid(); + } + + protected QueryBase(Guid id) + { + Id = id; + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/Contracts/Roles.cs b/src/Modules/Registrations/Application/Contracts/Roles.cs new file mode 100644 index 000000000..af075d176 --- /dev/null +++ b/src/Modules/Registrations/Application/Contracts/Roles.cs @@ -0,0 +1,8 @@ +namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts +{ + public class Roles + { + public const string Admin = "Admin"; + public const string User = "User"; + } +} \ No newline at end of file diff --git a/src/Modules/UserAccess/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommand.cs b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommand.cs similarity index 59% rename from src/Modules/UserAccess/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommand.cs rename to src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommand.cs index 2bc0fd088..0ec763f33 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommand.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommand.cs @@ -1,6 +1,6 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.ConfirmUserRegistration +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration { public class ConfirmUserRegistrationCommand : CommandBase { diff --git a/src/Modules/UserAccess/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommandHandler.cs b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommandHandler.cs similarity index 71% rename from src/Modules/UserAccess/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommandHandler.cs rename to src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommandHandler.cs index 48598696c..1aedd5d4f 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommandHandler.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommandHandler.cs @@ -1,23 +1,23 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; - -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.ConfirmUserRegistration -{ - internal class ConfirmUserRegistrationCommandHandler : ICommandHandler - { - private readonly IUserRegistrationRepository _userRegistrationRepository; - - public ConfirmUserRegistrationCommandHandler(IUserRegistrationRepository userRegistrationRepository) - { - _userRegistrationRepository = userRegistrationRepository; - } - - public async Task Handle(ConfirmUserRegistrationCommand request, CancellationToken cancellationToken) - { - var userRegistration = - await _userRegistrationRepository.GetByIdAsync(new UserRegistrationId(request.UserRegistrationId)); - - userRegistration.Confirm(); - } - } +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; + +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration +{ + internal class ConfirmUserRegistrationCommandHandler : ICommandHandler + { + private readonly IUserRegistrationRepository _userRegistrationRepository; + + public ConfirmUserRegistrationCommandHandler(IUserRegistrationRepository userRegistrationRepository) + { + _userRegistrationRepository = userRegistrationRepository; + } + + public async Task Handle(ConfirmUserRegistrationCommand request, CancellationToken cancellationToken) + { + var userRegistration = + await _userRegistrationRepository.GetByIdAsync(new UserRegistrationId(request.UserRegistrationId)); + + userRegistration.Confirm(); + } + } } \ No newline at end of file diff --git a/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedHandler.cs b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedHandler.cs new file mode 100644 index 000000000..f34fb675f --- /dev/null +++ b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedHandler.cs @@ -0,0 +1,27 @@ +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events; +using MediatR; + +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration +{ + public class UserRegistrationConfirmedHandler : INotificationHandler + { + private readonly IUserRegistrationRepository _userRegistrationRepository; + + public UserRegistrationConfirmedHandler( + IUserRegistrationRepository userRegistrationRepository) + { + _userRegistrationRepository = userRegistrationRepository; + } + + public Task Handle(UserRegistrationConfirmedDomainEvent @event, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + //// var userRegistration = await _userRegistrationRepository.GetByIdAsync(@event.UserRegistrationId); + // + // var user = userRegistration.CreateUser(); + // + // await _userRepository.AddAsync(user); + } + } +} \ No newline at end of file diff --git a/src/Modules/UserAccess/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQuery.cs b/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQuery.cs similarity index 60% rename from src/Modules/UserAccess/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQuery.cs rename to src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQuery.cs index 21225e233..3ec84e092 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQuery.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQuery.cs @@ -1,6 +1,6 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.GetUserRegistration +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.GetUserRegistration { public class GetUserRegistrationQuery : QueryBase { diff --git a/src/Modules/UserAccess/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQueryHandler.cs b/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQueryHandler.cs similarity index 86% rename from src/Modules/UserAccess/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQueryHandler.cs rename to src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQueryHandler.cs index 3bf7cfe4d..2fb53b4a5 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQueryHandler.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQueryHandler.cs @@ -1,8 +1,8 @@ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; -using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Queries; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Queries; using Dapper; -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.GetUserRegistration +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.GetUserRegistration { internal class GetUserRegistrationQueryHandler : IQueryHandler { @@ -26,7 +26,7 @@ public async Task Handle(GetUserRegistrationQuery query, Ca [UserRegistration].[LastName] as [{nameof(UserRegistrationDto.LastName)}], [UserRegistration].[Name] as [{nameof(UserRegistrationDto.Name)}], [UserRegistration].[StatusCode] as [{nameof(UserRegistrationDto.StatusCode)}] - FROM [users].[v_UserRegistrations] AS [UserRegistration] + FROM [registrations].[v_UserRegistrations] AS [UserRegistration] WHERE [UserRegistration].[Id] = @UserRegistrationId """; diff --git a/src/Modules/UserAccess/Application/UserRegistrations/GetUserRegistration/UserRegistrationDto.cs b/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationDto.cs similarity index 76% rename from src/Modules/UserAccess/Application/UserRegistrations/GetUserRegistration/UserRegistrationDto.cs rename to src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationDto.cs index 091015d5f..ba8acb0d9 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/GetUserRegistration/UserRegistrationDto.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationDto.cs @@ -1,4 +1,4 @@ -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.GetUserRegistration +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.GetUserRegistration { public class UserRegistrationDto { diff --git a/src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredEnqueueEmailConfirmationHandler.cs b/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredEnqueueEmailConfirmationHandler.cs similarity index 72% rename from src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredEnqueueEmailConfirmationHandler.cs rename to src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredEnqueueEmailConfirmationHandler.cs index b093b5d57..d5003c4d6 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredEnqueueEmailConfirmationHandler.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredEnqueueEmailConfirmationHandler.cs @@ -1,8 +1,8 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.SendUserRegistrationConfirmationEmail; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.SendUserRegistrationConfirmationEmail; using MediatR; -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser { public class NewUserRegisteredEnqueueEmailConfirmationHandler : INotificationHandler { diff --git a/src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredNotification.cs b/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredNotification.cs similarity index 67% rename from src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredNotification.cs rename to src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredNotification.cs index d5dfd9fee..f89956bce 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredNotification.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredNotification.cs @@ -1,8 +1,8 @@ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Events; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events; using Newtonsoft.Json; -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser { public class NewUserRegisteredNotification : DomainNotificationBase { diff --git a/src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredPublishEventHandler.cs b/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredPublishEventHandler.cs similarity index 85% rename from src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredPublishEventHandler.cs rename to src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredPublishEventHandler.cs index 2c41aff0e..5ae73b0e1 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredPublishEventHandler.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredPublishEventHandler.cs @@ -1,8 +1,8 @@ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; -using CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents; +using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using MediatR; -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser { public class NewUserRegisteredPublishEventHandler : INotificationHandler { diff --git a/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/PasswordManager.cs b/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/PasswordManager.cs new file mode 100644 index 000000000..aef993fc6 --- /dev/null +++ b/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/PasswordManager.cs @@ -0,0 +1,82 @@ +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser +{ + public class PasswordManager + { + public static string HashPassword(string password) + { + byte[] salt; + byte[] buffer2; + if (password == null) + { + throw new ArgumentNullException(nameof(password)); + } + + using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, 0x10, 0x3e8, HashAlgorithmName.SHA256)) + { + salt = bytes.Salt; + buffer2 = bytes.GetBytes(0x20); + } + + byte[] dst = new byte[0x31]; + Buffer.BlockCopy(salt, 0, dst, 1, 0x10); + Buffer.BlockCopy(buffer2, 0, dst, 0x11, 0x20); + return Convert.ToBase64String(dst); + } + + public static bool VerifyHashedPassword(string hashedPassword, string password) + { + byte[] buffer4; + if (hashedPassword == null) + { + return false; + } + + if (password == null) + { + throw new ArgumentNullException(nameof(password)); + } + + byte[] src = Convert.FromBase64String(hashedPassword); + if ((src.Length != 0x31) || (src[0] != 0)) + { + return false; + } + + byte[] dst = new byte[0x10]; + Buffer.BlockCopy(src, 1, dst, 0, 0x10); + byte[] buffer3 = new byte[0x20]; + Buffer.BlockCopy(src, 0x11, buffer3, 0, 0x20); + using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, dst, 0x3e8, HashAlgorithmName.SHA256)) + { + buffer4 = bytes.GetBytes(0x20); + } + + return ByteArraysEqual(buffer3, buffer4); + } + + [MethodImpl(MethodImplOptions.NoOptimization)] + private static bool ByteArraysEqual(byte[] a, byte[] b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + if (a == null || b == null || a.Length != b.Length) + { + return false; + } + + var areSame = true; + for (var i = 0; i < a.Length; i++) + { + areSame &= a[i] == b[i]; + } + + return areSame; + } + } +} \ No newline at end of file diff --git a/src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommand.cs b/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommand.cs similarity index 80% rename from src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommand.cs rename to src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommand.cs index b8257b6b4..e5bea6f3e 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommand.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommand.cs @@ -1,6 +1,6 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser { public class RegisterNewUserCommand : CommandBase { diff --git a/src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommandHandler.cs b/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommandHandler.cs similarity index 77% rename from src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommandHandler.cs rename to src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommandHandler.cs index cbcc313c7..955a0a66c 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommandHandler.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommandHandler.cs @@ -1,8 +1,7 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication; -using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser { internal class RegisterNewUserCommandHandler : ICommandHandler { diff --git a/src/Modules/UserAccess/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommand.cs b/src/Modules/Registrations/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommand.cs similarity index 69% rename from src/Modules/UserAccess/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommand.cs rename to src/Modules/Registrations/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommand.cs index f1e55149e..2437f70ce 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommand.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommand.cs @@ -1,8 +1,8 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; using Newtonsoft.Json; -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.SendUserRegistrationConfirmationEmail +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.SendUserRegistrationConfirmationEmail { public class SendUserRegistrationConfirmationEmailCommand : InternalCommandBase { diff --git a/src/Modules/UserAccess/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommandHandler.cs b/src/Modules/Registrations/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommandHandler.cs similarity index 83% rename from src/Modules/UserAccess/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommandHandler.cs rename to src/Modules/Registrations/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommandHandler.cs index f1bd10b7d..167daa01e 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommandHandler.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommandHandler.cs @@ -1,7 +1,7 @@ using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; -using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.SendUserRegistrationConfirmationEmail +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.SendUserRegistrationConfirmationEmail { internal class SendUserRegistrationConfirmationEmailCommandHandler : ICommandHandler { diff --git a/src/Modules/UserAccess/Application/UserRegistrations/UsersCounter.cs b/src/Modules/Registrations/Application/UserRegistrations/UsersCounter.cs similarity index 84% rename from src/Modules/UserAccess/Application/UserRegistrations/UsersCounter.cs rename to src/Modules/Registrations/Application/UserRegistrations/UsersCounter.cs index 723c97bb2..09550ede0 100644 --- a/src/Modules/UserAccess/Application/UserRegistrations/UsersCounter.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/UsersCounter.cs @@ -1,8 +1,8 @@ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; using Dapper; -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations { public class UsersCounter : IUsersCounter { diff --git a/src/Modules/Registrations/Domain/CompanyName.MyMeetings.Modules.Registrations.Domain.csproj b/src/Modules/Registrations/Domain/CompanyName.MyMeetings.Modules.Registrations.Domain.csproj new file mode 100644 index 000000000..2ef1a363a --- /dev/null +++ b/src/Modules/Registrations/Domain/CompanyName.MyMeetings.Modules.Registrations.Domain.csproj @@ -0,0 +1 @@ + diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/Events/NewUserRegisteredDomainEvent.cs b/src/Modules/Registrations/Domain/UserRegistrations/Events/NewUserRegisteredDomainEvent.cs similarity index 92% rename from src/Modules/UserAccess/Domain/UserRegistrations/Events/NewUserRegisteredDomainEvent.cs rename to src/Modules/Registrations/Domain/UserRegistrations/Events/NewUserRegisteredDomainEvent.cs index 0adce92ed..940ca1abf 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/Events/NewUserRegisteredDomainEvent.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/Events/NewUserRegisteredDomainEvent.cs @@ -1,6 +1,6 @@ using CompanyName.MyMeetings.BuildingBlocks.Domain; -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Events +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events { public class NewUserRegisteredDomainEvent : DomainEventBase { diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/Events/UserRegistrationConfirmedDomainEvent.cs b/src/Modules/Registrations/Domain/UserRegistrations/Events/UserRegistrationConfirmedDomainEvent.cs similarity index 80% rename from src/Modules/UserAccess/Domain/UserRegistrations/Events/UserRegistrationConfirmedDomainEvent.cs rename to src/Modules/Registrations/Domain/UserRegistrations/Events/UserRegistrationConfirmedDomainEvent.cs index deaa90744..f97343ded 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/Events/UserRegistrationConfirmedDomainEvent.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/Events/UserRegistrationConfirmedDomainEvent.cs @@ -1,6 +1,6 @@ using CompanyName.MyMeetings.BuildingBlocks.Domain; -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Events +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events { public class UserRegistrationConfirmedDomainEvent : DomainEventBase { diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/Events/UserRegistrationExpiredDomainEvent.cs b/src/Modules/Registrations/Domain/UserRegistrations/Events/UserRegistrationExpiredDomainEvent.cs similarity index 80% rename from src/Modules/UserAccess/Domain/UserRegistrations/Events/UserRegistrationExpiredDomainEvent.cs rename to src/Modules/Registrations/Domain/UserRegistrations/Events/UserRegistrationExpiredDomainEvent.cs index 9b47582aa..98ddee74d 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/Events/UserRegistrationExpiredDomainEvent.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/Events/UserRegistrationExpiredDomainEvent.cs @@ -1,6 +1,6 @@ using CompanyName.MyMeetings.BuildingBlocks.Domain; -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Events +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events { public class UserRegistrationExpiredDomainEvent : DomainEventBase { diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/IUserRegistrationRepository.cs b/src/Modules/Registrations/Domain/UserRegistrations/IUserRegistrationRepository.cs similarity index 71% rename from src/Modules/UserAccess/Domain/UserRegistrations/IUserRegistrationRepository.cs rename to src/Modules/Registrations/Domain/UserRegistrations/IUserRegistrationRepository.cs index ac797d530..9ea142f39 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/IUserRegistrationRepository.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/IUserRegistrationRepository.cs @@ -1,4 +1,4 @@ -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations { public interface IUserRegistrationRepository { diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/IUsersCounter.cs b/src/Modules/Registrations/Domain/UserRegistrations/IUsersCounter.cs similarity index 53% rename from src/Modules/UserAccess/Domain/UserRegistrations/IUsersCounter.cs rename to src/Modules/Registrations/Domain/UserRegistrations/IUsersCounter.cs index e3451cfea..ba6894a49 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/IUsersCounter.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/IUsersCounter.cs @@ -1,4 +1,4 @@ -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations { public interface IUsersCounter { diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule.cs b/src/Modules/Registrations/Domain/UserRegistrations/Rules/UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule.cs similarity index 88% rename from src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule.cs rename to src/Modules/Registrations/Domain/UserRegistrations/Rules/UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule.cs index 8cca80925..577231dff 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/Rules/UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule.cs @@ -1,6 +1,6 @@ using CompanyName.MyMeetings.BuildingBlocks.Domain; -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Rules +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules { public class UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule : IBusinessRule { diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserLoginMustBeUniqueRule.cs b/src/Modules/Registrations/Domain/UserRegistrations/Rules/UserLoginMustBeUniqueRule.cs similarity index 86% rename from src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserLoginMustBeUniqueRule.cs rename to src/Modules/Registrations/Domain/UserRegistrations/Rules/UserLoginMustBeUniqueRule.cs index 43513e366..f149c789b 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserLoginMustBeUniqueRule.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/Rules/UserLoginMustBeUniqueRule.cs @@ -1,6 +1,6 @@ using CompanyName.MyMeetings.BuildingBlocks.Domain; -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Rules +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules { public class UserLoginMustBeUniqueRule : IBusinessRule { diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedAfterExpirationRule.cs b/src/Modules/Registrations/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedAfterExpirationRule.cs similarity index 88% rename from src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedAfterExpirationRule.cs rename to src/Modules/Registrations/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedAfterExpirationRule.cs index 898214688..46ee436a1 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedAfterExpirationRule.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedAfterExpirationRule.cs @@ -1,6 +1,6 @@ using CompanyName.MyMeetings.BuildingBlocks.Domain; -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Rules +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules { public class UserRegistrationCannotBeConfirmedAfterExpirationRule : IBusinessRule { diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedMoreThanOnceRule.cs b/src/Modules/Registrations/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedMoreThanOnceRule.cs similarity index 87% rename from src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedMoreThanOnceRule.cs rename to src/Modules/Registrations/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedMoreThanOnceRule.cs index ba59993b4..1ce52e5ba 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedMoreThanOnceRule.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedMoreThanOnceRule.cs @@ -1,6 +1,6 @@ using CompanyName.MyMeetings.BuildingBlocks.Domain; -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Rules +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules { public class UserRegistrationCannotBeConfirmedMoreThanOnceRule : IBusinessRule { diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserRegistrationCannotBeExpiredMoreThanOnceRule.cs b/src/Modules/Registrations/Domain/UserRegistrations/Rules/UserRegistrationCannotBeExpiredMoreThanOnceRule.cs similarity index 87% rename from src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserRegistrationCannotBeExpiredMoreThanOnceRule.cs rename to src/Modules/Registrations/Domain/UserRegistrations/Rules/UserRegistrationCannotBeExpiredMoreThanOnceRule.cs index 478d6fc07..1ca1a8387 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/Rules/UserRegistrationCannotBeExpiredMoreThanOnceRule.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/Rules/UserRegistrationCannotBeExpiredMoreThanOnceRule.cs @@ -1,6 +1,6 @@ using CompanyName.MyMeetings.BuildingBlocks.Domain; -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Rules +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules { public class UserRegistrationCannotBeExpiredMoreThanOnceRule : IBusinessRule { diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/UserRegistration.cs b/src/Modules/Registrations/Domain/UserRegistrations/UserRegistration.cs similarity index 79% rename from src/Modules/UserAccess/Domain/UserRegistrations/UserRegistration.cs rename to src/Modules/Registrations/Domain/UserRegistrations/UserRegistration.cs index c7c261cb3..ab1f70ec3 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/UserRegistration.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/UserRegistration.cs @@ -1,9 +1,8 @@ using CompanyName.MyMeetings.BuildingBlocks.Domain; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Events; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Rules; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules; -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations { public class UserRegistration : Entity, IAggregateRoot { @@ -76,20 +75,6 @@ private UserRegistration( confirmLink)); } - public User CreateUser() - { - this.CheckRule(new UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule(_status)); - - return User.CreateFromUserRegistration( - this.Id, - this._login, - this._password, - this._email, - this._firstName, - this._lastName, - this._name); - } - public void Confirm() { this.CheckRule(new UserRegistrationCannotBeConfirmedMoreThanOnceRule(_status)); diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/UserRegistrationId.cs b/src/Modules/Registrations/Domain/UserRegistrations/UserRegistrationId.cs similarity index 73% rename from src/Modules/UserAccess/Domain/UserRegistrations/UserRegistrationId.cs rename to src/Modules/Registrations/Domain/UserRegistrations/UserRegistrationId.cs index 3cb893ef6..ebb1882cd 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/UserRegistrationId.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/UserRegistrationId.cs @@ -1,6 +1,6 @@ using CompanyName.MyMeetings.BuildingBlocks.Domain; -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations { public class UserRegistrationId : TypedIdValueBase { diff --git a/src/Modules/UserAccess/Domain/UserRegistrations/UserRegistrationStatus.cs b/src/Modules/Registrations/Domain/UserRegistrations/UserRegistrationStatus.cs similarity index 88% rename from src/Modules/UserAccess/Domain/UserRegistrations/UserRegistrationStatus.cs rename to src/Modules/Registrations/Domain/UserRegistrations/UserRegistrationStatus.cs index 7a29fc5b4..7006747ba 100644 --- a/src/Modules/UserAccess/Domain/UserRegistrations/UserRegistrationStatus.cs +++ b/src/Modules/Registrations/Domain/UserRegistrations/UserRegistrationStatus.cs @@ -1,6 +1,6 @@ using CompanyName.MyMeetings.BuildingBlocks.Domain; -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations { public class UserRegistrationStatus : ValueObject { diff --git a/src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj b/src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj new file mode 100644 index 000000000..8a81420d9 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/AllConstructorFinder.cs b/src/Modules/Registrations/Infrastructure/Configuration/AllConstructorFinder.cs new file mode 100644 index 000000000..455ee8fcf --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/AllConstructorFinder.cs @@ -0,0 +1,21 @@ +using System.Collections.Concurrent; +using System.Reflection; +using Autofac.Core.Activators.Reflection; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration +{ + internal class AllConstructorFinder : IConstructorFinder + { + private static readonly ConcurrentDictionary Cache = + new ConcurrentDictionary(); + + public ConstructorInfo[] FindConstructors(Type targetType) + { + var result = Cache.GetOrAdd( + targetType, + t => t.GetTypeInfo().DeclaredConstructors.ToArray()); + + return result.Length > 0 ? result : throw new NoConstructorsFoundException(targetType, this); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Assemblies.cs b/src/Modules/Registrations/Infrastructure/Configuration/Assemblies.cs new file mode 100644 index 000000000..0f23c6e6a --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Assemblies.cs @@ -0,0 +1,10 @@ +using System.Reflection; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration +{ + internal static class Assemblies + { + public static readonly Assembly Application = typeof(IRegistrationsModule).Assembly; + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Commands/ICommandHandler.cs b/src/Modules/Registrations/Infrastructure/Configuration/Commands/ICommandHandler.cs new file mode 100644 index 000000000..2f6b0ae04 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Commands/ICommandHandler.cs @@ -0,0 +1,16 @@ +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using MediatR; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Commands +{ + public interface ICommandHandler : IRequestHandler + where TCommand : ICommand + { + } + + public interface ICommandHandler : + IRequestHandler + where TCommand : ICommand + { + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Commands/ICommandsScheduler.cs b/src/Modules/Registrations/Infrastructure/Configuration/Commands/ICommandsScheduler.cs new file mode 100644 index 000000000..0c7cc406a --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Commands/ICommandsScheduler.cs @@ -0,0 +1,11 @@ +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Commands +{ + public interface ICommandsScheduler + { + Task EnqueueAsync(ICommand command); + + Task EnqueueAsync(ICommand command); + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Commands/InternalCommandBase.cs b/src/Modules/Registrations/Infrastructure/Configuration/Commands/InternalCommandBase.cs new file mode 100644 index 000000000..6ff89e12c --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Commands/InternalCommandBase.cs @@ -0,0 +1,29 @@ +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Commands +{ + public abstract class InternalCommandBase : ICommand + { + protected InternalCommandBase(Guid id) + { + Id = id; + } + + public Guid Id { get; } + } + + public abstract class InternalCommandBase : ICommand + { + protected InternalCommandBase() + { + Id = Guid.NewGuid(); + } + + protected InternalCommandBase(Guid id) + { + Id = id; + } + + public Guid Id { get; } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/DataAccess/DataAccessModule.cs b/src/Modules/Registrations/Infrastructure/Configuration/DataAccess/DataAccessModule.cs new file mode 100644 index 000000000..10311d7d1 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/DataAccess/DataAccessModule.cs @@ -0,0 +1,52 @@ +using Autofac; +using CompanyName.MyMeetings.BuildingBlocks.Application.Data; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.Extensions.Logging; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.DataAccess +{ + internal class DataAccessModule : Autofac.Module + { + private readonly string _databaseConnectionString; + private readonly ILoggerFactory _loggerFactory; + + internal DataAccessModule(string databaseConnectionString, ILoggerFactory loggerFactory) + { + _databaseConnectionString = databaseConnectionString; + _loggerFactory = loggerFactory; + } + + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType() + .As() + .WithParameter("connectionString", _databaseConnectionString) + .InstancePerLifetimeScope(); + + builder + .Register(c => + { + var dbContextOptionsBuilder = new DbContextOptionsBuilder(); + dbContextOptionsBuilder.UseSqlServer(_databaseConnectionString); + + dbContextOptionsBuilder + .ReplaceService(); + + return new RegistrationsContext(dbContextOptionsBuilder.Options, _loggerFactory); + }) + .AsSelf() + .As() + .InstancePerLifetimeScope(); + + var infrastructureAssembly = typeof(RegistrationsContext).Assembly; + + builder.RegisterAssemblyTypes(infrastructureAssembly) + .Where(type => type.Name.EndsWith("Repository")) + .AsImplementedInterfaces() + .InstancePerLifetimeScope() + .FindConstructorsWith(new AllConstructorFinder()); + } + } +} \ No newline at end of file diff --git a/src/Modules/UserAccess/Infrastructure/Configuration/Domain/DomainModule.cs b/src/Modules/Registrations/Infrastructure/Configuration/Domain/DomainModule.cs similarity index 53% rename from src/Modules/UserAccess/Infrastructure/Configuration/Domain/DomainModule.cs rename to src/Modules/Registrations/Infrastructure/Configuration/Domain/DomainModule.cs index a026da24d..94fe22a97 100644 --- a/src/Modules/UserAccess/Infrastructure/Configuration/Domain/DomainModule.cs +++ b/src/Modules/Registrations/Infrastructure/Configuration/Domain/DomainModule.cs @@ -1,8 +1,8 @@ using Autofac; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; -namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Domain +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Domain { internal class DomainModule : Module { diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Email/EmailModule.cs b/src/Modules/Registrations/Infrastructure/Configuration/Email/EmailModule.cs new file mode 100644 index 000000000..071ca0063 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Email/EmailModule.cs @@ -0,0 +1,35 @@ +using Autofac; +using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Email +{ + internal class EmailModule : Module + { + private readonly IEmailSender _emailSender; + private readonly EmailsConfiguration _configuration; + + public EmailModule( + EmailsConfiguration configuration, + IEmailSender emailSender) + { + _configuration = configuration; + _emailSender = emailSender; + } + + protected override void Load(ContainerBuilder builder) + { + if (_emailSender != null) + { + builder.RegisterInstance(_emailSender); + } + else + { + builder.RegisterType() + .As() + .WithParameter("configuration", _configuration) + .InstancePerLifetimeScope(); + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/EventsBusModule.cs b/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/EventsBusModule.cs new file mode 100644 index 000000000..1b51ea555 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/EventsBusModule.cs @@ -0,0 +1,29 @@ +using Autofac; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.EventsBus +{ + internal class EventsBusModule : Autofac.Module + { + private readonly IEventsBus _eventsBus; + + public EventsBusModule(IEventsBus eventsBus) + { + _eventsBus = eventsBus; + } + + protected override void Load(ContainerBuilder builder) + { + if (_eventsBus != null) + { + builder.RegisterInstance(_eventsBus).SingleInstance(); + } + else + { + builder.RegisterType() + .As() + .SingleInstance(); + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs b/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs new file mode 100644 index 000000000..e254c66c5 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs @@ -0,0 +1,30 @@ +using Autofac; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; +using Serilog; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.EventsBus +{ + public static class EventsBusStartup + { + public static void Initialize( + ILogger logger) + { + SubscribeToIntegrationEvents(logger); + } + + private static void SubscribeToIntegrationEvents(ILogger logger) + { + var eventBus = RegistrationsCompositionRoot.BeginLifetimeScope().Resolve(); + + //// SubscribeToIntegrationEvent(eventBus, logger); + } + + private static void SubscribeToIntegrationEvent(IEventsBus eventBus, ILogger logger) + where T : IntegrationEvent + { + logger.Information("Subscribe to {@IntegrationEvent}", typeof(T).FullName); + eventBus.Subscribe( + new IntegrationEventGenericHandler()); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs b/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs new file mode 100644 index 000000000..12f982d53 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs @@ -0,0 +1,39 @@ +using Autofac; +using CompanyName.MyMeetings.BuildingBlocks.Application.Data; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Serialization; +using Dapper; +using Newtonsoft.Json; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.EventsBus +{ + internal class IntegrationEventGenericHandler : IIntegrationEventHandler + where T : IntegrationEvent + { + public async Task Handle(T @event) + { + using (var scope = RegistrationsCompositionRoot.BeginLifetimeScope()) + { + using (var connection = scope.Resolve().GetOpenConnection()) + { + string type = @event.GetType().FullName; + var data = JsonConvert.SerializeObject(@event, new JsonSerializerSettings + { + ContractResolver = new AllPropertiesContractResolver() + }); + + var sql = "INSERT INTO [users].[InboxMessages] (Id, OccurredOn, Type, Data) " + + "VALUES (@Id, @OccurredOn, @Type, @Data)"; + + await connection.ExecuteScalarAsync(sql, new + { + @event.Id, + @event.OccurredOn, + type, + data + }); + } + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Logging/LoggingModule.cs b/src/Modules/Registrations/Infrastructure/Configuration/Logging/LoggingModule.cs new file mode 100644 index 000000000..fa2f40a72 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Logging/LoggingModule.cs @@ -0,0 +1,22 @@ +using Autofac; +using Serilog; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Logging +{ + internal class LoggingModule : Autofac.Module + { + private readonly ILogger _logger; + + internal LoggingModule(ILogger logger) + { + _logger = logger; + } + + protected override void Load(ContainerBuilder builder) + { + builder.RegisterInstance(_logger) + .As() + .SingleInstance(); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Mediation/MediatorModule.cs b/src/Modules/Registrations/Infrastructure/Configuration/Mediation/MediatorModule.cs new file mode 100644 index 000000000..b7a653e2e --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Mediation/MediatorModule.cs @@ -0,0 +1,93 @@ +using System.Reflection; +using Autofac; +using Autofac.Core; +using Autofac.Features.Variance; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using FluentValidation; +using MediatR; +using MediatR.Pipeline; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Mediation +{ + public class MediatorModule : Autofac.Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType() + .As() + .InstancePerDependency() + .IfNotRegistered(typeof(IServiceProvider)); + + builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly) + .AsImplementedInterfaces() + .InstancePerLifetimeScope(); + + var mediatorOpenTypes = new[] + { + typeof(IRequestHandler<,>), + typeof(INotificationHandler<>), + typeof(IValidator<>), + typeof(IRequestPreProcessor<>), + typeof(IRequestHandler<>), + typeof(IStreamRequestHandler<,>), + typeof(IRequestPostProcessor<,>), + typeof(IRequestExceptionHandler<,,>), + typeof(IRequestExceptionAction<,>), + typeof(ICommandHandler<>), + typeof(ICommandHandler<,>), + }; + builder.RegisterSource(new ScopedContravariantRegistrationSource( + mediatorOpenTypes)); + foreach (var mediatorOpenType in mediatorOpenTypes) + { + builder + .RegisterAssemblyTypes(Assemblies.Application, ThisAssembly) + .AsClosedTypesOf(mediatorOpenType) + .AsImplementedInterfaces() + .FindConstructorsWith(new AllConstructorFinder()); + } + + builder.RegisterGeneric(typeof(RequestPostProcessorBehavior<,>)).As(typeof(IPipelineBehavior<,>)); + builder.RegisterGeneric(typeof(RequestPreProcessorBehavior<,>)).As(typeof(IPipelineBehavior<,>)); + } + + private class ScopedContravariantRegistrationSource : IRegistrationSource + { + private readonly ContravariantRegistrationSource _source = new(); + private readonly List _types = new(); + + public ScopedContravariantRegistrationSource(params Type[] types) + { + ArgumentNullException.ThrowIfNull(types); + + if (!types.All(x => x.IsGenericTypeDefinition)) + { + throw new ArgumentException("Supplied types should be generic type definitions"); + } + + _types.AddRange(types); + } + + public IEnumerable RegistrationsFor( + Service service, + Func> registrationAccessor) + { + var components = _source.RegistrationsFor(service, registrationAccessor); + foreach (var c in components) + { + var defs = c.Target.Services + .OfType() + .Select(x => x.ServiceType.GetGenericTypeDefinition()); + + if (defs.Any(_types.Contains)) + { + yield return c; + } + } + } + + public bool IsAdapterForIndividualComponents => _source.IsAdapterForIndividualComponents; + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/CommandsExecutor.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/CommandsExecutor.cs new file mode 100644 index 000000000..0cde4fe09 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/CommandsExecutor.cs @@ -0,0 +1,27 @@ +using Autofac; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using MediatR; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing +{ + internal static class CommandsExecutor + { + internal static async Task Execute(ICommand command) + { + using (var scope = RegistrationsCompositionRoot.BeginLifetimeScope()) + { + var mediator = scope.Resolve(); + await mediator.Send(command); + } + } + + internal static async Task Execute(ICommand command) + { + using (var scope = RegistrationsCompositionRoot.BeginLifetimeScope()) + { + var mediator = scope.Resolve(); + return await mediator.Send(command); + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/IRecurringCommand.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/IRecurringCommand.cs new file mode 100644 index 000000000..2c3ed92d1 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/IRecurringCommand.cs @@ -0,0 +1,6 @@ +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing +{ + public interface IRecurringCommand + { + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/InboxMessageDto.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/InboxMessageDto.cs new file mode 100644 index 000000000..fe29d37d9 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/InboxMessageDto.cs @@ -0,0 +1,11 @@ +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Inbox +{ + public class InboxMessageDto + { + public Guid Id { get; set; } + + public string Type { get; set; } + + public string Data { get; set; } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommand.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommand.cs new file mode 100644 index 000000000..7d0b74a72 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommand.cs @@ -0,0 +1,8 @@ +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Inbox +{ + public class ProcessInboxCommand : CommandBase, IRecurringCommand + { + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs new file mode 100644 index 000000000..e5b47ece9 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs @@ -0,0 +1,67 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application.Data; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using Dapper; +using MediatR; +using Newtonsoft.Json; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Inbox +{ + internal class ProcessInboxCommandHandler : ICommandHandler + { + private readonly IMediator _mediator; + private readonly ISqlConnectionFactory _sqlConnectionFactory; + + public ProcessInboxCommandHandler(IMediator mediator, ISqlConnectionFactory sqlConnectionFactory) + { + _mediator = mediator; + _sqlConnectionFactory = sqlConnectionFactory; + } + + public async Task Handle(ProcessInboxCommand command, CancellationToken cancellationToken) + { + var connection = this._sqlConnectionFactory.GetOpenConnection(); + const string sql = $""" + SELECT + [InboxMessage].[Id] AS [{nameof(InboxMessageDto.Id)}], + [InboxMessage].[Type] AS [{nameof(InboxMessageDto.Type)}], + [InboxMessage].[Data] AS [{nameof(InboxMessageDto.Data)}] + FROM [users].[InboxMessages] AS [InboxMessage] + WHERE [InboxMessage].[ProcessedDate] IS NULL + ORDER BY [InboxMessage].[OccurredOn] + """; + + var messages = await connection.QueryAsync(sql); + + const string sqlUpdateProcessedDate = """ + UPDATE [users].[InboxMessages] + SET [ProcessedDate] = @Date + WHERE [Id] = @Id + """; + + foreach (var message in messages) + { + var messageAssembly = AppDomain.CurrentDomain.GetAssemblies() + .SingleOrDefault(assembly => message.Type.Contains(assembly.GetName().Name)); + + Type type = messageAssembly.GetType(message.Type); + var request = JsonConvert.DeserializeObject(message.Data, type); + + try + { + await _mediator.Publish((INotification)request, cancellationToken); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + await connection.ExecuteAsync(sqlUpdateProcessedDate, new + { + Date = DateTime.UtcNow, + message.Id + }); + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxJob.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxJob.cs new file mode 100644 index 000000000..4cff9a5ef --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxJob.cs @@ -0,0 +1,13 @@ +using Quartz; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Inbox +{ + [DisallowConcurrentExecution] + public class ProcessInboxJob : IJob + { + public async Task Execute(IJobExecutionContext context) + { + await CommandsExecutor.Execute(new ProcessInboxCommand()); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs new file mode 100644 index 000000000..72a4ebc00 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs @@ -0,0 +1,57 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application.Data; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Serialization; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using Dapper; +using Newtonsoft.Json; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.InternalCommands +{ + public class CommandsScheduler : ICommandsScheduler + { + private readonly ISqlConnectionFactory _sqlConnectionFactory; + + public CommandsScheduler(ISqlConnectionFactory sqlConnectionFactory) + { + _sqlConnectionFactory = sqlConnectionFactory; + } + + public async Task EnqueueAsync(ICommand command) + { + var connection = this._sqlConnectionFactory.GetOpenConnection(); + + const string sqlInsert = "INSERT INTO [users].[InternalCommands] ([Id], [EnqueueDate] , [Type], [Data]) VALUES " + + "(@Id, @EnqueueDate, @Type, @Data)"; + + await connection.ExecuteAsync(sqlInsert, new + { + command.Id, + EnqueueDate = DateTime.UtcNow, + Type = command.GetType().FullName, + Data = JsonConvert.SerializeObject(command, new JsonSerializerSettings + { + ContractResolver = new AllPropertiesContractResolver() + }) + }); + } + + public async Task EnqueueAsync(ICommand command) + { + var connection = this._sqlConnectionFactory.GetOpenConnection(); + + const string sqlInsert = "INSERT INTO [users].[InternalCommands] ([Id], [EnqueueDate] , [Type], [Data]) VALUES " + + "(@Id, @EnqueueDate, @Type, @Data)"; + + await connection.ExecuteAsync(sqlInsert, new + { + command.Id, + EnqueueDate = DateTime.UtcNow, + Type = command.GetType().FullName, + Data = JsonConvert.SerializeObject(command, new JsonSerializerSettings + { + ContractResolver = new AllPropertiesContractResolver() + }) + }); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommand.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommand.cs new file mode 100644 index 000000000..679b687a2 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommand.cs @@ -0,0 +1,8 @@ +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.InternalCommands +{ + internal class ProcessInternalCommandsCommand : CommandBase, IRecurringCommand + { + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs new file mode 100644 index 000000000..b5c39da7c --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs @@ -0,0 +1,88 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application.Data; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using Dapper; +using Newtonsoft.Json; +using Polly; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.InternalCommands +{ + internal class ProcessInternalCommandsCommandHandler : ICommandHandler + { + private readonly ISqlConnectionFactory _sqlConnectionFactory; + + public ProcessInternalCommandsCommandHandler( + ISqlConnectionFactory sqlConnectionFactory) + { + _sqlConnectionFactory = sqlConnectionFactory; + } + + public async Task Handle(ProcessInternalCommandsCommand command, CancellationToken cancellationToken) + { + var connection = this._sqlConnectionFactory.GetOpenConnection(); + + const string sql = $""" + SELECT + [Command].[Id] AS [{nameof(InternalCommandDto.Id)}], + [Command].[Type] AS [{nameof(InternalCommandDto.Type)}], + [Command].[Data] AS [{nameof(InternalCommandDto.Data)}] + FROM [users].[InternalCommands] AS [Command] + WHERE [Command].[ProcessedDate] IS NULL + ORDER BY [Command].[EnqueueDate] + """; + var commands = await connection.QueryAsync(sql); + + var internalCommandsList = commands.AsList(); + + var policy = Policy + .Handle() + .WaitAndRetryAsync(new[] + { + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(3) + }); + + foreach (var internalCommand in internalCommandsList) + { + var result = await policy.ExecuteAndCaptureAsync(() => ProcessCommand( + internalCommand)); + + if (result.Outcome == OutcomeType.Failure) + { + await connection.ExecuteScalarAsync( + """ + UPDATE [users].[InternalCommands] + SET + ProcessedDate = @NowDate, + Error = @Error + WHERE [Id] = @Id + """, + new + { + NowDate = DateTime.UtcNow, + Error = result.FinalException.ToString(), + internalCommand.Id + }); + } + } + } + + private async Task ProcessCommand( + InternalCommandDto internalCommand) + { + Type type = Assemblies.Application.GetType(internalCommand.Type); + dynamic commandToProcess = JsonConvert.DeserializeObject(internalCommand.Data, type); + + await CommandsExecutor.Execute(commandToProcess); + } + + private class InternalCommandDto + { + public Guid Id { get; set; } + + public string Type { get; set; } + + public string Data { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsJob.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsJob.cs new file mode 100644 index 000000000..862c8d926 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsJob.cs @@ -0,0 +1,13 @@ +using Quartz; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.InternalCommands +{ + [DisallowConcurrentExecution] + public class ProcessInternalCommandsJob : IJob + { + public async Task Execute(IJobExecutionContext context) + { + await CommandsExecutor.Execute(new ProcessInternalCommandsCommand()); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs new file mode 100644 index 000000000..2763e24cb --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs @@ -0,0 +1,91 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using Serilog; +using Serilog.Context; +using Serilog.Core; +using Serilog.Events; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing +{ + internal class LoggingCommandHandlerDecorator : ICommandHandler + where T : ICommand + { + private readonly ILogger _logger; + private readonly IExecutionContextAccessor _executionContextAccessor; + private readonly ICommandHandler _decorated; + + public LoggingCommandHandlerDecorator( + ILogger logger, + IExecutionContextAccessor executionContextAccessor, + ICommandHandler decorated) + { + _logger = logger; + _executionContextAccessor = executionContextAccessor; + _decorated = decorated; + } + + public async Task Handle(T command, CancellationToken cancellationToken) + { + if (command is IRecurringCommand) + { + await _decorated.Handle(command, cancellationToken); + } + + using ( + LogContext.Push( + new RequestLogEnricher(_executionContextAccessor), + new CommandLogEnricher(command))) + { + try + { + this._logger.Information( + "Executing command {Command}", + command.GetType().Name); + + await _decorated.Handle(command, cancellationToken); + + this._logger.Information("Command {Command} processed successful", command.GetType().Name); + } + catch (Exception exception) + { + this._logger.Error(exception, "Command {Command} processing failed", command.GetType().Name); + throw; + } + } + } + + private class CommandLogEnricher : ILogEventEnricher + { + private readonly ICommand _command; + + public CommandLogEnricher(ICommand command) + { + _command = command; + } + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + logEvent.AddOrUpdateProperty(new LogEventProperty("Context", new ScalarValue($"Command:{_command.Id.ToString()}"))); + } + } + + private class RequestLogEnricher : ILogEventEnricher + { + private readonly IExecutionContextAccessor _executionContextAccessor; + + public RequestLogEnricher(IExecutionContextAccessor executionContextAccessor) + { + _executionContextAccessor = executionContextAccessor; + } + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (_executionContextAccessor.IsAvailable) + { + logEvent.AddOrUpdateProperty(new LogEventProperty("CorrelationId", new ScalarValue(_executionContextAccessor.CorrelationId))); + } + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs new file mode 100644 index 000000000..f52abbd19 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs @@ -0,0 +1,88 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using Serilog; +using Serilog.Context; +using Serilog.Core; +using Serilog.Events; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing +{ + internal class LoggingCommandHandlerWithResultDecorator : ICommandHandler + where T : ICommand + { + private readonly ILogger _logger; + private readonly IExecutionContextAccessor _executionContextAccessor; + private readonly ICommandHandler _decorated; + + public LoggingCommandHandlerWithResultDecorator( + ILogger logger, + IExecutionContextAccessor executionContextAccessor, + ICommandHandler decorated) + { + _logger = logger; + _executionContextAccessor = executionContextAccessor; + _decorated = decorated; + } + + public async Task Handle(T command, CancellationToken cancellationToken) + { + using ( + LogContext.Push( + new RequestLogEnricher(_executionContextAccessor), + new CommandLogEnricher(command))) + { + try + { + this._logger.Information( + "Executing command {@Command}", + command); + + var result = await _decorated.Handle(command, cancellationToken); + + this._logger.Information("Command processed successful, result {Result}", result); + + return result; + } + catch (Exception exception) + { + this._logger.Error(exception, "Command processing failed"); + throw; + } + } + } + + private class CommandLogEnricher : ILogEventEnricher + { + private readonly ICommand _command; + + public CommandLogEnricher(ICommand command) + { + _command = command; + } + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + logEvent.AddOrUpdateProperty(new LogEventProperty("Context", new ScalarValue($"Command:{_command.Id.ToString()}"))); + } + } + + private class RequestLogEnricher : ILogEventEnricher + { + private readonly IExecutionContextAccessor _executionContextAccessor; + + public RequestLogEnricher(IExecutionContextAccessor executionContextAccessor) + { + _executionContextAccessor = executionContextAccessor; + } + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (_executionContextAccessor.IsAvailable) + { + logEvent.AddOrUpdateProperty(new LogEventProperty("CorrelationId", new ScalarValue(_executionContextAccessor.CorrelationId))); + } + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/OutboxMessageDto.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/OutboxMessageDto.cs new file mode 100644 index 000000000..2e1801977 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/OutboxMessageDto.cs @@ -0,0 +1,11 @@ +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Outbox +{ + public class OutboxMessageDto + { + public Guid Id { get; set; } + + public string Type { get; set; } + + public string Data { get; set; } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/OutboxModule.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/OutboxModule.cs new file mode 100644 index 000000000..3def0bff6 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/OutboxModule.cs @@ -0,0 +1,60 @@ +using Autofac; +using CompanyName.MyMeetings.BuildingBlocks.Application.Events; +using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Outbox; +using Module = Autofac.Module; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Outbox +{ + internal class OutboxModule : Module + { + private readonly BiDictionary _domainNotificationsMap; + + public OutboxModule(BiDictionary domainNotificationsMap) + { + _domainNotificationsMap = domainNotificationsMap; + } + + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType() + .As() + .FindConstructorsWith(new AllConstructorFinder()) + .InstancePerLifetimeScope(); + + CheckMappings(); + + builder.RegisterType() + .As() + .FindConstructorsWith(new AllConstructorFinder()) + .WithParameter("domainNotificationsMap", _domainNotificationsMap) + .SingleInstance(); + } + + private void CheckMappings() + { + var domainEventNotifications = Assemblies.Application + .GetTypes() + .Where(x => x.GetInterfaces().Contains(typeof(IDomainEventNotification))) + .ToList(); + + List notMappedNotifications = []; + foreach (var domainEventNotification in domainEventNotifications) + { + _domainNotificationsMap.TryGetBySecond(domainEventNotification, out var name); + + if (name == null) + { + notMappedNotifications.Add(domainEventNotification); + } + } + + if (notMappedNotifications.Any()) + { + throw new ApplicationException($"Domain Event Notifications {notMappedNotifications.Select(x => x.FullName).Aggregate((x, y) => x + "," + y)} not mapped"); + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommand.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommand.cs new file mode 100644 index 000000000..a0a042634 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommand.cs @@ -0,0 +1,8 @@ +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Outbox +{ + public class ProcessOutboxCommand : CommandBase, IRecurringCommand + { + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs new file mode 100644 index 000000000..d53474e84 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs @@ -0,0 +1,89 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application.Data; +using CompanyName.MyMeetings.BuildingBlocks.Application.Events; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using Dapper; +using MediatR; +using Newtonsoft.Json; +using Serilog.Context; +using Serilog.Core; +using Serilog.Events; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Outbox +{ + internal class ProcessOutboxCommandHandler : ICommandHandler + { + private readonly IMediator _mediator; + + private readonly ISqlConnectionFactory _sqlConnectionFactory; + + private readonly IDomainNotificationsMapper _domainNotificationsMapper; + + public ProcessOutboxCommandHandler( + IMediator mediator, + ISqlConnectionFactory sqlConnectionFactory, + IDomainNotificationsMapper domainNotificationsMapper) + { + _mediator = mediator; + _sqlConnectionFactory = sqlConnectionFactory; + _domainNotificationsMapper = domainNotificationsMapper; + } + + public async Task Handle(ProcessOutboxCommand command, CancellationToken cancellationToken) + { + var connection = this._sqlConnectionFactory.GetOpenConnection(); + const string sql = $""" + SELECT + [OutboxMessage].[Id] AS [{nameof(OutboxMessageDto.Id)}], + [OutboxMessage].[Type] AS [{nameof(OutboxMessageDto.Type)}], + [OutboxMessage].[Data] AS [{nameof(OutboxMessageDto.Data)}] + FROM [users].[OutboxMessages] AS [OutboxMessage] + WHERE [OutboxMessage].[ProcessedDate] IS NULL + ORDER BY [OutboxMessage].[OccurredOn] + """; + + var messages = await connection.QueryAsync(sql); + var messagesList = messages.AsList(); + + const string sqlUpdateProcessedDate = """ + UPDATE [users].[OutboxMessages] + SET [ProcessedDate] = @Date + WHERE [Id] = @Id + """; + if (messagesList.Count > 0) + { + foreach (var message in messagesList) + { + var type = _domainNotificationsMapper.GetType(message.Type); + var @event = JsonConvert.DeserializeObject(message.Data, type) as IDomainEventNotification; + + using (LogContext.Push(new OutboxMessageContextEnricher(@event))) + { + await this._mediator.Publish(@event, cancellationToken); + + await connection.ExecuteAsync(sqlUpdateProcessedDate, new + { + Date = DateTime.UtcNow, + message.Id + }); + } + } + } + } + + private class OutboxMessageContextEnricher : ILogEventEnricher + { + private readonly IDomainEventNotification _notification; + + public OutboxMessageContextEnricher(IDomainEventNotification notification) + { + _notification = notification; + } + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + logEvent.AddOrUpdateProperty(new LogEventProperty("Context", new ScalarValue($"OutboxMessage:{_notification.Id.ToString()}"))); + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxJob.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxJob.cs new file mode 100644 index 000000000..2275dbc13 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxJob.cs @@ -0,0 +1,13 @@ +using Quartz; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Outbox +{ + [DisallowConcurrentExecution] + public class ProcessOutboxJob : IJob + { + public async Task Execute(IJobExecutionContext context) + { + await CommandsExecutor.Execute(new ProcessOutboxCommand()); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/ProcessingModule.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/ProcessingModule.cs new file mode 100644 index 000000000..f30c18c12 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/ProcessingModule.cs @@ -0,0 +1,69 @@ +using Autofac; +using CompanyName.MyMeetings.BuildingBlocks.Application.Events; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.InternalCommands; +using MediatR; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing +{ + internal class ProcessingModule : Autofac.Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + + builder.RegisterGenericDecorator( + typeof(UnitOfWorkCommandHandlerDecorator<>), + typeof(ICommandHandler<>)); + + builder.RegisterGenericDecorator( + typeof(UnitOfWorkCommandHandlerWithResultDecorator<,>), + typeof(ICommandHandler<,>)); + + builder.RegisterGenericDecorator( + typeof(ValidationCommandHandlerDecorator<>), + typeof(ICommandHandler<>)); + + builder.RegisterGenericDecorator( + typeof(ValidationCommandHandlerWithResultDecorator<,>), + typeof(ICommandHandler<,>)); + + builder.RegisterGenericDecorator( + typeof(LoggingCommandHandlerDecorator<>), + typeof(IRequestHandler<>)); + + builder.RegisterGenericDecorator( + typeof(LoggingCommandHandlerWithResultDecorator<,>), + typeof(IRequestHandler<,>)); + + builder.RegisterGenericDecorator( + typeof(DomainEventsDispatcherNotificationHandlerDecorator<>), + typeof(INotificationHandler<>)); + + builder.RegisterAssemblyTypes(Assemblies.Application) + .AsClosedTypesOf(typeof(IDomainEventNotification<>)) + .InstancePerDependency() + .FindConstructorsWith(new AllConstructorFinder()); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerDecorator.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerDecorator.cs new file mode 100644 index 000000000..83590146d --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerDecorator.cs @@ -0,0 +1,42 @@ +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using Microsoft.EntityFrameworkCore; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing +{ + internal class UnitOfWorkCommandHandlerDecorator : ICommandHandler + where T : ICommand + { + private readonly ICommandHandler _decorated; + private readonly IUnitOfWork _unitOfWork; + private readonly RegistrationsContext _registrationsContext; + + public UnitOfWorkCommandHandlerDecorator( + ICommandHandler decorated, + IUnitOfWork unitOfWork, + RegistrationsContext registrationsContext) + { + _decorated = decorated; + _unitOfWork = unitOfWork; + _registrationsContext = registrationsContext; + } + + public async Task Handle(T command, CancellationToken cancellationToken) + { + await this._decorated.Handle(command, cancellationToken); + + if (command is InternalCommandBase) + { + var internalCommand = await _registrationsContext.InternalCommands.FirstOrDefaultAsync(x => x.Id == command.Id, cancellationToken: cancellationToken); + + if (internalCommand != null) + { + internalCommand.ProcessedDate = DateTime.UtcNow; + } + } + + await this._unitOfWork.CommitAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerWithResultDecorator.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerWithResultDecorator.cs new file mode 100644 index 000000000..9b306aa19 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerWithResultDecorator.cs @@ -0,0 +1,44 @@ +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using Microsoft.EntityFrameworkCore; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing +{ + internal class UnitOfWorkCommandHandlerWithResultDecorator : ICommandHandler + where T : ICommand + { + private readonly ICommandHandler _decorated; + private readonly IUnitOfWork _unitOfWork; + private readonly RegistrationsContext _registrationsContext; + + public UnitOfWorkCommandHandlerWithResultDecorator( + ICommandHandler decorated, + IUnitOfWork unitOfWork, + RegistrationsContext registrationsContext) + { + _decorated = decorated; + _unitOfWork = unitOfWork; + _registrationsContext = registrationsContext; + } + + public async Task Handle(T command, CancellationToken cancellationToken) + { + var result = await this._decorated.Handle(command, cancellationToken); + + if (command is InternalCommandBase) + { + var internalCommand = await _registrationsContext.InternalCommands.FirstOrDefaultAsync(x => x.Id == command.Id, cancellationToken: cancellationToken); + + if (internalCommand != null) + { + internalCommand.ProcessedDate = DateTime.UtcNow; + } + } + + await this._unitOfWork.CommitAsync(cancellationToken); + + return result; + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/ValidationCommandHandlerDecorator.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/ValidationCommandHandlerDecorator.cs new file mode 100644 index 000000000..1b1a4dcec --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/ValidationCommandHandlerDecorator.cs @@ -0,0 +1,38 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using FluentValidation; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing +{ + internal class ValidationCommandHandlerDecorator : ICommandHandler + where T : ICommand + { + private readonly IList> _validators; + private readonly ICommandHandler _decorated; + + public ValidationCommandHandlerDecorator( + IList> validators, + ICommandHandler decorated) + { + this._validators = validators; + _decorated = decorated; + } + + public async Task Handle(T command, CancellationToken cancellationToken) + { + var errors = _validators + .Select(v => v.Validate(command)) + .SelectMany(result => result.Errors) + .Where(error => error != null) + .ToList(); + + if (errors.Any()) + { + throw new InvalidCommandException(errors.Select(x => x.ErrorMessage).ToList()); + } + + await _decorated.Handle(command, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/ValidationCommandHandlerWithResultDecorator.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/ValidationCommandHandlerWithResultDecorator.cs new file mode 100644 index 000000000..01a5597e5 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/ValidationCommandHandlerWithResultDecorator.cs @@ -0,0 +1,39 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using FluentValidation; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing +{ + internal class ValidationCommandHandlerWithResultDecorator : ICommandHandler + where T : ICommand + { + private readonly IList> _validators; + + private readonly ICommandHandler _decorated; + + public ValidationCommandHandlerWithResultDecorator( + IList> validators, + ICommandHandler decorated) + { + this._validators = validators; + _decorated = decorated; + } + + public Task Handle(T command, CancellationToken cancellationToken) + { + var errors = _validators + .Select(v => v.Validate(command)) + .SelectMany(result => result.Errors) + .Where(error => error != null) + .ToList(); + + if (errors.Any()) + { + throw new InvalidCommandException(errors.Select(x => x.ErrorMessage).ToList()); + } + + return _decorated.Handle(command, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Quartz/QuartzModule.cs b/src/Modules/Registrations/Infrastructure/Configuration/Quartz/QuartzModule.cs new file mode 100644 index 000000000..aacbff8eb --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Quartz/QuartzModule.cs @@ -0,0 +1,14 @@ +using Autofac; +using Quartz; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Quartz +{ + public class QuartzModule : Autofac.Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterAssemblyTypes(ThisAssembly) + .Where(x => typeof(IJob).IsAssignableFrom(x)).InstancePerDependency(); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Quartz/QuartzStartup.cs b/src/Modules/Registrations/Infrastructure/Configuration/Quartz/QuartzStartup.cs new file mode 100644 index 000000000..30937961c --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Quartz/QuartzStartup.cs @@ -0,0 +1,112 @@ +using System.Collections.Specialized; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Inbox; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.InternalCommands; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Outbox; +using Quartz; +using Quartz.Impl; +using Quartz.Logging; +using Serilog; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Quartz +{ + internal static class QuartzStartup + { + internal static void Initialize(ILogger logger, long? internalProcessingPoolingInterval = null) + { + logger.Information("Quartz starting..."); + + var schedulerConfiguration = new NameValueCollection(); + schedulerConfiguration.Add("quartz.scheduler.instanceName", "Registrations"); + + ISchedulerFactory schedulerFactory = new StdSchedulerFactory(schedulerConfiguration); + IScheduler scheduler = schedulerFactory.GetScheduler().GetAwaiter().GetResult(); + + LogProvider.SetCurrentLogProvider(new SerilogLogProvider(logger)); + + scheduler.Start().GetAwaiter().GetResult(); + + var processOutboxJob = JobBuilder.Create().Build(); + ITrigger trigger; + if (internalProcessingPoolingInterval.HasValue) + { + trigger = + TriggerBuilder + .Create() + .StartNow() + .WithSimpleSchedule(x => + x.WithInterval(TimeSpan.FromMilliseconds(internalProcessingPoolingInterval.Value)) + .RepeatForever()) + .Build(); + } + else + { + trigger = + TriggerBuilder + .Create() + .StartNow() + .WithCronSchedule("0/2 * * ? * *") + .Build(); + } + + scheduler + .ScheduleJob(processOutboxJob, trigger) + .GetAwaiter().GetResult(); + + var processInboxJob = JobBuilder.Create().Build(); + + ITrigger processInboxTrigger; + if (internalProcessingPoolingInterval.HasValue) + { + processInboxTrigger = + TriggerBuilder + .Create() + .StartNow() + .WithSimpleSchedule(x => + x.WithInterval(TimeSpan.FromMilliseconds(internalProcessingPoolingInterval.Value)) + .RepeatForever()) + .Build(); + } + else + { + processInboxTrigger = + TriggerBuilder + .Create() + .StartNow() + .WithCronSchedule("0/2 * * ? * *") + .Build(); + } + + scheduler + .ScheduleJob(processInboxJob, processInboxTrigger) + .GetAwaiter().GetResult(); + + var processInternalCommandsJob = JobBuilder.Create().Build(); + + ITrigger processInternalCommandsTrigger; + if (internalProcessingPoolingInterval.HasValue) + { + processInternalCommandsTrigger = + TriggerBuilder + .Create() + .StartNow() + .WithSimpleSchedule(x => + x.WithInterval(TimeSpan.FromMilliseconds(internalProcessingPoolingInterval.Value)) + .RepeatForever()) + .Build(); + } + else + { + processInternalCommandsTrigger = + TriggerBuilder + .Create() + .StartNow() + .WithCronSchedule("0/2 * * ? * *") + .Build(); + } + + scheduler.ScheduleJob(processInternalCommandsJob, processInternalCommandsTrigger).GetAwaiter().GetResult(); + + logger.Information("Quartz started."); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Quartz/SerilogLogProvider.cs b/src/Modules/Registrations/Infrastructure/Configuration/Quartz/SerilogLogProvider.cs new file mode 100644 index 000000000..380f5355f --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/Quartz/SerilogLogProvider.cs @@ -0,0 +1,68 @@ +using Quartz.Logging; +using Serilog; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Quartz +{ + internal class SerilogLogProvider : ILogProvider + { + private readonly ILogger _logger; + + internal SerilogLogProvider(ILogger logger) + { + _logger = logger; + } + + public Logger GetLogger(string name) + { + return (level, func, exception, parameters) => + { + if (func == null) + { + return true; + } + + if (level == LogLevel.Debug || level == LogLevel.Trace) + { + _logger.Debug(exception, func(), parameters); + } + + if (level == LogLevel.Info) + { + _logger.Information(exception, func(), parameters); + } + + if (level == LogLevel.Warn) + { + _logger.Warning(exception, func(), parameters); + } + + if (level == LogLevel.Error) + { + _logger.Error(exception, func(), parameters); + } + + if (level == LogLevel.Fatal) + { + _logger.Fatal(exception, func(), parameters); + } + + return true; + }; + } + + public IDisposable OpenNestedContext(string message) + { + throw new NotImplementedException(); + } + + public IDisposable OpenMappedContext(string key, string value) + { + throw new NotImplementedException(); + } + + public IDisposable OpenMappedContext(string key, object value, bool destructure = false) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsCompositionRoot.cs b/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsCompositionRoot.cs new file mode 100644 index 000000000..5e9e3a291 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsCompositionRoot.cs @@ -0,0 +1,19 @@ +using Autofac; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration +{ + internal static class RegistrationsCompositionRoot + { + private static IContainer _container; + + internal static void SetContainer(IContainer container) + { + _container = container; + } + + internal static ILifetimeScope BeginLifetimeScope() + { + return _container.BeginLifetimeScope(); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs b/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs new file mode 100644 index 000000000..400efc315 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs @@ -0,0 +1,86 @@ +using Autofac; +using CompanyName.MyMeetings.BuildingBlocks.Application; +using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.DataAccess; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Domain; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Email; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.EventsBus; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Logging; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Mediation; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Outbox; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Quartz; +using Serilog; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration +{ + public class RegistrationsStartup + { + private static IContainer _container; + + public static void Initialize( + string connectionString, + IExecutionContextAccessor executionContextAccessor, + ILogger logger, + EmailsConfiguration emailsConfiguration, + string textEncryptionKey, + IEmailSender emailSender, + IEventsBus eventsBus, + long? internalProcessingPoolingInterval = null) + { + var moduleLogger = logger.ForContext("Module", "UserAccess"); + + ConfigureCompositionRoot( + connectionString, + executionContextAccessor, + logger, + emailsConfiguration, + textEncryptionKey, + emailSender, + eventsBus); + + QuartzStartup.Initialize(moduleLogger, internalProcessingPoolingInterval); + + EventsBusStartup.Initialize(moduleLogger); + } + + private static void ConfigureCompositionRoot( + string connectionString, + IExecutionContextAccessor executionContextAccessor, + ILogger logger, + EmailsConfiguration emailsConfiguration, + string textEncryptionKey, + IEmailSender emailSender, + IEventsBus eventsBus) + { + var containerBuilder = new ContainerBuilder(); + + containerBuilder.RegisterModule(new LoggingModule(logger.ForContext("Module", "UserAccess"))); + + var loggerFactory = new Serilog.Extensions.Logging.SerilogLoggerFactory(logger); + containerBuilder.RegisterModule(new DataAccessModule(connectionString, loggerFactory)); + containerBuilder.RegisterModule(new ProcessingModule()); + containerBuilder.RegisterModule(new EventsBusModule(eventsBus)); + containerBuilder.RegisterModule(new MediatorModule()); + + var domainNotificationsMap = new BiDictionary(); + domainNotificationsMap.Add("NewUserRegisteredNotification", typeof(NewUserRegisteredNotification)); + containerBuilder.RegisterModule(new OutboxModule(domainNotificationsMap)); + + containerBuilder.RegisterModule(new QuartzModule()); + containerBuilder.RegisterModule(new DomainModule()); + containerBuilder.RegisterModule(new EmailModule(emailsConfiguration, emailSender)); + //// containerBuilder.RegisterModule(new SecurityModule(textEncryptionKey)); + + containerBuilder.RegisterInstance(executionContextAccessor); + + _container = containerBuilder.Build(); + + RegistrationsCompositionRoot.SetContainer(_container); + } + } +} \ No newline at end of file diff --git a/src/Modules/UserAccess/Infrastructure/Domain/UserRegistrations/UserRegistrationEntityTypeConfiguration.cs b/src/Modules/Registrations/Infrastructure/Domain/UserRegistrations/UserRegistrationEntityTypeConfiguration.cs similarity index 82% rename from src/Modules/UserAccess/Infrastructure/Domain/UserRegistrations/UserRegistrationEntityTypeConfiguration.cs rename to src/Modules/Registrations/Infrastructure/Domain/UserRegistrations/UserRegistrationEntityTypeConfiguration.cs index bfcfedda7..f4196b368 100644 --- a/src/Modules/UserAccess/Infrastructure/Domain/UserRegistrations/UserRegistrationEntityTypeConfiguration.cs +++ b/src/Modules/Registrations/Infrastructure/Domain/UserRegistrations/UserRegistrationEntityTypeConfiguration.cs @@ -1,14 +1,14 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Domain.UserRegistrations +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Domain.UserRegistrations { internal class UserRegistrationEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable("UserRegistrations", "users"); + builder.ToTable("UserRegistrations", "registrations"); builder.HasKey(x => x.Id); diff --git a/src/Modules/Registrations/Infrastructure/Domain/UserRegistrations/UserRegistrationRepository.cs b/src/Modules/Registrations/Infrastructure/Domain/UserRegistrations/UserRegistrationRepository.cs new file mode 100644 index 000000000..b8316f61c --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Domain/UserRegistrations/UserRegistrationRepository.cs @@ -0,0 +1,25 @@ +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; +using Microsoft.EntityFrameworkCore; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Domain.UserRegistrations +{ + public class UserRegistrationRepository : IUserRegistrationRepository + { + private readonly RegistrationsContext _context; + + public UserRegistrationRepository(RegistrationsContext context) + { + _context = context; + } + + public async Task AddAsync(UserRegistration userRegistration) + { + await _context.AddAsync(userRegistration); + } + + public async Task GetByIdAsync(UserRegistrationId userRegistrationId) + { + return await _context.UserRegistrations.FirstOrDefaultAsync(x => x.Id == userRegistrationId); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/InternalCommands/InternalCommandEntityTypeConfiguration.cs b/src/Modules/Registrations/Infrastructure/InternalCommands/InternalCommandEntityTypeConfiguration.cs new file mode 100644 index 000000000..be34b221b --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/InternalCommands/InternalCommandEntityTypeConfiguration.cs @@ -0,0 +1,17 @@ +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.InternalCommands +{ + internal class InternalCommandEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("InternalCommands", "registrations"); + + builder.HasKey(b => b.Id); + builder.Property(b => b.Id).ValueGeneratedNever(); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Outbox/OutboxAccessor.cs b/src/Modules/Registrations/Infrastructure/Outbox/OutboxAccessor.cs new file mode 100644 index 000000000..27c250e71 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Outbox/OutboxAccessor.cs @@ -0,0 +1,24 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Outbox +{ + public class OutboxAccessor : IOutbox + { + private readonly RegistrationsContext _userAccessContext; + + public OutboxAccessor(RegistrationsContext userAccessContext) + { + _userAccessContext = userAccessContext; + } + + public void Add(OutboxMessage message) + { + _userAccessContext.OutboxMessages.Add(message); + } + + public Task Save() + { + return Task.CompletedTask; // Save is done automatically using EF Core Change Tracking mechanism during SaveChanges. + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Outbox/OutboxMessageEntityTypeConfiguration.cs b/src/Modules/Registrations/Infrastructure/Outbox/OutboxMessageEntityTypeConfiguration.cs new file mode 100644 index 000000000..bcd32e3a8 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Outbox/OutboxMessageEntityTypeConfiguration.cs @@ -0,0 +1,17 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Outbox +{ + internal class OutboxMessageEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("OutboxMessages", "registrations"); + + builder.HasKey(b => b.Id); + builder.Property(b => b.Id).ValueGeneratedNever(); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/RegistrationsContext.cs b/src/Modules/Registrations/Infrastructure/RegistrationsContext.cs new file mode 100644 index 000000000..a683efce7 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/RegistrationsContext.cs @@ -0,0 +1,35 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Domain.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.InternalCommands; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Outbox; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure +{ + public class RegistrationsContext : DbContext + { + public DbSet UserRegistrations { get; set; } + + public DbSet OutboxMessages { get; set; } + + public DbSet InternalCommands { get; set; } + + private readonly ILoggerFactory _loggerFactory; + + public RegistrationsContext(DbContextOptions options, ILoggerFactory loggerFactory) + : base(options) + { + _loggerFactory = loggerFactory; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new UserRegistrationEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new OutboxMessageEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new InternalCommandEntityTypeConfiguration()); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/RegistrationsModule.cs b/src/Modules/Registrations/Infrastructure/RegistrationsModule.cs new file mode 100644 index 000000000..59ff1f4a8 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/RegistrationsModule.cs @@ -0,0 +1,31 @@ +using Autofac; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing; +using MediatR; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure +{ + public class RegistrationsModule : IRegistrationsModule + { + public async Task ExecuteCommandAsync(ICommand command) + { + return await CommandsExecutor.Execute(command); + } + + public async Task ExecuteCommandAsync(ICommand command) + { + await CommandsExecutor.Execute(command); + } + + public async Task ExecuteQueryAsync(IQuery query) + { + using (var scope = RegistrationsCompositionRoot.BeginLifetimeScope()) + { + var mediator = scope.Resolve(); + + return await mediator.Send(query); + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/IntegrationEvents/Class1.cs b/src/Modules/Registrations/IntegrationEvents/Class1.cs new file mode 100644 index 000000000..bb581f45a --- /dev/null +++ b/src/Modules/Registrations/IntegrationEvents/Class1.cs @@ -0,0 +1,5 @@ +namespace CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; + +public class Class1 +{ +} \ No newline at end of file diff --git a/src/Modules/Registrations/IntegrationEvents/CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents.csproj b/src/Modules/Registrations/IntegrationEvents/CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents.csproj new file mode 100644 index 000000000..2ef1a363a --- /dev/null +++ b/src/Modules/Registrations/IntegrationEvents/CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents.csproj @@ -0,0 +1 @@ + diff --git a/src/Modules/UserAccess/IntegrationEvents/NewUserRegisteredIntegrationEvent.cs b/src/Modules/Registrations/IntegrationEvents/NewUserRegisteredIntegrationEvent.cs similarity index 91% rename from src/Modules/UserAccess/IntegrationEvents/NewUserRegisteredIntegrationEvent.cs rename to src/Modules/Registrations/IntegrationEvents/NewUserRegisteredIntegrationEvent.cs index fcb88dac3..11452344b 100644 --- a/src/Modules/UserAccess/IntegrationEvents/NewUserRegisteredIntegrationEvent.cs +++ b/src/Modules/Registrations/IntegrationEvents/NewUserRegisteredIntegrationEvent.cs @@ -1,6 +1,6 @@ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; -namespace CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents +namespace CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents { public class NewUserRegisteredIntegrationEvent : IntegrationEvent { diff --git a/src/Modules/Registrations/Tests/ArchTests/Application/ApplicationTests.cs b/src/Modules/Registrations/Tests/ArchTests/Application/ApplicationTests.cs new file mode 100644 index 000000000..6bb2fd79b --- /dev/null +++ b/src/Modules/Registrations/Tests/ArchTests/Application/ApplicationTests.cs @@ -0,0 +1,200 @@ +using System.Reflection; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Queries; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using CompanyName.MyMeetings.Modules.Registrations.ArchTests.SeedWork; +using FluentValidation; +using MediatR; +using NetArchTest.Rules; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace CompanyName.MyMeetings.Modules.Registrations.ArchTests.Application +{ + [TestFixture] + public class ApplicationTests : TestBase + { + [Test] + public void Command_Should_Be_Immutable() + { + var types = Types.InAssembly(ApplicationAssembly) + .That() + .Inherit(typeof(CommandBase)) + .Or() + .Inherit(typeof(CommandBase<>)) + .Or() + .Inherit(typeof(InternalCommandBase)) + .Or() + .Inherit(typeof(InternalCommandBase<>)) + .Or() + .ImplementInterface(typeof(ICommand)) + .Or() + .ImplementInterface(typeof(ICommand<>)) + .GetTypes(); + + AssertAreImmutable(types); + } + + [Test] + public void Query_Should_Be_Immutable() + { + var types = Types.InAssembly(ApplicationAssembly) + .That().ImplementInterface(typeof(IQuery<>)).GetTypes(); + + AssertAreImmutable(types); + } + + [Test] + public void CommandHandler_Should_Have_Name_EndingWith_CommandHandler() + { + var result = Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(ICommandHandler<>)) + .Or() + .ImplementInterface(typeof(ICommandHandler<,>)) + .And() + .DoNotHaveNameMatching(".*Decorator.*").Should() + .HaveNameEndingWith("CommandHandler") + .GetResult(); + + AssertArchTestResult(result); + } + + [Test] + public void QueryHandler_Should_Have_Name_EndingWith_QueryHandler() + { + var result = Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IQueryHandler<,>)) + .Should() + .HaveNameEndingWith("QueryHandler") + .GetResult(); + + AssertArchTestResult(result); + } + + [Test] + public void Command_And_Query_Handlers_Should_Not_Be_Public() + { + var types = Types.InAssembly(ApplicationAssembly) + .That() + .ImplementInterface(typeof(IQueryHandler<,>)) + .Or() + .ImplementInterface(typeof(ICommandHandler<>)) + .Or() + .ImplementInterface(typeof(ICommandHandler<,>)) + .Should().NotBePublic().GetResult().FailingTypes; + + AssertFailingTypes(types); + } + + [Test] + public void Validator_Should_Have_Name_EndingWith_Validator() + { + var result = Types.InAssembly(ApplicationAssembly) + .That() + .Inherit(typeof(AbstractValidator<>)) + .Should() + .HaveNameEndingWith("Validator") + .GetResult(); + + AssertArchTestResult(result); + } + + [Test] + public void Validators_Should_Not_Be_Public() + { + var types = Types.InAssembly(ApplicationAssembly) + .That() + .Inherit(typeof(AbstractValidator<>)) + .Should().NotBePublic().GetResult().FailingTypes; + + AssertFailingTypes(types); + } + + [Test] + public void InternalCommand_Should_Have_JsonConstructorAttribute() + { + var types = Types.InAssembly(ApplicationAssembly) + .That() + .Inherit(typeof(InternalCommandBase)) + .Or() + .Inherit(typeof(InternalCommandBase<>)) + .GetTypes(); + + List failingTypes = []; + + foreach (var type in types) + { + bool hasJsonConstructorDefined = false; + var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + foreach (var constructorInfo in constructors) + { + var jsonConstructorAttribute = constructorInfo.GetCustomAttributes(typeof(JsonConstructorAttribute), false); + if (jsonConstructorAttribute.Length > 0) + { + hasJsonConstructorDefined = true; + break; + } + } + + if (!hasJsonConstructorDefined) + { + failingTypes.Add(type); + } + } + + AssertFailingTypes(failingTypes); + } + + [Test] + public void MediatR_RequestHandler_Should_NotBe_Used_Directly() + { + var types = Types.InAssembly(ApplicationAssembly) + .That().DoNotHaveName("ICommandHandler`1") + .Should().ImplementInterface(typeof(IRequestHandler<>)) + .GetTypes(); + + List failingTypes = []; + foreach (var type in types) + { + bool isCommandHandler = type.GetInterfaces().Any(x => + x.IsGenericType && + x.GetGenericTypeDefinition() == typeof(ICommandHandler<>)); + bool isCommandWithResultHandler = type.GetInterfaces().Any(x => + x.IsGenericType && + x.GetGenericTypeDefinition() == typeof(ICommandHandler<,>)); + bool isQueryHandler = type.GetInterfaces().Any(x => + x.IsGenericType && + x.GetGenericTypeDefinition() == typeof(IQueryHandler<,>)); + if (!isCommandHandler && !isCommandWithResultHandler && !isQueryHandler) + { + failingTypes.Add(type); + } + } + + AssertFailingTypes(failingTypes); + } + + [Test] + public void Command_With_Result_Should_Not_Return_Unit() + { + Type commandWithResultHandlerType = typeof(ICommandHandler<,>); + IEnumerable types = Types.InAssembly(ApplicationAssembly) + .That().ImplementInterface(commandWithResultHandlerType) + .GetTypes().ToList(); + + List failingTypes = []; + foreach (Type type in types) + { + Type interfaceType = type.GetInterface(commandWithResultHandlerType.Name); + if (interfaceType?.GenericTypeArguments[1] == typeof(Unit)) + { + failingTypes.Add(type); + } + } + + AssertFailingTypes(failingTypes); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Tests/ArchTests/CompanyName.MyMeetings.Modules.Registrations.ArchTests.csproj b/src/Modules/Registrations/Tests/ArchTests/CompanyName.MyMeetings.Modules.Registrations.ArchTests.csproj new file mode 100644 index 000000000..a6968989f --- /dev/null +++ b/src/Modules/Registrations/Tests/ArchTests/CompanyName.MyMeetings.Modules.Registrations.ArchTests.csproj @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Modules/Registrations/Tests/ArchTests/Domain/DomainTests.cs b/src/Modules/Registrations/Tests/ArchTests/Domain/DomainTests.cs new file mode 100644 index 000000000..9ed15dac2 --- /dev/null +++ b/src/Modules/Registrations/Tests/ArchTests/Domain/DomainTests.cs @@ -0,0 +1,228 @@ +using System.Reflection; +using CompanyName.MyMeetings.BuildingBlocks.Domain; +using CompanyName.MyMeetings.Modules.Registrations.ArchTests.SeedWork; +using NetArchTest.Rules; +using NUnit.Framework; + +namespace CompanyName.MyMeetings.Modules.Registrations.ArchTests.Domain +{ + public class DomainTests : TestBase + { + [Test] + public void DomainEvent_Should_Be_Immutable() + { + var types = Types.InAssembly(DomainAssembly) + .That() + .Inherit(typeof(DomainEventBase)) + .Or() + .ImplementInterface(typeof(IDomainEvent)) + .GetTypes(); + + AssertAreImmutable(types); + } + + [Test] + public void ValueObject_Should_Be_Immutable() + { + var types = Types.InAssembly(DomainAssembly) + .That() + .Inherit(typeof(ValueObject)) + .GetTypes(); + + AssertAreImmutable(types); + } + + [Test] + public void Entity_Which_Is_Not_Aggregate_Root_Cannot_Have_Public_Members() + { + var types = Types.InAssembly(DomainAssembly) + .That() + .Inherit(typeof(Entity)) + .And().DoNotImplementInterface(typeof(IAggregateRoot)).GetTypes(); + + const BindingFlags bindingFlags = BindingFlags.DeclaredOnly | + BindingFlags.Public | + BindingFlags.Instance | + BindingFlags.Static; + + List failingTypes = []; + foreach (var type in types) + { + var publicFields = type.GetFields(bindingFlags); + var publicProperties = type.GetProperties(bindingFlags); + var publicMethods = type.GetMethods(bindingFlags); + + if (publicFields.Any() || publicProperties.Any() || publicMethods.Any()) + { + failingTypes.Add(type); + } + } + + AssertFailingTypes(failingTypes); + } + + [Test] + public void Entity_Cannot_Have_Reference_To_Other_AggregateRoot() + { + var entityTypes = Types.InAssembly(DomainAssembly) + .That() + .Inherit(typeof(Entity)).GetTypes(); + + var aggregateRoots = Types.InAssembly(DomainAssembly) + .That().ImplementInterface(typeof(IAggregateRoot)).GetTypes().ToList(); + + const BindingFlags bindingFlags = BindingFlags.DeclaredOnly | + BindingFlags.NonPublic | + BindingFlags.Instance; + + List failingTypes = []; + foreach (var type in entityTypes) + { + var fields = type.GetFields(bindingFlags); + + foreach (var field in fields) + { + if (aggregateRoots.Contains(field.FieldType) || + field.FieldType.GenericTypeArguments.Any(x => aggregateRoots.Contains(x))) + { + failingTypes.Add(type); + break; + } + } + + var properties = type.GetProperties(bindingFlags); + foreach (var property in properties) + { + if (aggregateRoots.Contains(property.PropertyType) || + property.PropertyType.GenericTypeArguments.Any(x => aggregateRoots.Contains(x))) + { + failingTypes.Add(type); + break; + } + } + } + + AssertFailingTypes(failingTypes); + } + + [Test] + public void Entity_Should_Have_Parameterless_Private_Constructor() + { + var entityTypes = Types.InAssembly(DomainAssembly) + .That() + .Inherit(typeof(Entity)).GetTypes(); + + List failingTypes = []; + foreach (var entityType in entityTypes) + { + bool hasPrivateParameterlessConstructor = false; + var constructors = entityType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance); + foreach (var constructorInfo in constructors) + { + if (constructorInfo.IsPrivate && constructorInfo.GetParameters().Length == 0) + { + hasPrivateParameterlessConstructor = true; + } + } + + if (!hasPrivateParameterlessConstructor) + { + failingTypes.Add(entityType); + } + } + + AssertFailingTypes(failingTypes); + } + + [Test] + public void Domain_Object_Should_Have_Only_Private_Constructors() + { + var domainObjectTypes = Types.InAssembly(DomainAssembly) + .That() + .Inherit(typeof(Entity)) + .Or() + .Inherit(typeof(ValueObject)) + .GetTypes(); + + List failingTypes = []; + foreach (var domainObjectType in domainObjectTypes) + { + var constructors = domainObjectType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); + foreach (var constructorInfo in constructors) + { + if (!constructorInfo.IsPrivate) + { + failingTypes.Add(domainObjectType); + } + } + } + + AssertFailingTypes(failingTypes); + } + + [Test] + public void ValueObject_Should_Have_Private_Constructor_With_Parameters_For_His_State() + { + var valueObjects = Types.InAssembly(DomainAssembly) + .That() + .Inherit(typeof(ValueObject)).GetTypes(); + + List failingTypes = []; + foreach (var entityType in valueObjects) + { + bool hasExpectedConstructor = false; + + const BindingFlags bindingFlags = BindingFlags.DeclaredOnly | + BindingFlags.Public | + BindingFlags.Instance; + var names = entityType.GetFields(bindingFlags).Select(x => x.Name.ToLower()).ToList(); + var propertyNames = entityType.GetProperties(bindingFlags).Select(x => x.Name.ToLower()).ToList(); + names.AddRange(propertyNames); + var constructors = entityType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance); + foreach (var constructorInfo in constructors) + { + var parameters = constructorInfo.GetParameters().Select(x => x.Name.ToLower()).ToList(); + + if (names.Intersect(parameters).Count() == names.Count) + { + hasExpectedConstructor = true; + break; + } + } + + if (!hasExpectedConstructor) + { + failingTypes.Add(entityType); + } + } + + AssertFailingTypes(failingTypes); + } + + [Test] + public void DomainEvent_Should_Have_DomainEventPostfix() + { + var result = Types.InAssembly(DomainAssembly) + .That() + .Inherit(typeof(DomainEventBase)) + .Or() + .ImplementInterface(typeof(IDomainEvent)) + .Should().HaveNameEndingWith("DomainEvent") + .GetResult(); + + AssertArchTestResult(result); + } + + [Test] + public void BusinessRule_Should_Have_RulePostfix() + { + var result = Types.InAssembly(DomainAssembly) + .That() + .ImplementInterface(typeof(IBusinessRule)) + .Should().HaveNameEndingWith("Rule") + .GetResult(); + + AssertArchTestResult(result); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Tests/ArchTests/Module/LayersTests.cs b/src/Modules/Registrations/Tests/ArchTests/Module/LayersTests.cs new file mode 100644 index 000000000..d4db509fe --- /dev/null +++ b/src/Modules/Registrations/Tests/ArchTests/Module/LayersTests.cs @@ -0,0 +1,43 @@ +using CompanyName.MyMeetings.Modules.Registrations.ArchTests.SeedWork; +using NetArchTest.Rules; +using NUnit.Framework; + +namespace CompanyName.MyMeetings.Modules.Registrations.ArchTests.Module +{ + [TestFixture] + public class LayersTests : TestBase + { + [Test] + public void DomainLayer_DoesNotHaveDependency_ToApplicationLayer() + { + var result = Types.InAssembly(DomainAssembly) + .Should() + .NotHaveDependencyOn(ApplicationAssembly.GetName().Name) + .GetResult(); + + AssertArchTestResult(result); + } + + [Test] + public void DomainLayer_DoesNotHaveDependency_ToInfrastructureLayer() + { + var result = Types.InAssembly(DomainAssembly) + .Should() + .NotHaveDependencyOn(ApplicationAssembly.GetName().Name) + .GetResult(); + + AssertArchTestResult(result); + } + + [Test] + public void ApplicationLayer_DoesNotHaveDependency_ToInfrastructureLayer() + { + var result = Types.InAssembly(ApplicationAssembly) + .Should() + .NotHaveDependencyOn(InfrastructureAssembly.GetName().Name) + .GetResult(); + + AssertArchTestResult(result); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Tests/ArchTests/SeedWork/TestBase.cs b/src/Modules/Registrations/Tests/ArchTests/SeedWork/TestBase.cs new file mode 100644 index 000000000..8f7d142d1 --- /dev/null +++ b/src/Modules/Registrations/Tests/ArchTests/SeedWork/TestBase.cs @@ -0,0 +1,44 @@ +using System.Reflection; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using CompanyName.MyMeetings.Modules.Registrations.Domain; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure; +using NetArchTest.Rules; +using NUnit.Framework; + +namespace CompanyName.MyMeetings.Modules.Registrations.ArchTests.SeedWork +{ + public abstract class TestBase + { + protected static Assembly ApplicationAssembly => typeof(CommandBase).Assembly; + + protected static Assembly DomainAssembly => typeof(UserRegistration).Assembly; + + protected static Assembly InfrastructureAssembly => typeof(RegistrationsContext).Assembly; + + protected static void AssertAreImmutable(IEnumerable types) + { + List failingTypes = []; + foreach (var type in types) + { + if (type.GetFields().Any(x => !x.IsInitOnly) || type.GetProperties().Any(x => x.CanWrite)) + { + failingTypes.Add(type); + break; + } + } + + AssertFailingTypes(failingTypes); + } + + protected static void AssertFailingTypes(IEnumerable types) + { + Assert.That(types, Is.Null.Or.Empty); + } + + protected static void AssertArchTestResult(TestResult result) + { + AssertFailingTypes(result.FailingTypes); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Tests/IntegrationTests/AssemblyInfo.cs b/src/Modules/Registrations/Tests/IntegrationTests/AssemblyInfo.cs new file mode 100644 index 000000000..87d16a8b8 --- /dev/null +++ b/src/Modules/Registrations/Tests/IntegrationTests/AssemblyInfo.cs @@ -0,0 +1,11 @@ +using NUnit.Framework; + +[assembly: NonParallelizable] +[assembly: LevelOfParallelism(1)] + +namespace CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests +{ + public class AssemblyInfo + { + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Tests/IntegrationTests/CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.csproj b/src/Modules/Registrations/Tests/IntegrationTests/CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.csproj new file mode 100644 index 000000000..a6968989f --- /dev/null +++ b/src/Modules/Registrations/Tests/IntegrationTests/CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.csproj @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Modules/Registrations/Tests/IntegrationTests/SeedWork/ExecutionContextMock.cs b/src/Modules/Registrations/Tests/IntegrationTests/SeedWork/ExecutionContextMock.cs new file mode 100644 index 000000000..bc9999c41 --- /dev/null +++ b/src/Modules/Registrations/Tests/IntegrationTests/SeedWork/ExecutionContextMock.cs @@ -0,0 +1,23 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application; + +namespace CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.SeedWork +{ + public class ExecutionContextMock : IExecutionContextAccessor + { + public ExecutionContextMock(Guid userId) + { + UserId = userId; + } + + public Guid UserId { get; private set; } + + public Guid CorrelationId { get; } + + public bool IsAvailable { get; } + + public void SetUserId(Guid userId) + { + this.UserId = userId; + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs b/src/Modules/Registrations/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs new file mode 100644 index 000000000..f0fc58760 --- /dev/null +++ b/src/Modules/Registrations/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs @@ -0,0 +1,35 @@ +using System.Data; +using System.Reflection; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Outbox; +using Dapper; +using MediatR; +using Newtonsoft.Json; + +namespace CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.SeedWork +{ + public class OutboxMessagesHelper + { + public static async Task> GetOutboxMessages(IDbConnection connection) + { + const string sql = $""" + SELECT + [OutboxMessage].[Id] as [{nameof(OutboxMessageDto.Id)}], + [OutboxMessage].[Type] as [{nameof(OutboxMessageDto.Type)}], + [OutboxMessage].[Data] as [{nameof(OutboxMessageDto.Data)}] + FROM [registrations].[OutboxMessages] AS [OutboxMessage] + ORDER BY [OutboxMessage].[OccurredOn] + """; + + var messages = await connection.QueryAsync(sql); + return messages.AsList(); + } + + public static T Deserialize(OutboxMessageDto message) + where T : class, INotification + { + Type type = Assembly.GetAssembly(typeof(CommandBase)).GetType(typeof(T).FullName); + return JsonConvert.DeserializeObject(message.Data, type) as T; + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Tests/IntegrationTests/SeedWork/TestBase.cs b/src/Modules/Registrations/Tests/IntegrationTests/SeedWork/TestBase.cs new file mode 100644 index 000000000..393d0c962 --- /dev/null +++ b/src/Modules/Registrations/Tests/IntegrationTests/SeedWork/TestBase.cs @@ -0,0 +1,80 @@ +using System.Data; +using System.Data.SqlClient; +using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; +using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration; +using Dapper; +using MediatR; +using NSubstitute; +using NUnit.Framework; +using Serilog; + +namespace CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.SeedWork +{ + public class TestBase + { + protected string ConnectionString { get; private set; } + + protected ILogger Logger { get; private set; } + + protected IRegistrationsModule RegistrationsModule { get; private set; } + + protected IEmailSender EmailSender { get; private set; } + + [SetUp] + public async Task BeforeEachTest() + { + const string connectionStringEnvironmentVariable = + "ASPNETCORE_MyMeetings_IntegrationTests_ConnectionString"; + ConnectionString = EnvironmentVariablesProvider.GetVariable(connectionStringEnvironmentVariable); + if (ConnectionString == null) + { + throw new ApplicationException( + $"Define connection string to integration tests database using environment variable: {connectionStringEnvironmentVariable}"); + } + + using (var sqlConnection = new SqlConnection(ConnectionString)) + { + await ClearDatabase(sqlConnection); + } + + Logger = Substitute.For(); + EmailSender = Substitute.For(); + + RegistrationsStartup.Initialize( + ConnectionString, + new ExecutionContextMock(Guid.NewGuid()), + Logger, + new EmailsConfiguration("from@email.com"), + "key", + EmailSender, + null); + + RegistrationsModule = new RegistrationsModule(); + } + + protected async Task GetLastOutboxMessage() + where T : class, INotification + { + using (var connection = new SqlConnection(ConnectionString)) + { + var messages = await OutboxMessagesHelper.GetOutboxMessages(connection); + + return OutboxMessagesHelper.Deserialize(messages.Last()); + } + } + + private static async Task ClearDatabase(IDbConnection connection) + { + const string sql = "DELETE FROM [registrations].[InboxMessages] " + + "DELETE FROM [registrations].[InternalCommands] " + + "DELETE FROM [registrations].[OutboxMessages] " + + "DELETE FROM [registrations].[UserRegistrations] "; + + await connection.ExecuteScalarAsync(sql); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/ConfirmUserRegistrationTests.cs b/src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/ConfirmUserRegistrationTests.cs new file mode 100644 index 000000000..cae62a773 --- /dev/null +++ b/src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/ConfirmUserRegistrationTests.cs @@ -0,0 +1,31 @@ +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.GetUserRegistration; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; +using CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.SeedWork; +using NUnit.Framework; + +namespace CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.UserRegistrations +{ + [TestFixture] + public class ConfirmUserRegistrationTests : TestBase + { + [Test] + public async Task ConfirmUserRegistration_Test() + { + var registrationId = await RegistrationsModule.ExecuteCommandAsync(new RegisterNewUserCommand( + UserRegistrationSampleData.Login, + UserRegistrationSampleData.Password, + UserRegistrationSampleData.Email, + UserRegistrationSampleData.FirstName, + UserRegistrationSampleData.LastName, + "confirmLink")); + + await RegistrationsModule.ExecuteCommandAsync(new ConfirmUserRegistrationCommand(registrationId)); + + var userRegistration = await RegistrationsModule.ExecuteQueryAsync(new GetUserRegistrationQuery(registrationId)); + + Assert.That(userRegistration.StatusCode, Is.EqualTo(UserRegistrationStatus.Confirmed.Value)); + } + } +} \ No newline at end of file diff --git a/src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/SendUserRegistrationConfirmationEmailTests.cs b/src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/SendUserRegistrationConfirmationEmailTests.cs similarity index 69% rename from src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/SendUserRegistrationConfirmationEmailTests.cs rename to src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/SendUserRegistrationConfirmationEmailTests.cs index ed2163247..2d5c2b38e 100644 --- a/src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/SendUserRegistrationConfirmationEmailTests.cs +++ b/src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/SendUserRegistrationConfirmationEmailTests.cs @@ -1,11 +1,11 @@ using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.SendUserRegistrationConfirmationEmail; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; -using CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.SeedWork; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.SendUserRegistrationConfirmationEmail; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; +using CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.SeedWork; using NSubstitute.ReceivedExtensions; using NUnit.Framework; -namespace CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.UserRegistrations +namespace CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.UserRegistrations { [TestFixture] public class SendUserRegistrationConfirmationEmailTests : TestBase @@ -16,7 +16,7 @@ public async Task SendUserRegistrationConfirmationEmail_Test() var registrationId = Guid.NewGuid(); var confirmLink = "confirmLink/"; - await UserAccessModule.ExecuteCommandAsync(new SendUserRegistrationConfirmationEmailCommand( + await RegistrationsModule.ExecuteCommandAsync(new SendUserRegistrationConfirmationEmailCommand( Guid.NewGuid(), new UserRegistrationId(registrationId), UserRegistrationSampleData.Email, diff --git a/src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/UserRegistrationSampleData.cs b/src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/UserRegistrationSampleData.cs similarity index 76% rename from src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/UserRegistrationSampleData.cs rename to src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/UserRegistrationSampleData.cs index 0cbe855eb..07fe182e2 100644 --- a/src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/UserRegistrationSampleData.cs +++ b/src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/UserRegistrationSampleData.cs @@ -1,4 +1,4 @@ -namespace CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.UserRegistrations +namespace CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.UserRegistrations { public struct UserRegistrationSampleData { diff --git a/src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/UserRegistrationTests.cs b/src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/UserRegistrationTests.cs similarity index 69% rename from src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/UserRegistrationTests.cs rename to src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/UserRegistrationTests.cs index e01449241..08830143a 100644 --- a/src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/UserRegistrationTests.cs +++ b/src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/UserRegistrationTests.cs @@ -1,10 +1,10 @@ using System.Data.SqlClient; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.GetUserRegistration; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser; -using CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.SeedWork; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.GetUserRegistration; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser; +using CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.SeedWork; using NUnit.Framework; -namespace CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.UserRegistrations +namespace CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.UserRegistrations { [TestFixture] public class UserRegistrationTests : TestBase @@ -12,7 +12,7 @@ public class UserRegistrationTests : TestBase [Test] public async Task RegisterNewUserCommand_Test() { - var registrationId = await UserAccessModule.ExecuteCommandAsync(new RegisterNewUserCommand( + var registrationId = await RegistrationsModule.ExecuteCommandAsync(new RegisterNewUserCommand( UserRegistrationSampleData.Login, UserRegistrationSampleData.Password, UserRegistrationSampleData.Email, @@ -20,7 +20,7 @@ public async Task RegisterNewUserCommand_Test() UserRegistrationSampleData.LastName, "confirmLink")); - var userRegistration = await UserAccessModule.ExecuteQueryAsync(new GetUserRegistrationQuery(registrationId)); + var userRegistration = await RegistrationsModule.ExecuteQueryAsync(new GetUserRegistrationQuery(registrationId)); Assert.That(userRegistration.Email, Is.EqualTo(UserRegistrationSampleData.Email)); Assert.That(userRegistration.Login, Is.EqualTo(UserRegistrationSampleData.Login)); diff --git a/src/Modules/Registrations/Tests/UnitTests/CompanyName.MyMeetings.Modules.Registrations.Domain.UnitTests.csproj b/src/Modules/Registrations/Tests/UnitTests/CompanyName.MyMeetings.Modules.Registrations.Domain.UnitTests.csproj new file mode 100644 index 000000000..a6968989f --- /dev/null +++ b/src/Modules/Registrations/Tests/UnitTests/CompanyName.MyMeetings.Modules.Registrations.Domain.UnitTests.csproj @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Modules/Registrations/Tests/UnitTests/SeedWork/DomainEventsTestHelper.cs b/src/Modules/Registrations/Tests/UnitTests/SeedWork/DomainEventsTestHelper.cs new file mode 100644 index 000000000..86f1f83a1 --- /dev/null +++ b/src/Modules/Registrations/Tests/UnitTests/SeedWork/DomainEventsTestHelper.cs @@ -0,0 +1,48 @@ +using System.Collections; +using System.Reflection; +using CompanyName.MyMeetings.BuildingBlocks.Domain; + +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UnitTests.SeedWork +{ + public class DomainEventsTestHelper + { + public static List GetAllDomainEvents(Entity aggregate) + { + List domainEvents = []; + + if (aggregate.DomainEvents != null) + { + domainEvents.AddRange(aggregate.DomainEvents); + } + + var fields = aggregate.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public).Concat(aggregate.GetType().BaseType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)).ToArray(); + + foreach (var field in fields) + { + var isEntity = typeof(Entity).IsAssignableFrom(field.FieldType); + + if (isEntity) + { + var entity = field.GetValue(aggregate) as Entity; + domainEvents.AddRange(GetAllDomainEvents(entity).ToList()); + } + + if (field.FieldType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(field.FieldType)) + { + if (field.GetValue(aggregate) is IEnumerable enumerable) + { + foreach (var en in enumerable) + { + if (en is Entity entityItem) + { + domainEvents.AddRange(GetAllDomainEvents(entityItem)); + } + } + } + } + } + + return domainEvents; + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Tests/UnitTests/SeedWork/TestBase.cs b/src/Modules/Registrations/Tests/UnitTests/SeedWork/TestBase.cs new file mode 100644 index 000000000..0887e985e --- /dev/null +++ b/src/Modules/Registrations/Tests/UnitTests/SeedWork/TestBase.cs @@ -0,0 +1,32 @@ +using CompanyName.MyMeetings.BuildingBlocks.Domain; +using NUnit.Framework; + +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UnitTests.SeedWork +{ + public abstract class TestBase + { + public static T AssertPublishedDomainEvent(Entity aggregate) + where T : IDomainEvent + { + var domainEvent = DomainEventsTestHelper.GetAllDomainEvents(aggregate).OfType().SingleOrDefault(); + + if (domainEvent == null) + { + throw new Exception($"{typeof(T).Name} event not published"); + } + + return domainEvent; + } + + public static void AssertBrokenRule(TestDelegate testDelegate) + where TRule : class, IBusinessRule + { + var message = $"Expected {typeof(TRule).Name} broken rule"; + var businessRuleValidationException = Assert.Catch(testDelegate, message); + if (businessRuleValidationException != null) + { + Assert.That(businessRuleValidationException.BrokenRule, Is.TypeOf(), message); + } + } + } +} \ No newline at end of file diff --git a/src/Modules/UserAccess/Tests/UnitTests/UserRegistrations/UserRegistrationTests.cs b/src/Modules/Registrations/Tests/UnitTests/UserRegistrations/UserRegistrationTests.cs similarity index 71% rename from src/Modules/UserAccess/Tests/UnitTests/UserRegistrations/UserRegistrationTests.cs rename to src/Modules/Registrations/Tests/UnitTests/UserRegistrations/UserRegistrationTests.cs index c05229164..58cc08bd6 100644 --- a/src/Modules/UserAccess/Tests/UnitTests/UserRegistrations/UserRegistrationTests.cs +++ b/src/Modules/Registrations/Tests/UnitTests/UserRegistrations/UserRegistrationTests.cs @@ -1,12 +1,11 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UnitTests.SeedWork; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Events; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Rules; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users.Events; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UnitTests.SeedWork; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules; using NSubstitute; using NUnit.Framework; -namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.UnitTests.UserRegistrations +namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UnitTests.UserRegistrations { [TestFixture] public class UserRegistrationTests : TestBase @@ -156,46 +155,46 @@ public void UserRegistration_WhenIsExpired_CannotBeExpiredAgain() AssertBrokenRule(() => { registration.Expire(); }); } - [Test] - public void CreateUser_WhenRegistrationIsConfirmed_IsSuccessful() - { - var usersCounter = Substitute.For(); - - var registration = UserRegistration.RegisterNewUser( - "login", - "password", - "test@email", - "firstName", - "lastName", - usersCounter, - "confirmLink"); - - registration.Confirm(); - - var user = registration.CreateUser(); - - var userCreated = AssertPublishedDomainEvent(user); - - Assert.That(user.Id, Is.EqualTo(registration.Id)); - Assert.That(userCreated.Id, Is.EqualTo(registration.Id)); - } - - [Test] - public void UserCreation_WhenRegistrationIsNotConfirmed_IsNotPossible() - { - var usersCounter = Substitute.For(); - - var registration = UserRegistration.RegisterNewUser( - "login", - "password", - "test@email", - "firstName", - "lastName", - usersCounter, - "confirmLink"); - - AssertBrokenRule( - () => { registration.CreateUser(); }); - } + // [Test] + // public void CreateUser_WhenRegistrationIsConfirmed_IsSuccessful() + // { + // var usersCounter = Substitute.For(); + // + // var registration = UserRegistration.RegisterNewUser( + // "login", + // "password", + // "test@email", + // "firstName", + // "lastName", + // usersCounter, + // "confirmLink"); + // + // registration.Confirm(); + // + // var user = registration.CreateUser(); + // + // var userCreated = AssertPublishedDomainEvent(user); + // + // Assert.That(user.Id, Is.EqualTo(registration.Id)); + // Assert.That(userCreated.Id, Is.EqualTo(registration.Id)); + //// } + + // [Test] + // public void UserCreation_WhenRegistrationIsNotConfirmed_IsNotPossible() + // { + // var usersCounter = Substitute.For(); + // + // var registration = UserRegistration.RegisterNewUser( + // "login", + // "password", + // "test@email", + // "firstName", + // "lastName", + // usersCounter, + // "confirmLink"); + // + // AssertBrokenRule( + // () => { registration.CreateUser(); }); + // } } } \ No newline at end of file diff --git a/src/Modules/UserAccess/Application/Authentication/PasswordManager.cs b/src/Modules/UserAccess/Application/Authentication/Authenticate/PasswordManager.cs similarity index 98% rename from src/Modules/UserAccess/Application/Authentication/PasswordManager.cs rename to src/Modules/UserAccess/Application/Authentication/Authenticate/PasswordManager.cs index ea7af0666..9ae17c478 100644 --- a/src/Modules/UserAccess/Application/Authentication/PasswordManager.cs +++ b/src/Modules/UserAccess/Application/Authentication/Authenticate/PasswordManager.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; using System.Security.Cryptography; -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication +namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication.Authenticate { public class PasswordManager { diff --git a/src/Modules/UserAccess/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedHandler.cs b/src/Modules/UserAccess/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedHandler.cs deleted file mode 100644 index 2420aa163..000000000 --- a/src/Modules/UserAccess/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedHandler.cs +++ /dev/null @@ -1,31 +0,0 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations.Events; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users; -using MediatR; - -namespace CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.ConfirmUserRegistration -{ - public class UserRegistrationConfirmedHandler : INotificationHandler - { - private readonly IUserRegistrationRepository _userRegistrationRepository; - - private readonly IUserRepository _userRepository; - - public UserRegistrationConfirmedHandler( - IUserRegistrationRepository userRegistrationRepository, - IUserRepository userRepository) - { - _userRegistrationRepository = userRegistrationRepository; - _userRepository = userRepository; - } - - public async Task Handle(UserRegistrationConfirmedDomainEvent @event, CancellationToken cancellationToken) - { - var userRegistration = await _userRegistrationRepository.GetByIdAsync(@event.UserRegistrationId); - - var user = userRegistration.CreateUser(); - - await _userRepository.AddAsync(user); - } - } -} \ No newline at end of file diff --git a/src/Modules/UserAccess/Application/Users/AddAdminUser/AddAdminUserCommandHandler.cs b/src/Modules/UserAccess/Application/Users/AddAdminUser/AddAdminUserCommandHandler.cs index 297b1d126..c5ce9bc99 100644 --- a/src/Modules/UserAccess/Application/Users/AddAdminUser/AddAdminUserCommandHandler.cs +++ b/src/Modules/UserAccess/Application/Users/AddAdminUser/AddAdminUserCommandHandler.cs @@ -1,4 +1,5 @@ using CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication; +using CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication.Authenticate; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users; diff --git a/src/Modules/UserAccess/Domain/Users/User.cs b/src/Modules/UserAccess/Domain/Users/User.cs index a26b01f92..690526c4d 100644 --- a/src/Modules/UserAccess/Domain/Users/User.cs +++ b/src/Modules/UserAccess/Domain/Users/User.cs @@ -1,5 +1,4 @@ using CompanyName.MyMeetings.BuildingBlocks.Domain; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users.Events; namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.Users @@ -51,7 +50,7 @@ public static User CreateAdmin( } internal static User CreateFromUserRegistration( - UserRegistrationId userRegistrationId, + Guid userId, string login, string password, string email, @@ -60,7 +59,7 @@ internal static User CreateFromUserRegistration( string name) { return new User( - userRegistrationId.Value, + userId, login, password, email, diff --git a/src/Modules/UserAccess/Infrastructure/Configuration/UserAccessStartup.cs b/src/Modules/UserAccess/Infrastructure/Configuration/UserAccessStartup.cs index ef5d3547b..fda9ddd11 100644 --- a/src/Modules/UserAccess/Infrastructure/Configuration/UserAccessStartup.cs +++ b/src/Modules/UserAccess/Infrastructure/Configuration/UserAccessStartup.cs @@ -1,18 +1,14 @@ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; -using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.DataAccess; -using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Domain; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Email; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.EventsBus; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Logging; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Mediation; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing; -using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.Outbox; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Quartz; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Security; using Serilog; @@ -64,15 +60,10 @@ private static void ConfigureCompositionRoot( var loggerFactory = new Serilog.Extensions.Logging.SerilogLoggerFactory(logger); containerBuilder.RegisterModule(new DataAccessModule(connectionString, loggerFactory)); - containerBuilder.RegisterModule(new DomainModule()); containerBuilder.RegisterModule(new ProcessingModule()); containerBuilder.RegisterModule(new EventsBusModule(eventsBus)); containerBuilder.RegisterModule(new MediatorModule()); - var domainNotificationsMap = new BiDictionary(); - domainNotificationsMap.Add("NewUserRegisteredNotification", typeof(NewUserRegisteredNotification)); - containerBuilder.RegisterModule(new OutboxModule(domainNotificationsMap)); - containerBuilder.RegisterModule(new QuartzModule()); containerBuilder.RegisterModule(new EmailModule(emailsConfiguration, emailSender)); containerBuilder.RegisterModule(new SecurityModule(textEncryptionKey)); diff --git a/src/Modules/UserAccess/Infrastructure/Domain/UserRegistrations/UserRegistrationRepository.cs b/src/Modules/UserAccess/Infrastructure/Domain/UserRegistrations/UserRegistrationRepository.cs deleted file mode 100644 index 8a304f904..000000000 --- a/src/Modules/UserAccess/Infrastructure/Domain/UserRegistrations/UserRegistrationRepository.cs +++ /dev/null @@ -1,25 +0,0 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; -using Microsoft.EntityFrameworkCore; - -namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Domain.UserRegistrations -{ - public class UserRegistrationRepository : IUserRegistrationRepository - { - private readonly UserAccessContext _userAccessContext; - - public UserRegistrationRepository(UserAccessContext userAccessContext) - { - _userAccessContext = userAccessContext; - } - - public async Task AddAsync(UserRegistration userRegistration) - { - await _userAccessContext.AddAsync(userRegistration); - } - - public async Task GetByIdAsync(UserRegistrationId userRegistrationId) - { - return await _userAccessContext.UserRegistrations.FirstOrDefaultAsync(x => x.Id == userRegistrationId); - } - } -} \ No newline at end of file diff --git a/src/Modules/UserAccess/Infrastructure/UserAccessContext.cs b/src/Modules/UserAccess/Infrastructure/UserAccessContext.cs index 8b94db909..14ad23193 100644 --- a/src/Modules/UserAccess/Infrastructure/UserAccessContext.cs +++ b/src/Modules/UserAccess/Infrastructure/UserAccessContext.cs @@ -1,8 +1,6 @@ using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users; -using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Domain.UserRegistrations; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Domain.Users; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.InternalCommands; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Outbox; @@ -13,8 +11,6 @@ namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure { public class UserAccessContext : DbContext { - public DbSet UserRegistrations { get; set; } - public DbSet Users { get; set; } public DbSet OutboxMessages { get; set; } @@ -31,7 +27,6 @@ public UserAccessContext(DbContextOptions options, ILoggerFactory loggerFactory) protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.ApplyConfiguration(new UserRegistrationEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new UserEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new OutboxMessageEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new InternalCommandEntityTypeConfiguration()); diff --git a/src/Modules/UserAccess/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs b/src/Modules/UserAccess/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs index eb85e0576..1f7bb7524 100644 --- a/src/Modules/UserAccess/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs +++ b/src/Modules/UserAccess/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs @@ -1,6 +1,6 @@ using System.Data; using System.Reflection; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser; +using CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication.Authenticate; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.Outbox; using Dapper; using MediatR; @@ -28,7 +28,7 @@ ORDER BY [OutboxMessage].[OccurredOn] public static T Deserialize(OutboxMessageDto message) where T : class, INotification { - Type type = Assembly.GetAssembly(typeof(NewUserRegisteredNotification)).GetType(typeof(T).FullName); + Type type = Assembly.GetAssembly(typeof(AuthenticateCommand)).GetType(typeof(T).FullName); return JsonConvert.DeserializeObject(message.Data, type) as T; } } diff --git a/src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/ConfirmUserRegistrationTests.cs b/src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/ConfirmUserRegistrationTests.cs deleted file mode 100644 index 337493a46..000000000 --- a/src/Modules/UserAccess/Tests/IntegrationTests/UserRegistrations/ConfirmUserRegistrationTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.ConfirmUserRegistration; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.GetUserRegistration; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser; -using CompanyName.MyMeetings.Modules.UserAccess.Domain.UserRegistrations; -using CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.SeedWork; -using NUnit.Framework; - -namespace CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.UserRegistrations -{ - [TestFixture] - public class ConfirmUserRegistrationTests : TestBase - { - [Test] - public async Task ConfirmUserRegistration_Test() - { - var registrationId = await UserAccessModule.ExecuteCommandAsync(new RegisterNewUserCommand( - UserRegistrationSampleData.Login, - UserRegistrationSampleData.Password, - UserRegistrationSampleData.Email, - UserRegistrationSampleData.FirstName, - UserRegistrationSampleData.LastName, - "confirmLink")); - - await UserAccessModule.ExecuteCommandAsync(new ConfirmUserRegistrationCommand(registrationId)); - - var userRegistration = await UserAccessModule.ExecuteQueryAsync(new GetUserRegistrationQuery(registrationId)); - - Assert.That(userRegistration.StatusCode, Is.EqualTo(UserRegistrationStatus.Confirmed.Value)); - } - } -} \ No newline at end of file diff --git a/src/Modules/UserAccess/Tests/IntegrationTests/Users/CreateUserTests.cs b/src/Modules/UserAccess/Tests/IntegrationTests/Users/CreateUserTests.cs index 347eaa72a..72bd45568 100644 --- a/src/Modules/UserAccess/Tests/IntegrationTests/Users/CreateUserTests.cs +++ b/src/Modules/UserAccess/Tests/IntegrationTests/Users/CreateUserTests.cs @@ -1,30 +1,30 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.ConfirmUserRegistration; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser; -using CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetUser; -using CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.SeedWork; -using CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.UserRegistrations; -using NUnit.Framework; - -namespace CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.Users -{ - [TestFixture] - public class CreateUserTests : TestBase - { - [Test] - public async Task CreateUser_Test() - { - var registrationId = await UserAccessModule.ExecuteCommandAsync(new RegisterNewUserCommand( - UserRegistrationSampleData.Login, - UserRegistrationSampleData.Password, - UserRegistrationSampleData.Email, - UserRegistrationSampleData.FirstName, - UserRegistrationSampleData.LastName, - "confirmLink")); - await UserAccessModule.ExecuteCommandAsync(new ConfirmUserRegistrationCommand(registrationId)); - - var user = await UserAccessModule.ExecuteQueryAsync(new GetUserQuery(registrationId)); - - Assert.That(user.Login, Is.EqualTo(UserRegistrationSampleData.Login)); - } - } -} \ No newline at end of file +// using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.ConfirmUserRegistration; +// using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser; +// using CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetUser; +// using CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.SeedWork; +// using CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.UserRegistrations; +// using NUnit.Framework; +// +// namespace CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.Users +// { +// [TestFixture] +// public class CreateUserTests : TestBase +// { +// [Test] +// public async Task CreateUser_Test() +// { +// var registrationId = await UserAccessModule.ExecuteCommandAsync(new RegisterNewUserCommand( +// UserRegistrationSampleData.Login, +// UserRegistrationSampleData.Password, +// UserRegistrationSampleData.Email, +// UserRegistrationSampleData.FirstName, +// UserRegistrationSampleData.LastName, +// "confirmLink")); +// await UserAccessModule.ExecuteCommandAsync(new ConfirmUserRegistrationCommand(registrationId)); +// +// var user = await UserAccessModule.ExecuteQueryAsync(new GetUserQuery(registrationId)); +// +// Assert.That(user.Login, Is.EqualTo(UserRegistrationSampleData.Login)); +// } +// } +// } \ No newline at end of file diff --git a/src/Tests/SUT/Helpers/UsersFactory.cs b/src/Tests/SUT/Helpers/UsersFactory.cs index 94fbd305c..0894f3cf4 100644 --- a/src/Tests/SUT/Helpers/UsersFactory.cs +++ b/src/Tests/SUT/Helpers/UsersFactory.cs @@ -1,6 +1,7 @@ -using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.ConfirmUserRegistration; -using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser; +using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using CompanyName.MyMeetings.Modules.UserAccess.Application.Users.AddAdminUser; using CompanyName.MyMeetings.SUT.SeedWork; @@ -27,7 +28,7 @@ await userAccessModule.ExecuteCommandAsync(new AddAdminUserCommand( } public static async Task GivenUser( - IUserAccessModule userAccessModule, + IRegistrationsModule registrationsModule, string connectionString, string login, string password, @@ -35,7 +36,7 @@ public static async Task GivenUser( string lastName, string email) { - var userRegistrationId = await userAccessModule.ExecuteCommandAsync(new RegisterNewUserCommand( + var userRegistrationId = await registrationsModule.ExecuteCommandAsync(new RegisterNewUserCommand( login, password, email, @@ -43,7 +44,7 @@ public static async Task GivenUser( lastName, email)); - await userAccessModule.ExecuteCommandAsync(new ConfirmUserRegistrationCommand(userRegistrationId)); + await registrationsModule.ExecuteCommandAsync(new ConfirmUserRegistrationCommand(userRegistrationId)); await AsyncOperationsHelper.WaitForProcessing(connectionString); diff --git a/src/Tests/SUT/SeedWork/TestBase.cs b/src/Tests/SUT/SeedWork/TestBase.cs index 1f7e49f87..13677c036 100644 --- a/src/Tests/SUT/SeedWork/TestBase.cs +++ b/src/Tests/SUT/SeedWork/TestBase.cs @@ -12,6 +12,9 @@ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Infrastructure; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration; +using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration; @@ -37,6 +40,8 @@ public class TestBase protected IUserAccessModule UserAccessModule { get; private set; } + protected IRegistrationsModule RegistrationsModule { get; private set; } + protected IMeetingsModule MeetingsModule { get; private set; } protected IAdministrationModule AdministrationModule { get; private set; } @@ -70,6 +75,8 @@ public async Task BeforeEachTest() EventsBus = new InMemoryEventBusClient(Logger); + InitializeRegistrationsModule(emailsConfiguration); + InitializeUserAccessModule(emailsConfiguration); InitializeMeetingsModule(emailsConfiguration); @@ -170,6 +177,24 @@ private void InitializeUserAccessModule(EmailsConfiguration emailsConfiguration) UserAccessModule = new UserAccessModule(); } + private void InitializeRegistrationsModule(EmailsConfiguration emailsConfiguration) + { + Logger = Substitute.For(); + EmailSender = Substitute.For(); + + RegistrationsStartup.Initialize( + ConnectionString, + ExecutionContextAccessor, + Logger, + emailsConfiguration, + "key", + EmailSender, + EventsBus, + 100); + + RegistrationsModule = new RegistrationsModule(); + } + private void SetConnectionString() { const string connectionStringEnvironmentVariable = "MyMeetings_SUTDatabaseConnectionString"; diff --git a/src/Tests/SUT/TestCases/CreateMeeting.cs b/src/Tests/SUT/TestCases/CreateMeeting.cs index fd2edf217..3bfd467e7 100644 --- a/src/Tests/SUT/TestCases/CreateMeeting.cs +++ b/src/Tests/SUT/TestCases/CreateMeeting.cs @@ -21,7 +21,7 @@ await UsersFactory.GivenAdmin( "testAdmin@mail.com"); var userId = await UsersFactory.GivenUser( - UserAccessModule, + RegistrationsModule, ConnectionString, "adamSmith@mail.com", "adamSmithPass", @@ -68,7 +68,7 @@ await TestPaymentsManager.BuySubscription( []); var attendeeUserId = await UsersFactory.GivenUser( - UserAccessModule, + RegistrationsModule, ConnectionString, "rickmorty@mail.com", "rickmortyPass", From 3de0939bff9e1bb54186f728e2d779e13e3838fa Mon Sep 17 00:00:00 2001 From: kgrzybek Date: Tue, 2 Jan 2024 21:19:20 +0100 Subject: [PATCH 2/6] refactor: extract Registrations WIP - implement registrations -> user access --- .../Views/v_UserRegistrations.sql | 3 +- .../ConfirmUserRegistration/IUserCreator.cs | 13 +++++++ .../UserRegistrationConfirmedHandler.cs | 27 ------------- .../UserRegistrationConfirmedNotification.cs | 12 ++++++ ...egistrationConfirmedNotificationHandler.cs | 36 ++++++++++++++++++ .../GetUserRegistrationQueryHandler.cs | 23 +---------- .../UserRegistrationDto.cs | 2 + .../UserRegistrationProvider.cs | 33 ++++++++++++++++ ...odules.Registrations.Infrastructure.csproj | 1 + .../Configuration/RegistrationsStartup.cs | 2 + .../Infrastructure/Users/UserAccessGateway.cs | 34 +++++++++++++++++ .../Users/CreateUser/CreateUserCommand.cs | 38 +++++++++++++++++++ .../CreateUser/CreateUserCommandHandler.cs | 28 ++++++++++++++ src/Modules/UserAccess/Domain/Users/User.cs | 2 +- 14 files changed, 204 insertions(+), 50 deletions(-) create mode 100644 src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/IUserCreator.cs delete mode 100644 src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedHandler.cs create mode 100644 src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotification.cs create mode 100644 src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotificationHandler.cs create mode 100644 src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationProvider.cs create mode 100644 src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs create mode 100644 src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommand.cs create mode 100644 src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommandHandler.cs diff --git a/src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Views/v_UserRegistrations.sql b/src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Views/v_UserRegistrations.sql index 68898d161..c931c13de 100644 --- a/src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Views/v_UserRegistrations.sql +++ b/src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Views/v_UserRegistrations.sql @@ -7,6 +7,7 @@ SELECT [UserRegistration].[FirstName], [UserRegistration].[LastName], [UserRegistration].[Name], - [UserRegistration].[StatusCode] + [UserRegistration].[StatusCode], + [UserRegistration].[Password] FROM [registrations].[UserRegistrations] AS [UserRegistration] GO \ No newline at end of file diff --git a/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/IUserCreator.cs b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/IUserCreator.cs new file mode 100644 index 000000000..cc3f607e2 --- /dev/null +++ b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/IUserCreator.cs @@ -0,0 +1,13 @@ +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration; + +public interface IUserCreator +{ + public Task Create( + Guid userRegistrationId, + string login, + string password, + string email, + string firstName, + string lastName, + string name); +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedHandler.cs b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedHandler.cs deleted file mode 100644 index f34fb675f..000000000 --- a/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; -using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events; -using MediatR; - -namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration -{ - public class UserRegistrationConfirmedHandler : INotificationHandler - { - private readonly IUserRegistrationRepository _userRegistrationRepository; - - public UserRegistrationConfirmedHandler( - IUserRegistrationRepository userRegistrationRepository) - { - _userRegistrationRepository = userRegistrationRepository; - } - - public Task Handle(UserRegistrationConfirmedDomainEvent @event, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - //// var userRegistration = await _userRegistrationRepository.GetByIdAsync(@event.UserRegistrationId); - // - // var user = userRegistration.CreateUser(); - // - // await _userRepository.AddAsync(user); - } - } -} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotification.cs b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotification.cs new file mode 100644 index 000000000..493513ff5 --- /dev/null +++ b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotification.cs @@ -0,0 +1,12 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application.Events; +using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events; + +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration; + +public class UserRegistrationConfirmedNotification : DomainNotificationBase +{ + public UserRegistrationConfirmedNotification(UserRegistrationConfirmedDomainEvent domainEvent, Guid id) + : base(domainEvent, id) + { + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotificationHandler.cs b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotificationHandler.cs new file mode 100644 index 000000000..ca64d77b4 --- /dev/null +++ b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotificationHandler.cs @@ -0,0 +1,36 @@ +using CompanyName.MyMeetings.BuildingBlocks.Application.Data; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.GetUserRegistration; +using MediatR; + +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration; + +public class UserRegistrationConfirmedNotificationHandler : INotificationHandler +{ + private readonly IUserCreator _userCreator; + + private readonly ISqlConnectionFactory _sqlConnectionFactory; + + public UserRegistrationConfirmedNotificationHandler(IUserCreator userCreator, ISqlConnectionFactory sqlConnectionFactory) + { + _userCreator = userCreator; + _sqlConnectionFactory = sqlConnectionFactory; + } + + public async Task Handle(UserRegistrationConfirmedNotification notification, CancellationToken cancellationToken) + { + var connection = _sqlConnectionFactory.GetOpenConnection(); + + var registration = await UserRegistrationProvider.GetById( + connection, + notification.DomainEvent.UserRegistrationId.Value); + + await _userCreator.Create( + registration.Id, + registration.Login, + registration.Password, + registration.Email, + registration.FirstName, + registration.LastName, + registration.Name); + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQueryHandler.cs b/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQueryHandler.cs index 2fb53b4a5..99eda70ae 100644 --- a/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQueryHandler.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQueryHandler.cs @@ -1,6 +1,5 @@ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Queries; -using Dapper; namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.GetUserRegistration { @@ -13,29 +12,11 @@ public GetUserRegistrationQueryHandler(ISqlConnectionFactory sqlConnectionFactor _sqlConnectionFactory = sqlConnectionFactory; } - public async Task Handle(GetUserRegistrationQuery query, CancellationToken cancellationToken) + public Task Handle(GetUserRegistrationQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); - const string sql = $""" - SELECT - [UserRegistration].[Id] as [{nameof(UserRegistrationDto.Id)}], - [UserRegistration].[Login] as [{nameof(UserRegistrationDto.Login)}], - [UserRegistration].[Email] as [{nameof(UserRegistrationDto.Email)}], - [UserRegistration].[FirstName] as [{nameof(UserRegistrationDto.FirstName)}], - [UserRegistration].[LastName] as [{nameof(UserRegistrationDto.LastName)}], - [UserRegistration].[Name] as [{nameof(UserRegistrationDto.Name)}], - [UserRegistration].[StatusCode] as [{nameof(UserRegistrationDto.StatusCode)}] - FROM [registrations].[v_UserRegistrations] AS [UserRegistration] - WHERE [UserRegistration].[Id] = @UserRegistrationId - """; - - return await connection.QuerySingleAsync( - sql, - new - { - query.UserRegistrationId - }); + return UserRegistrationProvider.GetById(connection, query.UserRegistrationId); } } } \ No newline at end of file diff --git a/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationDto.cs b/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationDto.cs index ba8acb0d9..dcf30b825 100644 --- a/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationDto.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationDto.cs @@ -15,5 +15,7 @@ public class UserRegistrationDto public string Name { get; set; } public string StatusCode { get; set; } + + public string Password { get; set; } } } \ No newline at end of file diff --git a/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationProvider.cs b/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationProvider.cs new file mode 100644 index 000000000..d505c3494 --- /dev/null +++ b/src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationProvider.cs @@ -0,0 +1,33 @@ +using System.Data; +using Dapper; + +namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.GetUserRegistration; + +internal static class UserRegistrationProvider +{ + internal static async Task GetById( + IDbConnection connection, + Guid userRegistrationId) + { + const string sql = $""" + SELECT + [UserRegistration].[Id] as [{nameof(UserRegistrationDto.Id)}], + [UserRegistration].[Login] as [{nameof(UserRegistrationDto.Login)}], + [UserRegistration].[Email] as [{nameof(UserRegistrationDto.Email)}], + [UserRegistration].[FirstName] as [{nameof(UserRegistrationDto.FirstName)}], + [UserRegistration].[LastName] as [{nameof(UserRegistrationDto.LastName)}], + [UserRegistration].[Name] as [{nameof(UserRegistrationDto.Name)}], + [UserRegistration].[StatusCode] as [{nameof(UserRegistrationDto.StatusCode)}], + [UserRegistration].[Password] as [{nameof(UserRegistrationDto.Password)}] + FROM [registrations].[v_UserRegistrations] AS [UserRegistration] + WHERE [UserRegistration].[Id] = @UserRegistrationId + """; + + return await connection.QuerySingleAsync( + sql, + new + { + userRegistrationId + }); + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj b/src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj index 8a81420d9..1336415cd 100644 --- a/src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj +++ b/src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj @@ -2,6 +2,7 @@ + diff --git a/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs b/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs index 400efc315..bb0b479a2 100644 --- a/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs +++ b/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs @@ -4,6 +4,7 @@ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration; using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser; using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.DataAccess; using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Domain; @@ -69,6 +70,7 @@ private static void ConfigureCompositionRoot( var domainNotificationsMap = new BiDictionary(); domainNotificationsMap.Add("NewUserRegisteredNotification", typeof(NewUserRegisteredNotification)); + domainNotificationsMap.Add("UserRegistrationConfirmedNotification", typeof(UserRegistrationConfirmedNotification)); containerBuilder.RegisterModule(new OutboxModule(domainNotificationsMap)); containerBuilder.RegisterModule(new QuartzModule()); diff --git a/src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs b/src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs new file mode 100644 index 000000000..23f81fcaa --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs @@ -0,0 +1,34 @@ +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration; +using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; +using CompanyName.MyMeetings.Modules.UserAccess.Application.Users.CreateUser; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Users; + +public class UserAccessGateway : IUserCreator +{ + private readonly IUserAccessModule _userAccessModule; + + public UserAccessGateway(IUserAccessModule userAccessModule) + { + _userAccessModule = userAccessModule; + } + + public async Task Create( + Guid userRegistrationId, + string login, + string password, + string email, + string firstName, + string lastName, + string name) + { + await _userAccessModule.ExecuteCommandAsync(new CreateUserCommand( + userRegistrationId, + login, + email, + firstName, + lastName, + name, + password)); + } +} \ No newline at end of file diff --git a/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommand.cs b/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommand.cs new file mode 100644 index 000000000..389e540e0 --- /dev/null +++ b/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommand.cs @@ -0,0 +1,38 @@ +using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; + +namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Users.CreateUser; + +public class CreateUserCommand : CommandBase +{ + public CreateUserCommand( + Guid userId, + string login, + string email, + string firstName, + string lastName, + string name, + string password) + { + UserId = userId; + Login = login; + Email = email; + FirstName = firstName; + LastName = lastName; + Name = name; + Password = password; + } + + public Guid UserId { get; } + + public string Login { get; } + + public string Email { get; } + + public string FirstName { get; } + + public string LastName { get; } + + public string Name { get; } + + public string Password { get; } +} \ No newline at end of file diff --git a/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommandHandler.cs b/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommandHandler.cs new file mode 100644 index 000000000..09a6e690d --- /dev/null +++ b/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommandHandler.cs @@ -0,0 +1,28 @@ +using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; +using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users; + +namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Users.CreateUser; + +public class CreateUserCommandHandler : ICommandHandler +{ + private readonly IUserRepository _userRepository; + + public CreateUserCommandHandler(IUserRepository userRepository) + { + _userRepository = userRepository; + } + + public async Task Handle(CreateUserCommand command, CancellationToken cancellationToken) + { + var user = User.CreateFromUserRegistration( + command.UserId, + command.Login, + command.Password, + command.Email, + command.FirstName, + command.LastName, + command.Name); + + await _userRepository.AddAsync(user); + } +} \ No newline at end of file diff --git a/src/Modules/UserAccess/Domain/Users/User.cs b/src/Modules/UserAccess/Domain/Users/User.cs index 690526c4d..f1aaec193 100644 --- a/src/Modules/UserAccess/Domain/Users/User.cs +++ b/src/Modules/UserAccess/Domain/Users/User.cs @@ -49,7 +49,7 @@ public static User CreateAdmin( UserRole.Administrator); } - internal static User CreateFromUserRegistration( + public static User CreateFromUserRegistration( Guid userId, string login, string password, From 516e5f2e624d968b6c20f0694bbe2909bcfbb3a7 Mon Sep 17 00:00:00 2001 From: kgrzybek Date: Sun, 18 Feb 2024 19:07:43 +0100 Subject: [PATCH 3/6] extract Registrations WIP --- .../CompanyName.MyMeetings.API.csproj | 3 + .../LoggingCommandHandlerDecorator.cs | 2 + .../LoggingCommandHandlerDecorator.cs | 10 ++- ...oggingCommandHandlerWithResultDecorator.cs | 13 +++- .../LoggingCommandHandlerDecorator.cs | 2 + ...oggingCommandHandlerWithResultDecorator.cs | 13 +++- .../UserRegistrations/UsersCounter.cs | 4 +- .../IntegrationEventGenericHandler.cs | 2 +- .../Inbox/ProcessInboxCommandHandler.cs | 4 +- .../InternalCommands/CommandsScheduler.cs | 4 +- .../ProcessInternalCommandsCommandHandler.cs | 4 +- ...oggingCommandHandlerWithResultDecorator.cs | 13 +++- .../Outbox/ProcessOutboxCommandHandler.cs | 4 +- .../Configuration/RegistrationsStartup.cs | 2 +- .../Infrastructure/Users/UserAccessGateway.cs | 1 - .../Users/CreateUser/CreateUserCommand.cs | 4 - .../CreateUser/CreateUserCommandHandler.cs | 7 +- src/Modules/UserAccess/Domain/Users/User.cs | 7 +- .../LoggingCommandHandlerDecorator.cs | 2 + ...oggingCommandHandlerWithResultDecorator.cs | 13 +++- .../Configuration/UserAccessStartup.cs | 3 + .../IntegrationTests/SeedWork/TestBase.cs | 1 - .../IntegrationTests/Users/CreateUserTests.cs | 73 +++++++++++-------- src/Tests/SUT/SeedWork/DatabaseCleaner.cs | 13 +++- 24 files changed, 137 insertions(+), 67 deletions(-) diff --git a/src/API/CompanyName.MyMeetings.API/CompanyName.MyMeetings.API.csproj b/src/API/CompanyName.MyMeetings.API/CompanyName.MyMeetings.API.csproj index 340f1f0bf..830947656 100644 --- a/src/API/CompanyName.MyMeetings.API/CompanyName.MyMeetings.API.csproj +++ b/src/API/CompanyName.MyMeetings.API/CompanyName.MyMeetings.API.csproj @@ -5,6 +5,9 @@ Linux ..\.. + + bin\Debug\CompanyName.MyMeetings.API.xml + diff --git a/src/Modules/Administration/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs b/src/Modules/Administration/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs index 6c4ba7483..d6233ab1c 100644 --- a/src/Modules/Administration/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs +++ b/src/Modules/Administration/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs @@ -30,6 +30,8 @@ public async Task Handle(T command, CancellationToken cancellationToken) if (command is IRecurringCommand) { await _decorated.Handle(command, cancellationToken); + + return; } using ( diff --git a/src/Modules/Meetings/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs b/src/Modules/Meetings/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs index 849a37df1..00c1bd5ea 100644 --- a/src/Modules/Meetings/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs +++ b/src/Modules/Meetings/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs @@ -30,6 +30,8 @@ public async Task Handle(T command, CancellationToken cancellationToken) if (command is IRecurringCommand) { await _decorated.Handle(command, cancellationToken); + + return; } using ( @@ -66,7 +68,9 @@ public CommandLogEnricher(ICommand command) public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - logEvent.AddOrUpdateProperty(new LogEventProperty("Context", new ScalarValue($"Command:{_command.Id.ToString()}"))); + logEvent.AddOrUpdateProperty(new LogEventProperty( + "Context", + new ScalarValue($"Command:{_command.Id.ToString()}"))); } } @@ -83,7 +87,9 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { if (_executionContextAccessor.IsAvailable) { - logEvent.AddOrUpdateProperty(new LogEventProperty("CorrelationId", new ScalarValue(_executionContextAccessor.CorrelationId))); + logEvent.AddOrUpdateProperty(new LogEventProperty( + "CorrelationId", + new ScalarValue(_executionContextAccessor.CorrelationId))); } } } diff --git a/src/Modules/Meetings/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs b/src/Modules/Meetings/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs index 955775b8b..1e4873adc 100644 --- a/src/Modules/Meetings/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs +++ b/src/Modules/Meetings/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs @@ -27,6 +27,11 @@ public LoggingCommandHandlerWithResultDecorator( public async Task Handle(T command, CancellationToken cancellationToken) { + if (command is IRecurringCommand) + { + return await _decorated.Handle(command, cancellationToken); + } + using ( LogContext.Push( new RequestLogEnricher(_executionContextAccessor), @@ -63,7 +68,9 @@ public CommandLogEnricher(ICommand command) public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - logEvent.AddOrUpdateProperty(new LogEventProperty("Context", new ScalarValue($"Command:{_command.Id.ToString()}"))); + logEvent.AddOrUpdateProperty(new LogEventProperty( + "Context", + new ScalarValue($"Command:{_command.Id.ToString()}"))); } } @@ -80,7 +87,9 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { if (_executionContextAccessor.IsAvailable) { - logEvent.AddOrUpdateProperty(new LogEventProperty("CorrelationId", new ScalarValue(_executionContextAccessor.CorrelationId))); + logEvent.AddOrUpdateProperty(new LogEventProperty( + "CorrelationId", + new ScalarValue(_executionContextAccessor.CorrelationId))); } } } diff --git a/src/Modules/Payments/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs b/src/Modules/Payments/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs index 8660a14a7..ff1c621a4 100644 --- a/src/Modules/Payments/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs +++ b/src/Modules/Payments/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs @@ -30,6 +30,8 @@ public async Task Handle(T command, CancellationToken cancellationToken) if (command is IRecurringCommand) { await _decorated.Handle(command, cancellationToken); + + return; } using ( diff --git a/src/Modules/Payments/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs b/src/Modules/Payments/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs index 9feb1da64..01c835d1f 100644 --- a/src/Modules/Payments/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs +++ b/src/Modules/Payments/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs @@ -27,6 +27,11 @@ public LoggingCommandHandlerWithResultDecorator( public async Task Handle(T command, CancellationToken cancellationToken) { + if (command is IRecurringCommand) + { + return await _decorated.Handle(command, cancellationToken); + } + using ( LogContext.Push( new RequestLogEnricher(_executionContextAccessor), @@ -63,7 +68,9 @@ public CommandLogEnricher(ICommand command) public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - logEvent.AddOrUpdateProperty(new LogEventProperty("Context", new ScalarValue($"Command:{_command.Id.ToString()}"))); + logEvent.AddOrUpdateProperty(new LogEventProperty( + "Context", + new ScalarValue($"Command:{_command.Id.ToString()}"))); } } @@ -80,7 +87,9 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { if (_executionContextAccessor.IsAvailable) { - logEvent.AddOrUpdateProperty(new LogEventProperty("CorrelationId", new ScalarValue(_executionContextAccessor.CorrelationId))); + logEvent.AddOrUpdateProperty(new LogEventProperty( + "CorrelationId", + new ScalarValue(_executionContextAccessor.CorrelationId))); } } } diff --git a/src/Modules/Registrations/Application/UserRegistrations/UsersCounter.cs b/src/Modules/Registrations/Application/UserRegistrations/UsersCounter.cs index 09550ede0..dfb2218bb 100644 --- a/src/Modules/Registrations/Application/UserRegistrations/UsersCounter.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/UsersCounter.cs @@ -19,8 +19,8 @@ public int CountUsersWithLogin(string login) const string sql = """ SELECT COUNT(*) - FROM [users].[v_Users] AS [User] - WHERE [User].[Login] = @Login + FROM [registrations].[v_UserRegistrations] AS [UserRegistration] + WHERE [UserRegistration].[Login] = @Login """; return connection.QuerySingle( sql, diff --git a/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs b/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs index 12f982d53..0f2cc799c 100644 --- a/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs +++ b/src/Modules/Registrations/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs @@ -22,7 +22,7 @@ public async Task Handle(T @event) ContractResolver = new AllPropertiesContractResolver() }); - var sql = "INSERT INTO [users].[InboxMessages] (Id, OccurredOn, Type, Data) " + + var sql = "INSERT INTO [registrations].[InboxMessages] (Id, OccurredOn, Type, Data) " + "VALUES (@Id, @OccurredOn, @Type, @Data)"; await connection.ExecuteScalarAsync(sql, new diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs index e5b47ece9..ba6d733b6 100644 --- a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs @@ -25,7 +25,7 @@ public async Task Handle(ProcessInboxCommand command, CancellationToken cancella [InboxMessage].[Id] AS [{nameof(InboxMessageDto.Id)}], [InboxMessage].[Type] AS [{nameof(InboxMessageDto.Type)}], [InboxMessage].[Data] AS [{nameof(InboxMessageDto.Data)}] - FROM [users].[InboxMessages] AS [InboxMessage] + FROM [registrations].[InboxMessages] AS [InboxMessage] WHERE [InboxMessage].[ProcessedDate] IS NULL ORDER BY [InboxMessage].[OccurredOn] """; @@ -33,7 +33,7 @@ ORDER BY [InboxMessage].[OccurredOn] var messages = await connection.QueryAsync(sql); const string sqlUpdateProcessedDate = """ - UPDATE [users].[InboxMessages] + UPDATE [registrations].[InboxMessages] SET [ProcessedDate] = @Date WHERE [Id] = @Id """; diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs index 72a4ebc00..ce40a7e64 100644 --- a/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs @@ -20,7 +20,7 @@ public async Task EnqueueAsync(ICommand command) { var connection = this._sqlConnectionFactory.GetOpenConnection(); - const string sqlInsert = "INSERT INTO [users].[InternalCommands] ([Id], [EnqueueDate] , [Type], [Data]) VALUES " + + const string sqlInsert = "INSERT INTO [registrations].[InternalCommands] ([Id], [EnqueueDate] , [Type], [Data]) VALUES " + "(@Id, @EnqueueDate, @Type, @Data)"; await connection.ExecuteAsync(sqlInsert, new @@ -39,7 +39,7 @@ public async Task EnqueueAsync(ICommand command) { var connection = this._sqlConnectionFactory.GetOpenConnection(); - const string sqlInsert = "INSERT INTO [users].[InternalCommands] ([Id], [EnqueueDate] , [Type], [Data]) VALUES " + + const string sqlInsert = "INSERT INTO [registrations].[InternalCommands] ([Id], [EnqueueDate] , [Type], [Data]) VALUES " + "(@Id, @EnqueueDate, @Type, @Data)"; await connection.ExecuteAsync(sqlInsert, new diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs index b5c39da7c..816c52998 100644 --- a/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs @@ -25,7 +25,7 @@ public async Task Handle(ProcessInternalCommandsCommand command, CancellationTok [Command].[Id] AS [{nameof(InternalCommandDto.Id)}], [Command].[Type] AS [{nameof(InternalCommandDto.Type)}], [Command].[Data] AS [{nameof(InternalCommandDto.Data)}] - FROM [users].[InternalCommands] AS [Command] + FROM [registrations].[InternalCommands] AS [Command] WHERE [Command].[ProcessedDate] IS NULL ORDER BY [Command].[EnqueueDate] """; @@ -51,7 +51,7 @@ ORDER BY [Command].[EnqueueDate] { await connection.ExecuteScalarAsync( """ - UPDATE [users].[InternalCommands] + UPDATE [registrations].[InternalCommands] SET ProcessedDate = @NowDate, Error = @Error diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs index f52abbd19..782901039 100644 --- a/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs @@ -27,6 +27,11 @@ public LoggingCommandHandlerWithResultDecorator( public async Task Handle(T command, CancellationToken cancellationToken) { + if (command is IRecurringCommand) + { + return await _decorated.Handle(command, cancellationToken); + } + using ( LogContext.Push( new RequestLogEnricher(_executionContextAccessor), @@ -63,7 +68,9 @@ public CommandLogEnricher(ICommand command) public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - logEvent.AddOrUpdateProperty(new LogEventProperty("Context", new ScalarValue($"Command:{_command.Id.ToString()}"))); + logEvent.AddOrUpdateProperty(new LogEventProperty( + "Context", + new ScalarValue($"Command:{_command.Id.ToString()}"))); } } @@ -80,7 +87,9 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { if (_executionContextAccessor.IsAvailable) { - logEvent.AddOrUpdateProperty(new LogEventProperty("CorrelationId", new ScalarValue(_executionContextAccessor.CorrelationId))); + logEvent.AddOrUpdateProperty(new LogEventProperty( + "CorrelationId", + new ScalarValue(_executionContextAccessor.CorrelationId))); } } } diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs index d53474e84..2b57ac1da 100644 --- a/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs @@ -37,7 +37,7 @@ public async Task Handle(ProcessOutboxCommand command, CancellationToken cancell [OutboxMessage].[Id] AS [{nameof(OutboxMessageDto.Id)}], [OutboxMessage].[Type] AS [{nameof(OutboxMessageDto.Type)}], [OutboxMessage].[Data] AS [{nameof(OutboxMessageDto.Data)}] - FROM [users].[OutboxMessages] AS [OutboxMessage] + FROM [registrations].[OutboxMessages] AS [OutboxMessage] WHERE [OutboxMessage].[ProcessedDate] IS NULL ORDER BY [OutboxMessage].[OccurredOn] """; @@ -46,7 +46,7 @@ ORDER BY [OutboxMessage].[OccurredOn] var messagesList = messages.AsList(); const string sqlUpdateProcessedDate = """ - UPDATE [users].[OutboxMessages] + UPDATE [registrations].[OutboxMessages] SET [ProcessedDate] = @Date WHERE [Id] = @Id """; diff --git a/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs b/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs index bb0b479a2..ad6430b19 100644 --- a/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs +++ b/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs @@ -60,7 +60,7 @@ private static void ConfigureCompositionRoot( { var containerBuilder = new ContainerBuilder(); - containerBuilder.RegisterModule(new LoggingModule(logger.ForContext("Module", "UserAccess"))); + containerBuilder.RegisterModule(new LoggingModule(logger.ForContext("Module", "Registrations"))); var loggerFactory = new Serilog.Extensions.Logging.SerilogLoggerFactory(logger); containerBuilder.RegisterModule(new DataAccessModule(connectionString, loggerFactory)); diff --git a/src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs b/src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs index 23f81fcaa..a4e1b6306 100644 --- a/src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs +++ b/src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs @@ -28,7 +28,6 @@ await _userAccessModule.ExecuteCommandAsync(new CreateUserCommand( email, firstName, lastName, - name, password)); } } \ No newline at end of file diff --git a/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommand.cs b/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommand.cs index 389e540e0..02f90c013 100644 --- a/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommand.cs +++ b/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommand.cs @@ -10,7 +10,6 @@ public CreateUserCommand( string email, string firstName, string lastName, - string name, string password) { UserId = userId; @@ -18,7 +17,6 @@ public CreateUserCommand( Email = email; FirstName = firstName; LastName = lastName; - Name = name; Password = password; } @@ -32,7 +30,5 @@ public CreateUserCommand( public string LastName { get; } - public string Name { get; } - public string Password { get; } } \ No newline at end of file diff --git a/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommandHandler.cs b/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommandHandler.cs index 09a6e690d..a6ce646db 100644 --- a/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommandHandler.cs +++ b/src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommandHandler.cs @@ -3,7 +3,7 @@ namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Users.CreateUser; -public class CreateUserCommandHandler : ICommandHandler +internal class CreateUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; @@ -14,14 +14,13 @@ public CreateUserCommandHandler(IUserRepository userRepository) public async Task Handle(CreateUserCommand command, CancellationToken cancellationToken) { - var user = User.CreateFromUserRegistration( + var user = User.CreateUser( command.UserId, command.Login, command.Password, command.Email, command.FirstName, - command.LastName, - command.Name); + command.LastName); await _userRepository.AddAsync(user); } diff --git a/src/Modules/UserAccess/Domain/Users/User.cs b/src/Modules/UserAccess/Domain/Users/User.cs index f1aaec193..aaa6ef764 100644 --- a/src/Modules/UserAccess/Domain/Users/User.cs +++ b/src/Modules/UserAccess/Domain/Users/User.cs @@ -49,14 +49,13 @@ public static User CreateAdmin( UserRole.Administrator); } - public static User CreateFromUserRegistration( + public static User CreateUser( Guid userId, string login, string password, string email, string firstName, - string lastName, - string name) + string lastName) { return new User( userId, @@ -65,7 +64,7 @@ public static User CreateFromUserRegistration( email, firstName, lastName, - name, + $"{firstName} {lastName}", UserRole.Member); } diff --git a/src/Modules/UserAccess/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs b/src/Modules/UserAccess/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs index 6d0c02a99..d303cf97f 100644 --- a/src/Modules/UserAccess/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs +++ b/src/Modules/UserAccess/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs @@ -30,6 +30,8 @@ public async Task Handle(T command, CancellationToken cancellationToken) if (command is IRecurringCommand) { await _decorated.Handle(command, cancellationToken); + + return; } using ( diff --git a/src/Modules/UserAccess/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs b/src/Modules/UserAccess/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs index abf3770a2..19e0b1119 100644 --- a/src/Modules/UserAccess/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs +++ b/src/Modules/UserAccess/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs @@ -27,6 +27,11 @@ public LoggingCommandHandlerWithResultDecorator( public async Task Handle(T command, CancellationToken cancellationToken) { + if (command is IRecurringCommand) + { + return await _decorated.Handle(command, cancellationToken); + } + using ( LogContext.Push( new RequestLogEnricher(_executionContextAccessor), @@ -63,7 +68,9 @@ public CommandLogEnricher(ICommand command) public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - logEvent.AddOrUpdateProperty(new LogEventProperty("Context", new ScalarValue($"Command:{_command.Id.ToString()}"))); + logEvent.AddOrUpdateProperty(new LogEventProperty( + "Context", + new ScalarValue($"Command:{_command.Id.ToString()}"))); } } @@ -80,7 +87,9 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { if (_executionContextAccessor.IsAvailable) { - logEvent.AddOrUpdateProperty(new LogEventProperty("CorrelationId", new ScalarValue(_executionContextAccessor.CorrelationId))); + logEvent.AddOrUpdateProperty(new LogEventProperty( + "CorrelationId", + new ScalarValue(_executionContextAccessor.CorrelationId))); } } } diff --git a/src/Modules/UserAccess/Infrastructure/Configuration/UserAccessStartup.cs b/src/Modules/UserAccess/Infrastructure/Configuration/UserAccessStartup.cs index fda9ddd11..f68de0851 100644 --- a/src/Modules/UserAccess/Infrastructure/Configuration/UserAccessStartup.cs +++ b/src/Modules/UserAccess/Infrastructure/Configuration/UserAccessStartup.cs @@ -1,6 +1,7 @@ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; +using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.DataAccess; @@ -9,6 +10,7 @@ using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Logging; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Mediation; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing; +using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.Outbox; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Quartz; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Security; using Serilog; @@ -63,6 +65,7 @@ private static void ConfigureCompositionRoot( containerBuilder.RegisterModule(new ProcessingModule()); containerBuilder.RegisterModule(new EventsBusModule(eventsBus)); containerBuilder.RegisterModule(new MediatorModule()); + containerBuilder.RegisterModule(new OutboxModule(new BiDictionary())); containerBuilder.RegisterModule(new QuartzModule()); containerBuilder.RegisterModule(new EmailModule(emailsConfiguration, emailSender)); diff --git a/src/Modules/UserAccess/Tests/IntegrationTests/SeedWork/TestBase.cs b/src/Modules/UserAccess/Tests/IntegrationTests/SeedWork/TestBase.cs index 5135fa6cc..b0224a51e 100644 --- a/src/Modules/UserAccess/Tests/IntegrationTests/SeedWork/TestBase.cs +++ b/src/Modules/UserAccess/Tests/IntegrationTests/SeedWork/TestBase.cs @@ -72,7 +72,6 @@ private static async Task ClearDatabase(IDbConnection connection) const string sql = "DELETE FROM [users].[InboxMessages] " + "DELETE FROM [users].[InternalCommands] " + "DELETE FROM [users].[OutboxMessages] " + - "DELETE FROM [users].[UserRegistrations] " + "DELETE FROM [users].[Users] " + "DELETE FROM [users].[RolesToPermissions] " + "DELETE FROM [users].[UserRoles] " + diff --git a/src/Modules/UserAccess/Tests/IntegrationTests/Users/CreateUserTests.cs b/src/Modules/UserAccess/Tests/IntegrationTests/Users/CreateUserTests.cs index 72bd45568..385911575 100644 --- a/src/Modules/UserAccess/Tests/IntegrationTests/Users/CreateUserTests.cs +++ b/src/Modules/UserAccess/Tests/IntegrationTests/Users/CreateUserTests.cs @@ -1,30 +1,43 @@ -// using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.ConfirmUserRegistration; -// using CompanyName.MyMeetings.Modules.UserAccess.Application.UserRegistrations.RegisterNewUser; -// using CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetUser; -// using CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.SeedWork; -// using CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.UserRegistrations; -// using NUnit.Framework; -// -// namespace CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.Users -// { -// [TestFixture] -// public class CreateUserTests : TestBase -// { -// [Test] -// public async Task CreateUser_Test() -// { -// var registrationId = await UserAccessModule.ExecuteCommandAsync(new RegisterNewUserCommand( -// UserRegistrationSampleData.Login, -// UserRegistrationSampleData.Password, -// UserRegistrationSampleData.Email, -// UserRegistrationSampleData.FirstName, -// UserRegistrationSampleData.LastName, -// "confirmLink")); -// await UserAccessModule.ExecuteCommandAsync(new ConfirmUserRegistrationCommand(registrationId)); -// -// var user = await UserAccessModule.ExecuteQueryAsync(new GetUserQuery(registrationId)); -// -// Assert.That(user.Login, Is.EqualTo(UserRegistrationSampleData.Login)); -// } -// } -// } \ No newline at end of file +using CompanyName.MyMeetings.Modules.UserAccess.Application.Users.CreateUser; +using CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetUser; +using CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.SeedWork; +using NUnit.Framework; + +namespace CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.Users +{ + [TestFixture] + public class CreateUserTests : TestBase + { + [Test] + public async Task CreateUser_Test() + { + var userId = Guid.NewGuid(); + await UserAccessModule.ExecuteCommandAsync(new CreateUserCommand( + userId, + UserSampleData.Login, + UserSampleData.Email, + UserSampleData.FirstName, + UserSampleData.LastName, + UserSampleData.Password)); + + var user = await UserAccessModule.ExecuteQueryAsync(new GetUserQuery(userId)); + + Assert.That(user.Login, Is.EqualTo(UserSampleData.Login)); + Assert.That(user.Email, Is.EqualTo(UserSampleData.Email)); + Assert.That(user.Name, Is.EqualTo($"{UserSampleData.FirstName} {UserSampleData.LastName}")); + } + } + + public struct UserSampleData + { + public static string Login => "jdoe"; + + public static string Email => "jdoe@mail.com"; + + public static string FirstName => "John"; + + public static string LastName => "Doe"; + + public static string Password => "qwerty"; + } +} \ No newline at end of file diff --git a/src/Tests/SUT/SeedWork/DatabaseCleaner.cs b/src/Tests/SUT/SeedWork/DatabaseCleaner.cs index efb633bb2..efe65a8da 100644 --- a/src/Tests/SUT/SeedWork/DatabaseCleaner.cs +++ b/src/Tests/SUT/SeedWork/DatabaseCleaner.cs @@ -16,6 +16,8 @@ internal static async Task ClearAllData(IDbConnection connection) await ClearPayments(connection); await ClearUsers(connection); + + await ClearRegistration(connection); } private static async Task ClearUsers(IDbConnection connection) @@ -26,7 +28,6 @@ private static async Task ClearUsers(IDbConnection connection) "DELETE FROM [users].[OutboxMessages] " + "DELETE FROM [users].[Permissions] " + "DELETE FROM [users].[RolesToPermissions] " + - "DELETE FROM [users].[UserRegistrations] " + "DELETE FROM [users].[UserRoles] " + "DELETE FROM [users].[Users] "; @@ -92,5 +93,15 @@ private static async Task ClearAdministration(IDbConnection connection) await connection.ExecuteScalarAsync(clearAdministrationSql); } + + private static async Task ClearRegistration(IDbConnection connection) + { + const string sql = "DELETE FROM [registrations].[InboxMessages] " + + "DELETE FROM [registrations].[InternalCommands] " + + "DELETE FROM [registrations].[OutboxMessages] " + + "DELETE FROM [registrations].[UserRegistrations] "; + + await connection.ExecuteScalarAsync(sql); + } } } \ No newline at end of file From 9625a4b448f82b45417d42b3db0d02d8742a9ae4 Mon Sep 17 00:00:00 2001 From: kgrzybek Date: Sun, 18 Feb 2024 20:58:39 +0100 Subject: [PATCH 4/6] extract Registrations WIP --- src/API/CompanyName.MyMeetings.API/Startup.cs | 10 ++++++++++ .../ConfirmUserRegistration/IUserCreator.cs | 3 +-- ...erRegistrationConfirmedNotificationHandler.cs | 3 +-- ...s.Modules.Registrations.Infrastructure.csproj | 8 ++------ .../Configuration/Domain/DomainModule.cs | 6 ++++++ .../Processing/LoggingCommandHandlerDecorator.cs | 2 ++ .../Configuration/RegistrationsStartup.cs | 4 +++- .../UserAccess/UserAccessAutofacModule.cs | 16 ++++++++++++++++ .../Infrastructure/Users/UserAccessGateway.cs | 3 +-- 9 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 src/Modules/Registrations/Infrastructure/Configuration/UserAccess/UserAccessAutofacModule.cs diff --git a/src/API/CompanyName.MyMeetings.API/Startup.cs b/src/API/CompanyName.MyMeetings.API/Startup.cs index d5d02157d..ed5a0906e 100644 --- a/src/API/CompanyName.MyMeetings.API/Startup.cs +++ b/src/API/CompanyName.MyMeetings.API/Startup.cs @@ -14,6 +14,7 @@ using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Identity; using Hellang.Middleware.ProblemDetails; @@ -172,6 +173,15 @@ private void InitializeModules(ILifetimeScope container) _logger, emailsConfiguration, null); + + RegistrationsStartup.Initialize( + _configuration[MeetingsConnectionString], + executionContextAccessor, + _logger, + emailsConfiguration, + _configuration["Security:TextEncryptionKey"], + null, + null); } } } \ No newline at end of file diff --git a/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/IUserCreator.cs b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/IUserCreator.cs index cc3f607e2..115c94530 100644 --- a/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/IUserCreator.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/IUserCreator.cs @@ -8,6 +8,5 @@ public Task Create( string password, string email, string firstName, - string lastName, - string name); + string lastName); } \ No newline at end of file diff --git a/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotificationHandler.cs b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotificationHandler.cs index ca64d77b4..756c30bc1 100644 --- a/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotificationHandler.cs +++ b/src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotificationHandler.cs @@ -30,7 +30,6 @@ await _userCreator.Create( registration.Password, registration.Email, registration.FirstName, - registration.LastName, - registration.Name); + registration.LastName); } } \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj b/src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj index 1336415cd..64938ceb8 100644 --- a/src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj +++ b/src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj @@ -1,10 +1,6 @@  - - + - - - - \ No newline at end of file + diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Domain/DomainModule.cs b/src/Modules/Registrations/Infrastructure/Configuration/Domain/DomainModule.cs index 94fe22a97..2333f4fd2 100644 --- a/src/Modules/Registrations/Infrastructure/Configuration/Domain/DomainModule.cs +++ b/src/Modules/Registrations/Infrastructure/Configuration/Domain/DomainModule.cs @@ -1,6 +1,8 @@ using Autofac; using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration; using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Users; namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Domain { @@ -11,6 +13,10 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType() .As() .InstancePerLifetimeScope(); + + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); } } } \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs b/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs index 2763e24cb..139308a5e 100644 --- a/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs +++ b/src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs @@ -30,6 +30,8 @@ public async Task Handle(T command, CancellationToken cancellationToken) if (command is IRecurringCommand) { await _decorated.Handle(command, cancellationToken); + + return; } using ( diff --git a/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs b/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs index ad6430b19..ce49bef52 100644 --- a/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs +++ b/src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs @@ -15,6 +15,7 @@ using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing; using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Outbox; using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Quartz; +using CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.UserAccess; using Serilog; namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration @@ -33,7 +34,7 @@ public static void Initialize( IEventsBus eventsBus, long? internalProcessingPoolingInterval = null) { - var moduleLogger = logger.ForContext("Module", "UserAccess"); + var moduleLogger = logger.ForContext("Module", "Registrations"); ConfigureCompositionRoot( connectionString, @@ -67,6 +68,7 @@ private static void ConfigureCompositionRoot( containerBuilder.RegisterModule(new ProcessingModule()); containerBuilder.RegisterModule(new EventsBusModule(eventsBus)); containerBuilder.RegisterModule(new MediatorModule()); + containerBuilder.RegisterModule(new UserAccessAutofacModule()); var domainNotificationsMap = new BiDictionary(); domainNotificationsMap.Add("NewUserRegisteredNotification", typeof(NewUserRegisteredNotification)); diff --git a/src/Modules/Registrations/Infrastructure/Configuration/UserAccess/UserAccessAutofacModule.cs b/src/Modules/Registrations/Infrastructure/Configuration/UserAccess/UserAccessAutofacModule.cs new file mode 100644 index 000000000..d3589a333 --- /dev/null +++ b/src/Modules/Registrations/Infrastructure/Configuration/UserAccess/UserAccessAutofacModule.cs @@ -0,0 +1,16 @@ +using Autofac; +using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; +using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure; + +namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.UserAccess +{ + public class UserAccessAutofacModule : Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + } + } +} \ No newline at end of file diff --git a/src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs b/src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs index a4e1b6306..32082df1d 100644 --- a/src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs +++ b/src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs @@ -19,8 +19,7 @@ public async Task Create( string password, string email, string firstName, - string lastName, - string name) + string lastName) { await _userAccessModule.ExecuteCommandAsync(new CreateUserCommand( userRegistrationId, From ffd41873cc363f94724865eb1c7597c9b7bbdcb8 Mon Sep 17 00:00:00 2001 From: kgrzybek Date: Sun, 18 Feb 2024 22:09:17 +0100 Subject: [PATCH 5/6] extract Registrations WIP - docs --- README.md | 5 +++-- docs/C4/c3_components.puml | 7 +++++++ docs/Images/Architecture_high_level.png | Bin 95138 -> 95138 bytes docs/Project/MyMeetings.vpp | Bin 1307648 -> 1415168 bytes 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c09373cff..6e4019b11 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,7 @@ As can be found on the website of the author of this model ([Simon Brown](https: #### 3.0.2 C2 Container -![](http://www.plantuml.com/plantuml/png/5OrDgiCm30RtxnIl1uW5fQkk0Zr8SIoHcDXIq0-XFNtZpVTjXfdPFAj7Rt-togK5KcZxtzmFUm9eFjDQVOibZBG8Ex6d8XtsLR-VXNReWj6oJbrOseLEvnX4X9xDIG6b6BmUKExl8SYLITCnYZCnNly3) +![](http://www.plantuml.com/plantuml/png/dPDBZvim4CVlV0etfqJg1bNggQTbasWtLRAbUQZgAJ8mJInX6_dH2bNxtMi3DG-PYfG3DkFv_yQUFtwK2WkbQHwy8opDTGRefbGXFuMXmB-MLw9kEj4IHCgP0gQMAQTXaMDcDGzIPJb9mjN7XzY6pljTIB5K84peikAquCng2iiFZgLPePSdhbc6HJbFFxn7its9zW2AiAj4TS9iWNu7o3mhKn4J1c8k2snIZt4qCyC1SAe-7-tq-Rn5FxX0wWO85qLEKgm8PxD5fQzUhXej_zQ92Z8e7CLRLzK-RkTpNENQTJCiNpSdioQW5dZIQKFSyqpdOAiq0IVVaKHWGK1wzJYZX16fH7KEHubwuOaiQrgKfY3RtPnD2DMnYP02bzICBIAk0rFr0gvktotaq8rE8ttLe64EFstj9osDPbFDaVcmX2r352WNPKVi9kudoB322PPs3-jcsXe_BBbE1dvQISTJP6csjanlGB4jxCSCRAqmGrEdmrfVBQZlwcIACzOoMbkd0_hEMihGrt7KMr2_tuBW07bzkt51wgDKRhwSJh4HsDLgeUz3azXDw7TuDCtrRfpKD6OyeNTmNzPmizsVDjP9JkGDP1r5YgCGIPt8L9344VodkP_J7lV_6SD-t4_mDkm-ZBUltijvL5o0aGjGWXZSFplQZuuJ_Bri8sDAVmURxt7LUDTwmN6z1LmEViJu5GXsqVSlvzFbs_RqSjbjdZUlw_aYU0IMQPh_0G00) #### 3.0.3 C3 Component (high-level) @@ -330,7 +330,8 @@ As can be found on the website of the author of this model ([Simon Brown](https: 2. Authenticate and authorize request (using User Access module) 3. Delegate work to specific module sending Command or Query 4. Return response -- **User Access** - responsible for user authentication, authorization and registration +- **User Access** - responsible for user authentication and authorization +- **Registrations** - responsible for user registration - **Meetings** - implements Meetings Bounded Context: creating meeting groups, meetings - **Administration** - implements Administration Bounded Context: implements administrative tasks like meeting group proposal verification - **Payments** - implements Payments Bounded Context: implements all functionalities associated with payments diff --git a/docs/C4/c3_components.puml b/docs/C4/c3_components.puml index 4baedbc91..1d4877ae8 100644 --- a/docs/C4/c3_components.puml +++ b/docs/C4/c3_components.puml @@ -10,6 +10,7 @@ System_Boundary(c1, "My Meetings System") { Component(administrationModule, "Administration", ".NET Libraries") Component(userAccessModule, "User Access", ".NET Libraries") Component(paymentsModule, "Payments", ".NET Libraries") + Component(registrationsModule, "Registrations", ".NET Libraries") ComponentQueue(eventsBus, "Events Bus", "In memory") Boundary(database, "Database") { @@ -17,6 +18,7 @@ System_Boundary(c1, "My Meetings System") { ComponentDb(administrationData, "Administration data", "schema") ComponentDb(userAccessData, "User Access data", "schema") ComponentDb(paymentsData, "Payments data", "schema") + ComponentDb(registrationsData, "Registrations data", "schema") } } } @@ -26,15 +28,20 @@ Rel(api, meetingsModule, "Uses") Rel(api, administrationModule, "Uses") Rel(api, userAccessModule, "Uses") Rel(api, paymentsModule, "Uses") +Rel(api, registrationsModule, "Uses") Rel(meetingsModule, eventsBus, "Publishes event to / subscribes") Rel(administrationModule, eventsBus, "Publishes event to / subscribes") Rel(userAccessModule, eventsBus, "Publishes event to / subscribes") Rel(paymentsModule, eventsBus, "Publishes event to / subscribes") +Rel(registrationsModule, eventsBus, "Publishes event to / subscribes") Rel(meetingsModule, meetingsModuleData, "Store / retrieve") Rel(administrationModule, administrationData, "Store / retrieve") Rel(userAccessModule, userAccessData, "Store / retrieve") Rel(paymentsModule, paymentsData, "Store / retrieve") +Rel(registrationsModule, registrationsData, "Store / retrieve") + +Rel_R(registrationsModule, userAccessModule, "Uses") LAYOUT_WITH_LEGEND() diff --git a/docs/Images/Architecture_high_level.png b/docs/Images/Architecture_high_level.png index 1744853e064e3248cad69d02ec3f8018003b13cd..ecf32a209b98c982cf1e65637804474cc4d8109f 100644 GIT binary patch delta 34542 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 BG3tKH2X?Kz-VHhn%TfKqtd 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 diff --git a/docs/Project/MyMeetings.vpp b/docs/Project/MyMeetings.vpp index 74da8cd33dcdad8b866d292f4d6b5e891c93ebf5..39d94f6b2da34f8bc38b64a6d259ffa83ed774fa 100644 GIT binary patch delta 148029 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 delta 68876 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 C%rDCT From b96b9fd8b7163dc3a7c26cbc6670a90a1926afde Mon Sep 17 00:00:00 2001 From: kgrzybek Date: Sun, 18 Feb 2024 22:21:27 +0100 Subject: [PATCH 6/6] libs: upgrade System.Data.SqlClient to 4.8.6 --- build/_build.csproj | 2 +- src/Directory.Packages.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/_build.csproj b/build/_build.csproj index c163fce45..7723782b4 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index a686a92dc..1f3aeb373 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -29,7 +29,7 @@ - +