Repository: kgrzybek/modular-monolith-with-ddd Branch: master Commit: 91c8ef24b4cb Files: 1289 Total size: 2.2 MB Directory structure: gitextract_sty9z65e/ ├── .github/ │ └── workflows/ │ └── buildPipeline.yml ├── .gitignore ├── .nuke/ │ ├── build.schema.json │ └── parameters.json ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── build/ │ ├── .editorconfig │ ├── Build.cs │ ├── BuildIntegrationTests.cs │ ├── Configuration.cs │ ├── Database.cs │ ├── Directory.Build.props │ ├── Directory.Build.targets │ ├── SUTCreator.cs │ ├── Utils/ │ │ └── SqlReadinessChecker.cs │ ├── _build.csproj │ └── _build.csproj.DotSettings ├── build.cmd ├── build.ps1 ├── build.sh ├── docker-compose.yml ├── docs/ │ ├── C4/ │ │ ├── c1_system_context.puml │ │ ├── c2_container.puml │ │ ├── c3_components.puml │ │ ├── c3_components_module.puml │ │ └── c4_class.puml │ ├── PlantUML/ │ │ ├── Commenting_Conceptual_Model.puml │ │ └── Conceptual_Model.puml │ ├── Project/ │ │ └── MyMeetings.vpp │ ├── architecture-decision-log/ │ │ ├── 0001-record-architecture-decisions.md │ │ ├── 0002-use_modular-monolith-system-architecture.md │ │ ├── 0003-use_dotnetcore_and_csharp.md │ │ ├── 0004-divide-the-system-into-4-modules.md │ │ ├── 0005-create-one-rest-api-module.md │ │ ├── 0006-create-facade-between-api-and-business-module.md │ │ ├── 0007-use-cqrs-architectural-style.md │ │ ├── 0008-allow-return-result-after-command-processing.md │ │ ├── 0009-use-2-layered-architectural-style-for-reads.md │ │ ├── 0010-use-clean-architecture-for-writes.md │ │ ├── 0011-create-rich-domain-models.md │ │ ├── 0012-use-domain-driven-design-tactical-patterns.md │ │ ├── 0013-protect-business-invariants-using-exceptions.md │ │ ├── 0014-event-driven-communication-between-modules.md │ │ ├── 0015-use-in-memory-events-bus.md │ │ ├── 0016-create-ioc-container-per-module.md │ │ └── 0017-implement-archictecture-tests.md │ ├── catalog-of-terms/ │ │ ├── Aggregate-DDD/ │ │ │ ├── README.md │ │ │ └── aggregate-ddd.puml │ │ ├── Command/ │ │ │ ├── README.md │ │ │ └── command.puml │ │ ├── Decorator-Pattern/ │ │ │ ├── README.md │ │ │ └── decorator-pattern.puml │ │ ├── Dependency-Injection/ │ │ │ ├── README.md │ │ │ └── dependency-injection.puml │ │ ├── Domain-Event/ │ │ │ ├── README.md │ │ │ └── domain-event.puml │ │ ├── Entity-DDD/ │ │ │ ├── README.md │ │ │ └── entity-ddd.puml │ │ ├── Event/ │ │ │ └── README.md │ │ ├── Event-Driven-Architecture/ │ │ │ └── README.md │ │ ├── Event-Sourcing/ │ │ │ └── README.md │ │ ├── Event-Storming/ │ │ │ └── README.md │ │ ├── Integration-Event/ │ │ │ └── README.md │ │ ├── README.md │ │ ├── Strategy-Pattern/ │ │ │ ├── README.md │ │ │ └── strategy-pattern.puml │ │ └── ValueObject-DDD/ │ │ ├── README.md │ │ └── value-object-ddd.puml │ └── mutation-tests-reports/ │ └── mutation-report.html ├── runIntegrationTests.cmd └── src/ ├── .dockerignore ├── .editorconfig ├── API/ │ ├── CompanyName.MyMeetings.API/ │ │ ├── CompanyName.MyMeetings.API.csproj │ │ ├── Configuration/ │ │ │ ├── Authorization/ │ │ │ │ ├── AttributeAuthorizationHandler.cs │ │ │ │ ├── AuthorizationChecker.cs │ │ │ │ ├── HasPermissionAttribute.cs │ │ │ │ ├── HasPermissionAuthorizationHandler.cs │ │ │ │ ├── HasPermissionAuthorizationRequirement.cs │ │ │ │ └── NoPermissionRequiredAttribute.cs │ │ │ ├── ExecutionContext/ │ │ │ │ ├── CorrelationMiddleware.cs │ │ │ │ └── ExecutionContextAccessor.cs │ │ │ ├── Extensions/ │ │ │ │ └── SwaggerExtensions.cs │ │ │ └── Validation/ │ │ │ ├── BusinessRuleValidationExceptionProblemDetails.cs │ │ │ └── InvalidCommandProblemDetails.cs │ │ ├── Modules/ │ │ │ ├── Administration/ │ │ │ │ ├── AdministrationAutofacModule.cs │ │ │ │ ├── AdministrationPermissions.cs │ │ │ │ └── MeetingGroupProposals/ │ │ │ │ └── MeetingGroupProposalsController.cs │ │ │ ├── Meetings/ │ │ │ │ ├── Countries/ │ │ │ │ │ └── CountriesController.cs │ │ │ │ ├── MeetingCommentingConfiguration/ │ │ │ │ │ └── MeetingCommentingConfigurationController.cs │ │ │ │ ├── MeetingComments/ │ │ │ │ │ ├── AddMeetingCommentRequest.cs │ │ │ │ │ ├── EditMeetingCommentRequest.cs │ │ │ │ │ └── MeetingCommentsController.cs │ │ │ │ ├── MeetingGroupProposals/ │ │ │ │ │ ├── MeetingGroupProposalsController.cs │ │ │ │ │ └── ProposeMeetingGroupRequest.cs │ │ │ │ ├── MeetingGroups/ │ │ │ │ │ ├── CreateNewMeetingGroupRequest.cs │ │ │ │ │ ├── EditMeetingGroupGeneralAttributesRequest.cs │ │ │ │ │ └── MeetingGroupsController.cs │ │ │ │ ├── Meetings/ │ │ │ │ │ ├── AddMeetingAttendeeRequest.cs │ │ │ │ │ ├── ChangeMeetingMainAttributesRequest.cs │ │ │ │ │ ├── CreateMeetingRequest.cs │ │ │ │ │ ├── MeetingsController.cs │ │ │ │ │ ├── RemoveMeetingAttendeeRequest.cs │ │ │ │ │ ├── SetMeetingAttendeeRequest.cs │ │ │ │ │ └── SetMeetingHostRequest.cs │ │ │ │ ├── MeetingsAutofacModule.cs │ │ │ │ └── MeetingsPermissions.cs │ │ │ ├── Payments/ │ │ │ │ ├── MeetingFees/ │ │ │ │ │ ├── CreateMeetingFeePaymentRequest.cs │ │ │ │ │ ├── MeetingFeePaymentsController.cs │ │ │ │ │ └── RegisterMeetingFeePaymentRequest.cs │ │ │ │ ├── Payers/ │ │ │ │ │ └── PayersController.cs │ │ │ │ ├── PaymentsAutofacModule.cs │ │ │ │ ├── PaymentsPermissions.cs │ │ │ │ ├── PriceListItems/ │ │ │ │ │ ├── ChangePriceListItemAttributesRequest.cs │ │ │ │ │ ├── CreatePriceListItemRequest.cs │ │ │ │ │ ├── GetPriceListItemRequest.cs │ │ │ │ │ └── PriceListItemsController.cs │ │ │ │ ├── RegisterSubscriptionRenewalPaymentRequest.cs │ │ │ │ ├── SubscriptionRenewalsController.cs │ │ │ │ └── Subscriptions/ │ │ │ │ ├── BuySubscriptionRequest.cs │ │ │ │ ├── RegisterSubscriptionPaymentRequest.cs │ │ │ │ ├── RenewSubscriptionRequest.cs │ │ │ │ ├── SubscriptionPaymentsController.cs │ │ │ │ └── SubscriptionsController.cs │ │ │ └── UserAccess/ │ │ │ ├── AuthenticatedUserController.cs │ │ │ ├── EmailsController.cs │ │ │ ├── RegisterNewUserRequest.cs │ │ │ ├── UserAccessAutofacModule.cs │ │ │ └── UserRegistrationsController.cs │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── Startup.cs │ │ ├── appsettings.Development.json │ │ ├── appsettings.Production.json │ │ ├── appsettings.json │ │ ├── entrypoint.sh │ │ └── tempkey.rsa │ └── RequestExamples/ │ ├── Authentication.http │ ├── Users.http │ └── http-client.env.json ├── BuildingBlocks/ │ ├── Application/ │ │ ├── CompanyName.MyMeetings.BuildingBlocks.Application.csproj │ │ ├── Data/ │ │ │ └── ISqlConnectionFactory.cs │ │ ├── Emails/ │ │ │ ├── EmailMessage.cs │ │ │ └── IEmailSender.cs │ │ ├── Events/ │ │ │ ├── DomainNotificationBase.cs │ │ │ └── IDomainEventNotification.cs │ │ ├── IExecutionContextAccessor.cs │ │ ├── InvalidCommandException.cs │ │ ├── Outbox/ │ │ │ ├── IOutbox.cs │ │ │ └── OutboxMessage.cs │ │ └── Queries/ │ │ ├── IPagedQuery.cs │ │ ├── PageData.cs │ │ └── PagedQueryHelper.cs │ ├── Domain/ │ │ ├── BusinessRuleValidationException.cs │ │ ├── CompanyName.MyMeetings.BuildingBlocks.Domain.csproj │ │ ├── DomainEventBase.cs │ │ ├── Entity.cs │ │ ├── IAggregateRoot.cs │ │ ├── IBusinessRule.cs │ │ ├── IDomainEvent.cs │ │ ├── IgnoreMemberAttribute.cs │ │ ├── TypedIdValueBase.cs │ │ └── ValueObject.cs │ ├── Infrastructure/ │ │ ├── BiDictionary.cs │ │ ├── CompanyName.MyMeetings.BuildingBlocks.Infrastructure.csproj │ │ ├── DomainEventsDispatching/ │ │ │ ├── DomainEventsAccessor.cs │ │ │ ├── DomainEventsDispatcher.cs │ │ │ ├── DomainEventsDispatcherNotificationHandlerDecorator.cs │ │ │ ├── DomainNotificationsMapper.cs │ │ │ ├── IDomainEventsAccessor.cs │ │ │ ├── IDomainEventsDispatcher.cs │ │ │ ├── IDomainNotificationsMapper.cs │ │ │ └── UnitOfWorkCommandHandlerDecorator.cs │ │ ├── Emails/ │ │ │ ├── EmailSender.cs │ │ │ └── EmailsConfiguration.cs │ │ ├── EventBus/ │ │ │ ├── IEventsBus.cs │ │ │ ├── IIntegrationEventHandler.cs │ │ │ ├── InMemoryEventBus.cs │ │ │ ├── InMemoryEventBusClient.cs │ │ │ └── IntegrationEvent.cs │ │ ├── IUnitOfWork.cs │ │ ├── Inbox/ │ │ │ └── InboxMessage.cs │ │ ├── InternalCommands/ │ │ │ ├── IInternalCommandsMapper.cs │ │ │ ├── InternalCommand.cs │ │ │ └── InternalCommandsMapper.cs │ │ ├── Serialization/ │ │ │ └── AllPropertiesContractResolver.cs │ │ ├── ServiceProviderWrapper.cs │ │ ├── SqlConnectionFactory.cs │ │ ├── StronglyTypedIdValueConverterSelector.cs │ │ ├── TypedIdValueConverter.cs │ │ └── UnitOfWork.cs │ └── Tests/ │ ├── Application.UnitTests/ │ │ ├── CompanyName.MyMeetings.BuildingBlocks.Application.UnitTests.csproj │ │ └── Queries/ │ │ └── PagedQueryHelperTests.cs │ └── IntegrationTests/ │ ├── CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.csproj │ ├── EnvironmentVariablesProvider.cs │ └── Probing/ │ ├── AssertErrorException.cs │ ├── IProbe.cs │ ├── Poller.cs │ └── Timeout.cs ├── CompanyName.MyMeetings.sln ├── Database/ │ ├── .dockerignore │ ├── ClearDatabase.sql │ ├── CompanyName.MyMeetings.Database/ │ │ ├── CompanyName.MyMeetings.Database.sqlproj │ │ ├── Scripts/ │ │ │ ├── ClearDatabase.sql │ │ │ ├── CreateDatabase.sql │ │ │ ├── CreateDatabase_Linux.sql │ │ │ ├── CreateDatabase_Windows.sql │ │ │ ├── CreateStructure.sql │ │ │ ├── Migrations/ │ │ │ │ └── 1_0_0_0/ │ │ │ │ ├── 0001_initial_structure.sql │ │ │ │ ├── 0002_change_meeting_comments_edit_date_type_and_add_meeting_comments_view.sql │ │ │ │ ├── 0003_add_meetings_countries_table.sql │ │ │ │ ├── 0004_add_meeting_commenting_configurations_table.sql │ │ │ │ ├── 0005_add_payer_id_to_subcription_details_view.sql │ │ │ │ ├── 0006_add_member_meeting_groups_view.sql │ │ │ │ ├── 0007_add_meeting_attendees_view.sql │ │ │ │ ├── 0008_add_meeting_details_view.sql │ │ │ │ ├── 0009_add_mock_emails_table.sql │ │ │ │ ├── 0010_add_member_meetings_view.sql │ │ │ │ ├── 0011_add_likes_count_to_meeting_comments_table.sql │ │ │ │ ├── 0012_add_likes_count_to_meeting_comments_view.sql │ │ │ │ ├── 0013_add_meeting_member_comment_likes_table.sql │ │ │ │ └── 0014_add_missing_tables_for_registrations.sql │ │ │ ├── SeedDatabase.sql │ │ │ └── Seeds/ │ │ │ └── 0001_SeedCountries.sql │ │ └── Structure/ │ │ ├── Security/ │ │ │ └── Schemas.sql │ │ ├── administration/ │ │ │ ├── Tables/ │ │ │ │ ├── InboxMessages.sql │ │ │ │ ├── InternalCommands.sql │ │ │ │ ├── MeetingGroupProposals.sql │ │ │ │ ├── Members.sql │ │ │ │ └── OutboxMessages.sql │ │ │ └── Views/ │ │ │ ├── v_MeetingGroupProposals.sql │ │ │ └── v_Members.sql │ │ ├── app/ │ │ │ └── Tables/ │ │ │ ├── Emails.sql │ │ │ └── MigrationsJournal.sql │ │ ├── meetings/ │ │ │ ├── Tables/ │ │ │ │ ├── Countries.sql │ │ │ │ ├── InboxMessages.sql │ │ │ │ ├── InternalCommands.sql │ │ │ │ ├── MeetingAttendees.sql │ │ │ │ ├── MeetingCommentingConfigurations.sql │ │ │ │ ├── MeetingComments.sql │ │ │ │ ├── MeetingGroupMembers.sql │ │ │ │ ├── MeetingGroupProposals.sql │ │ │ │ ├── MeetingGroups.sql │ │ │ │ ├── MeetingMemberCommentLikes.sql │ │ │ │ ├── MeetingNotAttendees.sql │ │ │ │ ├── MeetingWaitlistMembers.sql │ │ │ │ ├── Meetings.sql │ │ │ │ ├── MemberSubscriptions.sql │ │ │ │ ├── Members.sql │ │ │ │ └── OutboxMessages.sql │ │ │ └── Views/ │ │ │ ├── v_Countries.sql │ │ │ ├── v_MeetingAttendees.sql │ │ │ ├── v_MeetingComments.sql │ │ │ ├── v_MeetingDetails.sql │ │ │ ├── v_MeetingGroupMembers.sql │ │ │ ├── v_MeetingGroupProposals.sql │ │ │ ├── v_MeetingGroups.sql │ │ │ ├── v_Meetings.sql │ │ │ ├── v_MemberMeetingGroups.sql │ │ │ ├── v_MemberMeetings.sql │ │ │ └── v_Members.sql │ │ ├── payments/ │ │ │ ├── Tables/ │ │ │ │ ├── InboxMessages.sql │ │ │ │ ├── InternalCommands.sql │ │ │ │ ├── MeetingFees.sql │ │ │ │ ├── Messages.sql │ │ │ │ ├── OutboxMessages.sql │ │ │ │ ├── Payers.sql │ │ │ │ ├── PriceListItems.sql │ │ │ │ ├── Streams.sql │ │ │ │ ├── SubscriptionCheckpoints.sql │ │ │ │ ├── SubscriptionDetails.sql │ │ │ │ └── SubscriptionPayments.sql │ │ │ └── Types/ │ │ │ └── NewStreamMessages.sql │ │ ├── registrations/ │ │ │ ├── Tables/ │ │ │ │ ├── InboxMessages.sql │ │ │ │ ├── InternalCommands.sql │ │ │ │ ├── OutboxMessages.sql │ │ │ │ └── UserRegistrations.sql │ │ │ └── Views/ │ │ │ └── v_UserRegistrations.sql │ │ └── users/ │ │ ├── Tables/ │ │ │ ├── InboxMessages.sql │ │ │ ├── InternalCommands.sql │ │ │ ├── OutboxMessages.sql │ │ │ ├── Permissions.sql │ │ │ ├── RolesToPermissions.sql │ │ │ ├── UserRoles.sql │ │ │ └── Users.sql │ │ └── Views/ │ │ ├── v_UserPermissions.sql │ │ ├── v_UserRoles.sql │ │ └── v_Users.sql │ ├── CompanyName.MyMeetings.Database.Build/ │ │ └── CompanyName.MyMeetings.Database.Build.csproj │ ├── DatabaseMigrator/ │ │ ├── .dockerignore │ │ ├── DatabaseMigrator.csproj │ │ ├── Program.cs │ │ └── SerilogUpgradeLog.cs │ ├── Dockerfile │ ├── Dockerfile_DatabaseMigrator │ ├── InitializeDatabase.sql │ ├── entrypoint.sh │ ├── entrypoint_DatabaseMigrator.sh │ └── wait-for-it.sh ├── Directory.Build.props ├── Directory.Build.targets ├── Directory.Packages.props ├── Dockerfile ├── Modules/ │ ├── Administration/ │ │ ├── Application/ │ │ │ ├── CompanyName.MyMeetings.Modules.Administration.Application.csproj │ │ │ ├── Configuration/ │ │ │ │ ├── Commands/ │ │ │ │ │ ├── ICommandHandler.cs │ │ │ │ │ ├── ICommandsScheduler.cs │ │ │ │ │ └── InternalCommandBase.cs │ │ │ │ └── Queries/ │ │ │ │ └── IQueryHandler.cs │ │ │ ├── Contracts/ │ │ │ │ ├── CommandBase.cs │ │ │ │ ├── IAdministrationModule.cs │ │ │ │ ├── ICommand.cs │ │ │ │ ├── IQuery.cs │ │ │ │ ├── IRecurringCommand.cs │ │ │ │ └── QueryBase.cs │ │ │ ├── MeetingGroupProposals/ │ │ │ │ ├── AcceptMeetingGroupProposal/ │ │ │ │ │ ├── AcceptMeetingGroupProposalCommand.cs │ │ │ │ │ ├── AcceptMeetingGroupProposalCommandHandler.cs │ │ │ │ │ ├── MeetingGroupProposalAcceptedNotification.cs │ │ │ │ │ └── MeetingGroupProposalAcceptedNotificationHandler.cs │ │ │ │ ├── GetMeetingGroupProposal/ │ │ │ │ │ ├── GetMeetingGroupProposalQuery.cs │ │ │ │ │ ├── GetMeetingGroupProposalQueryHandler.cs │ │ │ │ │ └── MeetingGroupProposalDto.cs │ │ │ │ ├── GetMeetingGroupProposals/ │ │ │ │ │ ├── GetMeetingGroupProposalsQuery.cs │ │ │ │ │ └── GetMeetingGroupProposalsQueryHandler.cs │ │ │ │ ├── MeetingGroupProposedIntegrationEventHandler.cs │ │ │ │ └── RequestMeetingGroupProposalVerification/ │ │ │ │ ├── RequestMeetingGroupProposalVerificationCommand.cs │ │ │ │ └── RequestMeetingGroupProposalVerificationCommandHandler.cs │ │ │ └── Members/ │ │ │ ├── CreateMember/ │ │ │ │ ├── CreateMemberCommand.cs │ │ │ │ └── CreateMemberCommandHandler.cs │ │ │ ├── GetMember/ │ │ │ │ ├── GetMemberQuery.cs │ │ │ │ ├── GetMemberQueryHandler.cs │ │ │ │ └── MemberDto.cs │ │ │ └── NewUserRegisteredIntegrationEventHandler.cs │ │ ├── Domain/ │ │ │ ├── CompanyName.MyMeetings.Modules.Administration.Domain.csproj │ │ │ ├── MeetingGroupProposals/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── MeetingGroupProposalAcceptedDomainEvent.cs │ │ │ │ │ ├── MeetingGroupProposalRejectedDomainEvent.cs │ │ │ │ │ └── MeetingGroupProposalVerificationRequestedDomainEvent.cs │ │ │ │ ├── IMeetingGroupProposalRepository.cs │ │ │ │ ├── MeetingGroupLocation.cs │ │ │ │ ├── MeetingGroupProposal.cs │ │ │ │ ├── MeetingGroupProposalDecision.cs │ │ │ │ ├── MeetingGroupProposalId.cs │ │ │ │ ├── MeetingGroupProposalStatus.cs │ │ │ │ └── Rules/ │ │ │ │ ├── MeetingGroupProposalCanBeVerifiedOnceRule.cs │ │ │ │ └── MeetingGroupProposalRejectionMustHaveAReasonRule.cs │ │ │ ├── Members/ │ │ │ │ ├── Events/ │ │ │ │ │ └── MemberCreatedDomainEvent.cs │ │ │ │ ├── IMemberRepository.cs │ │ │ │ ├── Member.cs │ │ │ │ └── MemberId.cs │ │ │ └── Users/ │ │ │ ├── IUserContext.cs │ │ │ └── UserId.cs │ │ ├── Infrastructure/ │ │ │ ├── AdministrationContext.cs │ │ │ ├── AdministrationModule.cs │ │ │ ├── CompanyName.MyMeetings.Modules.Administration.Infrastructure.csproj │ │ │ ├── Configuration/ │ │ │ │ ├── AdministrationCompositionRoot.cs │ │ │ │ ├── AdministrationStartup.cs │ │ │ │ ├── AllConstructorFinder.cs │ │ │ │ ├── Assemblies.cs │ │ │ │ ├── Authentication/ │ │ │ │ │ └── AuthenticationModule.cs │ │ │ │ ├── DataAccess/ │ │ │ │ │ └── DataAccessModule.cs │ │ │ │ ├── EventsBus/ │ │ │ │ │ ├── EventsBusModule.cs │ │ │ │ │ ├── EventsBusStartup.cs │ │ │ │ │ └── IntegrationEventGenericHandler.cs │ │ │ │ ├── Logging/ │ │ │ │ │ └── LoggingModule.cs │ │ │ │ ├── Mediation/ │ │ │ │ │ └── MediatorModule.cs │ │ │ │ ├── Processing/ │ │ │ │ │ ├── CommandsExecutor.cs │ │ │ │ │ ├── IRecurringCommand.cs │ │ │ │ │ ├── Inbox/ │ │ │ │ │ │ ├── InboxMessageDto.cs │ │ │ │ │ │ ├── ProcessInboxCommand.cs │ │ │ │ │ │ ├── ProcessInboxCommandHandler.cs │ │ │ │ │ │ └── ProcessInboxJob.cs │ │ │ │ │ ├── InternalCommands/ │ │ │ │ │ │ ├── CommandsScheduler.cs │ │ │ │ │ │ ├── InternalCommandsModule.cs │ │ │ │ │ │ ├── ProcessInternalCommandsCommand.cs │ │ │ │ │ │ ├── ProcessInternalCommandsCommandHandler.cs │ │ │ │ │ │ └── ProcessInternalCommandsJob.cs │ │ │ │ │ ├── LoggingCommandHandlerDecorator.cs │ │ │ │ │ ├── LoggingCommandHandlerWithResultDecorator.cs │ │ │ │ │ ├── Outbox/ │ │ │ │ │ │ ├── OutboxMessageDto.cs │ │ │ │ │ │ ├── OutboxModule.cs │ │ │ │ │ │ ├── ProcessOutboxCommand.cs │ │ │ │ │ │ ├── ProcessOutboxCommandHandler.cs │ │ │ │ │ │ └── ProcessOutboxJob.cs │ │ │ │ │ ├── ProcessingModule.cs │ │ │ │ │ ├── UnitOfWorkCommandHandlerDecorator.cs │ │ │ │ │ ├── UnitOfWorkCommandHandlerWithResultDecorator.cs │ │ │ │ │ ├── ValidationCommandHandlerDecorator.cs │ │ │ │ │ └── ValidationCommandHandlerWithResultDecorator.cs │ │ │ │ ├── Quartz/ │ │ │ │ │ ├── QuartzModule.cs │ │ │ │ │ ├── QuartzStartup.cs │ │ │ │ │ └── SerilogLogProvider.cs │ │ │ │ └── Users/ │ │ │ │ └── UserContext.cs │ │ │ ├── Domain/ │ │ │ │ ├── MeetingGroupProposals/ │ │ │ │ │ ├── MeetingGroupProposalEntityTypeConfiguration.cs │ │ │ │ │ └── MeetingGroupProposalRepository.cs │ │ │ │ └── Members/ │ │ │ │ ├── MemberEntityTypeConfiguration.cs │ │ │ │ └── MemberRepository.cs │ │ │ ├── InternalCommands/ │ │ │ │ └── InternalCommandEntityTypeConfiguration.cs │ │ │ └── Outbox/ │ │ │ ├── OutboxAccessor.cs │ │ │ └── OutboxMessageEntityTypeConfiguration.cs │ │ ├── IntegrationEvents/ │ │ │ ├── CompanyName.MyMeetings.Modules.Administration.IntegrationEvents.csproj │ │ │ └── MeetingGroupProposals/ │ │ │ └── MeetingGroupProposalAcceptedIntegrationEvent.cs │ │ └── Tests/ │ │ ├── ArchTests/ │ │ │ ├── Application/ │ │ │ │ └── ApplicationTests.cs │ │ │ ├── CompanyName.MyMeetings.Modules.Administration.ArchTests.csproj │ │ │ ├── Domain/ │ │ │ │ └── DomainTests.cs │ │ │ ├── Module/ │ │ │ │ └── LayersTests.cs │ │ │ └── SeedWork/ │ │ │ └── TestBase.cs │ │ ├── IntegrationTests/ │ │ │ ├── AssemblyInfo.cs │ │ │ ├── CompanyName.MyMeetings.Modules.Administration.IntegrationTests.csproj │ │ │ ├── MeetingGroupProposals/ │ │ │ │ ├── MeetingGroupProposalSampleData.cs │ │ │ │ └── MeetingGroupProposalTests.cs │ │ │ ├── Members/ │ │ │ │ ├── CreateMemberTests.cs │ │ │ │ └── MemberSampleData.cs │ │ │ └── SeedWork/ │ │ │ ├── ExecutionContextMock.cs │ │ │ ├── OutboxMessagesHelper.cs │ │ │ └── TestBase.cs │ │ └── UnitTests/ │ │ ├── CompanyName.MyMeetings.Modules.Administration.Domain.UnitTests.csproj │ │ ├── MeetingGroupProposals/ │ │ │ └── MeetingGroupProposalTests.cs │ │ ├── Members/ │ │ │ └── MemberTests.cs │ │ └── SeedWork/ │ │ ├── DomainEventsTestHelper.cs │ │ └── TestBase.cs │ ├── Meetings/ │ │ ├── Application/ │ │ │ ├── CompanyName.MyMeetings.Modules.Meetings.Application.csproj │ │ │ ├── Configuration/ │ │ │ │ ├── Commands/ │ │ │ │ │ ├── ICommandHandler.cs │ │ │ │ │ ├── ICommandsScheduler.cs │ │ │ │ │ └── InternalCommandBase.cs │ │ │ │ └── Queries/ │ │ │ │ └── IQueryHandler.cs │ │ │ ├── Contracts/ │ │ │ │ ├── CommandBase.cs │ │ │ │ ├── ICommand.cs │ │ │ │ ├── IMeetingsModule.cs │ │ │ │ ├── IQuery.cs │ │ │ │ ├── IRecurringCommand.cs │ │ │ │ └── QueryBase.cs │ │ │ ├── Countries/ │ │ │ │ ├── CountryDto.cs │ │ │ │ ├── GetAllCountriesQuery.cs │ │ │ │ └── GetAllCountriesQueryHandler.cs │ │ │ ├── MeetingCommentingConfigurations/ │ │ │ │ ├── DisableMeetingCommentingConfiguration/ │ │ │ │ │ ├── DisableMeetingCommentingConfigurationCommand.cs │ │ │ │ │ └── DisableMeetingCommentingConfigurationCommandHandler.cs │ │ │ │ ├── EnableMeetingCommentingConfiguration/ │ │ │ │ │ ├── EnableMeetingCommentingConfigurationCommand.cs │ │ │ │ │ └── EnableMeetingCommentingConfigurationCommandHandler.cs │ │ │ │ ├── GetMeetingCommentingConfiguration/ │ │ │ │ │ ├── GetMeetingCommentingConfigurationQuery.cs │ │ │ │ │ ├── GetMeetingCommentingConfigurationQueryHandler.cs │ │ │ │ │ └── MeetingCommentingConfigurationDto.cs │ │ │ │ └── MeetingCreatedEventHandler.cs │ │ │ ├── MeetingComments/ │ │ │ │ ├── AddMeetingComment/ │ │ │ │ │ ├── AddMeetingCommentCommand.cs │ │ │ │ │ ├── AddMeetingCommentCommandHandler.cs │ │ │ │ │ └── AddMeetingCommentCommandValidator.cs │ │ │ │ ├── AddMeetingCommentLike/ │ │ │ │ │ ├── AddMeetingCommentLikeCommand.cs │ │ │ │ │ └── AddMeetingCommentLikeCommandHandler.cs │ │ │ │ ├── AddMeetingCommentReply/ │ │ │ │ │ ├── AddReplyToMeetingCommentCommand.cs │ │ │ │ │ └── AddReplyToMeetingCommentCommandHandler.cs │ │ │ │ ├── EditMeetingComment/ │ │ │ │ │ ├── EditMeetingCommentCommand.cs │ │ │ │ │ ├── EditMeetingCommentCommandHandler.cs │ │ │ │ │ └── EditMeetingCommentCommandValidator.cs │ │ │ │ ├── GetMeetingCommentLikers/ │ │ │ │ │ ├── GetMeetingCommentLikersQuery.cs │ │ │ │ │ ├── GetMeetingCommentLikersQueryHandler.cs │ │ │ │ │ └── MeetingCommentLikerDto.cs │ │ │ │ ├── GetMeetingComments/ │ │ │ │ │ ├── GetMeetingCommentsQuery.cs │ │ │ │ │ ├── GetMeetingCommentsQueryHandler.cs │ │ │ │ │ └── MeetingCommentDto.cs │ │ │ │ ├── MeetingCommentLikedNotification.cs │ │ │ │ ├── MeetingCommentLikedNotificationHandler.cs │ │ │ │ ├── MeetingCommentUnlikeNotificationHandler.cs │ │ │ │ ├── MeetingCommentUnlikedNotification.cs │ │ │ │ ├── RemoveMeetingComment/ │ │ │ │ │ ├── RemoveMeetingCommentCommand.cs │ │ │ │ │ └── RemoveMeetingCommentCommandHandler.cs │ │ │ │ └── RemoveMeetingCommentLike/ │ │ │ │ ├── RemoveMeetingCommentLikeCommand.cs │ │ │ │ └── RemoveMeetingCommentLikeCommandHandler.cs │ │ │ ├── MeetingGroupProposals/ │ │ │ │ ├── AcceptMeetingGroupProposal/ │ │ │ │ │ ├── AcceptMeetingGroupProposalCommand.cs │ │ │ │ │ ├── AcceptMeetingGroupProposalCommandHandler.cs │ │ │ │ │ ├── AcceptMeetingGroupProposalCommandValidator.cs │ │ │ │ │ ├── MeetingGroupProposalAcceptedNotification.cs │ │ │ │ │ └── MeetingGroupProposalAcceptedNotificationHandler.cs │ │ │ │ ├── GetAllMeetingGroupProposals/ │ │ │ │ │ ├── GetAllMeetingGroupProposalsQuery.cs │ │ │ │ │ └── GetAllMeetingGroupProposalsQueryHandler.cs │ │ │ │ ├── GetMeetingGroupProposal/ │ │ │ │ │ ├── GetMeetingGroupProposalQuery.cs │ │ │ │ │ ├── GetMeetingGroupProposalQueryHandler.cs │ │ │ │ │ └── MeetingGroupProposalDto.cs │ │ │ │ ├── GetMemberMeetingGroupProposals/ │ │ │ │ │ ├── GetMemberMeetingGroupProposalsQuery.cs │ │ │ │ │ └── GetMemberMeetingGroupProposalsQueryHandler.cs │ │ │ │ ├── MeetingGroupProposalAcceptedIntegrationEventHandler.cs │ │ │ │ ├── MeetingGroupProposedNotification.cs │ │ │ │ ├── MeetingGroupProposedNotificationHandler.cs │ │ │ │ └── ProposeMeetingGroup/ │ │ │ │ ├── ProposeMeetingGroupCommand.cs │ │ │ │ ├── ProposeMeetingGroupCommandHandler.cs │ │ │ │ └── ProposeMeetingGroupCommandValidator.cs │ │ │ ├── MeetingGroups/ │ │ │ │ ├── CreateNewMeetingGroup/ │ │ │ │ │ ├── CreateNewMeetingGroupCommand.cs │ │ │ │ │ └── CreateNewMeetingGroupCommandHandler.cs │ │ │ │ ├── EditMeetingGroupGeneralAttributes/ │ │ │ │ │ ├── EditMeetingGroupGeneralAttributesCommand.cs │ │ │ │ │ └── EditMeetingGroupGeneralAttributesCommandHandler.cs │ │ │ │ ├── GetAllMeetingGroups/ │ │ │ │ │ ├── GetAllMeetingGroupsQuery.cs │ │ │ │ │ ├── GetAllMeetingGroupsQueryHandler.cs │ │ │ │ │ └── MeetingGroupDto.cs │ │ │ │ ├── GetAuthenticationMemberMeetingGroups/ │ │ │ │ │ ├── GetAuthenticationMemberMeetingGroupsQuery.cs │ │ │ │ │ ├── GetAuthenticationMemberMeetingGroupsQueryHandler.cs │ │ │ │ │ └── MemberMeetingGroupDto.cs │ │ │ │ ├── GetMeetingGroupDetails/ │ │ │ │ │ ├── GetMeetingGroupDetailsQuery.cs │ │ │ │ │ ├── GetMeetingGroupDetailsQueryHandler.cs │ │ │ │ │ └── MeetingGroupDetailsDto.cs │ │ │ │ ├── JoinToGroup/ │ │ │ │ │ ├── JoinToGroupCommand.cs │ │ │ │ │ └── JoinToGroupCommandHandler.cs │ │ │ │ ├── LeaveMeetingGroup/ │ │ │ │ │ ├── LeaveMeetingGroupCommand.cs │ │ │ │ │ └── LeaveMeetingGroupCommandHandler.cs │ │ │ │ ├── MeetingGroupCreatedNotification.cs │ │ │ │ ├── MeetingGroupCreatedSendEmailHandler.cs │ │ │ │ ├── SendMeetingGroupCreatedEmail/ │ │ │ │ │ ├── SendMeetingGroupCreatedEmailCommand.cs │ │ │ │ │ └── SendMeetingGroupCreatedEmailCommandHandler.cs │ │ │ │ └── SetMeetingGroupExpirationDate/ │ │ │ │ ├── SetMeetingGroupExpirationDateCommand.cs │ │ │ │ └── SetMeetingGroupExpirationDateCommandHandler.cs │ │ │ ├── Meetings/ │ │ │ │ ├── AddMeetingAttendee/ │ │ │ │ │ ├── AddMeetingAttendeeCommand.cs │ │ │ │ │ └── AddMeetingAttendeeCommandHandler.cs │ │ │ │ ├── AddMeetingNotAttendee/ │ │ │ │ │ ├── AddMeetingNotAttendeeCommand.cs │ │ │ │ │ └── AddMeetingNotAttendeeCommandHandler.cs │ │ │ │ ├── CancelMeeting/ │ │ │ │ │ ├── CancelMeetingCommand.cs │ │ │ │ │ └── CancelMeetingCommandHandler.cs │ │ │ │ ├── ChangeMeetingMainAttributes/ │ │ │ │ │ ├── ChangeMeetingMainAttributesCommand.cs │ │ │ │ │ └── ChangeMeetingMainAttributesCommandHandler.cs │ │ │ │ ├── ChangeNotAttendeeDecision/ │ │ │ │ │ ├── ChangeNotAttendeeDecisionCommand.cs │ │ │ │ │ └── ChangeNotAttendeeDecisionCommandHandler.cs │ │ │ │ ├── CreateMeeting/ │ │ │ │ │ ├── CreateMeetingCommand.cs │ │ │ │ │ └── CreateMeetingCommandHandler.cs │ │ │ │ ├── GetAuthenticatedMemberMeetings/ │ │ │ │ │ ├── GetAuthenticatedMemberMeetingsQuery.cs │ │ │ │ │ ├── GetAuthenticatedMemberMeetingsQueryHandler.cs │ │ │ │ │ └── MemberMeetingDto.cs │ │ │ │ ├── GetMeetingAttendees/ │ │ │ │ │ ├── GetMeetingAttendeesQuery.cs │ │ │ │ │ ├── GetMeetingAttendeesQueryHandler.cs │ │ │ │ │ └── MeetingAttendeeDto.cs │ │ │ │ ├── GetMeetingDetails/ │ │ │ │ │ ├── GetMeetingDetailsQuery.cs │ │ │ │ │ ├── GetMeetingDetailsQueryHandler.cs │ │ │ │ │ └── MeetingDetailsDto.cs │ │ │ │ ├── MarkMeetingAttendeeFeeAsPayedCommand.cs │ │ │ │ ├── MarkMeetingAttendeeFeeAsPayedCommandHandler.cs │ │ │ │ ├── MeetingDto.cs │ │ │ │ ├── MeetingFeePaidIntegrationEventHandler.cs │ │ │ │ ├── MeetingsQueryHelper.cs │ │ │ │ ├── RemoveMeetingAttendee/ │ │ │ │ │ ├── RemoveMeetingAttendeeCommand.cs │ │ │ │ │ └── RemoveMeetingAttendeeCommandHandler.cs │ │ │ │ ├── SendMeetingAttendeeAddedEmail/ │ │ │ │ │ ├── MeetingAttendeeAddedNotification.cs │ │ │ │ │ ├── MeetingAttendeeAddedNotificationHandler.cs │ │ │ │ │ ├── MeetingAttendeeAddedPublishEventNotificationHandler.cs │ │ │ │ │ ├── SendMeetingAttendeeAddedEmailCommand.cs │ │ │ │ │ └── SendMeetingAttendeeAddedEmailCommandHandler.cs │ │ │ │ ├── SetMeetingAttendeeRole/ │ │ │ │ │ ├── SetMeetingAttendeeRoleCommand.cs │ │ │ │ │ └── SetMeetingAttendeeRoleCommandHandler.cs │ │ │ │ ├── SetMeetingHostRole/ │ │ │ │ │ ├── SetMeetingHostRoleCommand.cs │ │ │ │ │ └── SetMeetingHostRoleCommandHandler.cs │ │ │ │ ├── SignOffMemberFromWaitlist/ │ │ │ │ │ ├── SignOffMemberFromWaitlistCommand.cs │ │ │ │ │ └── SignOffMemberFromWaitlistCommandHandler.cs │ │ │ │ └── SignUpMemberToWaitlist/ │ │ │ │ ├── SignUpMemberToWaitlistCommand.cs │ │ │ │ └── SignUpMemberToWaitlistCommandHandler.cs │ │ │ ├── MemberSubscriptions/ │ │ │ │ ├── ChangeSubscriptionExpirationDateForMember/ │ │ │ │ │ ├── ChangeSubscriptionExpirationDateForMemberCommand.cs │ │ │ │ │ └── ChangeSubscriptionExpirationDateForMemberCommandHandler.cs │ │ │ │ ├── MemberSubscriptionExpirationDateChangedNotification.cs │ │ │ │ ├── MemberSubscriptionExpirationDateChangedNotificationHandler.cs │ │ │ │ └── SubscriptionExpirationDateChangedIntegrationEventHandler.cs │ │ │ └── Members/ │ │ │ ├── CreateMember/ │ │ │ │ ├── CreateMemberCommand.cs │ │ │ │ ├── CreateMemberCommandHandler.cs │ │ │ │ ├── MemberCratedNotificationHandler.cs │ │ │ │ ├── MemberCreatedNotification.cs │ │ │ │ └── NewUserRegisteredIntegrationEventHandler.cs │ │ │ ├── MemberContext.cs │ │ │ ├── MemberDto.cs │ │ │ └── MembersQueryHelper.cs │ │ ├── Domain/ │ │ │ ├── CompanyName.MyMeetings.Modules.Meetings.Domain.csproj │ │ │ ├── MeetingCommentingConfigurations/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── MeetingCommentingConfigurationCreatedDomainEvent.cs │ │ │ │ │ ├── MeetingCommentingDisabledDomainEvent.cs │ │ │ │ │ └── MeetingCommentingEnabledDomainEvent.cs │ │ │ │ ├── IMeetingCommentingConfigurationRepository.cs │ │ │ │ ├── MeetingCommentingConfiguration.cs │ │ │ │ ├── MeetingCommentingConfigurationId.cs │ │ │ │ └── Rules/ │ │ │ │ ├── MeetingCommentingCanBeDisabledOnlyByGroupOrganizerRule.cs │ │ │ │ └── MeetingCommentingCanBeEnabledOnlyByGroupOrganizerRule.cs │ │ │ ├── MeetingComments/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── MeetingCommentAddedDomainEvent.cs │ │ │ │ │ ├── MeetingCommentEditedDomainEvent.cs │ │ │ │ │ ├── MeetingCommentRemovedDomainEvent.cs │ │ │ │ │ └── ReplyToMeetingCommentAddedDomainEvent.cs │ │ │ │ ├── IMeetingCommentRepository.cs │ │ │ │ ├── MeetingComment.cs │ │ │ │ ├── MeetingCommentId.cs │ │ │ │ └── Rules/ │ │ │ │ ├── CommentCanBeAddedOnlyByMeetingGroupMemberRule.cs │ │ │ │ ├── CommentCanBeCreatedOnlyIfCommentingForMeetingEnabledRule.cs │ │ │ │ ├── CommentCanBeEditedOnlyIfCommentingForMeetingEnabledRule.cs │ │ │ │ ├── CommentCanBeLikedOnlyByMeetingGroupMemberRule.cs │ │ │ │ ├── CommentCannotBeLikedByTheSameMemberMoreThanOnceRule.cs │ │ │ │ ├── CommentTextMustBeProvidedRule.cs │ │ │ │ ├── MeetingCommentCanBeEditedOnlyByAuthorRule.cs │ │ │ │ ├── MeetingCommentCanBeRemovedOnlyByAuthorOrGroupOrganizerRule.cs │ │ │ │ └── RemovingReasonCanBeProvidedOnlyByGroupOrganizerRule.cs │ │ │ ├── MeetingGroupProposals/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── MeetingGroupProposalAcceptedDomainEvent.cs │ │ │ │ │ └── MeetingGroupProposedDomainEvent.cs │ │ │ │ ├── IMeetingGroupProposalRepository.cs │ │ │ │ ├── MeetingGroupProposal.cs │ │ │ │ ├── MeetingGroupProposalId.cs │ │ │ │ ├── MeetingGroupProposalStatus.cs │ │ │ │ └── Rules/ │ │ │ │ └── MeetingGroupProposalCannotBeAcceptedMoreThanOnceRule.cs │ │ │ ├── MeetingGroups/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── MeetingAttendeeChangedDecisionDomainEvent.cs │ │ │ │ │ ├── MeetingGroupCreatedDomainEvent.cs │ │ │ │ │ ├── MeetingGroupGeneralAttributesEditedDomainEvent.cs │ │ │ │ │ ├── MeetingGroupMemberLeftGroupDomainEvent.cs │ │ │ │ │ ├── MeetingGroupPaymentInfoUpdatedDomainEvent.cs │ │ │ │ │ ├── MeetingNotAttendeeChangedDecisionDomainEvent.cs │ │ │ │ │ └── NewMeetingGroupMemberJoinedDomainEvent.cs │ │ │ │ ├── IMeetingGroupRepository.cs │ │ │ │ ├── MeetingGroup.cs │ │ │ │ ├── MeetingGroupId.cs │ │ │ │ ├── MeetingGroupLocation.cs │ │ │ │ ├── MeetingGroupMember.cs │ │ │ │ ├── MeetingGroupMemberRole.cs │ │ │ │ ├── Policies/ │ │ │ │ │ ├── MeetingGroupExpirationDatePolicy.cs │ │ │ │ │ └── MeetingGroupMemberData.cs │ │ │ │ └── Rules/ │ │ │ │ ├── MeetingCanBeOrganizedOnlyByPayedGroupRule.cs │ │ │ │ ├── MeetingGroupMemberCannotBeAddedTwiceRule.cs │ │ │ │ ├── MeetingHostMustBeAMeetingGroupMemberRule.cs │ │ │ │ └── NotActualGroupMemberCannotLeaveGroupRule.cs │ │ │ ├── MeetingMemberCommentLikes/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── MeetingCommentLikedDomainEvent.cs │ │ │ │ │ └── MeetingCommentUnlikedDomainEvent.cs │ │ │ │ ├── IMeetingMemberCommentLikesRepository.cs │ │ │ │ ├── MeetingMemberCommentLike.cs │ │ │ │ └── MeetingMemberCommentLikeId.cs │ │ │ ├── Meetings/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── MeetingAttendeeAddedDomainEvent.cs │ │ │ │ │ ├── MeetingAttendeeFeePaidDomainEvent.cs │ │ │ │ │ ├── MeetingAttendeeRemovedDomainEvent.cs │ │ │ │ │ ├── MeetingCanceledDomainEvent.cs │ │ │ │ │ ├── MeetingCreatedDomainEvent.cs │ │ │ │ │ ├── MeetingEditedDomainEvent.cs │ │ │ │ │ ├── MeetingMainAttributesChangedDomainEvent.cs │ │ │ │ │ ├── MeetingNotAttendeeAddedDomainEvent.cs │ │ │ │ │ ├── MeetingWaitlistMemberAddedDomainEvent.cs │ │ │ │ │ ├── MemberSetAsAttendeeDomainEvent.cs │ │ │ │ │ ├── MemberSignedOffFromMeetingWaitlistDomainEvent.cs │ │ │ │ │ └── NewMeetingHostSetDomainEvent.cs │ │ │ │ ├── IMeetingRepository.cs │ │ │ │ ├── Meeting.cs │ │ │ │ ├── MeetingAttendee.cs │ │ │ │ ├── MeetingAttendeeRole.cs │ │ │ │ ├── MeetingId.cs │ │ │ │ ├── MeetingLimits.cs │ │ │ │ ├── MeetingLocation.cs │ │ │ │ ├── MeetingNotAttendee.cs │ │ │ │ ├── MeetingTerm.cs │ │ │ │ ├── MeetingWaitlistMember.cs │ │ │ │ ├── MoneyValue.cs │ │ │ │ ├── Rules/ │ │ │ │ │ ├── AttendeeCanBeAddedOnlyInRsvpTermRule.cs │ │ │ │ │ ├── AttendeesLimitCannotBeChangedToSmallerThanActiveAttendeesRule.cs │ │ │ │ │ ├── MeetingAttendeeMustBeAMemberOfGroupRule.cs │ │ │ │ │ ├── MeetingAttendeesLimitCannotBeNegativeRule.cs │ │ │ │ │ ├── MeetingAttendeesLimitMustBeGreaterThanGuestsLimitRule.cs │ │ │ │ │ ├── MeetingAttendeesNumberIsAboveLimitRule.cs │ │ │ │ │ ├── MeetingCannotBeChangedAfterStartRule.cs │ │ │ │ │ ├── MeetingGuestsLimitCannotBeNegativeRule.cs │ │ │ │ │ ├── MeetingGuestsNumberIsAboveLimitRule.cs │ │ │ │ │ ├── MeetingMustHaveAtLeastOneHostRule.cs │ │ │ │ │ ├── MemberCannotBeAnAttendeeOfMeetingMoreThanOnceRule.cs │ │ │ │ │ ├── MemberCannotBeMoreThanOnceOnMeetingWaitlistRule.cs │ │ │ │ │ ├── MemberCannotBeNotAttendeeTwiceRule.cs │ │ │ │ │ ├── MemberCannotHaveSetAttendeeRoleMoreThanOnceRule.cs │ │ │ │ │ ├── MemberOnWaitlistMustBeAMemberOfGroupRule.cs │ │ │ │ │ ├── NotActiveMemberOfWaitlistCannotBeSignedOffRule.cs │ │ │ │ │ ├── NotActiveNotAttendeeCannotChangeDecisionRule.cs │ │ │ │ │ ├── OnlyActiveAttendeeCanBeRemovedFromMeetingRule.cs │ │ │ │ │ ├── OnlyMeetingAttendeeCanHaveChangedRoleRule.cs │ │ │ │ │ ├── OnlyMeetingOrGroupOrganizerCanSetMeetingMemberRolesRule.cs │ │ │ │ │ └── ReasonOfRemovingAttendeeFromMeetingMustBeProvidedRule.cs │ │ │ │ └── Term.cs │ │ │ ├── Members/ │ │ │ │ ├── Events/ │ │ │ │ │ └── MemberCreatedDomainEvent.cs │ │ │ │ ├── IMemberContext.cs │ │ │ │ ├── IMemberRepository.cs │ │ │ │ ├── MeetingGroupMemberData.cs │ │ │ │ ├── Member.cs │ │ │ │ ├── MemberId.cs │ │ │ │ └── MemberSubscriptions/ │ │ │ │ ├── Events/ │ │ │ │ │ └── MemberSubscriptionExpirationDateChangedDomainEvent.cs │ │ │ │ ├── IMemberSubscriptionRepository.cs │ │ │ │ ├── MemberSubscription.cs │ │ │ │ └── MemberSubscriptionId.cs │ │ │ └── SharedKernel/ │ │ │ └── SystemClock.cs │ │ ├── Infrastructure/ │ │ │ ├── CompanyName.MyMeetings.Modules.Meetings.Infrastructure.csproj │ │ │ ├── Configuration/ │ │ │ │ ├── AllConstructorFinder.cs │ │ │ │ ├── Assemblies.cs │ │ │ │ ├── Authentication/ │ │ │ │ │ └── AuthenticationModule.cs │ │ │ │ ├── DataAccess/ │ │ │ │ │ └── DataAccessModule.cs │ │ │ │ ├── Email/ │ │ │ │ │ └── EmailModule.cs │ │ │ │ ├── EventsBus/ │ │ │ │ │ ├── EventsBusModule.cs │ │ │ │ │ ├── EventsBusStartup.cs │ │ │ │ │ └── IntegrationEventGenericHandler.cs │ │ │ │ ├── Logging/ │ │ │ │ │ └── LoggingModule.cs │ │ │ │ ├── Mediation/ │ │ │ │ │ └── MediatorModule.cs │ │ │ │ ├── MeetingsCompositionRoot.cs │ │ │ │ ├── MeetingsStartup.cs │ │ │ │ ├── Processing/ │ │ │ │ │ ├── CommandsExecutor.cs │ │ │ │ │ ├── IRecurringCommand.cs │ │ │ │ │ ├── Inbox/ │ │ │ │ │ │ ├── InboxMessageDto.cs │ │ │ │ │ │ ├── ProcessInboxCommand.cs │ │ │ │ │ │ ├── ProcessInboxCommandHandler.cs │ │ │ │ │ │ └── ProcessInboxJob.cs │ │ │ │ │ ├── InternalCommands/ │ │ │ │ │ │ ├── CommandsScheduler.cs │ │ │ │ │ │ ├── ProcessInternalCommandsCommand.cs │ │ │ │ │ │ ├── ProcessInternalCommandsCommandHandler.cs │ │ │ │ │ │ └── ProcessInternalCommandsJob.cs │ │ │ │ │ ├── LoggingCommandHandlerDecorator.cs │ │ │ │ │ ├── LoggingCommandHandlerWithResultDecorator.cs │ │ │ │ │ ├── Outbox/ │ │ │ │ │ │ ├── OutboxMessageDto.cs │ │ │ │ │ │ ├── OutboxModule.cs │ │ │ │ │ │ ├── ProcessOutboxCommand.cs │ │ │ │ │ │ ├── ProcessOutboxCommandHandler.cs │ │ │ │ │ │ └── ProcessOutboxJob.cs │ │ │ │ │ ├── ProcessingModule.cs │ │ │ │ │ ├── UnitOfWorkCommandHandlerDecorator.cs │ │ │ │ │ ├── UnitOfWorkCommandHandlerWithResultDecorator.cs │ │ │ │ │ ├── ValidationCommandHandlerDecorator.cs │ │ │ │ │ └── ValidationCommandHandlerWithResultDecorator.cs │ │ │ │ └── Quartz/ │ │ │ │ ├── QuartzModule.cs │ │ │ │ ├── QuartzStartup.cs │ │ │ │ └── SerilogLogProvider.cs │ │ │ ├── Domain/ │ │ │ │ ├── MeetingCommentingConfigurations/ │ │ │ │ │ ├── MeetingCommentingConfigurationEntityTypeConfiguration.cs │ │ │ │ │ └── MeetingCommentingConfigurationRepository.cs │ │ │ │ ├── MeetingComments/ │ │ │ │ │ ├── MeetingCommentEntityTypeConfiguration.cs │ │ │ │ │ └── MeetingCommentRepository.cs │ │ │ │ ├── MeetingGroupProposals/ │ │ │ │ │ ├── MeetingGroupProposalEntityTypeConfiguration.cs │ │ │ │ │ └── MeetingGroupProposalRepository.cs │ │ │ │ ├── MeetingGroups/ │ │ │ │ │ ├── MeetingGroupRepository.cs │ │ │ │ │ └── MeetingGroupsEntityTypeConfiguration.cs │ │ │ │ ├── MeetingMemberCommentLikes/ │ │ │ │ │ ├── MeetingMemberCommentLikeEntityTypeConfiguration.cs │ │ │ │ │ └── MeetingMemberCommentLikeRepository.cs │ │ │ │ ├── Meetings/ │ │ │ │ │ ├── MeetingEntityTypeConfiguration.cs │ │ │ │ │ └── MeetingRepository.cs │ │ │ │ └── Members/ │ │ │ │ ├── MemberEntityTypeConfiguration.cs │ │ │ │ ├── MemberRepository.cs │ │ │ │ └── MemberSubscriptions/ │ │ │ │ ├── MemberSubscriptionEntityTypeConfiguration.cs │ │ │ │ └── MemberSubscriptionRepository.cs │ │ │ ├── InternalCommands/ │ │ │ │ └── InternalCommandEntityTypeConfiguration.cs │ │ │ ├── MeetingsContext.cs │ │ │ ├── MeetingsModule.cs │ │ │ └── Outbox/ │ │ │ ├── OutboxAccessor.cs │ │ │ └── OutboxMessageEntityTypeConfiguration.cs │ │ ├── IntegrationEvents/ │ │ │ ├── CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents.csproj │ │ │ ├── MeetingAttendeeAddedIntegrationEvent.cs │ │ │ ├── MeetingGroupProposedIntegrationEvent.cs │ │ │ └── MemberCreatedIntegrationEvent.cs │ │ └── Tests/ │ │ ├── ArchTests/ │ │ │ ├── Application/ │ │ │ │ └── ApplicationTests.cs │ │ │ ├── CompanyName.MyMeetings.Modules.Meetings.ArchTests.csproj │ │ │ ├── Domain/ │ │ │ │ └── DomainTests.cs │ │ │ ├── Module/ │ │ │ │ └── LayersTests.cs │ │ │ └── SeedWork/ │ │ │ └── TestBase.cs │ │ ├── IntegrationTests/ │ │ │ ├── AssemblyInfo.cs │ │ │ ├── CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.csproj │ │ │ ├── Countries/ │ │ │ │ ├── 0001_SeedCountries.sql │ │ │ │ └── GetCountriesTests.cs │ │ │ ├── MeetingCommentLikes/ │ │ │ │ ├── AddMeetingCommentLikeTests.cs │ │ │ │ ├── GetLikedMeetingCommentProbe.cs │ │ │ │ ├── GetMeetingCommentsProbe.cs │ │ │ │ └── RemoveMeetingCommentLikeTests.cs │ │ │ ├── MeetingCommentingConfigurations/ │ │ │ │ ├── CreateMeetingCommentingConfigurationTests.cs │ │ │ │ ├── DisableMeetingCommentingConfigurationTests.cs │ │ │ │ └── EnableMeetingCommentingConfigurationTests.cs │ │ │ ├── MeetingComments/ │ │ │ │ ├── AddMeetingCommentTests.cs │ │ │ │ ├── AddReplyToMeetingCommentTests.cs │ │ │ │ ├── EditMeetingCommentTests.cs │ │ │ │ ├── GetMeetingCommentsTests.cs │ │ │ │ └── RemoveMeetingCommentTests.cs │ │ │ ├── MeetingGroupProposals/ │ │ │ │ ├── GetMeetingGroupProposalsTests.cs │ │ │ │ ├── MeetingGroupProposalSampleData.cs │ │ │ │ └── ProposeMeetingGroupTests.cs │ │ │ ├── MeetingGroups/ │ │ │ │ └── CreateNewMeetingGroupTests.cs │ │ │ ├── Meetings/ │ │ │ │ ├── MeetingCreateTests.cs │ │ │ │ └── MeetingHelper.cs │ │ │ └── SeedWork/ │ │ │ ├── EventsBusMock.cs │ │ │ ├── ExecutionContextMock.cs │ │ │ ├── OutboxMessagesHelper.cs │ │ │ └── TestBase.cs │ │ └── UnitTests/ │ │ ├── CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.csproj │ │ ├── MeetingGroupProposals/ │ │ │ └── MeetingGroupProposalTests.cs │ │ ├── MeetingGroups/ │ │ │ └── MeetingGroupTests.cs │ │ ├── Meetings/ │ │ │ ├── MeetingAddAttendeeTests.cs │ │ │ ├── MeetingAddNotAttendeeTests.cs │ │ │ ├── MeetingCommentTests.cs │ │ │ ├── MeetingCommentingConfigurationTests.cs │ │ │ ├── MeetingLimitsTests.cs │ │ │ ├── MeetingRolesTests.cs │ │ │ ├── MeetingTests.cs │ │ │ ├── MeetingTestsBase.cs │ │ │ └── MeetingWaitlistTests.cs │ │ ├── Members/ │ │ │ └── MemberTests.cs │ │ └── SeedWork/ │ │ ├── DomainEventsTestHelper.cs │ │ └── TestBase.cs │ ├── Payments/ │ │ ├── Application/ │ │ │ ├── CompanyName.MyMeetings.Modules.Payments.Application.csproj │ │ │ ├── Configuration/ │ │ │ │ ├── Commands/ │ │ │ │ │ ├── ICommandHandler.cs │ │ │ │ │ ├── ICommandsScheduler.cs │ │ │ │ │ └── InternalCommandBase.cs │ │ │ │ ├── Projections/ │ │ │ │ │ ├── IProjector.cs │ │ │ │ │ └── ProjectorBase.cs │ │ │ │ └── Queries/ │ │ │ │ └── IQueryHandler.cs │ │ │ ├── Contracts/ │ │ │ │ ├── CommandBase.cs │ │ │ │ ├── ICommand.cs │ │ │ │ ├── IPaymentsModule.cs │ │ │ │ ├── IQuery.cs │ │ │ │ ├── IRecurringCommand.cs │ │ │ │ └── QueryBase.cs │ │ │ ├── MeetingFees/ │ │ │ │ ├── CreateMeetingFee/ │ │ │ │ │ ├── CreateMeetingFeeCommand.cs │ │ │ │ │ └── CreateMeetingFeeCommandHandler.cs │ │ │ │ ├── CreateMeetingFeePayment/ │ │ │ │ │ ├── CreateMeetingFeePaymentCommand.cs │ │ │ │ │ └── CreateMeetingFeePaymentCommandHandler.cs │ │ │ │ ├── GetMeetingFees/ │ │ │ │ │ ├── GetMeetingFeesQuery.cs │ │ │ │ │ ├── GetMeetingFeesQueryHandler.cs │ │ │ │ │ ├── MeetingFeeDto.cs │ │ │ │ │ └── MeetingFeesProjector.cs │ │ │ │ ├── MarkMeetingFeeAsPaid/ │ │ │ │ │ ├── MarkMeetingFeeAsPaidCommand.cs │ │ │ │ │ ├── MarkMeetingFeeAsPaidCommandHandler.cs │ │ │ │ │ ├── MeetingFeePaidNotification.cs │ │ │ │ │ └── MeetingFeePaidNotificationHandler.cs │ │ │ │ ├── MarkMeetingFeePaymentAsPaid/ │ │ │ │ │ ├── MarkMeetingFeePaymentAsPaidCommand.cs │ │ │ │ │ ├── MarkMeetingFeePaymentAsPaidCommandHandler.cs │ │ │ │ │ ├── MeetingFeePaymentPaidNotification.cs │ │ │ │ │ └── MeetingFeePaymentPaidNotificationHandler.cs │ │ │ │ └── MeetingAttendeeAddedIntegrationEventHandler.cs │ │ │ ├── Payers/ │ │ │ │ ├── CreatePayer/ │ │ │ │ │ ├── CreatePayerCommand.cs │ │ │ │ │ ├── CreatePayerCommandHandler.cs │ │ │ │ │ └── NewUserRegisteredIntegrationEventHandler.cs │ │ │ │ ├── GetPayer/ │ │ │ │ │ ├── GetPayerQuery.cs │ │ │ │ │ ├── GetPayerQueryHandler.cs │ │ │ │ │ ├── PayerDetailsProjector.cs │ │ │ │ │ └── PayerDto.cs │ │ │ │ └── GetPayerEmail/ │ │ │ │ └── PayerEmailProvider.cs │ │ │ ├── PriceListItems/ │ │ │ │ ├── ActivatePriceListItem/ │ │ │ │ │ ├── ActivatePriceListItemCommand.cs │ │ │ │ │ └── ActivatePriceListItemCommandHandler.cs │ │ │ │ ├── ChangePriceListItemAttributes/ │ │ │ │ │ ├── ChangePriceListItemAttributesCommand.cs │ │ │ │ │ └── ChangePriceListItemAttributesCommandHandler.cs │ │ │ │ ├── CreatePriceListItem/ │ │ │ │ │ ├── CreatePriceListItemCommand.cs │ │ │ │ │ └── CreatePriceListItemCommandHandler.cs │ │ │ │ ├── DeactivatePriceListItem/ │ │ │ │ │ ├── DeactivatePriceListItemCommand.cs │ │ │ │ │ └── DeactivatePriceListItemCommandHandler.cs │ │ │ │ ├── GetPriceListItem/ │ │ │ │ │ ├── GetPriceListItemQuery.cs │ │ │ │ │ ├── GetPriceListItemQueryHandler.cs │ │ │ │ │ ├── PriceListItemMoneyValueDto.cs │ │ │ │ │ └── PriceListItemsProjector.cs │ │ │ │ ├── GetPriceListItems/ │ │ │ │ │ ├── GetPriceListItemsQuery.cs │ │ │ │ │ └── GetPriceListItemsQueryHandler.cs │ │ │ │ ├── PriceListFactory.cs │ │ │ │ └── PriceListItemDto.cs │ │ │ └── Subscriptions/ │ │ │ ├── BuySubscription/ │ │ │ │ ├── BuySubscriptionCommand.cs │ │ │ │ └── BuySubscriptionCommandHandler.cs │ │ │ ├── BuySubscriptionRenewal/ │ │ │ │ ├── BuySubscriptionRenewalCommand.cs │ │ │ │ └── BuySubscriptionRenewalCommandHandler.cs │ │ │ ├── CreateSubscription/ │ │ │ │ ├── CreateSubscriptionCommand.cs │ │ │ │ ├── CreateSubscriptionCommandHandler.cs │ │ │ │ ├── SubscriptionCreatedEnqueueEmailConfirmationHandler.cs │ │ │ │ ├── SubscriptionCreatedNotification.cs │ │ │ │ └── SubscriptionCreatedNotificationHandler.cs │ │ │ ├── ExpireSubscription/ │ │ │ │ ├── ExpireSubscriptionCommand.cs │ │ │ │ └── ExpireSubscriptionCommandHandler.cs │ │ │ ├── ExpireSubscriptionPayment/ │ │ │ │ ├── ExpireSubscriptionPaymentCommand.cs │ │ │ │ └── ExpireSubscriptionPaymentCommandHandler.cs │ │ │ ├── ExpireSubscriptionPayments/ │ │ │ │ ├── ExpireSubscriptionPaymentsCommand.cs │ │ │ │ └── ExpireSubscriptionPaymentsCommandHandler.cs │ │ │ ├── ExpireSubscriptions/ │ │ │ │ ├── ExpireSubscriptionsCommand.cs │ │ │ │ └── ExpireSubscriptionsCommandHandler.cs │ │ │ ├── GetPayerSubscription/ │ │ │ │ ├── GetAuthenticatedPayerSubscriptionQuery.cs │ │ │ │ └── GetAuthenticatedPayerSubscriptionQueryHandler.cs │ │ │ ├── GetSubscriptionDetails/ │ │ │ │ ├── GetSubscriptionDetailsQuery.cs │ │ │ │ ├── GetSubscriptionDetailsQueryHandler.cs │ │ │ │ ├── SubscriptionDetailsDto.cs │ │ │ │ └── SubscriptionDetailsProjector.cs │ │ │ ├── GetSubscriptionPayments/ │ │ │ │ ├── GetSubscriptionPaymentsQuery.cs │ │ │ │ ├── GetSubscriptionPaymentsQueryHandler.cs │ │ │ │ ├── SubscriptionPaymentDto.cs │ │ │ │ └── SubscriptionPaymentsProjector.cs │ │ │ ├── MarkSubscriptionPaymentAsPaid/ │ │ │ │ ├── MarkSubscriptionPaymentAsPaidCommand.cs │ │ │ │ ├── MarkSubscriptionPaymentAsPaidCommandHandler.cs │ │ │ │ ├── SubscriptionPaymentPaidNotification.cs │ │ │ │ └── SubscriptionPaymentPaidNotificationHandler.cs │ │ │ ├── MarkSubscriptionRenewalPaymentAsPaid/ │ │ │ │ ├── MarkSubscriptionRenewalPaymentAsPaidCommand.cs │ │ │ │ ├── MarkSubscriptionRenewalPaymentAsPaidCommandHandler.cs │ │ │ │ ├── SubscriptionRenewalPaymentAsPaidNotificationHandler.cs │ │ │ │ └── SubscriptionRenewalPaymentPaidNotification.cs │ │ │ ├── RenewSubscription/ │ │ │ │ ├── RenewSubscriptionCommand.cs │ │ │ │ ├── RenewSubscriptionCommandHandler.cs │ │ │ │ ├── SubscriptionRenewedEnqueueEmailConfirmationHandler.cs │ │ │ │ ├── SubscriptionRenewedNotification.cs │ │ │ │ └── SubscriptionRenewedNotificationHandler.cs │ │ │ ├── SendSubscriptionCreationConfirmationEmail/ │ │ │ │ ├── SendSubscriptionCreationConfirmationEmailCommand.cs │ │ │ │ └── SendSubscriptionCreationConfirmationEmailCommandHandler.cs │ │ │ └── SendSubscriptionRenewalConfirmationEmail/ │ │ │ ├── SendSubscriptionRenewalConfirmationEmailCommand.cs │ │ │ └── SendSubscriptionRenewalConfirmationEmailCommandHandler.cs │ │ ├── Domain/ │ │ │ ├── CompanyName.MyMeetings.Modules.Payments.Domain.csproj │ │ │ ├── MeetingFeePayments/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── MeetingFeePaymentCreatedDomainEvent.cs │ │ │ │ │ ├── MeetingFeePaymentExpiredDomainEvent.cs │ │ │ │ │ └── MeetingFeePaymentPaidDomainEvent.cs │ │ │ │ ├── MeetingFeePayment.cs │ │ │ │ ├── MeetingFeePaymentId.cs │ │ │ │ ├── MeetingFeePaymentSnapshot.cs │ │ │ │ └── MeetingFeePaymentStatus.cs │ │ │ ├── MeetingFees/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── MeetingFeeCanceledDomainEvent.cs │ │ │ │ │ ├── MeetingFeeCreatedDomainEvent.cs │ │ │ │ │ ├── MeetingFeeExpiredDomainEvent.cs │ │ │ │ │ └── MeetingFeePaidDomainEvent.cs │ │ │ │ ├── MeetingFee.cs │ │ │ │ ├── MeetingFeeId.cs │ │ │ │ ├── MeetingFeeSnapshot.cs │ │ │ │ ├── MeetingFeeStatus.cs │ │ │ │ └── MeetingId.cs │ │ │ ├── Payers/ │ │ │ │ ├── Events/ │ │ │ │ │ └── PayerCreatedDomainEvent.cs │ │ │ │ ├── IPayerContext.cs │ │ │ │ ├── IPayerRepository.cs │ │ │ │ ├── Payer.cs │ │ │ │ └── PayerId.cs │ │ │ ├── PriceListItems/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── PriceListItemActivatedDomainEvent.cs │ │ │ │ │ ├── PriceListItemAttributesChangedDomainEvent.cs │ │ │ │ │ ├── PriceListItemCreatedDomainEvent.cs │ │ │ │ │ └── PriceListItemDeactivatedDomainEvent.cs │ │ │ │ ├── PriceList.cs │ │ │ │ ├── PriceListItem.cs │ │ │ │ ├── PriceListItemCategory.cs │ │ │ │ ├── PriceListItemData.cs │ │ │ │ ├── PriceListItemId.cs │ │ │ │ └── PricingStrategies/ │ │ │ │ ├── DirectValueFromPriceListPricingStrategy.cs │ │ │ │ ├── DirectValuePricingStrategy.cs │ │ │ │ ├── DiscountedValueFromPriceListPricingStrategy.cs │ │ │ │ └── IPricingStrategy.cs │ │ │ ├── SeedWork/ │ │ │ │ ├── AggregateId.cs │ │ │ │ ├── AggregateRoot.cs │ │ │ │ ├── IAggregateStore.cs │ │ │ │ ├── MoneyValue.cs │ │ │ │ ├── Rules/ │ │ │ │ │ ├── MoneyMustHaveTheSameCurrencyRule.cs │ │ │ │ │ └── ValueOfMoneyMustNotBeNegativeRule.cs │ │ │ │ └── SystemClock.cs │ │ │ ├── SubscriptionPayments/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── SubscriptionPaymentCreatedDomainEvent.cs │ │ │ │ │ ├── SubscriptionPaymentExpiredDomainEvent.cs │ │ │ │ │ └── SubscriptionPaymentPaidDomainEvent.cs │ │ │ │ ├── Rules/ │ │ │ │ │ ├── PriceForSubscriptionMustBeDefinedRule.cs │ │ │ │ │ └── PriceOfferMustMatchPriceInPriceListRule.cs │ │ │ │ ├── SubscriptionPayment.cs │ │ │ │ ├── SubscriptionPaymentId.cs │ │ │ │ ├── SubscriptionPaymentSnapshot.cs │ │ │ │ └── SubscriptionPaymentStatus.cs │ │ │ ├── SubscriptionRenewalPayments/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── SubscriptionRenewalPaymentCreatedDomainEvent.cs │ │ │ │ │ └── SubscriptionRenewalPaymentPaidDomainEvent.cs │ │ │ │ ├── Rules/ │ │ │ │ │ └── PriceOfferMustMatchPriceInPriceListRule.cs │ │ │ │ ├── SubscriptionRenewalPayment.cs │ │ │ │ ├── SubscriptionRenewalPaymentId.cs │ │ │ │ ├── SubscriptionRenewalPaymentSnapshot.cs │ │ │ │ └── SubscriptionRenewalPaymentStatus.cs │ │ │ ├── Subscriptions/ │ │ │ │ ├── Events/ │ │ │ │ │ ├── SubscriptionCreatedDomainEvent.cs │ │ │ │ │ ├── SubscriptionExpiredDomainEvent.cs │ │ │ │ │ └── SubscriptionRenewedDomainEvent.cs │ │ │ │ ├── SubscriberId.cs │ │ │ │ ├── Subscription.cs │ │ │ │ ├── SubscriptionDateExpirationCalculator.cs │ │ │ │ ├── SubscriptionId.cs │ │ │ │ ├── SubscriptionPeriod.cs │ │ │ │ └── SubscriptionStatus.cs │ │ │ └── Users/ │ │ │ ├── IUserContext.cs │ │ │ └── UserId.cs │ │ ├── Infrastructure/ │ │ │ ├── AggregateStore/ │ │ │ │ ├── AggregateStoreDomainEventsAccessor.cs │ │ │ │ ├── DomainEventTypeMappings.cs │ │ │ │ ├── ICheckpointStore.cs │ │ │ │ ├── SqlOutboxAccessor.cs │ │ │ │ ├── SqlServerCheckpointStore.cs │ │ │ │ ├── SqlStreamAggregateStore.cs │ │ │ │ ├── SubscriptionCode.cs │ │ │ │ └── SubscriptionsManager.cs │ │ │ ├── CompanyName.MyMeetings.Modules.Payments.Infrastructure.csproj │ │ │ ├── Configuration/ │ │ │ │ ├── AllConstructorFinder.cs │ │ │ │ ├── Assemblies.cs │ │ │ │ ├── Authentication/ │ │ │ │ │ ├── AuthenticationModule.cs │ │ │ │ │ └── PayerContext.cs │ │ │ │ ├── DataAccess/ │ │ │ │ │ └── DataAccessModule.cs │ │ │ │ ├── DatabaseSchema.cs │ │ │ │ ├── Email/ │ │ │ │ │ └── EmailModule.cs │ │ │ │ ├── EventsBus/ │ │ │ │ │ ├── EventsBusModule.cs │ │ │ │ │ ├── EventsBusStartup.cs │ │ │ │ │ └── IntegrationEventGenericHandler.cs │ │ │ │ ├── Logging/ │ │ │ │ │ └── LoggingModule.cs │ │ │ │ ├── Mediation/ │ │ │ │ │ └── MediatorModule.cs │ │ │ │ ├── PaymentsCompositionRoot.cs │ │ │ │ ├── PaymentsStartup.cs │ │ │ │ ├── Processing/ │ │ │ │ │ ├── CommandsExecutor.cs │ │ │ │ │ ├── Inbox/ │ │ │ │ │ │ ├── InboxMessageDto.cs │ │ │ │ │ │ ├── ProcessInboxCommand.cs │ │ │ │ │ │ ├── ProcessInboxCommandHandler.cs │ │ │ │ │ │ └── ProcessInboxJob.cs │ │ │ │ │ ├── InternalCommands/ │ │ │ │ │ │ ├── CommandsScheduler.cs │ │ │ │ │ │ ├── ProcessInternalCommandsCommand.cs │ │ │ │ │ │ ├── ProcessInternalCommandsCommandHandler.cs │ │ │ │ │ │ └── ProcessInternalCommandsJob.cs │ │ │ │ │ ├── LoggingCommandHandlerDecorator.cs │ │ │ │ │ ├── LoggingCommandHandlerWithResultDecorator.cs │ │ │ │ │ ├── Outbox/ │ │ │ │ │ │ ├── OutboxMessageDto.cs │ │ │ │ │ │ ├── OutboxModule.cs │ │ │ │ │ │ ├── ProcessOutboxCommand.cs │ │ │ │ │ │ ├── ProcessOutboxCommandHandler.cs │ │ │ │ │ │ └── ProcessOutboxJob.cs │ │ │ │ │ ├── PaymentsUnitOfWork.cs │ │ │ │ │ ├── ProcessingModule.cs │ │ │ │ │ ├── UnitOfWorkCommandHandlerDecorator.cs │ │ │ │ │ ├── UnitOfWorkCommandHandlerWithResultDecorator.cs │ │ │ │ │ ├── ValidationCommandHandlerDecorator.cs │ │ │ │ │ └── ValidationCommandHandlerWithResultDecorator.cs │ │ │ │ └── Quartz/ │ │ │ │ ├── Jobs/ │ │ │ │ │ ├── ExpireSubscriptionPaymentsJob.cs │ │ │ │ │ └── ExpireSubscriptionsJob.cs │ │ │ │ ├── QuartzModule.cs │ │ │ │ ├── QuartzStartup.cs │ │ │ │ └── SerilogLogProvider.cs │ │ │ ├── InternalCommands/ │ │ │ │ └── InternalCommandEntityTypeConfiguration.cs │ │ │ └── PaymentsModule.cs │ │ ├── IntegrationEvents/ │ │ │ ├── CompanyName.MyMeetings.Modules.Payments.IntegrationEvents.csproj │ │ │ ├── MeetingFeePaidIntegrationEvent.cs │ │ │ └── SubscriptionExpirationDateChangedIntegrationEvent.cs │ │ └── Tests/ │ │ ├── ArchTests/ │ │ │ ├── Application/ │ │ │ │ └── ApplicationTests.cs │ │ │ ├── CompanyName.MyMeetings.Modules.Payments.ArchTests.csproj │ │ │ ├── Domain/ │ │ │ │ └── DomainTests.cs │ │ │ ├── Module/ │ │ │ │ └── LayersTests.cs │ │ │ └── SeedWork/ │ │ │ └── TestBase.cs │ │ ├── IntegrationTests/ │ │ │ ├── AssemblyInfo.cs │ │ │ ├── CompanyName.MyMeetings.Modules.Payments.IntegrationTests.csproj │ │ │ ├── MeetingFees/ │ │ │ │ └── MeetingFeesTests.cs │ │ │ ├── Payers/ │ │ │ │ ├── PayerSampleData.cs │ │ │ │ └── PayerTests.cs │ │ │ ├── PriceList/ │ │ │ │ └── PriceListHelper.cs │ │ │ ├── SeedWork/ │ │ │ │ ├── EventsBusMock.cs │ │ │ │ ├── ExecutionContextMock.cs │ │ │ │ ├── OutboxMessagesHelper.cs │ │ │ │ └── TestBase.cs │ │ │ └── Subscriptions/ │ │ │ ├── BuySubscriptionTests.cs │ │ │ ├── GetSubscriptionPaymentsProbe.cs │ │ │ ├── SubscriptionLifecycleTests.cs │ │ │ └── SubscriptionPaymentsTests.cs │ │ └── UnitTests/ │ │ ├── CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.csproj │ │ ├── Payers/ │ │ │ └── PayerTests.cs │ │ ├── PriceListItems/ │ │ │ └── PriceListItemTests.cs │ │ ├── SeedWork/ │ │ │ ├── DomainEventsTestHelper.cs │ │ │ └── TestBase.cs │ │ ├── SubscriptionPayments/ │ │ │ ├── SubscriptionPaymentTests.cs │ │ │ └── SubscriptionPaymentTestsBase.cs │ │ ├── SubscriptionRenewalPayments/ │ │ │ ├── SubscriptionRenewalPaymentTests.cs │ │ │ └── SubscriptionRenewalPaymentTestsBase.cs │ │ └── Subscriptions/ │ │ ├── SubscriptionDateExpirationCalculatorTests.cs │ │ └── SubscriptionTests.cs │ ├── Registrations/ │ │ ├── Application/ │ │ │ ├── CompanyName.MyMeetings.Modules.Registrations.Application.csproj │ │ │ ├── Configuration/ │ │ │ │ ├── Commands/ │ │ │ │ │ ├── ICommandHandler.cs │ │ │ │ │ ├── ICommandsScheduler.cs │ │ │ │ │ └── InternalCommandBase.cs │ │ │ │ └── Queries/ │ │ │ │ └── IQueryHandler.cs │ │ │ ├── Contracts/ │ │ │ │ ├── CommandBase.cs │ │ │ │ ├── CustomClaimTypes.cs │ │ │ │ ├── ICommand.cs │ │ │ │ ├── IQuery.cs │ │ │ │ ├── IRecurringCommand.cs │ │ │ │ ├── IRegistrationsModule.cs │ │ │ │ ├── QueryBase.cs │ │ │ │ └── Roles.cs │ │ │ └── UserRegistrations/ │ │ │ ├── ConfirmUserRegistration/ │ │ │ │ ├── ConfirmUserRegistrationCommand.cs │ │ │ │ ├── ConfirmUserRegistrationCommandHandler.cs │ │ │ │ ├── IUserCreator.cs │ │ │ │ ├── UserRegistrationConfirmedNotification.cs │ │ │ │ └── UserRegistrationConfirmedNotificationHandler.cs │ │ │ ├── GetUserRegistration/ │ │ │ │ ├── GetUserRegistrationQuery.cs │ │ │ │ ├── GetUserRegistrationQueryHandler.cs │ │ │ │ ├── UserRegistrationDto.cs │ │ │ │ └── UserRegistrationProvider.cs │ │ │ ├── RegisterNewUser/ │ │ │ │ ├── NewUserRegisteredEnqueueEmailConfirmationHandler.cs │ │ │ │ ├── NewUserRegisteredNotification.cs │ │ │ │ ├── NewUserRegisteredPublishEventHandler.cs │ │ │ │ ├── PasswordManager.cs │ │ │ │ ├── RegisterNewUserCommand.cs │ │ │ │ └── RegisterNewUserCommandHandler.cs │ │ │ ├── SendUserRegistrationConfirmationEmail/ │ │ │ │ ├── SendUserRegistrationConfirmationEmailCommand.cs │ │ │ │ └── SendUserRegistrationConfirmationEmailCommandHandler.cs │ │ │ └── UsersCounter.cs │ │ ├── Domain/ │ │ │ ├── CompanyName.MyMeetings.Modules.Registrations.Domain.csproj │ │ │ └── UserRegistrations/ │ │ │ ├── Events/ │ │ │ │ ├── NewUserRegisteredDomainEvent.cs │ │ │ │ ├── UserRegistrationConfirmedDomainEvent.cs │ │ │ │ └── UserRegistrationExpiredDomainEvent.cs │ │ │ ├── IUserRegistrationRepository.cs │ │ │ ├── IUsersCounter.cs │ │ │ ├── Rules/ │ │ │ │ ├── UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule.cs │ │ │ │ ├── UserLoginMustBeUniqueRule.cs │ │ │ │ ├── UserRegistrationCannotBeConfirmedAfterExpirationRule.cs │ │ │ │ ├── UserRegistrationCannotBeConfirmedMoreThanOnceRule.cs │ │ │ │ └── UserRegistrationCannotBeExpiredMoreThanOnceRule.cs │ │ │ ├── UserRegistration.cs │ │ │ ├── UserRegistrationId.cs │ │ │ └── UserRegistrationStatus.cs │ │ ├── Infrastructure/ │ │ │ ├── CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj │ │ │ ├── Configuration/ │ │ │ │ ├── AllConstructorFinder.cs │ │ │ │ ├── Assemblies.cs │ │ │ │ ├── Commands/ │ │ │ │ │ ├── ICommandHandler.cs │ │ │ │ │ ├── ICommandsScheduler.cs │ │ │ │ │ └── InternalCommandBase.cs │ │ │ │ ├── DataAccess/ │ │ │ │ │ └── DataAccessModule.cs │ │ │ │ ├── Domain/ │ │ │ │ │ └── DomainModule.cs │ │ │ │ ├── Email/ │ │ │ │ │ └── EmailModule.cs │ │ │ │ ├── EventsBus/ │ │ │ │ │ ├── EventsBusModule.cs │ │ │ │ │ ├── EventsBusStartup.cs │ │ │ │ │ └── IntegrationEventGenericHandler.cs │ │ │ │ ├── Logging/ │ │ │ │ │ └── LoggingModule.cs │ │ │ │ ├── Mediation/ │ │ │ │ │ └── MediatorModule.cs │ │ │ │ ├── Processing/ │ │ │ │ │ ├── CommandsExecutor.cs │ │ │ │ │ ├── IRecurringCommand.cs │ │ │ │ │ ├── Inbox/ │ │ │ │ │ │ ├── InboxMessageDto.cs │ │ │ │ │ │ ├── ProcessInboxCommand.cs │ │ │ │ │ │ ├── ProcessInboxCommandHandler.cs │ │ │ │ │ │ └── ProcessInboxJob.cs │ │ │ │ │ ├── InternalCommands/ │ │ │ │ │ │ ├── CommandsScheduler.cs │ │ │ │ │ │ ├── ProcessInternalCommandsCommand.cs │ │ │ │ │ │ ├── ProcessInternalCommandsCommandHandler.cs │ │ │ │ │ │ └── ProcessInternalCommandsJob.cs │ │ │ │ │ ├── LoggingCommandHandlerDecorator.cs │ │ │ │ │ ├── LoggingCommandHandlerWithResultDecorator.cs │ │ │ │ │ ├── Outbox/ │ │ │ │ │ │ ├── OutboxMessageDto.cs │ │ │ │ │ │ ├── OutboxModule.cs │ │ │ │ │ │ ├── ProcessOutboxCommand.cs │ │ │ │ │ │ ├── ProcessOutboxCommandHandler.cs │ │ │ │ │ │ └── ProcessOutboxJob.cs │ │ │ │ │ ├── ProcessingModule.cs │ │ │ │ │ ├── UnitOfWorkCommandHandlerDecorator.cs │ │ │ │ │ ├── UnitOfWorkCommandHandlerWithResultDecorator.cs │ │ │ │ │ ├── ValidationCommandHandlerDecorator.cs │ │ │ │ │ └── ValidationCommandHandlerWithResultDecorator.cs │ │ │ │ ├── Quartz/ │ │ │ │ │ ├── QuartzModule.cs │ │ │ │ │ ├── QuartzStartup.cs │ │ │ │ │ └── SerilogLogProvider.cs │ │ │ │ ├── RegistrationsCompositionRoot.cs │ │ │ │ ├── RegistrationsStartup.cs │ │ │ │ └── UserAccess/ │ │ │ │ └── UserAccessAutofacModule.cs │ │ │ ├── Domain/ │ │ │ │ └── UserRegistrations/ │ │ │ │ ├── UserRegistrationEntityTypeConfiguration.cs │ │ │ │ └── UserRegistrationRepository.cs │ │ │ ├── InternalCommands/ │ │ │ │ └── InternalCommandEntityTypeConfiguration.cs │ │ │ ├── Outbox/ │ │ │ │ ├── OutboxAccessor.cs │ │ │ │ └── OutboxMessageEntityTypeConfiguration.cs │ │ │ ├── RegistrationsContext.cs │ │ │ ├── RegistrationsModule.cs │ │ │ └── Users/ │ │ │ └── UserAccessGateway.cs │ │ ├── IntegrationEvents/ │ │ │ ├── Class1.cs │ │ │ ├── CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents.csproj │ │ │ └── NewUserRegisteredIntegrationEvent.cs │ │ └── Tests/ │ │ ├── ArchTests/ │ │ │ ├── Application/ │ │ │ │ └── ApplicationTests.cs │ │ │ ├── CompanyName.MyMeetings.Modules.Registrations.ArchTests.csproj │ │ │ ├── Domain/ │ │ │ │ └── DomainTests.cs │ │ │ ├── Module/ │ │ │ │ └── LayersTests.cs │ │ │ └── SeedWork/ │ │ │ └── TestBase.cs │ │ ├── IntegrationTests/ │ │ │ ├── AssemblyInfo.cs │ │ │ ├── CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.csproj │ │ │ ├── SeedWork/ │ │ │ │ ├── ExecutionContextMock.cs │ │ │ │ ├── OutboxMessagesHelper.cs │ │ │ │ └── TestBase.cs │ │ │ └── UserRegistrations/ │ │ │ ├── ConfirmUserRegistrationTests.cs │ │ │ ├── SendUserRegistrationConfirmationEmailTests.cs │ │ │ ├── UserRegistrationSampleData.cs │ │ │ └── UserRegistrationTests.cs │ │ └── UnitTests/ │ │ ├── CompanyName.MyMeetings.Modules.Registrations.Domain.UnitTests.csproj │ │ ├── SeedWork/ │ │ │ ├── DomainEventsTestHelper.cs │ │ │ └── TestBase.cs │ │ └── UserRegistrations/ │ │ └── UserRegistrationTests.cs │ └── UserAccess/ │ ├── Application/ │ │ ├── Authentication/ │ │ │ └── Authenticate/ │ │ │ ├── AuthenticateCommand.cs │ │ │ ├── AuthenticateCommandHandler.cs │ │ │ ├── AuthenticateCommandValidator.cs │ │ │ ├── AuthenticationResult.cs │ │ │ ├── PasswordManager.cs │ │ │ └── UserDto.cs │ │ ├── Authorization/ │ │ │ ├── GetAuthenticatedUserPermissions/ │ │ │ │ ├── GetAuthenticatedUserPermissionsQuery.cs │ │ │ │ └── GetAuthenticatedUserPermissionsQueryHandler.cs │ │ │ └── GetUserPermissions/ │ │ │ ├── GetUserPermissionsQuery.cs │ │ │ ├── GetUserPermissionsQueryHandler.cs │ │ │ └── UserPermissionDto.cs │ │ ├── CompanyName.MyMeetings.Modules.UserAccess.Application.csproj │ │ ├── Configuration/ │ │ │ ├── Commands/ │ │ │ │ ├── ICommandHandler.cs │ │ │ │ ├── ICommandsScheduler.cs │ │ │ │ └── InternalCommandBase.cs │ │ │ └── Queries/ │ │ │ └── IQueryHandler.cs │ │ ├── Contracts/ │ │ │ ├── CommandBase.cs │ │ │ ├── CustomClaimTypes.cs │ │ │ ├── ICommand.cs │ │ │ ├── IQuery.cs │ │ │ ├── IRecurringCommand.cs │ │ │ ├── IUserAccessModule.cs │ │ │ ├── QueryBase.cs │ │ │ └── Roles.cs │ │ ├── Emails/ │ │ │ ├── EmailDto.cs │ │ │ ├── GetAllEmailsQuery.cs │ │ │ └── GetAllEmailsQueryHandler.cs │ │ └── Users/ │ │ ├── AddAdminUser/ │ │ │ ├── AddAdminUserCommand.cs │ │ │ └── AddAdminUserCommandHandler.cs │ │ ├── CreateUser/ │ │ │ ├── CreateUserCommand.cs │ │ │ └── CreateUserCommandHandler.cs │ │ ├── GetAuthenticatedUser/ │ │ │ ├── GetAuthenticatedUserQuery.cs │ │ │ └── GetAuthenticatedUserQueryHandler.cs │ │ └── GetUser/ │ │ ├── GetUserQuery.cs │ │ ├── GetUserQueryHandler.cs │ │ └── UserDto.cs │ ├── Domain/ │ │ ├── CompanyName.MyMeetings.Modules.UserAccess.Domain.csproj │ │ └── Users/ │ │ ├── Events/ │ │ │ └── UserCreatedDomainEvent.cs │ │ ├── IUserRepository.cs │ │ ├── User.cs │ │ ├── UserId.cs │ │ └── UserRole.cs │ ├── Infrastructure/ │ │ ├── CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.csproj │ │ ├── Configuration/ │ │ │ ├── AllConstructorFinder.cs │ │ │ ├── Assemblies.cs │ │ │ ├── DataAccess/ │ │ │ │ └── DataAccessModule.cs │ │ │ ├── Email/ │ │ │ │ └── EmailModule.cs │ │ │ ├── EventsBus/ │ │ │ │ ├── EventsBusModule.cs │ │ │ │ ├── EventsBusStartup.cs │ │ │ │ └── IntegrationEventGenericHandler.cs │ │ │ ├── Identity/ │ │ │ │ └── IdentityConfiguration.cs │ │ │ ├── Logging/ │ │ │ │ └── LoggingModule.cs │ │ │ ├── Mediation/ │ │ │ │ └── MediatorModule.cs │ │ │ ├── Processing/ │ │ │ │ ├── CommandsExecutor.cs │ │ │ │ ├── IRecurringCommand.cs │ │ │ │ ├── Inbox/ │ │ │ │ │ ├── InboxMessageDto.cs │ │ │ │ │ ├── ProcessInboxCommand.cs │ │ │ │ │ ├── ProcessInboxCommandHandler.cs │ │ │ │ │ └── ProcessInboxJob.cs │ │ │ │ ├── InternalCommands/ │ │ │ │ │ ├── CommandsScheduler.cs │ │ │ │ │ ├── ProcessInternalCommandsCommand.cs │ │ │ │ │ ├── ProcessInternalCommandsCommandHandler.cs │ │ │ │ │ └── ProcessInternalCommandsJob.cs │ │ │ │ ├── LoggingCommandHandlerDecorator.cs │ │ │ │ ├── LoggingCommandHandlerWithResultDecorator.cs │ │ │ │ ├── Outbox/ │ │ │ │ │ ├── OutboxMessageDto.cs │ │ │ │ │ ├── OutboxModule.cs │ │ │ │ │ ├── ProcessOutboxCommand.cs │ │ │ │ │ ├── ProcessOutboxCommandHandler.cs │ │ │ │ │ └── ProcessOutboxJob.cs │ │ │ │ ├── ProcessingModule.cs │ │ │ │ ├── UnitOfWorkCommandHandlerDecorator.cs │ │ │ │ ├── UnitOfWorkCommandHandlerWithResultDecorator.cs │ │ │ │ ├── ValidationCommandHandlerDecorator.cs │ │ │ │ └── ValidationCommandHandlerWithResultDecorator.cs │ │ │ ├── Quartz/ │ │ │ │ ├── QuartzModule.cs │ │ │ │ ├── QuartzStartup.cs │ │ │ │ └── SerilogLogProvider.cs │ │ │ ├── Security/ │ │ │ │ ├── AesDataProtector.cs │ │ │ │ ├── IDataProtector.cs │ │ │ │ └── SecurityModule.cs │ │ │ ├── UserAccessCompositionRoot.cs │ │ │ └── UserAccessStartup.cs │ │ ├── Domain/ │ │ │ └── Users/ │ │ │ ├── UserEntityTypeConfiguration.cs │ │ │ └── UserRepository.cs │ │ ├── IdentityServer/ │ │ │ ├── IdentityServerConfig.cs │ │ │ ├── ProfileService.cs │ │ │ └── ResourceOwnerPasswordValidator.cs │ │ ├── InternalCommands/ │ │ │ └── InternalCommandEntityTypeConfiguration.cs │ │ ├── Outbox/ │ │ │ ├── OutboxAccessor.cs │ │ │ └── OutboxMessageEntityTypeConfiguration.cs │ │ ├── UserAccessContext.cs │ │ └── UserAccessModule.cs │ ├── IntegrationEvents/ │ │ └── CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents.csproj │ └── Tests/ │ ├── ArchTests/ │ │ ├── Application/ │ │ │ └── ApplicationTests.cs │ │ ├── CompanyName.MyMeetings.Modules.UserAccess.ArchTests.csproj │ │ ├── Domain/ │ │ │ └── DomainTests.cs │ │ ├── Module/ │ │ │ └── LayersTests.cs │ │ └── SeedWork/ │ │ └── TestBase.cs │ ├── IntegrationTests/ │ │ ├── AssemblyInfo.cs │ │ ├── CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.csproj │ │ ├── SeedWork/ │ │ │ ├── ExecutionContextMock.cs │ │ │ ├── OutboxMessagesHelper.cs │ │ │ └── TestBase.cs │ │ └── Users/ │ │ └── CreateUserTests.cs │ └── UnitTests/ │ ├── CompanyName.MyMeetings.Modules.UserAccess.Domain.UnitTests.csproj │ └── SeedWork/ │ ├── DomainEventsTestHelper.cs │ └── TestBase.cs ├── Tests/ │ ├── ArchTests/ │ │ ├── Api/ │ │ │ └── ApiTests.cs │ │ ├── CompanyName.MyMeetings.ArchTests.csproj │ │ ├── Modules/ │ │ │ └── ModuleTests.cs │ │ └── SeedWork/ │ │ └── TestBase.cs │ ├── IntegrationTests/ │ │ ├── AssemblyInfo.cs │ │ ├── CompanyName.MyMeetings.IntegrationTests.csproj │ │ ├── CreateMeetingGroup/ │ │ │ └── CreateMeetingGroupTests.cs │ │ └── SeedWork/ │ │ ├── ExecutionContextMock.cs │ │ └── TestBase.cs │ └── SUT/ │ ├── CompanyName.MyMeetings.SUT.csproj │ ├── Helpers/ │ │ ├── MeetingGroupsFactory.cs │ │ ├── TestMeetingFactory.cs │ │ ├── TestMeetingGroupManager.cs │ │ ├── TestMeetingManager.cs │ │ ├── TestPaymentsManager.cs │ │ ├── TestPriceListManager.cs │ │ └── UsersFactory.cs │ ├── Scripts/ │ │ └── SeedPermissions.sql │ ├── SeedWork/ │ │ ├── AsyncOperationsHelper.cs │ │ ├── DatabaseCleaner.cs │ │ ├── ExecutionContextMock.cs │ │ ├── Probing/ │ │ │ ├── AssertErrorException.cs │ │ │ ├── IProbe.cs │ │ │ ├── Poller.cs │ │ │ └── Timeout.cs │ │ └── TestBase.cs │ └── TestCases/ │ ├── CleanDatabaseTestCase.cs │ ├── CreateMeeting.cs │ └── OnlyAdminTestCase.cs ├── entrypoint.sh ├── global.json └── stylecop.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/buildPipeline.yml ================================================ name: CI Pipeline on: push: branches: [master] pull_request: branches: [master] jobs: build: name: Build and run Unit and Architecture Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Run build run: ./build.sh BuildAndUnitTests --configuration Release integration: name: Build and run Integration Tests needs: [build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Run build run: ./build.sh RunAllIntegrationTests ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates .vscode/ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot **/wwwroot/lib/ !/wwwroot/lib/signalr !/wwwroot/lib/toastr # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.publishsettings node_modules/ orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml pub/ /src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 /src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 /src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json #Ignore marker-file used to know which docker files we have. .eshopdocker_* /src/Web/WebMVC/wwwroot/lib /src/Web/WebMVC/wwwroot/css/site.min.css **/.kube/** .mfractor #Ignore logs folder [Ll]ogs #Ignore uploaded files folder UploadedFiles /src/CompanyName.MyMeetings.v3.ncrunchsolution #Nuke working directory .nuke-working-directory /src/API/CompanyName.MyMeetings.API/tempkey.jwk ================================================ FILE: .nuke/build.schema.json ================================================ { "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/build", "title": "Build Schema", "definitions": { "build": { "type": "object", "properties": { "Configuration": { "type": "string", "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", "enum": [ "Debug", "Release" ] }, "Continue": { "type": "boolean", "description": "Indicates to continue a previously failed build attempt" }, "DatabaseConnectionString": { "type": "string", "description": "Modular Monolith database connection string" }, "Help": { "type": "boolean", "description": "Shows the help text for this build assembly" }, "Host": { "type": "string", "description": "Host for execution. Default is 'automatic'", "enum": [ "AppVeyor", "AzurePipelines", "Bamboo", "Bitbucket", "Bitrise", "GitHubActions", "GitLab", "Jenkins", "Rider", "SpaceAutomation", "TeamCity", "Terminal", "TravisCI", "VisualStudio", "VSCode" ] }, "NoLogo": { "type": "boolean", "description": "Disables displaying the NUKE logo" }, "Partition": { "type": "string", "description": "Partition to use on CI" }, "Plan": { "type": "boolean", "description": "Shows the execution plan (HTML)" }, "Profile": { "type": "array", "description": "Defines the profiles to load", "items": { "type": "string" } }, "Root": { "type": "string", "description": "Root directory during build execution" }, "Skip": { "type": "array", "description": "List of targets to be skipped. Empty list skips all dependencies", "items": { "type": "string", "enum": [ "ArchitectureTests", "BuildAdministrationModuleIntegrationTests", "BuildAndUnitTests", "BuildMeetingsModuleIntegrationTests", "BuildPaymentsModuleIntegrationTests", "BuildSystemIntegrationTests", "BuildUserAccessModuleIntegrationTests", "Clean", "Compile", "CompileDbUpMigrator", "CompileDbUpMigratorForIntegrationTests", "CreateDatabase", "MigrateDatabase", "PrepareInputFiles", "PrepareSqlServer", "PrepareSUT", "Restore", "RunAdministrationModuleIntegrationTests", "RunAllIntegrationTests", "RunDatabaseMigrations", "RunMeetingsModuleIntegrationTests", "RunPaymentsModuleIntegrationTests", "RunSystemIntegrationTests", "RunUserAccessModuleIntegrationTests", "UnitTests" ] } }, "Solution": { "type": "string", "description": "Path to a solution file that is automatically loaded" }, "SUTTestName": { "type": "string", "description": "SUT creator test name to execute" }, "Target": { "type": "array", "description": "List of targets to be invoked. Default is '{default_target}'", "items": { "type": "string", "enum": [ "ArchitectureTests", "BuildAdministrationModuleIntegrationTests", "BuildAndUnitTests", "BuildMeetingsModuleIntegrationTests", "BuildPaymentsModuleIntegrationTests", "BuildSystemIntegrationTests", "BuildUserAccessModuleIntegrationTests", "Clean", "Compile", "CompileDbUpMigrator", "CompileDbUpMigratorForIntegrationTests", "CreateDatabase", "MigrateDatabase", "PrepareInputFiles", "PrepareSqlServer", "PrepareSUT", "Restore", "RunAdministrationModuleIntegrationTests", "RunAllIntegrationTests", "RunDatabaseMigrations", "RunMeetingsModuleIntegrationTests", "RunPaymentsModuleIntegrationTests", "RunSystemIntegrationTests", "RunUserAccessModuleIntegrationTests", "UnitTests" ] } }, "Verbosity": { "type": "string", "description": "Logging verbosity during build execution. Default is 'Normal'", "enum": [ "Minimal", "Normal", "Quiet", "Verbose" ] } } } } } ================================================ FILE: .nuke/parameters.json ================================================ { "$schema": "./build.schema.json", "Solution": "src/CompanyName.MyMeetings.sln" } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Kamil Grzybek Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Modular Monolith with DDD Full Modular Monolith .NET application with Domain-Driven Design approach. ## Announcement ![](docs/Images/glory_to_ukraine.jpg) Learn, use and benefit from this project only if: - You **condemn Russia and its military aggression against Ukraine** - You **recognize that Russia is an occupant that unlawfully invaded a sovereign state** - You **support Ukraine's territorial integrity, including its claims over temporarily occupied territories of Crimea and Donbas** - You **reject false narratives perpetuated by Russian state propaganda** Otherwise, leave this project immediately and educate yourself. Putin, idi nachuj. ## CI ![](https://github.com/kgrzybek/modular-monolith-with-ddd/workflows/Build%20Pipeline/badge.svg) ## FrontEnd application FrontEnd application : [Modular Monolith With DDD: FrontEnd React application](https://github.com/kgrzybek/modular-monolith-with-ddd-fe-react) ## Table of contents [1. Introduction](#1-introduction)   [1.1 Purpose of this Repository](#11-purpose-of-this-repository)   [1.2 Out of Scope](#12-out-of-scope)   [1.3 Reason](#13-reason)   [1.4 Disclaimer](#14-disclaimer)   [1.5 Give a Star](#15-give-a-star)   [1.6 Share It](#16-share-it) [2. Domain](#2-domain)   [2.1 Description](#21-description)   [2.2 Conceptual Model](#22-conceptual-model)   [2.3 Event Storming](#23-event-storming) [3. Architecture](#3-architecture)   [3.0 C4 Model](#30-c4-model)   [3.1 High Level View](#31-high-level-view)   [3.2 Module Level View](#32-module-level-view)   [3.3 API and Module Communication](#33-api-and-module-communication)   [3.4 Module Requests Processing via CQRS](#34-module-requests-processing-via-cqrs)   [3.5 Domain Model Principles and Attributes](#35-domain-model-principles-and-attributes)   [3.6 Cross-Cutting Concerns](#36-cross-cutting-concerns)   [3.7 Modules Integration](#37-modules-integration)   [3.8 Internal Processing](#38-internal-processing)   [3.9 Security](#39-security)   [3.10 Unit Tests](#310-unit-tests)   [3.11 Architecture Decision Log](#311-architecture-decision-log)   [3.12 Architecture Unit Tests](#312-architecture-unit-tests)   [3.13 Integration Tests](#313-integration-tests)   [3.14 System Integration Testing](#314-system-integration-testing)   [3.15 Event Sourcing](#315-event-sourcing)   [3.16 Database change management](#316-database-change-management)   [3.17 Continuous Integration](#317-continuous-integration)   [3.18 Static code analysis](#318-static-code-analysis)   [3.19 System Under Test SUT](#319-system-under-test-sut)   [3.20 Mutation Testing](#320-mutation-testing) [4. Technology](#4-technology) [5. How to Run](#5-how-to-run) [6. Contribution](#6-contribution) [7. Roadmap](#7-roadmap) [8. Authors](#8-authors) [9. License](#9-license) [10. Inspirations and Recommendations](#10-inspirations-and-recommendations) ## 1. Introduction ### 1.1 Purpose of this Repository This is a list of the main goals of this repository: - Showing how you can implement a **monolith** application in a **modular** way - Presentation of the **full implementation** of an application - This is not another simple application - This is not another proof of concept (PoC) - The goal is to present the implementation of an application that would be ready to run in production - Showing the application of **best practices** and **object-oriented programming principles** - Presentation of the use of **design patterns**. When, how and why they can be used - Presentation of some **architectural** considerations, decisions, approaches - Presentation of the implementation using **Domain-Driven Design** approach (**tactical** patterns) - Presentation of the implementation of **Unit Tests** for Domain Model (Testable Design in mind) - Presentation of the implementation of **Integration Tests** - Presentation of the implementation of **Event Sourcing** - Presentation of **C4 Model** - Presentation of **diagram as text** approach ### 1.2 Out of Scope This is a list of subjects which are out of scope for this repository: - Business requirements gathering and analysis - System analysis - Domain exploration - Domain distillation - Domain-Driven Design **strategic** patterns - Architecture evaluation, quality attributes analysis - Integration, system tests - Project management - Infrastructure - Containerization - Software engineering process - Deployment process - Maintenance - Documentation ### 1.3 Reason The reason for creating this repository is the lack of something similar. Most sample applications on GitHub have at least one of the following issues: - Very, very simple - few entities and use cases implemented - Not finished (for example there is no authentication, logging, etc..) - Poorly designed (in my opinion) - Poorly implemented (in my opinion) - Not well described - Assumptions and decisions are not clearly explained - Implements "Orders" domain - yes, everyone knows this domain, but something different is needed - Implemented in old technology - Not maintained To sum up, there are some very good examples, but there are far too few of them. This repository has the task of filling this gap at some level. ### 1.4 Disclaimer Software architecture should always be created to resolve specific **business problems**. Software architecture always supports some quality attributes and at the same time does not support others. A lot of other factors influence your software architecture - your team, opinions, preferences, experiences, technical constraints, time, budget, etc. Always functional requirements, quality attributes, technical constraints and other factors should be considered before an architectural decision is made. Because of the above, the architecture and implementation presented in this repository is **one of the many ways** to solve some problems. Take from this repository as much as you want, use it as you like but remember to **always pick the best solution which is appropriate to the problem class you have**. ### 1.5 Give a Star My primary focus in this project is on quality. Creating a good quality product involves a lot of analysis, research and work. It takes a lot of time. If you like this project, learned something or you are using it in your applications, please give it a star :star:. This is the best motivation for me to continue this work. Thanks! ### 1.6 Share It There are very few really good examples of this type of application. If you think this repository makes a difference and is worth it, please share it with your friends and on social networks. I will be extremely grateful. ## 2. Domain ### 2.1 Description **Definition:** > Domain - A sphere of knowledge, influence, or activity. The subject area to which the user applies a program is the domain of the software. [Domain-Driven Design Reference](http://domainlanguage.com/ddd/reference/), Eric Evans The **Meeting Groups** domain was selected for the purposes of this project based on the [Meetup.com](https://www.meetup.com/) system. **Main reasons for selecting this domain:** - It is common, a lot of people use the Meetup site to organize or attend meetings - There is a system for it, so everyone can check this implementation against a working site which supports this domain - It is not complex so it is easy to understand - It is not trivial - there are some business rules and logic and it is not just CRUD operations - You don't need much specific domain knowledge unlike other domains like financing, banking, medical - It is not big so it is easier to implement **Meetings** The main business entities are `Member`, `Meeting Group` and `Meeting`. A `Member` can create a `Meeting Group`, be part of a `Meeting Group` or can attend a `Meeting`. A `Meeting Group Member` can be an `Organizer` of this group or a normal `Member`. Only an `Organizer` of a `Meeting Group` can create a new `Meeting`. A `Meeting` has attendees, not attendees (`Members` which declare they will not attend the `Meeting`) and `Members` on the `Waitlist`. A `Meeting` can have an attendee limit. If the limit is reached, `Members` can only sign up to the `Waitlist`. A `Meeting Attendee` can bring guests to the `Meeting`. The number of guests allowed is an attribute of the `Meeting`. Bringing guests can be unallowed. A `Meeting Attendee` can have one of two roles: `Attendee` or `Host`. A `Meeting` must have at least one `Host`. The `Host` is a special role which grants permission to edit `Meeting` information or change the attendees list. A `Member` can comment `Meetings`. A `Member` can reply to, like other `Comments`. `Organizer` manages commenting of `Meeting` by `Meeting Commenting Configuration`. `Organizer` can delete any `Comment`. Each `Meeting Group` must have an organizer with active `Subscription`. One organizer can cover 3 `Meeting Groups` by his `Subscription`. Additionally, Meeting organizer can set an `Event Fee`. Each `Meeting Attendee` is obliged to pay the fee. All guests should be paid by `Meeting Attendee` too. **Administration** To create a new `Meeting Group`, a `Member` needs to propose the group. A `Meeting Group Proposal` is sent to `Administrators`. An `Administrator` can accept or reject a `Meeting Group Proposal`. If a `Meeting Group Proposal` is accepted, a `Meeting Group` is created. **Payments** Each `Member` who is the `Payer` can buy the `Subscription`. He needs to pay the `Subscription Payment`. `Subscription` can expire so `Subscription Renewal` is required (by `Subscription Renewal Payment` payment to keep `Subscription` active). When the `Meeting` fee is required, the `Payer` needs to pay `Meeting Fee` (through `Meeting Fee Payment`). **Users** Each `Administrator`, `Member` and `Payer` is a `User`. To be a `User`, `User Registration` is required and confirmed. Each `User` is assigned one or more `User Role`. Each `User Role` has set of `Permissions`. A `Permission` defines whether `User` can invoke a particular action. ### 2.2 Conceptual Model **Definition:** > Conceptual Model - A conceptual model is a representation of a system, made of the composition of concepts that are used to help people know, understand, or simulate a subject the model represents. [Wikipedia - Conceptual model](https://en.wikipedia.org/wiki/Conceptual_model) **Conceptual Model** PlantUML version: ![](https://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/kgrzybek/modular-monolith-with-ddd/master/docs/PlantUML/Conceptual_Model.puml) VisualParadigm version (not maintained, only for demonstration): ![](docs/Images/Conceptual_Model.png) **Conceptual Model of commenting feature** ![](https://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/kgrzybek/modular-monolith-with-ddd/master/docs/PlantUML/Commenting_Conceptual_Model.puml) ### 2.3 Event Storming While a Conceptual Model focuses on structures and relationships between them, **behavior** and **events** that occur in our domain are more important. There are many ways to show behavior and events. One of them is a light technique called [Event Storming](https://www.eventstorming.com/) which is becoming more popular. Below are presented 3 main business processes using this technique: user registration, meeting group creation and meeting organization. Note: Event Storming is a light, live workshop. One of the possible outputs of this workshop is presented here. Even if you are not doing Event Storming workshops, this type of process presentation can be very valuable to you and your stakeholders. **User Registration process** ------ ![](docs/Images/User_Registration.jpg) ------ **Meeting Group creation** ![](docs/Images/Meeting_Group_Creation.jpg) ------ **Meeting organization** ![](docs/Images/Meeting_Organization.jpg) ------ **Payments** ![](docs/Images/Payments_EventStorming_Design.jpg) [Download high resolution file](docs/Images/Payments_EventStorming_Design_HighRes.jpg) ------ ## 3. Architecture ### 3.0 C4 Model [C4 model](https://c4model.com/) is a lean graphical notation technique for modelling the architecture of software systems.
As can be found on the website of the author of this model ([Simon Brown](https://simonbrown.je/)): *The C4 model was created as a way to help software development teams describe and communicate software architecture, both during up-front design sessions and when retrospectively documenting an existing codebase*
*Model C4* defines 4 levels (views) of the system architecture: *System Context*, *Container*, *Component* and *Code*. Below are examples of each of these levels that describe the architecture of this system.
*Note: The [PlantUML](https://plantuml.com/) (diagram as text) component was used to describe all C4 model levels. Additionally, for levels C1-C3, a [C4-PlantUML](https://github.com/plantuml-stdlib/C4-PlantUML) plug-in connecting PlantUML with the C4 model was used*. #### 3.0.1 C1 System Context ![](http://www.plantuml.com/plantuml/png/7OrDgeD048JtxnGl1z0ca5LMGWuYutIZulIqz0_6d3vZDbLG5Dytc2VruF9tMsikWHHQ_XVttPu0cev-Nds9AOmqItMgtcTXs6Rzd1Djm89HadOiLKgxTiSLY0YSp4a19Hky7f3levrjuV77UNk_Nzg1AhR-0W00) #### 3.0.2 C2 Container ![](docs/C4/C2_Containers.png) #### 3.0.3 C3 Component (high-level) ![](docs/C4/C3_Components.png) #### 3.0.4 C3 Component (module-level) ![](http://www.plantuml.com/plantuml/png/jLHFRzCm5B_dKsI1GojjBOKn5QH9wxeTAgrem7QUdEGrjHJRaVqCgX3V7QVUl7XkbnA2BusUVt_y_7xrXK8YKRCoEi8rC8Yhab0U7L6UbJg7U8rOgS_ZiIG_HmN5jKwr0fa9Zi1nb0asDWHU2vmep4kQZkUd9xTrwNvvCsP48KXJUfWBLWbUSwhQB9hbkIlTaMAGC02al539SVmsBUQY5F8yUNEQmRkpZyamn9ESKKuLIe9KS9y57zBfsNGN2twOBtMfNzYy_pIPJ4bTMmcEJzNLTXcPwFj68R27Iw5vJkHca4sEusIvYPUFXuuj81d6lwBOB0TacoV8hA8lEBFRXIFKovrqGBROUj_yZBvStvaz2PRWuFR3CtjKNefSbs2epifMd5lWwAWBlf94eTGPQjcK6Faxxc0tD9N6kxuw98KwVvxZiCLgLbKbpRRJQ_eqoZsON0b6gATlApr8BpX2OTDtlKrLqoNOx6vubJvtGv0qnveJ9BMmojR0oAkIlwCmB_vVoALcvfNRi-FB10dovGxEaQ-Q30yoRsOgS6vizcnhCnKwsdhFPc7k0jy0qlq8BeC-i4vYu1laiSN4fTBp-gf1my0zr4REzX3RLpjPy9W14yqc7DXA6raZ77s3qhwaUn-tUmM64W8RIV5HkvLw8Be4XHnVj3CXZCtV7P0WEOpXXk7WZL7uIMWTY0_VUxklg_u7aLstlzUcLt8unkvD42JjxFR1-gn_2L-tlY-0vvgLVm00) #### 3.0.5 C4 Code (meeting group aggregate) ![](http://www.plantuml.com/plantuml/png/5OqxheD0303xTugN0x1kg58XvI3HObk0yAwHFqB9wGFDJ3FIJ1xL8flyFRQEaiHfyhz67Fu4i7gMPOirvtGsr1xSew0ss1VxVcRUeIcbL1kQTfKh7SuRH0IjUh01AJgyHi3nZLBTot7V9kvq-GS0) ### 3.1 High Level View ![](docs/Images/Architecture_high_level.png) **Module descriptions:** - **API** - Very thin ASP.NET MVC Core REST API application. Main responsibilities are: 1. Accept request 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 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 - **In Memory Events Bus** - Publish/Subscribe implementation to asynchronously integrate all modules using events ([Event Driven Architecture](https://en.wikipedia.org/wiki/Event-driven_architecture)). **Key assumptions:** 1. API contains no application logic 2. API communicates with Modules using a small interface to send Queries and Commands 3. Each Module has its own interface which is used by API 4. **Modules communicate each other only asynchronously using Events Bus** - direct method calls are not allowed 5. Each Module **has it's own data** in a separate schema - shared data is not allowed - Module data could be moved into separate databases if desired 6. Modules can only have a dependency on the integration events assembly of other Module (see [Module level view](#32-module-level-view)) 7. Each Module has its own [Composition Root](https://freecontent.manning.com/dependency-injection-in-net-2nd-edition-understanding-the-composition-root/), which implies that each Module has its own Inversion-of-Control container 8. API as a host needs to initialize each module and each module has an initialization method 9. Each Module is **highly encapsulated** - only required types and members are public, the rest are internal or private ### 3.2 Module Level View ![](docs/Images/Module_level_diagram.png) Each Module has [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) and consists of the following submodules (assemblies): - **Application** - the application logic submodule which is responsible for requests processing: use cases, domain events, integration events, internal commands. - **Domain** - Domain Model in Domain-Driven Design terms implements the applicable [Bounded Context](https://martinfowler.com/bliki/BoundedContext.html) - **Infrastructure** - infrastructural code responsible for module initialization, background processing, data access, communication with Events Bus and other external components or systems - **IntegrationEvents** - **Contracts** published to the Events Bus; only this assembly can be called by other modules ![](docs/Images/VSSolution.png) **Note:** Application, Domain and Infrastructure assemblies could be merged into one assembly. Some people like horizontal layering or more decomposition, some don't. Implementing the Domain Model or Infrastructure in separate assembly allows encapsulation using the [`internal`](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/internal) keyword. Sometimes Bounded Context logic is not worth it because it is too simple. As always, be pragmatic and take whatever approach you like. ### 3.3 API and Module Communication The API only communicates with Modules in two ways: during module initialization and request processing. **Module initialization** Each module has a static ``Initialize`` method which is invoked in the API ``Startup`` class. All configuration needed by this module should be provided as arguments to this method. All services are configured during initialization and the Composition Root is created using the Inversion-of-Control Container. ```csharp public static void Initialize( string connectionString, IExecutionContextAccessor executionContextAccessor, ILogger logger, EmailsConfiguration emailsConfiguration) { var moduleLogger = logger.ForContext("Module", "Meetings"); ConfigureCompositionRoot(connectionString, executionContextAccessor, moduleLogger, emailsConfiguration); QuartzStartup.Initialize(moduleLogger); EventsBusStartup.Initialize(moduleLogger); } ``` **Request processing** Each module has the same interface signature exposed to the API. It contains 3 methods: command with result, command without result and query. ```csharp public interface IMeetingsModule { Task ExecuteCommandAsync(ICommand command); Task ExecuteCommandAsync(ICommand command); Task ExecuteQueryAsync(IQuery query); } ``` **Note:** Some people say that processing a command should not return a result. This is an understandable approach but sometimes impractical, especially when you want to immediately return the ID of a newly created resource. Sometimes the boundary between Command and Query is blurry. One example is ``AuthenticateCommand`` - it returns a token but it is not a query because it has a side effect. ### 3.4 Module Requests Processing via CQRS Processing of Commands and Queries is separated by applying the architectural style/pattern [Command Query Responsibility Segregation (CQRS)](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs). ![](docs/Images/CQRS.jpg) Commands are processed using *Write Model* which is implemented using DDD tactical patterns: ```csharp internal class CreateNewMeetingGroupCommandHandler : ICommandHandler { private readonly IMeetingGroupRepository _meetingGroupRepository; private readonly IMeetingGroupProposalRepository _meetingGroupProposalRepository; internal CreateNewMeetingGroupCommandHandler( IMeetingGroupRepository meetingGroupRepository, IMeetingGroupProposalRepository meetingGroupProposalRepository) { _meetingGroupRepository = meetingGroupRepository; _meetingGroupProposalRepository = meetingGroupProposalRepository; } public async Task Handle(CreateNewMeetingGroupCommand request, CancellationToken cancellationToken) { var meetingGroupProposal = await _meetingGroupProposalRepository.GetByIdAsync(request.MeetingGroupProposalId); var meetingGroup = meetingGroupProposal.CreateMeetingGroup(); await _meetingGroupRepository.AddAsync(meetingGroup); } } ``` Queries are processed using *Read Model* which is implemented by executing raw SQL statements on database views: ```csharp internal class GetAllMeetingGroupsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; internal GetAllMeetingGroupsQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle(GetAllMeetingGroupsQuery request, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MeetingGroup].[Id] as [{nameof(MeetingGroupDto.Id)}] , [MeetingGroup].[Name] as [{nameof(MeetingGroupDto.Name)}], [MeetingGroup].[Description] as [{nameof(MeetingGroupDto.Description)}] [MeetingGroup].[LocationCountryCode] as [{nameof(MeetingGroupDto.LocationCountryCode)}], [MeetingGroup].[LocationCity] as [{nameof(MeetingGroupDto.LocationCity)}] FROM [meetings].[v_MeetingGroups] AS [MeetingGroup] """; var meetingGroups = await connection.QueryAsync(sql); return meetingGroups.AsList(); } } ``` **Key advantages:** - Solution is appropriate to the problem - reading and writing needs are usually different - Supports [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single_responsibility_principle) (SRP) - one handler does one thing - Supports [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle) (ISP) - each handler implements interface with exactly one method - Supports [Parameter Object pattern](https://refactoring.com/catalog/introduceParameterObject.html) - Commands and Queries are objects which are easy to serialize/deserialize - Easy way to apply [Decorator pattern](https://en.wikipedia.org/wiki/Decorator_pattern) to handle cross-cutting concerns - Supports Loose Coupling by use of the [Mediator pattern](https://en.wikipedia.org/wiki/Mediator_pattern) - separates invoker of request from handler of request **Disadvantage:** - Mediator pattern introduces extra indirection and is harder to reason about which class handles the request For more information: [Simple CQRS implementation with raw SQL and DDD](https://www.kamilgrzybek.com/design/simple-cqrs-implementation-with-raw-sql-and-ddd/) ### 3.5 Domain Model Principles and Attributes The Domain Model, which is the central and most critical part in the system, should be designed with special attention. Here are some key principles and attributes which are applied to Domain Models of each module: 1. **High level of encapsulation** All members are ``private`` by default, then ``internal`` - only ``public`` at the very edge. 2. **High level of PI (Persistence Ignorance)** No dependencies to infrastructure, databases, etc. All classes are [POCOs](https://en.wikipedia.org/wiki/Plain_old_CLR_object). 3. **Rich in behavior** All business logic is located in the Domain Model. No leaks to the application layer or elsewhere. 4. **Low level of Primitive Obsession** Primitive attributes of Entites grouped together using ValueObjects. 5. **Business language** All classes, methods and other members are named in business language used in this Bounded Context. 6. **Testable** The Domain Model is a critical part of the system so it should be easy to test (Testable Design). ```csharp public class MeetingGroup : Entity, IAggregateRoot { public MeetingGroupId Id { get; private set; } private string _name; private string _description; private MeetingGroupLocation _location; private MemberId _creatorId; private List _members; private DateTime _createDate; private DateTime? _paymentDateTo; internal static MeetingGroup CreateBasedOnProposal( MeetingGroupProposalId meetingGroupProposalId, string name, string description, MeetingGroupLocation location, MemberId creatorId) { return new MeetingGroup(meetingGroupProposalId, name, description, location, creatorId); } public Meeting CreateMeeting( string title, MeetingTerm term, string description, MeetingLocation location, int? attendeesLimit, int guestsLimit, Term rsvpTerm, MoneyValue eventFee, List hostsMembersIds, MemberId creatorId) { this.CheckRule(new MeetingCanBeOrganizedOnlyByPayedGroupRule(_paymentDateTo)); this.CheckRule(new MeetingHostMustBeAMeetingGroupMemberRule(creatorId, hostsMembersIds, _members)); return new Meeting(this.Id, title, term, description, location, attendeesLimit, guestsLimit, rsvpTerm, eventFee, hostsMembersIds, creatorId); } ``` ### 3.6 Cross-Cutting Concerns To support [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single_responsibility_principle) and [Don't Repeat Yourself](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) principles, the implementation of cross-cutting concerns is done using the [Decorator Pattern](https://en.wikipedia.org/wiki/Decorator_pattern). Each Command processor is decorated by 3 decorators: logging, validation and unit of work. ![](docs/Images/Decorator.jpg) **Logging** The Logging decorator logs execution, arguments and processing of each Command. This way each log inside a processor has the log context of the processing command. ```csharp 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) { return await _decorated.Handle(command, cancellationToken); } using ( LogContext.Push( new RequestLogEnricher(_executionContextAccessor), new CommandLogEnricher(command))) { try { this._logger.Information( "Executing command {Command}", command.GetType().Name); var result = await _decorated.Handle(command, cancellationToken); this._logger.Information("Command {Command} processed successful", command.GetType().Name); return result; } 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))); } } } } ``` **Validation** The Validation decorator performs Command data validation. It checks rules against Command arguments using the FluentValidation library. ```csharp 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 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()) { var errorBuilder = new StringBuilder(); errorBuilder.AppendLine("Invalid command, reason: "); foreach (var error in errors) { errorBuilder.AppendLine(error.ErrorMessage); } throw new InvalidCommandException(errorBuilder.ToString(), null); } return _decorated.Handle(command, cancellationToken); } } ``` **Unit Of Work** All Command processing has side effects. To avoid calling commit on every handler, `UnitOfWorkCommandHandlerDecorator` is used. It additionally marks `InternalCommand` as processed (if it is Internal Command) and dispatches all Domain Events (as part of [Unit Of Work](https://martinfowler.com/eaaCatalog/unitOfWork.html)). ```csharp public class UnitOfWorkCommandHandlerDecorator : ICommandHandler where T:ICommand { private readonly ICommandHandler _decorated; private readonly IUnitOfWork _unitOfWork; private readonly MeetingsContext _meetingContext; public UnitOfWorkCommandHandlerDecorator( ICommandHandler decorated, IUnitOfWork unitOfWork, MeetingsContext meetingContext) { _decorated = decorated; _unitOfWork = unitOfWork; _meetingContext = meetingContext; } public async Task Handle(T command, CancellationToken cancellationToken) { await this._decorated.Handle(command, cancellationToken); if (command is InternalCommandBase) { var internalCommand = await _meetingContext.InternalCommands.FirstOrDefaultAsync(x => x.Id == command.Id, cancellationToken: cancellationToken); if (internalCommand != null) { internalCommand.ProcessedDate = DateTime.UtcNow; } } await this._unitOfWork.CommitAsync(cancellationToken); } } ``` ### 3.7 Modules Integration Integration between modules is strictly **asynchronous** using Integration Events and the In Memory Event Bus as broker. In this way coupling between modules is minimal and exists only on the structure of Integration Events. **Modules don't share data** so it is not possible nor desirable to create a transaction which spans more than one module. To ensure maximum reliability, the [Outbox / Inbox pattern](http://www.kamilgrzybek.com/design/the-outbox-pattern/) is used. This pattern provides accordingly *"At-Least-Once delivery"* and *"At-Least-Once processing"*. ![](docs/Images/OutboxInbox.jpg) The Outbox and Inbox is implemented using two SQL tables and a background worker for each module. The background worker is implemented using the Quartz.NET library. **Saving to Outbox:** ![](docs/Images/OutboxSave.png) **Processing Outbox:** ![](docs/Images/OutboxProcessing.png) ### 3.8 Internal Processing The main principle of this system is that you can change its state only by calling a specific Command. Commands can be called not only by the API, but by the processing module itself. The main use case which implements this mechanism is data processing in eventual consistency mode when we want to process something in a different process and transaction. This applies, for example, to Inbox processing because we want to do something (calling a Command) based on an Integration Event from the Inbox. This idea is taken from Alberto's Brandolini's Event Storming picture called "The picture that explains “almost” everything" which shows that every side effect (domain event) is created by invoking a Command on Aggregate. See [EventStorming cheat sheet](https://xebia.com/blog/eventstorming-cheat-sheet/) article for more details. Implementation of internal processing is very similar to implementation of the Outbox and Inbox. One SQL table and one background worker for processing. Each internally processing Command must inherit from `InternalCommandBase` class: ```csharp internal abstract class InternalCommandBase : ICommand { public Guid Id { get; } protected InternalCommandBase(Guid id) { this.Id = id; } } ``` This is important because the `UnitOfWorkCommandHandlerDecorator` must mark an internal Command as processed during committing: ```csharp public async Task Handle(T command, CancellationToken cancellationToken) { await this._decorated.Handle(command, cancellationToken); if (command is InternalCommandBase) { var internalCommand = await _meetingContext.InternalCommands.FirstOrDefaultAsync(x => x.Id == command.Id, cancellationToken: cancellationToken); if (internalCommand != null) { internalCommand.ProcessedDate = DateTime.UtcNow; } } await this._unitOfWork.CommitAsync(cancellationToken); } ``` ### 3.9 Security **Authentication** Authentication is implemented using JWT Token and Bearer scheme using IdentityServer. For now, only one authentication method is implemented: forms style authentication (username and password) via the OAuth2 [Resource Owner Password Grant Type](https://www.oauth.com/oauth2-servers/access-tokens/password-grant/). It requires implementation of the `IResourceOwnerPasswordValidator` interface: ```csharp public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { private readonly IUserAccessModule _userAccessModule; public ResourceOwnerPasswordValidator(IUserAccessModule userAccessModule) { _userAccessModule = userAccessModule; } public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { var authenticationResult = await _userAccessModule.ExecuteCommandAsync(new AuthenticateCommand(context.UserName, context.Password)); if (!authenticationResult.IsAuthenticated) { context.Result = new GrantValidationResult( TokenRequestErrors.InvalidGrant, authenticationResult.AuthenticationError); return; } context.Result = new GrantValidationResult( authenticationResult.User.Id.ToString(), "forms", authenticationResult.User.Claims); } } ``` **Authorization** Authorization is achieved by implementing [RBAC (Role Based Access Control)](https://en.wikipedia.org/wiki/Role-based_access_control) using Permissions. Permissions are more granular and a much better way to secure your application than Roles alone. Each User has a set of Roles and each Role contains one or more Permission. The User's set of Permissions is extracted from all Roles the User belongs to. Permissions are always checked on `Controller` level - never Roles: ```csharp [HttpPost] [Route("")] [HasPermission(MeetingsPermissions.ProposeMeetingGroup)] public async Task ProposeMeetingGroup(ProposeMeetingGroupRequest request) { await _meetingsModule.ExecuteCommandAsync( new ProposeMeetingGroupCommand( request.Name, request.Description, request.LocationCity, request.LocationCountryCode)); return Ok(); } ``` ### 3.10 Unit Tests **Definition:** >A unit test is an automated piece of code that invokes the unit of work being tested, and then checks some assumptions about a single end result of that unit. A unit test is almost always written using a unit testing framework. It can be written easily and runs quickly. It’s trustworthy, readable, and maintainable. It’s consistent in its results as long as production code hasn’t changed. [Art of Unit Testing 2nd Edition](https://www.manning.com/books/the-art-of-unit-testing-second-edition) Roy Osherove **Attributes of good unit test** - Automated - Maintainable - Runs very fast (in ms) - Consistent, Deterministic (always the same result) - Isolated from other tests - Readable - Can be executed by anyone - Testing public API, not internal behavior (overspecification) - Looks like production code - Treated as production code **Implementation** Unit tests should mainly test business logic (domain model):
![](docs/Images/unit_tests.jpg) Each unit test has 3 standard sections: Arrange, Act and Assert: ![](docs/Images/UnitTestsGeneral.jpg) **1\. Arrange** The Arrange section is responsible for preparing the Aggregate for testing the public method that we want to test. This public method is often called (from the unit tests perspective) the SUT (system under test). Creating an Aggregate ready for testing involves **calling one or more other public constructors/methods** on the Domain Model. At first it may seem that we are testing too many things at the same time, but this is not true. We need to be one hundred percent sure that the Aggregate is in a state exactly as it will be in production. This can only be ensured when we: - **Use only public API of Domain Model** - Don't use [InternalsVisibleToAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.internalsvisibletoattribute?view=netframework-4.8) class - This exposes the Domain Model to the Unit Tests library, removing encapsulation so our tests and production code are treated differently and it is a very bad thing - Don't use [ConditionalAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.conditionalattribute?view=netframework-4.8) classes - it reduces readability and increases complexity - Don't create any special constructors/factory methods for tests (even with conditional compilation symbols) - Special constructor/factory method only for unit tests causes duplication of business logic in the test itself and focuses on state - this kind of approach causes the test to be very sensitive to changes and hard to maintain - Don't remove encapsulation from Domain Model (for example: change keywords from `internal`/`private` to `public`) - Don't make methods `protected` to inherit from tested class and in this way provide access to internal methods/properties **Isolation of external dependencies** There are 2 main concepts - stubs and mocks: > A stub is a controllable replacement for an existing dependency (or collaborator) in the system. By using a stub, you can test your code without dealing with the dependency directly. >A mock object is a fake object in the system that decides whether the unit test has passed or failed. It does so by verifying whether the object under test called the fake object as expected. There’s usually no more than one mock per test. >[Art of Unit Testing 2nd Edition](https://www.manning.com/books/the-art-of-unit-testing-second-edition) Roy Osherove Good advice: use stubs if you need to, but try to avoid mocks. Mocking causes us to test too many internal things and leads to overspecification. **2\. Act** This section is very easy - we execute **exactly one** public method on aggregate (SUT). **3\. Assert** In this section we check expectations. There are only 2 possible outcomes: - Method completed and Domain Event(s) published - Business rule was broken Simple example: ```csharp [Test] public void NewUserRegistration_WithUniqueLogin_IsSuccessful() { // Arrange var usersCounter = Substitute.For(); // Act var userRegistration = UserRegistration.RegisterNewUser( "login", "password", "test@email", "firstName", "lastName", usersCounter); // Assert var newUserRegisteredDomainEvent = AssertPublishedDomainEvent(userRegistration); Assert.That(newUserRegisteredDomainEvent.UserRegistrationId, Is.EqualTo(userRegistration.Id)); } [Test] public void NewUserRegistration_WithoutUniqueLogin_BreaksUserLoginMustBeUniqueRule() { // Arrange var usersCounter = Substitute.For(); usersCounter.CountUsersWithLogin("login").Returns(x => 1); // Assert AssertBrokenRule(() => { // Act UserRegistration.RegisterNewUser( "login", "password", "test@email", "firstName", "lastName", usersCounter); }); } ``` Advanced example: ```csharp [Test] public void AddAttendee_WhenMemberIsAlreadyAttendeeOfMeeting_IsNotPossible() { // Arrange var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var newMemberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newMemberId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newMemberId, 0); // Assert AssertBrokenRule(() => { // Act meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newMemberId, 0); }); } ``` `CreateMeetingTestData` method is an implementation of [SUT Factory](https://blog.ploeh.dk/2009/02/13/SUTFactory/) described by Mark Seemann which allows keeping common creation logic in one place: ```csharp protected MeetingTestData CreateMeetingTestData(MeetingTestDataOptions options) { var proposalMemberId = options.CreatorId ?? new MemberId(Guid.NewGuid()); var meetingProposal = MeetingGroupProposal.ProposeNew( "name", "description", new MeetingGroupLocation("Warsaw", "PL"), proposalMemberId); meetingProposal.Accept(); var meetingGroup = meetingProposal.CreateMeetingGroup(); meetingGroup.UpdatePaymentInfo(DateTime.Now.AddDays(1)); var meetingTerm = options.MeetingTerm ?? new MeetingTerm(DateTime.UtcNow.AddDays(1), DateTime.UtcNow.AddDays(2)); var rsvpTerm = options.RvspTerm ?? Term.NoTerm; var meeting = meetingGroup.CreateMeeting("title", meetingTerm, "description", new MeetingLocation("Name", "Address", "PostalCode", "City"), options.AttendeesLimit, options.GuestsLimit, rsvpTerm, MoneyValue.Zero, new List(), proposalMemberId); DomainEventsTestHelper.ClearAllDomainEvents(meetingGroup); return new MeetingTestData(meetingGroup, meeting); } ``` ### 3.11 Architecture Decision Log All Architectural Decisions (AD) are documented in the [Architecture Decision Log (ADL)](docs/architecture-decision-log). More information about documenting architecture-related decisions in this way : [https://github.com/joelparkerhenderson/architecture_decision_record](https://github.com/joelparkerhenderson/architecture_decision_record) ### 3.12 Architecture Unit Tests In some cases it is not possible to enforce the application architecture, design or established conventions using compiler (compile-time). For this reason, code implementations can diverge from the original design and architecture. We want to minimize this behavior, not only by code review.
To do this, unit tests of system architecture, design, major conventions and assumptions have been written. In .NET there is special library for this task: [NetArchTest](https://github.com/BenMorris/NetArchTest). This library has been written based on the very popular JAVA architecture unit tests library - [ArchUnit](https://www.archunit.org/).
Using this kind of tests we can test proper layering of our application, dependencies, encapsulation, immutability, DDD correct implementation, naming, conventions and so on - everything what we need to test. Example:
![](docs/Images/architecture_unit_tests.png) More information about architecture unit tests here: [https://blogs.oracle.com/javamagazine/unit-test-your-architecture-with-archunit](https://blogs.oracle.com/javamagazine/unit-test-your-architecture-with-archunit) ### 3.13 Integration Tests #### Definition "Integration Test" term is blurred. It can mean test between classes, modules, services, even systems - see [this](https://martinfowler.com/bliki/IntegrationTest.html) article (by Martin Fowler).
For this reason, the definition of integration test in this project is as follows:
- it verifies how system works in integration with "out-of-process" dependencies - database, messaging system, file system or external API - it tests particular use case - it can be slow (as opposed to Unit Test) #### Approach - **Do not mock dependencies over which you have full control** (like database). Full control dependency means you can always revert all changes (remove side-effects) and no one can notice it. They are not visible to others. See next point, please. - **Use "production", normal, real database version**. Some use e.g. in memory repository, some use light databases instead "production" version. This is still mocking. Testing makes sense if we have full confidence in testing. You can't trust the test if you know that the infrastructure in the production environment will vary. Be always as close to production environment as possible. - **Mock dependencies over which you don't have control**. No control dependency means you can't remove side-effects after interaction with this dependency (external API, messaging system, SMTP server etc.). They can be visible to others. #### Implementation Integration test should test exactly one use case. One use case is represented by one Command/Query processing so CommandHandler/QueryHandler in Application layer is perfect starting point for running the Integration Test:
![](docs/Images/integration_tests.jpg) For each test, the following preparation steps must be performed:
1. Clear database 2. Prepare mocks 3. Initialize testing module ```csharp [SetUp] public async Task BeforeEachTest() { const string connectionStringEnvironmentVariable = "ASPNETCORE_MyMeetings_IntegrationTests_ConnectionString"; ConnectionString = Environment.GetEnvironmentVariable(connectionStringEnvironmentVariable, EnvironmentVariableTarget.Machine); 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(); EventsBus = new EventsBusMock(); ExecutionContext = new ExecutionContextMock(Guid.NewGuid()); PaymentsStartup.Initialize( ConnectionString, ExecutionContext, Logger, EventsBus, false); PaymentsModule = new PaymentsModule(); } ``` After preparation, test is performed on clear database. Usually, it is the execution of some (or many) Commands and:
a) running a Query or/and
b) verifying mocks
to check the result. ```csharp [TestFixture] public class MeetingPaymentTests : TestBase { [Test] public async Task CreateMeetingPayment_Test() { PayerId payerId = new PayerId(Guid.NewGuid()); MeetingId meetingId = new MeetingId(Guid.NewGuid()); decimal value = 100; string currency = "EUR"; await PaymentsModule.ExecuteCommandAsync(new CreateMeetingPaymentCommand(Guid.NewGuid(), payerId, meetingId, value, currency)); var payment = await PaymentsModule.ExecuteQueryAsync(new GetMeetingPaymentQuery(meetingId.Value, payerId.Value)); Assert.That(payment.PayerId, Is.EqualTo(payerId.Value)); Assert.That(payment.MeetingId, Is.EqualTo(meetingId.Value)); Assert.That(payment.FeeValue, Is.EqualTo(value)); Assert.That(payment.FeeCurrency, Is.EqualTo(currency)); } } ``` Each Command/Query processing is a separate execution (with different object graph resolution, context, database connection etc.) thanks to Composition Root of each module. This behavior is important and desirable. ### 3.14 System Integration Testing #### Definition [System Integration Testing (SIT)](https://en.wikipedia.org/wiki/System_integration_testing) is performed to verify the interactions between the modules of a software system. It involves the overall testing of a complete system of many subsystem components or elements. #### Implementation Implementation of system integration tests is based on approach of integration testing of modules in isolation (invoking commands and queries) described in the previous section. The problem is that in this case we are dealing with **asynchronous communication**. Due to asynchrony, our **test must wait for the result** at certain times. To correctly implement such tests, the **Sampling** technique and implementation described in the [Growing Object-Oriented Software, Guided by Tests](https://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627) book was used: >An asynchronous test must wait for success and use timeouts to detect failure. This implies that every tested activity must have an observable effect: a test must affect the system so that its observable state becomes different. This sounds obvious but it drives how we think about writing asynchronous tests. If an activity has no observable effect, there is nothing the test can wait for, and therefore no way for the test to synchronize with the system it is testing. There are two ways a test can observe the system: by sampling its observable state or by listening for events that it sends out. ![](docs/Images/SystemIntegrationTests.jpg) Test below: 1. Creates Meeting Group Proposal in Meetings module 2. Waits until Meeting Group Proposal to verification will be available in Administration module with 10 seconds timeout 3. Accepts Meeting Group Proposal in Administration module 4. Waits until Meeting Group is created in Meetings module with 15 seconds timeout ```csharp public class CreateMeetingGroupTests : TestBase { [Test] public async Task CreateMeetingGroupScenario_WhenProposalIsAccepted() { var meetingGroupId = await MeetingsModule.ExecuteCommandAsync( new ProposeMeetingGroupCommand("Name", "Description", "Location", "PL")); AssertEventually( new GetMeetingGroupProposalFromAdministrationProbe(meetingGroupId, AdministrationModule), 10000); await AdministrationModule.ExecuteCommandAsync(new AcceptMeetingGroupProposalCommand(meetingGroupId)); AssertEventually( new GetCreatedMeetingGroupFromMeetingsProbe(meetingGroupId, MeetingsModule), 15000); } private class GetCreatedMeetingGroupFromMeetingsProbe : IProbe { private readonly Guid _expectedMeetingGroupId; private readonly IMeetingsModule _meetingsModule; private List _allMeetingGroups; public GetCreatedMeetingGroupFromMeetingsProbe( Guid expectedMeetingGroupId, IMeetingsModule meetingsModule) { _expectedMeetingGroupId = expectedMeetingGroupId; _meetingsModule = meetingsModule; } public bool IsSatisfied() { return _allMeetingGroups != null && _allMeetingGroups.Any(x => x.Id == _expectedMeetingGroupId); } public async Task SampleAsync() { _allMeetingGroups = await _meetingsModule.ExecuteQueryAsync(new GetAllMeetingGroupsQuery()); } public string DescribeFailureTo() => $"Meeting group with ID: {_expectedMeetingGroupId} is not created"; } private class GetMeetingGroupProposalFromAdministrationProbe : IProbe { private readonly Guid _expectedMeetingGroupProposalId; private MeetingGroupProposalDto _meetingGroupProposal; private readonly IAdministrationModule _administrationModule; public GetMeetingGroupProposalFromAdministrationProbe(Guid expectedMeetingGroupProposalId, IAdministrationModule administrationModule) { _expectedMeetingGroupProposalId = expectedMeetingGroupProposalId; _administrationModule = administrationModule; } public bool IsSatisfied() { if (_meetingGroupProposal == null) { return false; } if (_meetingGroupProposal.Id == _expectedMeetingGroupProposalId && _meetingGroupProposal.StatusCode == MeetingGroupProposalStatus.ToVerify.Value) { return true; } return false; } public async Task SampleAsync() { try { _meetingGroupProposal = await _administrationModule.ExecuteQueryAsync( new GetMeetingGroupProposalQuery(_expectedMeetingGroupProposalId)); } catch { // ignored } } public string DescribeFailureTo() => $"Meeting group proposal with ID: {_expectedMeetingGroupProposalId} to verification not created"; } } ``` Poller class implementation (based on example in the book): ```csharp public class Poller { private readonly int _timeoutMillis; private readonly int _pollDelayMillis; public Poller(int timeoutMillis) { _timeoutMillis = timeoutMillis; _pollDelayMillis = 1000; } public void Check(IProbe probe) { var timeout = new Timeout(_timeoutMillis); while (!probe.IsSatisfied()) { if (timeout.HasTimedOut()) { throw new AssertErrorException(DescribeFailureOf(probe)); } Thread.Sleep(_pollDelayMillis); probe.SampleAsync(); } } private static string DescribeFailureOf(IProbe probe) { return probe.DescribeFailureTo(); } } ``` ### 3.15 Event Sourcing #### Theory During the implementation of the Payment module, *Event Sourcing* was used. *Event Sourcing* is a way of preserving the state of our system by recording a sequence of events. No less, no more. It is important here to really restore the state of our application from events. If we collect events only for auditing purposes, it is an [Audit Log/Trail](https://en.wikipedia.org/wiki/Audit_trail) - not the *Event Sourcing*. The main elements of *Event Sourcing* are as follows: - Events Stream - Objects that are restored based on events. There are 2 types of such objects depending on the purpose: -- Objects responsible for the change of state. In Domain-Driven Design they will be *Aggregates*. -- *Projections*: read models prepared for a specific purpose - *Subscriptions* : a way to receive information about new events - *Snapshots*: from time to time, objects saved in the traditional way for performance purposes. Mainly used if there are many events to restore the object from the entire event history. (Note: there is currently no snapshot implementation in the project) ![](docs/Images/ES_elements.jpg) #### Tool In order not to reinvent the wheel, the *SQL Stream Store* library was used. As the [documentation](https://sqlstreamstore.readthedocs.io/en/latest/) says: *SQL Stream Store is a .NET library to assist with developing applications that use event sourcing or wish to use stream based patterns over a relational database and existing operational infrastructure.* Like every library, it has its limitations and assumptions (I recommend the linked documentation chapter "Things you need to know before adopting"). For me, the most important 2 points from this chapter are: 1. *"Subscriptions (and thus projections) are **eventually consistent** and always will be."* This means that there will always be an inconsistency time from saving the event to the stream and processing the event by the projector(s). 2. *"No support for ambient System.Transaction scopes enforcing the concept of the stream as the consistency and transactional boundary."* This means that if we save the event to a events stream and want to save something **in the same transaction**, we must use [TransactionScope](https://learn.microsoft.com/en-us/dotnet/api/system.transactions.transactionscope?view=net-8.0). If we cannot use *TransactionScope* for some reason, we must accept the Eventual Consistency also in this case. Other popular tools: - [EventStore](https://eventstore.com/) *"An industrial-strength database solution built from the ground up for event sourcing."* - [Marten](https://martendb.io/) *".NET Transactional Document DB and Event Store on PostgreSQL"* #### Implementation There are 2 main "flows" to handle: - Command handling: change of state - adding new events to stream (writing) - Projection of events to create read models ##### Command Handling The whole process looks like this: ![](docs/Images/ES_command_handling.png) 1. We create / update an aggregate by creating an event 2. We add changes to the Aggregate Store. This is the class responsible for writing / loading our aggregates. We are not saving changes yet. 3. As part of Unit Of Work a) Aggregate Store adds events to the stream b) messages are added to the Outbox Command Handler: ```csharp public class BuySubscriptionCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; private readonly IPayerContext _payerContext; private readonly ISqlConnectionFactory _sqlConnectionFactory; public BuySubscriptionCommandHandler( IAggregateStore aggregateStore, IPayerContext payerContext, ISqlConnectionFactory sqlConnectionFactory) { _aggregateStore = aggregateStore; _payerContext = payerContext; _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(BuySubscriptionCommand command, CancellationToken cancellationToken) { var priceList = await PriceListProvider.GetPriceList(_sqlConnectionFactory.GetOpenConnection()); var subscriptionPayment = SubscriptionPayment.Buy( _payerContext.PayerId, SubscriptionPeriod.Of(command.SubscriptionTypeCode), command.CountryCode, MoneyValue.Of(command.Value, command.Currency), priceList); _aggregateStore.AppendChanges(subscriptionPayment); return subscriptionPayment.Id; } } ``` `SubscriptionPayment` Aggregate: ```csharp public class SubscriptionPayment : AggregateRoot { private PayerId _payerId; private SubscriptionPeriod _subscriptionPeriod; private string _countryCode; private SubscriptionPaymentStatus _subscriptionPaymentStatus; private MoneyValue _value; protected override void Apply(IDomainEvent @event) { this.When((dynamic)@event); } public static SubscriptionPayment Buy( PayerId payerId, SubscriptionPeriod period, string countryCode, MoneyValue priceOffer, PriceList priceList) { var priceInPriceList = priceList.GetPrice(countryCode, period, PriceListItemCategory.New); CheckRule(new PriceOfferMustMatchPriceInPriceListRule(priceOffer, priceInPriceList)); var subscriptionPayment = new SubscriptionPayment(); var subscriptionPaymentCreated = new SubscriptionPaymentCreatedDomainEvent( Guid.NewGuid(), payerId.Value, period.Code, countryCode, SubscriptionPaymentStatus.WaitingForPayment.Code, priceOffer.Value, priceOffer.Currency); subscriptionPayment.Apply(subscriptionPaymentCreated); subscriptionPayment.AddDomainEvent(subscriptionPaymentCreated); return subscriptionPayment; } private void When(SubscriptionPaymentCreatedDomainEvent @event) { this.Id = @event.SubscriptionPaymentId; _payerId = new PayerId(@event.PayerId); _subscriptionPeriod = SubscriptionPeriod.Of(@event.SubscriptionPeriodCode); _countryCode = @event.CountryCode; _subscriptionPaymentStatus = SubscriptionPaymentStatus.Of(@event.Status); _value = MoneyValue.Of(@event.Value, @event.Currency); } ``` `AggregateRoot` base class: ```csharp public abstract class AggregateRoot { public Guid Id { get; protected set; } public int Version { get; private set; } private readonly List _domainEvents; protected AggregateRoot() { _domainEvents = new List(); Version = -1; } protected void AddDomainEvent(IDomainEvent @event) { _domainEvents.Add(@event); } public IReadOnlyCollection GetDomainEvents() => _domainEvents.AsReadOnly(); public void Load(IEnumerable history) { foreach (var e in history) { Apply(e); Version++; } } protected abstract void Apply(IDomainEvent @event); protected static void CheckRule(IBusinessRule rule) { if (rule.IsBroken()) { throw new BusinessRuleValidationException(rule); } } } ``` Aggregate Store implementation with SQL Stream Store library usage: ```csharp public class SqlStreamAggregateStore : IAggregateStore { private readonly IStreamStore _streamStore; private readonly List _appendedChanges; private readonly List _aggregatesToSave; public SqlStreamAggregateStore( ISqlConnectionFactory sqlConnectionFactory) { _appendedChanges = new List(); _streamStore = new MsSqlStreamStore( new MsSqlStreamStoreSettings(sqlConnectionFactory.GetConnectionString()) { Schema = DatabaseSchema.Name }); _aggregatesToSave = new List(); } public async Task Save() { foreach (var aggregateToSave in _aggregatesToSave) { await _streamStore.AppendToStream( GetStreamId(aggregateToSave.Aggregate), aggregateToSave.Aggregate.Version, aggregateToSave.Messages.ToArray()); } _aggregatesToSave.Clear(); } public async Task Load(AggregateId aggregateId) where T : AggregateRoot { var streamId = GetStreamId(aggregateId); IList domainEvents = new List(); ReadStreamPage readStreamPage; do { readStreamPage = await _streamStore.ReadStreamForwards(streamId, StreamVersion.Start, maxCount: 100); var messages = readStreamPage.Messages; foreach (var streamMessage in messages) { Type type = DomainEventTypeMappings.Dictionary[streamMessage.Type]; var jsonData = await streamMessage.GetJsonData(); var domainEvent = JsonConvert.DeserializeObject(jsonData, type) as IDomainEvent; domainEvents.Add(domainEvent); } } while (!readStreamPage.IsEnd); var aggregate = (T)Activator.CreateInstance(typeof(T), true); aggregate.Load(domainEvents); return aggregate; } ``` ##### Events Projection The whole process looks like this: ![](docs/Images/ES_events_projection.png) 1. Special class `Subscriptions Manager` subscribes to Events Store (using SQL Store Stream library) 2. Events Store raises `StreamMessageRecievedEvent` 3. `Subscriptions Manager` invokes all projectors 4. If projector know how to handle given event, it updates particular read model. In current implementation it updates special table in SQL database. `SubscriptionsManager` class implementation: ```csharp public class SubscriptionsManager { private readonly IStreamStore _streamStore; public SubscriptionsManager( IStreamStore streamStore) { _streamStore = streamStore; } public void Start() { long? actualPosition; using (var scope = PaymentsCompositionRoot.BeginLifetimeScope()) { var checkpointStore = scope.Resolve(); actualPosition = checkpointStore.GetCheckpoint(SubscriptionCode.All); } _streamStore.SubscribeToAll(actualPosition, StreamMessageReceived); } public void Stop() { _streamStore.Dispose(); } private static async Task StreamMessageReceived( IAllStreamSubscription subscription, StreamMessage streamMessage, CancellationToken cancellationToken) { var type = DomainEventTypeMappings.Dictionary[streamMessage.Type]; var jsonData = await streamMessage.GetJsonData(cancellationToken); var domainEvent = JsonConvert.DeserializeObject(jsonData, type) as IDomainEvent; using var scope = PaymentsCompositionRoot.BeginLifetimeScope(); var projectors = scope.Resolve>(); var tasks = projectors .Select(async projector => { await projector.Project(domainEvent); }); await Task.WhenAll(tasks); var checkpointStore = scope.Resolve(); await checkpointStore.StoreCheckpoint(SubscriptionCode.All, streamMessage.Position); } } ``` Example projector: ```csharp internal class SubscriptionDetailsProjector : ProjectorBase, IProjector { private readonly IDbConnection _connection; public SubscriptionDetailsProjector(ISqlConnectionFactory sqlConnectionFactory) { _connection = sqlConnectionFactory.GetOpenConnection(); } public async Task Project(IDomainEvent @event) { await When((dynamic) @event); } private async Task When(SubscriptionRenewedDomainEvent subscriptionRenewed) { var period = SubscriptionPeriod.GetName(subscriptionRenewed.SubscriptionPeriodCode); await _connection.ExecuteScalarAsync("UPDATE payments.SubscriptionDetails " + "SET " + "[Status] = @Status, " + "[ExpirationDate] = @ExpirationDate, " + "[Period] = @Period " + "WHERE [Id] = @SubscriptionId", new { subscriptionRenewed.SubscriptionId, subscriptionRenewed.Status, subscriptionRenewed.ExpirationDate, period }); } private async Task When(SubscriptionExpiredDomainEvent subscriptionExpired) { await _connection.ExecuteScalarAsync("UPDATE payments.SubscriptionDetails " + "SET " + "[Status] = @Status " + "WHERE [Id] = @SubscriptionId", new { subscriptionExpired.SubscriptionId, subscriptionExpired.Status }); } private async Task When(SubscriptionCreatedDomainEvent subscriptionCreated) { var period = SubscriptionPeriod.GetName(subscriptionCreated.SubscriptionPeriodCode); await _connection.ExecuteScalarAsync("INSERT INTO payments.SubscriptionDetails " + "([Id], [Period], [Status], [CountryCode], [ExpirationDate]) " + "VALUES (@SubscriptionId, @Period, @Status, @CountryCode, @ExpirationDate)", new { subscriptionCreated.SubscriptionId, period, subscriptionCreated.Status, subscriptionCreated.CountryCode, subscriptionCreated.ExpirationDate }); } } ``` #### Sample view of Event Store Sample *Event Store* view after execution of SubscriptionLifecycleTests Integration Test which includes following steps: 1. Creating Price List 2. Buying Subscription 3. Renewing Subscription 4. Expiring Subscription looks like this (*SQL Stream Store* table - *payments.Messages*): ![](docs/Images/ES_event_store_db_sample.png) ### 3.16 Database Change Management Database change management is accomplished by *migrations/transitions* versioning. Additionally, the current state of the database structure is also versioned. Migrations are applied using a simple [DatabaseMigrator](src/Database/DatabaseMigrator) console application that uses the [DbUp](https://dbup.readthedocs.io/en/latest/) library. The current state of the database structure is kept in the [SSDT Database Project](https://docs.microsoft.com/en-us/sql/ssdt/how-to-create-a-new-database-project). The database update is performed by running the following command: ```shell dotnet DatabaseMigrator.dll "connection_string" "scripts_directory_path" ``` The entire solution is described in detail in the following articles: 1. [Database change management](https://www.kamilgrzybek.com/database/database-change-management/) (theory) 2. [Using database project and DbUp for database management](https://www.kamilgrzybek.com/database/using-database-project-and-dbup-for-database-management/) (implementation) ### 3.17 Continuous Integration #### Definition As defined on [Martin Fowler's website](https://martinfowler.com/articles/continuousIntegration.html): > *Continuous Integration is a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily - leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible.* #### YAML Implementation [OBSOLETE] *Originally the build was implemented using yaml and GitHub Actions functionality. Currently, the build is implemented with NUKE (see next section). See [buildPipeline.yml](.github/workflows/buildPipeline.yml)* file history. ##### Pipeline description CI was implemented using [GitHub Actions](https://docs.github.com/en/actions/getting-started-with-github-actions/about-github-actions). For this purpose, one workflow, which triggers on Pull Request to *master* branch or Push to *master* branch was created. It contains 2 jobs: - build test, execute Unit Tests and Architecture Tests - execute Integration Tests ![](docs/Images/ci.jpg) **Steps description**
a) Checkout repository - clean checkout of git repository
b) Setup .NET - install .NET 8.0 SDK
c) Install dependencies - resolve NuGet packages
d) Build - build solution
e) Run Unit Tests - run automated Unit Tests (see section 3.10)
f) Run Architecture Tests - run automated Architecture Tests (see section 3.12)
g) Initialize containers - setup Docker container for MS SQL Server
h) Wait for SQL Server initialization - after container initialization MS SQL Server is not ready, initialization of server itself takes some time so 30 seconds timeout before execution of next step is needed
i) Create Database - create and initialize database
j) Migrate Database - execute database upgrade using *DatabaseMigrator* application (see 3.16 section)
k) Run Integration Tests - perform Integration and System Integration Testing (see section 3.13 and 3.14)
##### Workflow definition Workflow definition: [buildPipeline.yml](.github/workflows/buildPipeline.yml) ##### Example workflow execution Example workflow output: ![](docs/Images/ci_job1.png) ![](docs/Images/ci_job2.png) #### NUKE [Nuke](https://nuke.build/) is *the cross-platform build automation solution for .NET with C# DSL.* The 2 main advantages of its use over pure yaml defined in GitHub actions are as follows: - You run the same code on local machine and in the build server. See [buildPipeline.yml](.github/workflows/buildPipeline.yml) - You use C# with all the goodness (debugging, compilation, packages, refactoring and so on) This is how one of the stage definition looks like (execute Build, Unit Tests, Architecture Tests) [Build.cs](build/Build.cs): ```csharp partial class Build : NukeBuild { /// Support plugins are available for: /// - JetBrains ReSharper https://nuke.build/resharper /// - JetBrains Rider https://nuke.build/rider /// - Microsoft VisualStudio https://nuke.build/visualstudio /// - Microsoft VSCode https://nuke.build/vscode public static int Main () => Execute(x => x.Compile); [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; [Solution] readonly Solution Solution; Target Clean => _ => _ .Before(Restore) .Executes(() => { EnsureCleanDirectory(WorkingDirectory); }); Target Restore => _ => _ .Executes(() => { DotNetRestore(s => s .SetProjectFile(Solution)); }); Target Compile => _ => _ .DependsOn(Restore) .Executes(() => { DotNetBuild(s => s .SetProjectFile(Solution) .SetConfiguration(Configuration) .EnableNoRestore()); }); Target UnitTests => _ => _ .DependsOn(Compile) .Executes(() => { DotNetTest(s => s .SetProjectFile(Solution) .SetFilter("UnitTests") .SetConfiguration(Configuration) .EnableNoRestore() .EnableNoBuild()); }); Target ArchitectureTests => _ => _ .DependsOn(UnitTests) .Executes(() => { DotNetTest(s => s .SetProjectFile(Solution) .SetFilter("ArchTests") .SetConfiguration(Configuration) .EnableNoRestore() .EnableNoBuild()); }); Target BuildAndUnitTests => _ => _ .Triggers(ArchitectureTests) .Executes(() => { }); } ``` If you want to see more complex scenario when integration tests are executed (with SQL Server database creation using docker) see [BuildIntegrationTests.cs](build/BuildIntegrationTests.cs) file. #### SQL Server database project build Currently, compilation of database projects is not supported by the .NET Core and dotnet tool. For this reason, the [MSBuild.Sdk.SqlProj](https://github.com/rr-wfm/MSBuild.Sdk.SqlProj/) library was used. In order to do that, you need to create .NET standard library, change SDK and create links to scripts folders. Final [database project](src/Database/CompanyName.MyMeetings.Database.Build/CompanyName.MyMeetings.Database.Build.csproj) looks as follows: ```xml netstandard2.0 ``` ### 3.18 Static code analysis In order to standardize the appearance of the code and increase its readability, the [StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers) library was used. This library implements StyleCop rules using the .NET Compiler Platform and is responsible for the static code analysis.
Using this library is trivial - it is just added as a NuGet package to all projects. There are many ways to configure rules, but currently the best way to do this is to edit the [.editorconfig](src/.editorconfig) file. More information can be found at the link above.
**Note! Static code analysis works best when the following points are met:**
1. Each developer has an IDE that respects the rules and helps to follow them 2. The rules are checked during the project build process as part of Continuous Integration 3. The rules are set to *help your system grow*. **Static analysis is not a value in itself.** Some rules may not make complete sense and should be turned off. Other rules may have higher priority. It all depends on the project, company standards and people involved in the project. Be pragmatic. ### 3.19 System Under Test SUT There is always a need to prepare the entire system in a specific state, e.g. for manual, exploratory, UX / UI tests. The fact that the tests are performed manually does not mean that we cannot automate the preparation phase (Given / Arrange). Thanks to the automation of system state preparation ([System Under Test](https://en.wikipedia.org/wiki/System_under_test)), we are able to recreate exactly the same state in any environment. In addition, such automation can be used later to automate the entire test (e.g. through an [3.13 Integration Tests](#313-integration-tests)).
The implementation of such automation based on the use of NUKE and the test framework is presented below. As in the case of integration testing, we use the public API of modules. ![](docs/Images/sut-preparation.jpg) Below is a SUT whose task is to go through the whole process - from setting up a *Meeting Group*, through its *Payment*, adding a new *Meeting* and signing up for it by another user. ```csharp public class CreateMeeting : TestBase { protected override bool PerformDatabaseCleanup => true; [Test] public async Task Prepare() { await UsersFactory.GivenAdmin( UserAccessModule, "testAdmin@mail.com", "testAdminPass", "Jane Doe", "Jane", "Doe", "testAdmin@mail.com"); var userId = await UsersFactory.GivenUser( UserAccessModule, ConnectionString, "adamSmith@mail.com", "adamSmithPass", "Adam", "Smith", "adamSmith@mail.com"); ExecutionContextAccessor.SetUserId(userId); var meetingGroupId = await MeetingGroupsFactory.GivenMeetingGroup( MeetingsModule, AdministrationModule, ConnectionString, "Software Craft", "Group for software craft passionates", "Warsaw", "PL"); await TestPriceListManager.AddPriceListItems(PaymentsModule, ConnectionString); await TestPaymentsManager.BuySubscription( PaymentsModule, ExecutionContextAccessor); SetDate(new DateTime(2022, 7, 1, 10, 0, 0)); var meetingId = await TestMeetingFactory.GivenMeeting( MeetingsModule, meetingGroupId, "Tactical DDD", new DateTime(2022, 7, 10, 18, 0, 0), new DateTime(2022, 7, 10, 20, 0, 0), "Meeting about Tactical DDD patterns", "Location Name", "Location Address", "01-755", "Warsaw", 50, 0, null, null, 0, null, new List() ); var attendeeUserId = await UsersFactory.GivenUser( UserAccessModule, ConnectionString, "rickmorty@mail.com", "rickmortyPass", "Rick", "Morty", "rickmorty@mail.com"); ExecutionContextAccessor.SetUserId(attendeeUserId); await TestMeetingGroupManager.JoinToGroup(MeetingsModule, meetingGroupId); await TestMeetingManager.AddAttendee(MeetingsModule, meetingId, guestsNumber: 1); } } ``` You can create this SUT using following *NUKE* target providing connection string and particular test name: ```shell .\build PrepareSUT --DatabaseConnectionString "connection_string" --SUTTestName CreateMeeting ``` ### 3.20 Mutation Testing #### Description Mutation testing is an approach to test and evaluate our existing tests. During mutation testing a special framework modifies pieces of our code and runs our tests. These modifications are called *mutations* or *mutants*. If a given *mutation* does not cause a failure of at least once test, it means that the mutant has *survived* so our tests are probably not sufficient. #### Example In this repository, the [Stryker.NET](https://stryker-mutator.io/docs/stryker-net/Introduction) framework was used for mutation testing. In the simplest use, after installation, all you need to do is enter the directory of tests that you want to mutate and run the following command: ```shell dotnet stryker ``` The result of this command is the *mutation report file*. Assuming we want to test the unit tests of the Meetings module, such a [report](docs/mutation-tests-reports/mutation-report.html) has been generated. This is its first page: ![](docs/Images/mutation_testing_report.png) Let us analyze one of the places where the mutant survived. This is the *AddNotAttendee* method of the *Meeting* class. This method is used to add a *Member* to the list of people who have decided not to attend the meeting. According to the logic, if the same person previously indicated that he was going to the *Meeting* and later changed his mind, then if there is someone on the *Waiting List*, he should be added to the attendees. Based on requirements, this should be the person who signed up on the *Waiting List* **first** (based on **SignUpDate**). ![](docs/Images/mutation_testing_example.png) As you can see, the mutation framework changed our sorting in linq query (from default ascending to descending). However, each test was successful, so it means that mutant survived so we don't have a test that checks the correct sort based on *SignUpDate*. From the example above, one more important thing can be deduced - **code coverage is insufficient**. In the given example, this code is covered, but our tests do not check the given requirement, therefore our code may have errors. Mutation testing allow to detect such situations. Of course, as with any tool, we should use it wisely, as not every case requires our attention. ## 4. Technology List of technologies, frameworks and libraries used for implementation: - [.NET 8.0](https://dotnet.microsoft.com/download) (platform). Note for Visual Studio users: **VS 2019** is required. - [MS SQL Server Express](https://www.microsoft.com/en-us/sql-server/sql-server-editions-express) (database) - [Entity Framework Core 8.0](https://docs.microsoft.com/en-us/ef/core/) (ORM Write Model implementation for DDD) - [Autofac](https://autofac.org/) (Inversion of Control Container) - [IdentityServer4](http://docs.identityserver.io) (Authentication and Authorization) - [Serilog](https://serilog.net/) (structured logging) - [Hellang.Middleware.ProblemDetails](https://github.com/khellang/Middleware/tree/master/src/ProblemDetails) (API Problem Details support) - [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle) (Swagger automated documentation) - [Dapper](https://github.com/StackExchange/Dapper) (micro ORM for Read Model) - [Newtonsoft.Json](https://www.newtonsoft.com/json) (serialization/deserialization to/from JSON) - [Quartz.NET](https://www.quartz-scheduler.net/) (background processing) - [FluentValidation](https://fluentvalidation.net/) (data validation) - [MediatR](https://github.com/jbogard/MediatR) (mediator implementation) - [Postman](https://www.getpostman.com/) (API tests) - [NUnit](https://nunit.org/) (Testing framework) - [NSubstitute](https://nsubstitute.github.io/) (Testing isolation framework) - [Visual Paradigm Community Edition](https://www.visual-paradigm.com/download/community.jsp) (CASE tool for modeling and documentation) - [NetArchTest](https://github.com/BenMorris/NetArchTest) (Architecture Unit Tests library) - [Polly](https://github.com/App-vNext/Polly) (Resilience and transient-fault-handling library) - [SQL Stream Store](https://github.com/SQLStreamStore) (Library to assist with Event Sourcing) - [DbUp](https://dbup.readthedocs.io/en/latest/) (Database migrations deployment) - [SSDT Database Project](https://docs.microsoft.com/en-us/sql/ssdt/how-to-create-a-new-database-project) (Database structure versioning) - [GitHub Actions](https://docs.github.com/en/actions) (Continuous Integration workflows implementation) - [StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers) (Static code analysis library) - [PlantUML](https://plantuml.com) (UML diagrams from textual description, diagrams as text) - [C4 Model](https://c4model.com/) (Model for visualising software architecture) - [C4-PlantUML](https://github.com/plantuml-stdlib/C4-PlantUML) (C4 Model for PlantUML plugin) - [NUKE](https://nuke.build/) (Build automation system) - [MSBuild.Sdk.SqlProj](https://github.com/rr-wfm/MSBuild.Sdk.SqlProj/) (Database project compilation) - [Stryker.NET](https://stryker-mutator.io/docs/stryker-net/Introduction) (Mutation Testing framework) ## 5. How to Run ### Install .NET 8.0 SDK - [Download](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) and install .NET 8.0 SDK ### Create database - Download and install MS SQL Server Express or other - Create an empty database using [CreateDatabase_Windows.sql](src/Database/CompanyName.MyMeetings.Database/Scripts/CreateDatabase_Windows.sql) or [CreateDatabase_Linux.sql](src/Database/CompanyName.MyMeetings.Database/Scripts/CreateDatabase_Linux.sql). Script adds **app** schema which is needed for migrations journal table. Change database file path if needed. - Run database migrations using **MigrateDatabase** NUKE target by executing the build.sh script present in the root folder: ```shell .\build MigrateDatabase --DatabaseConnectionString "connection_string" ``` *"connection_string"* - connection string to your database ### Seed database - Execute [SeedDatabase.sql](src/Database/CompanyName.MyMeetings.Database/Scripts/SeedDatabase.sql) script - 2 test users will be created - check the script for usernames and passwords ### Configure connection string Set a database connection string called `MeetingsConnectionString` in the root of the API project's appsettings.json or use [Secrets](https://blogs.msdn.microsoft.com/mihansen/2017/09/10/managing-secrets-in-net-core-2-0-apps/) Example config setting in appsettings.json for a database called `MyMeetings`: ```json { "MeetingsConnectionString": "Server=(localdb)\\mssqllocaldb;Database=MyMeetings;Trusted_Connection=True;" } ``` ### Configure startup in IDE - Set the Startup Item in your IDE to the API Project, not IIS Express ### Authenticate - Once it is running you'll need a token to make API calls. This is done via OAuth2 [Resource Owner Password Grant Type](https://www.oauth.com/oauth2-servers/access-tokens/password-grant/). By default IdentityServer is configured with the following: - `client_id = ro.client` - `client_secret = secret` **(this is literally the value - not a statement that this value is secret!)** - `scope = myMeetingsAPI openid profile` - `grant_type = password` Include the credentials of a test user created in the [SeedDatabase.sql](src/Database/CompanyName.MyMeetings.Database/Scripts/SeedDatabase.sql) script - for example: - `username = testMember@mail.com` - `password = testMemberPass` **Example HTTP Request for an Access Token:** ```http POST /connect/token HTTP/1.1 Host: localhost:5000 grant_type=password &username=testMember@mail.com &password=testMemberPass &client_id=ro.client &client_secret=secret ``` This will fetch an access token for this user to make authorized API requests using the HTTP request header `Authorization: Bearer ` If you use a tool such as Postman to test your API, the token can be fetched and stored within the tool itself and appended to all API calls. Check your tool documentation for instructions. ### Run using Docker Compose You can run whole application using [docker compose](https://docs.docker.com/compose/) from root folder: ```shell docker-compose up ``` It will create following services:
- MS SQL Server Database - Database Migrator - Application ### Run Integration Tests in Docker You can run all Integration Tests in Docker (exactly the same process is executed on CI) using **RunAllIntegrationTests** NUKE target: ```shell .\build RunAllIntegrationTests ``` ## 6. Contribution This project is still under analysis and development. I assume its maintenance for a long time and I would appreciate your contribution to it. Please let me know by creating an Issue or Pull Request. ## 7. Roadmap List of features/tasks/approaches to add: | Name | Status | Release date | |------------------------------------| -------- |--------------| | Domain Model Unit Tests |Completed | 2019-09-10 | | Architecture Decision Log update | Completed | 2019-11-09 | | Integration automated tests | Completed | 2020-02-24 | | Migration to .NET Core 3.1 |Completed | 2020-03-04 | | System Integration Testing | Completed | 2020-03-28 | | More advanced Payments module | Completed | 2020-07-11 | | Event Sourcing implementation | Completed | 2020-07-11 | | Database Change Management | Completed | 2020-08-23 | | Continuous Integration | Completed | 2020-09-01 | | StyleCop Static Code Analysis | Completed | 2020-09-05 | | FrontEnd SPA application | Completed | 2020-11-08 | | Docker support | Completed | 2020-11-26 | | PlantUML Conceptual Model | Completed | 2021-03-22 | | C4 Model | Completed | 2021-03-29 | | Meeting comments feature | Completed | 2021-03-30 | | NUKE build automation | Completed | 2021-06-15 | | Database project compilation on CI | Completed | 2021-06-15 | | System Under Test implementation | Completed | 2022-07-17 | | Mutation Testing | Completed | 2022-08-23 | | Migration to .NET 8.0 | Completed | 2023-12-09 | NOTE: Please don't hesitate to suggest something else or a change to the existing code. All proposals will be considered. ## 8. Authors Kamil Grzybek Blog: [https://kamilgrzybek.com](https://kamilgrzybek.com) Twitter: [https://twitter.com/kamgrzybek](https://twitter.com/kamgrzybek) LinkedIn: [https://www.linkedin.com/in/kamilgrzybek/](https://www.linkedin.com/in/kamilgrzybek/) GitHub: [https://github.com/kgrzybek](https://github.com/kgrzybek) ### 8.1 Main contributors - [Andrei Ganichev](https://github.com/AndreiGanichev) - [Bela Istok](https://github.com/bistok) - [Almar Aubel](https://github.com/AlmarAubel) ## 9. License The project is under [MIT license](https://opensource.org/licenses/MIT). ## 10. Inspirations and Recommendations ### Modular Monolith - ["Modular Monolith: A Primer"](https://www.kamilgrzybek.com/design/modular-monolith-primer/) Modular Monolith architecture article series, Kamil Grzybek - ["Modular Monolith Architecture: One to rule them all"](https://www.youtube.com/watch?v=njDSXUWeik0) presentation, Kamil Grzybek - ["Modular Monoliths"](https://www.youtube.com/watch?v=5OjqD-ow8GE) presentation, Simon Brown - ["Majestic Modular Monoliths"](https://www.youtube.com/watch?v=BOvxJaklcr0) presentation, Axel Fontaine - ["Building Better Monoliths – Modulithic Applications with Spring Boot"](https://speakerdeck.com/olivergierke/building-better-monoliths-modulithic-applications-with-spring-boot-cd16e6ec-d334-497d-b9f6-3f92d5db035a) slides, Oliver Drotbohm - ["MonolithFirst"](https://martinfowler.com/bliki/MonolithFirst.html) article, Martin Fowler - ["Pattern: Monolithic Architecture"](https://microservices.io/patterns/monolithic.html) pattern description, Chris Richardson ### Domain-Driven Design - ["Domain-Driven Design: Tackling Complexity in the Heart of Software"](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215) book, Eric Evans - ["Implementing Domain-Driven Design"](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577) book, Vaughn Vernon - ["Domain-Driven Design Distilled"](https://www.amazon.com/dp/0134434420) book, Vaughn Vernon - ["Patterns, Principles, and Practices of Domain-Driven Design"](https://www.amazon.com/Patterns-Principles-Practices-Domain-Driven-Design-ebook/dp/B00XLYUA0W) book, Scott Millett, Nick Tune - ["Secure By Design"](https://www.amazon.com/Secure-Design-Daniel-Deogun/dp/1617294357) book, Daniel Deogun, Dan Bergh Johnsson, Daniel Sawano - ["Hands-On Domain-Driven Design with .NET Core: Tackling complexity in the heart of software by putting DDD principles into practice"](https://www.amazon.com/Hands-Domain-Driven-Design-NET-ebook/dp/B07C5WSR9B) book, Alexey Zimarev - ["Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#"](https://www.amazon.com/Domain-Modeling-Made-Functional-Domain-Driven-ebook/dp/B07B44BPFB) book, Scott Wlaschin - ["DDD by examples - library"](https://github.com/ddd-by-examples/library) GH repository, Jakub Pilimon, Bartłomiej Słota - ["IDDD_Samples"](https://github.com/VaughnVernon/IDDD_Samples) GH repository, Vaughn Vernon - ["IDDD_Samples_NET"](https://github.com/VaughnVernon/IDDD_Samples_NET) GH repository, Vaughn Vernon - ["Awesome Domain-Driven Design"](https://github.com/heynickc/awesome-ddd) GH repository, Nick Chamberlain ### Application Architecture - ["Patterns of Enterprise Application Architecture"](https://martinfowler.com/books/eaa.html) book, Martin Fowler - ["Dependency Injection Principles, Practices, and Patterns"](https://www.manning.com/books/dependency-injection-principles-practices-patterns) book, Steven van Deursen, Mark Seemann - ["Clean Architecture: A Craftsman's Guide to Software Structure and Design (Robert C. Martin Series"](https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164) book, Robert C. Martin - ["The Clean Architecture"](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) article, Robert C. Martin - ["The Onion Architecture"](https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/) article series, Jeffrey Palermo - ["Hexagonal/Ports & Adapters Architecture"](https://web.archive.org/web/20180822100852/http://alistair.cockburn.us/Hexagonal+architecture) article, Alistair Cockburn - ["DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together"](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/) article, Herberto Graca ### Software Architecture - ["Software Architecture in Practice (3rd Edition)"](https://www.amazon.com/Software-Architecture-Practice-3rd-Engineering/dp/0321815734) book, Len Bass, Paul Clements, Rick Kazman - ["Software Architecture for Developers Vol 1 & 2"](https://softwarearchitecturefordevelopers.com/) book, Simon Brown - ["Just Enough Software Architecture: A Risk-Driven Approach"](https://www.amazon.com/Just-Enough-Software-Architecture-Risk-Driven/dp/0984618104) book, George H. Fairbanks - ["Software Systems Architecture: Working With Stakeholders Using Viewpoints and Perspectives (2nd Edition)"](https://www.amazon.com/Software-Systems-Architecture-Stakeholders-Perspectives/dp/032171833X/) book, Nick Rozanski, Eóin Woods - ["Design It!: From Programmer to Software Architect (The Pragmatic Programmers)"](https://www.amazon.com/Design-Programmer-Architect-Pragmatic-Programmers/dp/1680502093) book, Michael Keeling ### System Architecture - ["Enterprise Integration Patterns : Designing, Building, and Deploying Messaging Solutions"](https://www.enterpriseintegrationpatterns.com/) book and catalogue, Gregor Hohpe, Bobby Woolf - ["Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems "](https://www.amazon.com/Designing-Data-Intensive-Applications-Reliable-Maintainable/dp/1449373321) book, Martin Kleppman - ["Building Evolutionary Architectures: Support Constant Change"](https://www.amazon.com/Building-Evolutionary-Architectures-Support-Constant/dp/1491986360) book, Neal Ford - ["Building Microservices: Designing Fine-Grained Systems"](https://www.amazon.com/Building-Microservices-Designing-Fine-Grained-Systems/dp/1491950358) book, Sam Newman ### Design - ["Refactoring: Improving the Design of Existing Code"](https://www.amazon.com/Refactoring-Improving-Design-Existing-Code/dp/0201485672) book, Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts - ["Clean Code: A Handbook of Agile Software Craftsmanship"](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) book, Robert C. Martin - ["Agile Principles, Patterns, and Practices in C#"](https://www.amazon.com/Agile-Principles-Patterns-Practices-C/dp/0131857258) book, Robert C. Martin - ["Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development (3rd Edition)"](https://www.amazon.com/Applying-UML-Patterns-Introduction-Object-Oriented/dp/0131489062) book, Craig Larman - ["Working Effectively with Legacy Code"](https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052) book, Michael Feathers - ["Code Complete: A Practical Handbook of Software Construction, Second Edition"](https://www.amazon.com/Code-Complete-Practical-Handbook-Construction/dp/0735619670) book, Steve McConnell - ["Design Patterns: Elements of Reusable Object-Oriented Software"](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612) book, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides ### Craftsmanship - ["The Clean Coder: A Code of Conduct for Professional Programmers"](https://www.amazon.com/Clean-Coder-Conduct-Professional-Programmers/dp/0137081073) book, Robert C. Martin - ["The Pragmatic Programmer: From Journeyman to Master"](https://www.amazon.com/Pragmatic-Programmer-Journeyman-Master/dp/020161622X) book, Andrew Hunt ### Testing - ["The Art of Unit Testing: with examples in C#"](https://www.amazon.com/Art-Unit-Testing-examples/dp/1617290890) book, Roy Osherove - ["Unit Test Your Architecture with ArchUnit"](https://blogs.oracle.com/javamagazine/unit-test-your-architecture-with-archunit) article, Jonas Havers - ["Unit Testing Principles, Practices, and Patterns"](https://www.amazon.com/Unit-Testing-Principles-Practices-Patterns/dp/1617296279) book, Vladimir Khorikov - ["Growing Object-Oriented Software, Guided by Tests"](https://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627) book, Steve Freeman, Nat Pryce - [Automated Tests](https://www.kamilgrzybek.com/blog/series/automated-tests) article series, Kamil Grzybek ### UML - ["UML Distilled: A Brief Guide to the Standard Object Modeling Language (3rd Edition)"](https://www.amazon.com/UML-Distilled-Standard-Modeling-Language/dp/0321193687) book, Martin Fowler ### Event Storming - ["Introducing EventStorming"](https://leanpub.com/introducing_eventstorming) book, Alberto Brandolini - ["Awesome EventStorming"](https://github.com/mariuszgil/awesome-eventstorming) GH repository, Mariusz Gil ### Event Sourcing - ["Hands-On Domain-Driven Design with .NET Core: Tackling complexity in the heart of software by putting DDD principles into practice"](https://www.amazon.com/Hands-Domain-Driven-Design-NET-ebook/dp/B07C5WSR9B) book, Alexey Zimarev - ["Versioning in an Event Sourced System"](https://leanpub.com/esversioning) book, Greg Young - [Hands-On-Domain-Driven-Design-with-.NET-Core](https://github.com/PacktPublishing/Hands-On-Domain-Driven-Design-with-.NET-Core) GH repository, Alexey Zimarev - [EventSourcing.NetCore](https://github.com/oskardudycz/EventSourcing.NetCore) GH repository, Oskar Dudycz ================================================ FILE: azure-pipelines.yml ================================================ # ASP.NET Core (.NET Framework) # Build and test ASP.NET Core projects targeting the full .NET Framework. # Add steps that publish symbols, save build artifacts, and more: # https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core trigger: - master pool: vmImage: 'windows-latest' variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' steps: - task: NuGetToolInstaller@1 - task: NuGetCommand@2 inputs: restoreSolution: '$(solution)' - task: VSBuild@1 inputs: solution: '$(solution)' msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: VSTest@2 inputs: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' ================================================ FILE: build/.editorconfig ================================================ [*.cs] dotnet_style_qualification_for_field = false:warning dotnet_style_qualification_for_property = false:warning dotnet_style_qualification_for_method = false:warning dotnet_style_qualification_for_event = false:warning dotnet_style_require_accessibility_modifiers = never:warning csharp_style_expression_bodied_methods = true:silent csharp_style_expression_bodied_properties = true:warning csharp_style_expression_bodied_indexers = true:warning csharp_style_expression_bodied_accessors = true:warning ================================================ FILE: build/Build.cs ================================================ using Nuke.Common; using Nuke.Common.IO; using Nuke.Common.ProjectModel; using Nuke.Common.Tools.DotNet; using static Nuke.Common.Tools.DotNet.DotNetTasks; partial class Build : NukeBuild { /// Support plugins are available for: /// - JetBrains ReSharper https://nuke.build/resharper /// - JetBrains Rider https://nuke.build/rider /// - Microsoft VisualStudio https://nuke.build/visualstudio /// - Microsoft VSCode https://nuke.build/vscode public static int Main () => Execute(x => x.Compile); [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; [Solution] readonly Solution Solution; Target Clean => _ => _ .Before(Restore) .Executes(() => { AbsolutePath.Create(WorkingDirectory).CreateOrCleanDirectory(); }); Target Restore => _ => _ .Executes(() => { DotNetRestore(s => s .SetProjectFile(Solution)); }); Target Compile => _ => _ .DependsOn(Restore) .Executes(() => { DotNetBuild(s => s .SetProjectFile(Solution) .SetConfiguration(Configuration) .EnableNoRestore()); }); Target UnitTests => _ => _ .DependsOn(Compile) .Executes(() => { DotNetTest(s => s .SetProjectFile(Solution) .SetFilter("UnitTests") .SetConfiguration(Configuration) .EnableNoRestore() .EnableNoBuild()); }); Target ArchitectureTests => _ => _ .DependsOn(UnitTests) .Executes(() => { DotNetTest(s => s .SetProjectFile(Solution) .SetFilter("ArchTests") .SetConfiguration(Configuration) .EnableNoRestore() .EnableNoBuild()); }); Target BuildAndUnitTests => _ => _ .Triggers(ArchitectureTests) .Executes(() => { }); } ================================================ FILE: build/BuildIntegrationTests.cs ================================================ using System; using System.Linq; using Nuke.Common; using Nuke.Common.IO; using Nuke.Common.Tools.Docker; using Nuke.Common.Tools.DotNet; using Utils; using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.Tools.DotNet.DotNetTasks; public partial class Build { static AbsolutePath WorkingDirectory => RootDirectory / ".nuke-working-directory"; static AbsolutePath OutputDirectory => WorkingDirectory / "output"; static AbsolutePath OutputDbUbMigratorBuildDirectory => OutputDirectory / "dbUpMigrator"; static AbsolutePath InputFilesDirectory => WorkingDirectory / "input-files"; static AbsolutePath DatabaseDirectory => RootDirectory / "src" / "Database" / "CompanyName.MyMeetings.Database" / "Scripts"; const string CreateDatabaseScriptName = "CreateDatabase_Linux.sql"; const string InputFilesDirectoryName = "input-files"; Target PrepareInputFiles => _ => _ .DependsOn(Clean) .Executes(() => { string createDatabaseFile = DatabaseDirectory / CreateDatabaseScriptName; string createDatabaseFileTarget = InputFilesDirectory / CreateDatabaseScriptName; CopyFile(createDatabaseFile, createDatabaseFileTarget, FileExistsPolicy.Overwrite); }); const string SqlServerPassword = "123qwe!@#QWE"; const string SqlServerUser = "sa"; const string SqlServerPort = "1401"; Target PrepareSqlServer => _ => _ .DependsOn(PrepareInputFiles) .Executes(() => { DockerTasks.DockerRun(s => s .EnableRm() .SetName("sql-server-db") .SetImage("mcr.microsoft.com/mssql/server") .SetEnv( $"SA_PASSWORD={SqlServerPassword}", "ACCEPT_EULA=Y", "MSSQL_PID=Express") .SetPublish($"{SqlServerPort}:1433") .SetMount($"type=bind,source=\"{InputFilesDirectory}\",target=/{InputFilesDirectoryName},readonly") .EnableDetach()); SqlReadinessChecker.WaitForSqlSever( $"Server=127.0.0.1,{SqlServerPort};Database=master;User={SqlServerUser};Password={SqlServerPassword};Encrypt=False;"); }); Target CreateDatabase => _ => _ .DependsOn(PrepareSqlServer) .Executes(() => { DockerTasks.DockerExec(s => s .EnableInteractive() .SetContainer("sql-server-db") .SetCommand("/bin/sh") .SetArgs("-c", $"./opt/mssql-tools/bin/sqlcmd -d master -i ./{InputFilesDirectoryName}/{CreateDatabaseScriptName} -U {SqlServerUser} -P {SqlServerPassword}")); }); Target CompileDbUpMigratorForIntegrationTests => _ => _ .DependsOn(CreateDatabase) .Executes(() => { var dbUpMigratorProject = Solution.GetAllProjects("DatabaseMigrator").First(); DotNetBuild(s => s .SetProjectFile(dbUpMigratorProject) .SetConfiguration(Configuration) .SetOutputDirectory(OutputDbUbMigratorBuildDirectory) ); }); AbsolutePath DbUpMigratorPath => OutputDbUbMigratorBuildDirectory / "DatabaseMigrator.dll"; readonly string MyMeetingsDatabaseConnectionString = $"Server=127.0.0.1,{SqlServerPort};Database=MyMeetings;User={SqlServerUser};Password={SqlServerPassword};Encrypt=False;"; Target RunDatabaseMigrations => _ => _ .DependsOn(CompileDbUpMigratorForIntegrationTests) .Executes(() => { AbsolutePath migrationsPath = DatabaseDirectory / "Migrations"; DotNet($"{DbUpMigratorPath} {MyMeetingsDatabaseConnectionString} {migrationsPath}"); }); const string MeetingsModuleIntegrationTestsAssemblyName = "CompanyName.MyMeetings.Modules.Meetings.IntegrationTests"; Target BuildMeetingsModuleIntegrationTests => _ => _ .DependsOn(RunDatabaseMigrations) .Executes(() => { var integrationTest = Solution.GetAllProjects(MeetingsModuleIntegrationTestsAssemblyName).First(); DotNetBuild(s => s .SetProjectFile(integrationTest) .DisableNoRestore()); }); const string MyMeetingsDatabaseEnvName = "ASPNETCORE_MyMeetings_IntegrationTests_ConnectionString"; Target RunMeetingsModuleIntegrationTests => _ => _ .DependsOn(BuildMeetingsModuleIntegrationTests) .Executes(() => { var integrationTest = Solution.GetAllProjects(MeetingsModuleIntegrationTestsAssemblyName).First(); Environment.SetEnvironmentVariable( MyMeetingsDatabaseEnvName, MyMeetingsDatabaseConnectionString); DotNetTest(s => s .EnableNoBuild() .SetProjectFile(integrationTest)); }); const string AdministrationModuleIntegrationTestsAssemblyName = "CompanyName.MyMeetings.Modules.Administration.IntegrationTests"; Target BuildAdministrationModuleIntegrationTests => _ => _ .DependsOn(RunDatabaseMigrations) .Executes(() => { var integrationTest = Solution.GetAllProjects(AdministrationModuleIntegrationTestsAssemblyName).First(); DotNetBuild(s => s .SetProjectFile(integrationTest) .DisableNoRestore()); }); Target RunAdministrationModuleIntegrationTests => _ => _ .DependsOn(BuildAdministrationModuleIntegrationTests) .Executes(() => { var integrationTest = Solution.GetAllProjects(AdministrationModuleIntegrationTestsAssemblyName).First(); Environment.SetEnvironmentVariable( MyMeetingsDatabaseEnvName, MyMeetingsDatabaseConnectionString); DotNetTest(s => s .EnableNoBuild() .SetProjectFile(integrationTest)); }); const string UserAccessModuleIntegrationTestsAssemblyName = "CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests"; Target BuildUserAccessModuleIntegrationTests => _ => _ .DependsOn(RunDatabaseMigrations) .Executes(() => { var integrationTest = Solution.GetAllProjects(UserAccessModuleIntegrationTestsAssemblyName).First(); DotNetBuild(s => s .SetProjectFile(integrationTest) .DisableNoRestore()); }); Target RunUserAccessModuleIntegrationTests => _ => _ .DependsOn(BuildUserAccessModuleIntegrationTests) .Executes(() => { var integrationTest = Solution.GetAllProjects(UserAccessModuleIntegrationTestsAssemblyName).First(); Environment.SetEnvironmentVariable( MyMeetingsDatabaseEnvName, MyMeetingsDatabaseConnectionString); DotNetTest(s => s .EnableNoBuild() .SetProjectFile(integrationTest)); }); const string PaymentsModuleIntegrationTestsAssemblyName = "CompanyName.MyMeetings.Modules.Payments.IntegrationTests"; Target BuildPaymentsModuleIntegrationTests => _ => _ .DependsOn(RunDatabaseMigrations) .Executes(() => { var integrationTest = Solution.GetAllProjects(PaymentsModuleIntegrationTestsAssemblyName).First(); DotNetBuild(s => s .SetProjectFile(integrationTest) .DisableNoRestore()); }); Target RunPaymentsModuleIntegrationTests => _ => _ .DependsOn(BuildPaymentsModuleIntegrationTests) .Executes(() => { var integrationTest = Solution.GetAllProjects(PaymentsModuleIntegrationTestsAssemblyName).First(); Environment.SetEnvironmentVariable( MyMeetingsDatabaseEnvName, MyMeetingsDatabaseConnectionString); DotNetTest(s => s .EnableNoBuild() .SetProjectFile(integrationTest)); }); const string SystemIntegrationTestsAssemblyName = "CompanyName.MyMeetings.IntegrationTests"; Target BuildSystemIntegrationTests => _ => _ .DependsOn(RunDatabaseMigrations) .Executes(() => { var integrationTest = Solution.GetAllProjects(SystemIntegrationTestsAssemblyName).First(); DotNetBuild(s => s .SetProjectFile(integrationTest) .DisableNoRestore()); }); Target RunSystemIntegrationTests => _ => _ .DependsOn(BuildSystemIntegrationTests) .Executes(() => { var integrationTest = Solution.GetAllProjects(SystemIntegrationTestsAssemblyName).First(); Environment.SetEnvironmentVariable( MyMeetingsDatabaseEnvName, MyMeetingsDatabaseConnectionString); DotNetTest(s => s .EnableNoBuild() .SetProjectFile(integrationTest)); }); Target RunAllIntegrationTests => _ => _ .DependsOn( RunAdministrationModuleIntegrationTests, RunMeetingsModuleIntegrationTests, RunPaymentsModuleIntegrationTests, RunUserAccessModuleIntegrationTests, RunSystemIntegrationTests) .Executes(() => { }); } ================================================ FILE: build/Configuration.cs ================================================ using System.ComponentModel; using Nuke.Common.Tooling; [TypeConverter(typeof(TypeConverter))] public class Configuration : Enumeration { public static Configuration Debug = new Configuration { Value = nameof(Debug) }; public static Configuration Release = new Configuration { Value = nameof(Release) }; public static implicit operator string(Configuration configuration) { return configuration.Value; } } ================================================ FILE: build/Database.cs ================================================ using System.Linq; using Nuke.Common; using Nuke.Common.Tools.DotNet; using static Nuke.Common.Tools.DotNet.DotNetTasks; public partial class Build { Target CompileDbUpMigrator => _ => _ .Executes(() => { var dbUpMigratorProject = Solution.GetAllProjects("DatabaseMigrator").First(); DotNetBuild(s => s .SetProjectFile(dbUpMigratorProject) .SetConfiguration(Configuration) .SetOutputDirectory(OutputDbUbMigratorBuildDirectory) ); }); [Parameter("Modular Monolith database connection string")] readonly string DatabaseConnectionString; Target MigrateDatabase => _ => _ .Requires(() => DatabaseConnectionString != null) .DependsOn(CompileDbUpMigrator) .Executes(() => { var migrationsPath = DatabaseDirectory / "Migrations"; DotNet($"{DbUpMigratorPath} {DatabaseConnectionString} {migrationsPath}"); }); } ================================================ FILE: build/Directory.Build.props ================================================ ================================================ FILE: build/Directory.Build.targets ================================================ ================================================ FILE: build/SUTCreator.cs ================================================ using System; using System.Collections.Generic; using Nuke.Common; using Nuke.Common.Tools.DotNet; using static Nuke.Common.Tools.DotNet.DotNetTasks; public partial class Build { [Parameter("SUT creator test name to execute")] readonly string SUTTestName; readonly IDictionary TestCasesMap = new Dictionary { {"CleanDatabase", "CompanyName.MyMeetings.SUT.TestCases.CleanDatabaseTestCase.Prepare"}, {"OnlyAdmin", "CompanyName.MyMeetings.SUT.TestCases.OnlyAdminTestCase.Prepare"}, {"CreateMeeting", "CompanyName.MyMeetings.SUT.TestCases.CreateMeeting.Prepare"} }; Target PrepareSUT => _ => _ .Requires(() => SUTTestName != null) .Requires(() => DatabaseConnectionString != null) .Executes(() => { Environment.SetEnvironmentVariable( "MyMeetings_SUTDatabaseConnectionString", DatabaseConnectionString, EnvironmentVariableTarget.Process); var sutTestProject = Solution.GetProject("CompanyName.MyMeetings.SUT"); var fullyQualifiedName = TestCasesMap[SUTTestName]; DotNetTest(s => s .SetProjectFile(sutTestProject) .SetFilter($"FullyQualifiedName={fullyQualifiedName}")); }); } ================================================ FILE: build/Utils/SqlReadinessChecker.cs ================================================ using System; using System.Threading; using Dapper; using System.Data.SqlClient; namespace Utils { public static class SqlReadinessChecker { public static void WaitForSqlSever(string connectionString) { using var connection = new SqlConnection(connectionString); const int maxTryCounts = 30; var tryCounts = 0; while (true) { tryCounts++; try { connection.QuerySingle("SELECT @@Version"); Serilog.Log.Information("Sql Server started"); break; } catch { Serilog.Log.Information("Sql Server not ready"); if (tryCounts > maxTryCounts) { throw new Exception("Sql Server cannot start."); } } Thread.Sleep(2000); } } } } ================================================ FILE: build/_build.csproj ================================================ Exe net8.0 CS0649;CS0169 .. .. 1 true ================================================ FILE: build/_build.csproj.DotSettings ================================================  DO_NOT_SHOW DO_NOT_SHOW DO_NOT_SHOW DO_NOT_SHOW Implicit Implicit ExpressionBody 0 NEXT_LINE True False 120 IF_OWNER_IS_SINGLE_LINE WRAP_IF_LONG False <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> True True True True True True True True True ================================================ FILE: build.cmd ================================================ :; set -eo pipefail :; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) :; ${SCRIPT_DIR}/build.sh "$@" :; exit $? @ECHO OFF powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* ================================================ FILE: build.ps1 ================================================ [CmdletBinding()] Param( [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] [string[]]$BuildArguments ) Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### # CONFIGURATION ########################################################################### $BuildProjectFile = "$PSScriptRoot\build\_build.csproj" $TempDirectory = "$PSScriptRoot\\.nuke\temp" $DotNetGlobalFile = "$PSScriptRoot\\global.json" $DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" $DotNetChannel = "Current" $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 $env:DOTNET_MULTILEVEL_LOOKUP = 0 ########################################################################### # EXECUTION ########################################################################### function ExecSafe([scriptblock] $cmd) { & $cmd if ($LASTEXITCODE) { exit $LASTEXITCODE } } # If dotnet CLI is installed globally and it matches requested version, use for execution if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` $(dotnet --version) -and $LASTEXITCODE -eq 0) { $env:DOTNET_EXE = (Get-Command "dotnet").Path } else { # Download install script $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) # If global.json exists, load expected version if (Test-Path $DotNetGlobalFile) { $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { $DotNetVersion = $DotNetGlobal.sdk.version } } # Install by channel or version $DotNetDirectory = "$TempDirectory\dotnet-win" if (!(Test-Path variable:DotNetVersion)) { ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } } else { ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" } Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } ================================================ FILE: build.sh ================================================ #!/usr/bin/env bash bash --version 2>&1 | head -n 1 set -eo pipefail SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) ########################################################################### # CONFIGURATION ########################################################################### BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" DOTNET_CHANNEL="Current" export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 export DOTNET_MULTILEVEL_LOOKUP=0 ########################################################################### # EXECUTION ########################################################################### function FirstJsonValue { perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" } # If dotnet CLI is installed globally and it matches requested version, use for execution if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then export DOTNET_EXE="$(command -v dotnet)" else # Download install script DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" mkdir -p "$TEMP_DIRECTORY" curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" chmod +x "$DOTNET_INSTALL_FILE" # If global.json exists, load expected version if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") if [[ "$DOTNET_VERSION" == "" ]]; then unset DOTNET_VERSION fi fi # Install by channel or version DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" if [[ -z ${DOTNET_VERSION+x} ]]; then "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path else "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path fi export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" fi echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" ================================================ FILE: docker-compose.yml ================================================ version: '3.4' services: backend: container_name: mymeetings_backend build: context: ./src/ ports: - "5000:8080" networks: - starfish-crm-network environment: - Meetings_MeetingsConnectionString=Server=mymeetingsdb,1433;Database=MyMeetings;User=sa;Password=Test@12345;Encrypt=False; depends_on: - migrator restart: on-failure mymeetingsdb: build: ./src/Database/ platform: linux/amd64 ports: - 1445:1433 networks: - starfish-crm-network migrator: container_name: mymeetings_db_migrator build: context: ./src/ dockerfile: ./Database/Dockerfile_DatabaseMigrator networks: - starfish-crm-network environment: - ASPNETCORE_MyMeetings_IntegrationTests_ConnectionString=Server=mymeetingsdb,1433;Database=MyMeetings;User=sa;Password=Test@12345;Encrypt=False; command: [ "./wait-for-it.sh", "mymeetingsdb:1433", "--timeout=60", "--", "/bin/bash", "/entrypoint_DatabaseMigrator.sh" ] restart: on-failure networks: starfish-crm-network: ================================================ FILE: docs/C4/c1_system_context.puml ================================================ @startuml C1 System Context !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml Person(memberPerson, "Member", "Organizer of meeting groups, meetings, member of group, meeting attendee") Person(adminPerson, "Administrator", "Administrator of the system") System(myMeetingsSystem, "My Meetings") System_Ext(emailSystem, "Email Service") System_Ext(paymentGatewaySystem, "Payment Gateway") Rel(memberPerson, myMeetingsSystem, "Organize meeting groups and participate in meetings") Rel(adminPerson, myMeetingsSystem, "Manage members, meeting groups, meetings") Rel(myMeetingsSystem, emailSystem, "Request email send") Rel(emailSystem, memberPerson, "Send email") Rel(emailSystem, adminPerson, "Send email") Rel(myMeetingsSystem, paymentGatewaySystem, "Delegate the payment") Rel(paymentGatewaySystem, myMeetingsSystem , "Return info about payment") Rel(memberPerson, paymentGatewaySystem , "Pay in") LAYOUT_WITH_LEGEND() @enduml ================================================ FILE: docs/C4/c2_container.puml ================================================ @startuml C2 Containers !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml Person(memberPerson, "Member", "Organizer of meeting groups, meetings, member of group, meeting attendee") Person(adminPerson, "Administrator", "Administrator of the system") System_Boundary(c1, "My Meetings System") { Container(spa, "SPA", "ReactJS", "GUI for the application") Container(api, "My Meetings API", ".NET Core", "Backend") ContainerDb(database, "Database", "Microsoft SQL", "Data about meeting groups, members, meetings etc", "msql_server") } System_Ext(emailSystem, "Email System", "3rd party SMTP server") System_Ext(paymentGateway, "Payment Gateway", "3rd party payment service") Rel(memberPerson, spa, "Uses", "HTTP") Rel(adminPerson, spa, "Uses", "HTTP") Rel(spa, api, "Uses", "HTTP") Rel_R(api, database, "Reads/Writes", "SQL") Rel_L(api, emailSystem, "Sends email using", "SMTP") Rel_D(api, paymentGateway, "Delegate the payment", "HTTP") Rel(memberPerson, paymentGateway, "Make payment via", "HTTP") LAYOUT_WITH_LEGEND() @enduml ================================================ FILE: docs/C4/c3_components.puml ================================================ @startuml C3 Components !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml System_Boundary(c1, "My Meetings System") { Container(spa, "SPA", "ReactJS", "GUI for the application") Boundary(myMeetingsApi, "My Meetings API") { Component(api, "API", ".NET Core API") Component(meetingsModule, "Meetings", ".NET Libraries") 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") { ComponentDb(meetingsModuleData, "Meetings data", "schema") ComponentDb(administrationData, "Administration data", "schema") ComponentDb(userAccessData, "User Access data", "schema") ComponentDb(paymentsData, "Payments data", "schema") ComponentDb(registrationsData, "Registrations data", "schema") } } } Rel(spa, api, "Uses", "HTTP") 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() @enduml ================================================ FILE: docs/C4/c3_components_module.puml ================================================ @startuml C3 Components Module (zoom-in) !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml AddComponentTag("meetings", $bgColor="#4ce065") AddComponentTag("administration", $bgColor="#e3b868") AddComponentTag("eventsBus", $bgColor="#29118a", $fontColor="#ffffff") System_Boundary(c1, "My Meetings System") { Boundary(myMeetingsApi, "My Meetings API") { Component(api, "API", ".NET Core API") Boundary(meetingsModule, "Meetings Module") { Component(meetingsInfrastructure, "Meetings.Infrastructure", ".NET Library", $tags="meetings") Component(meetingsApplication, "Meetings.Application", ".NET Library", $tags="meetings") Component(meetingsDomain, "Meetings.Domain", ".NET Library", $tags="meetings") Component(meetingsIntegrationEvents, "Meetings.IntegrationEvents", ".NET Library", $tags="meetings") ComponentDb(meetingsData, "Meetings data", "schema", $tags="meetings") } Boundary(administrationModule, "Administration Module") { Component(administrationInfrastructure, "Administration.Infrastructure", ".NET Library", $tags="administration") Component(administrationApplication, "Administration.Application", ".NET Library", $tags="administration") Component(administrationDomain, "Administration.Domain", ".NET Library", $tags="administration") Component(administrationIntegrationEvents, "Administration.IntegrationEvents", ".NET Library", $tags="administration") ComponentDb(administrationData, "Administration data", "schema", $tags="administration") } ComponentQueue(eventsBus, "Events Bus", "In memory", $tags="eventsBus") } } Rel(api, meetingsInfrastructure, "Uses") Rel(api, meetingsApplication, "Uses") Rel(meetingsInfrastructure, meetingsApplication, "Uses") Rel(meetingsInfrastructure, meetingsDomain, "Uses") Rel(meetingsApplication, meetingsDomain, "Uses") Rel(meetingsApplication, meetingsIntegrationEvents, "Uses") Rel(meetingsInfrastructure, meetingsData, "Uses") Rel(meetingsInfrastructure, eventsBus, "Uses") Rel(api, administrationInfrastructure, "Uses") Rel(api, administrationApplication, "Uses") Rel(administrationInfrastructure, administrationApplication, "Uses") Rel(administrationInfrastructure, administrationDomain, "Uses") Rel(administrationApplication, administrationDomain, "Uses") Rel(administrationApplication, administrationIntegrationEvents, "Uses") Rel(administrationInfrastructure, administrationData, "Uses") Rel(administrationInfrastructure, eventsBus, "Uses") Rel(administrationApplication, meetingsIntegrationEvents, "Uses") Rel(meetingsApplication, administrationIntegrationEvents, "Uses") @enduml ================================================ FILE: docs/C4/c4_class.puml ================================================ @startuml C4 Code package "Meeting Groups Aggregate" <> { class "MeetingGroupId" << VO >> { } class "MeetingGroup" << Entity, AR >> { -string: _description -DateTime: _createDate -DateTime: _paymentDateTo {static} MeetingGroup CreateBasedOnProposal() Meeting CreateMeeting(..) void SetExpirationDate(DateTime dateTo) void JoinToGroupMember(MemberId memberId) void LeaveGroup(MemberId memberId) void EditGeneralAttributes(...) bool IsMemberOfGroup(MemberId attendeeId) bool IsOrganizer(MemberId memberId) } class "MeetingGroupLocation" << VO >> { string City string CountryCode {static} MeetingGroupLocation Create(...) } class "MeetingGroupMember" << Entity >> { ~DateTime JoinedDate -bool _isActive -DateTime? _leaveDate {static} MeetingGroupMember CreateNew(...) void Leave() ~bool IsMember(MemberId memberId) ~bool IsOrganizer(MemberId memberId) } class "MemberId" << VO >> { } class "MeetingGroupMemberRole" << VO >> { string Value } } class "Member" << Entity, AR >> { } "MeetingGroup" *-- "MeetingGroupId" : of "MeetingGroup" *-- "MeetingGroupLocation" : for "MeetingGroup" "1" *-- "0..*" "MeetingGroupMember" : member of "MeetingGroupMember" *-- "MemberId" : assigned to "MeetingGroupMemberRole" --* "MeetingGroupMember" : is assigned to "Member" o-- "MemberId" : of @enduml ================================================ FILE: docs/PlantUML/Commenting_Conceptual_Model.puml ================================================ @startuml object "Meeting" as Meeting object "Member" as Member object "Meeting Commenting Configuration" as MeetingCommentingConfiguration object "Meeting Comment" as MeetingComment object "Meeting Member Comment Like" as MeetingMemberCommentLike Meeting "1"-->"0..*" MeetingComment : has MeetingCommentingConfiguration "1"-->"1" Meeting : enables\ncommenting\nof MeetingComment "1"-->"0..*" MeetingMemberCommentLike : has MeetingComment "1"-->"0..*" MeetingComment : is reply to Member --> Meeting : comments Member --> MeetingComment : replies to,\nlikes Member --> MeetingCommentingConfiguration : configures @enduml ================================================ FILE: docs/PlantUML/Conceptual_Model.puml ================================================ @startuml scale max 2000 width package "User Access" #f3e8f8 { object Permission object "User Role" as UserRole object User object "User Registration" as UserRegistration enum "User Registration Status" as UserRegistrationStatus { WaitingForConfirmation Confirmed Expired } User "1"-->"0..*" UserRole : has User --> UserRegistration : created for UserRole "1..*"-->"1..*" Permission : has UserRegistration --> UserRegistrationStatus : is in } package "Administration" #ffeddb { object Administrator object "Meeting Group Proposal" as Administration.MeetingGroupProposal enum "Meeting Group Proposal Decision" as MeetingGroupProposalDecision { Accept Reject } enum "Meeting Group Proposal Status" as Administration.MeetingGroupProposalDecisionStatus { Accepted InVerification Rejected } Administrator "1"-->"0..*" MeetingGroupProposalDecision : makes MeetingGroupProposalDecision --> Administration.MeetingGroupProposal : for Administration.MeetingGroupProposal --> Administration.MeetingGroupProposalDecisionStatus: is in Administrator --> User : is a } package "Meetings" #e4f7e4 { object "Meeting" as Meeting object "Member" as Member object "Meeting Group Proposal" as Meeting.MeetingGroupProposal object "Meeting Attendee" as MeetingAttendee object "Meeting Group" as MeetingGroup object "Meeting Not Attendee" as MeetingNotAttendee object "Meeting Waitlist Member" as MeetingWaitlistMember object "Meeting Location" as MeetingLocation object "Member Subscription" as Meeting.MemberSubscription enum "Meeting Group Proposal Status" as Meeting.MeetingGroupProposalStatus { InVerification Accepted Rejected } Member --> Meeting.MeetingGroupProposal : proposes Member "1"-->"0..*" MeetingAttendee : is a Member "1"-->"0..*" MeetingNotAttendee : is a Member "1"-->"0..*" MeetingWaitlistMember : is a Member --> Meeting.MemberSubscription : has Meeting "1"-->"1..*" MeetingAttendee : attendees Meeting "1"-->"0..*" MeetingNotAttendee : not attendees Meeting --> MeetingLocation : has Meeting.MeetingGroupProposal --> Meeting.MeetingGroupProposalStatus : is in MeetingGroup "1"-->"0..*" Meetings : organizes MeetingGroup "0..1"-->"1" Meeting.MeetingGroupProposal : created after acceptance of MeetingWaitlistMember "0..*"-->"1" Meeting : waits for place for Meeting.MemberSubscription --> MeetingGroup : covers Member --> User: is a Meeting.MeetingGroupProposal --> Administration.MeetingGroupProposal : sent to verification } package "Payments" #ffc1c1 { object "Payer" as Payer object "Meeting Fee" as MeetingFee object "Meeting Fee Payment" as MeetingFeePayment object "Subscription" as Payments.Subscription object "Subscription Payment" as SubscriptionPayment object "Subscription Renewal Payment" as SubscriptionRenewalPayment object "Price List" as PriceList object "Price List Item" as PriceListItem enum "Subscription Status" as SubscriptionStatus { Active Expired } enum "Subscription Payment Status" as SubscriptionPaymentStatus { WaitingForPayment Paid Expired } enum "Subscription Renewal Payment Status" as SubscriptionRenewalPaymentStatus { WaitingForPayment Paid Expired } enum "Meeting Fee Payment Status" as MeetingFeePaymentStatus { WaitingForPayment Paid Expired } enum "Price List Item Category" as PriceListItemCategory { New Renewal } enum "Subscription Period" as SubscriptionPeriod { Month HalfYear } Payer "1"-->"0..*" MeetingFee : pays for Payer "1"--> "0..*" Payments.Subscription : buys MeetingFeePayment "0..*"-->"1" MeetingFee : for MeetingFeePayment --> MeetingFeePaymentStatus : is in Payments.Subscription "1"-->"0..*" SubscriptionRenewalPayment: extended by Payments.Subscription --> SubscriptionStatus : is in Payments.Subscription --> SubscriptionPeriod : is for SubscriptionPayment "1..*"-->"1" Payments.Subscription : is for SubscriptionPayment --> SubscriptionPaymentStatus : is in SubscriptionPayment --> SubscriptionPeriod : is for SubscriptionRenewalPayment --> SubscriptionRenewalPaymentStatus: is in SubscriptionRenewalPayment --> SubscriptionPeriod : is for PriceListItem --> SubscriptionPeriod : is for PriceListItem "0..*"-->"1" Country: is for PriceListItem --> PriceListItemCategory : is for PriceList "1"-->"1..*" PriceListItem : contains Payer --> Member: is a Payer --> User: is a Payments.Subscription -- Meeting.MemberSubscription } @enduml ================================================ FILE: docs/architecture-decision-log/0001-record-architecture-decisions.md ================================================ # 1. Record architecture decisions Date: 2019-10-28 ## Status Accepted ## Context As the project is an example of a more advanced monolith architecture, it is necessary to save all architectural decisions in one place. ## Decision For all architectural decisions Architecture Decision Log (ADL) is created. All decisions will be recorded as Architecture Decision Records (ADR). Each ADR will be recorded using [Michael Nygard template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions), which contains following sections: Status, Context, Decision and Consequences. ## Consequences All architectural decisions should be recorded in log. Old decisions should be recorded as well with an approximate decision date. New decisions should be recorded on a regular basis. ================================================ FILE: docs/architecture-decision-log/0002-use_modular-monolith-system-architecture.md ================================================ # 2. Use Modular Monolith System Architecture Date: 2019-07-01 Log date: 2019-10-28 ## Status Accepted ## Context An advanced example of Modular Monolith architecture and tactical DDD implementation in .NET is missing on the internet. ## Decision I decided to create nontrivial application using Modular Monolith architecture and Domain-Driven Design tactical patterns. ## Consequences - All modules must run in one single process as single application (Monolith) - All modules should have maximum autonomy (Modular) - DDD Bounded Contexts will be used to divide monolith into modules - DDD tactical patterns will be used to implement most of modules ================================================ FILE: docs/architecture-decision-log/0003-use_dotnetcore_and_csharp.md ================================================ # 3. Use .NET Core and C# language Date: 2019-07-01 Log date: 2019-10-28 ## Status Accepted ## Context As it is monolith, only one language (or platform) must be selected for implementation. ## Decision I decided to use: - .NET Core platform - it is new generation multi-platform, fully supported by Microsoft and open-source community, optimized and designed to replace old .NET Framework - C# language - most popuplar language in .NET ecosystem, I have 12 years commercial experience - F# will not be used, I don't have commercial experience with it ## Consequences - Whole application will be implemented in C# object-oriented language in .NET Core framework - .NET Core applications can be executed on Windows, MacOS, Linux ================================================ FILE: docs/architecture-decision-log/0004-divide-the-system-into-4-modules.md ================================================ # 4. Divide the system into 4 modules Date: 2019-07-01 Log date: 2019-11-02 ## Status Accepted ## Context The MyMeetings domain contains 4 main subdomains: Meetings (core domain), Administration (supporting subdomain), Payments (supporting subdomain) and User Access (generic domain). We use Modular Monolith architecture so we need to implement one application which solves all requirements from all domains listed above. We need to modularize our system. ## Possible solutions 1. Create one "MyMeetings" module and divide it into sub-modules. This solution is simpler to implement at the beginning. We do not have to set module boundaries and think how to communicate between them. On the other hand, this causes a lack of autonomy and can lead to Big Ball Of Mud anti-pattern. 2. Create 4 modules based on Bounded Contexts which in this scenario maps 1:1 to domains. This solution is more difficult at the beginning. We need to set modules boundaries, communication strategy between modules and have more advanced infrastructure code. It is a more complex solution. On the other hand, it supports autonomy, maintainability, readability. We can develop our Domain Models in all of the Bounded Contexts independently. ## Decision Solution 2. We created 4 modules: Meetings, Administration, Payments, User Access. The key factor here is module autonomy and maintainability. We want to develop each module independently. This is more cleaner solution. It involves more work at the beginning but we want to invest. ## Consequences - We can implement each module/Bounded Context independently. - We need to set clear boundaries between modules and communication strategy between modules (and implement them) - We need to define the API of each module - The API/GUI layer needs to know about all of the modules - We need to create shared libraries/classes to limit boilerplate code which will be the same in all modules - Complexity of the whole solution will increase - Complexity of each module will decrease - We will have clear separation of concerns - In addition to the application, we must divide the data - We will have business concepts modeled in a proper way - without "godclasses" which do everything - We can delegate development of particular module to defined team, work should be done without any conflicts on codebase ================================================ FILE: docs/architecture-decision-log/0005-create-one-rest-api-module.md ================================================ # 5. Create one REST API module Date: 2019-07-01 Log date: 2019-11-04 ## Status Accepted ## Context We need to expose the API of our application to the outside world. For now, we expect one client of our application - FrontEnd SPA application. ## Possible solutions 1. Create one .NET Core MVC host application which contains all endpoints. This host application will have references to all business modules and communicates with them directly:
Host/API references:
Administration module
Meetings module
Payments module
User Access module
2. Create one .NET Core MVC host application and multiple APIs projects per module. Each API project should have endpoints which are handled by particular business module:
Host references:
Administration API references Administration module
Meetings API references Meetings module
Payments API references Payments module
User Access API references User Access module
## Decision Solution 1. Creating separate API projects for each module will add complexity and little value. Grouping endpoints for a particular business module in a special directory is enough. Another layer on top of the module is unnecessary. ## Consequences - We will have only one API layer/module - Each controller has responsibility to delegate Command/Query processing to appropriate module - We don't need to scan other projects than host for controllers, routes and other MVC mechanisms - API configuration is easier - Overall complexity of API layer is lower - Complexity of each controller is a little bit higher - Build time will be shorter (less projects) ================================================ FILE: docs/architecture-decision-log/0006-create-facade-between-api-and-business-module.md ================================================ # 6. Create façade between API and business module Date: 2019-07-01 Log date: 2019-11-04 ## Status Accepted ## Context Our API layer should communicate with business modules to fulfill client requests. To support the maximum level of autonomy, each module should expose a minimal set of operations (the module API/contract/interface). ## Decision Each module will provide implementation for one interface with 3 methods:
```csharp Task ExecuteCommandAsync(ICommand command); Task ExecuteCommandAsync(ICommand command); Task ExecuteQueryAsync(IQuery query); ``` This interface will act as a façade (Façade pattern) between API and module. Only Commands, Queries and returned objects (which are part of this interface) should be visible to the API. Everything else should be hidden behind the façade (module encapsulation). ## Consequences - API can communicate with the module only by façade (the interface). - Implementation of API is simpler - We can change module implementation and API does not require change if the interface is not changed - We need to focus on module encapsulation, sometimes it involves additional work (like instantiation using internal constructors) ================================================ FILE: docs/architecture-decision-log/0007-use-cqrs-architectural-style.md ================================================ # 7. Use CQRS architectural style Date: 2019-07-01 Log date: 2019-11-04 ## Status Accepted ## Context Our application should handle 2 types of requests - reading and writing.
For now, it looks like:
- for reading, we need data model in relational form to return data in tabular/flattened way (tables, lists, dictionaries). - for writing, we need to have a graph of objects to perform more sophisticated work like validations, business rules checks, calculations. ## Decision We applied the CQRS architectural style/pattern for each business module. Each module will have a separate model for reading and writing. For now, it will be the simplest CQRS implementation when the read model is immediate consistent. This kind of separation is useful even in simple modules like User Access. ## Consequences - Façade method of each module should take as parameter only Command or Query object - We have optimized models for writes and reads (SRP principle). - We can process Commands and Queries in different ways - As Command or Query is an object, we can easily serialize them and save/log them. ================================================ FILE: docs/architecture-decision-log/0008-allow-return-result-after-command-processing.md ================================================ # 8. Allow return result after command processing Date: 2019-07-01 Log date: 2019-11-04 ## Status Accepted ## Context The theory of the CQRS and the CQS principle says that we should not return any information as the result of Command processing. The result should be always "void". However, sometimes we need to return some data immediately as part of the same request. ## Decision We decided to allow in some cases return results after command processing. Especially, when we create something and we need to return the ID of created object or don't know if request is Command or Query (like Authentication). ## Consequences - We will have two definitions of Commands and CommandHandlers - with and without result - It will add some complexity to processing commands (like implementation of decorators) - We can immediately return the ID of created object/resource. We don't need a second call (query) to retrieve this ID. - We should be careful to not overuse this approach (sticking as much as possible to the CQRS) ================================================ FILE: docs/architecture-decision-log/0009-use-2-layered-architectural-style-for-reads.md ================================================ # 9. Use 2 layered architectural style for reads Date: 2019-07-01 Log date: 2019-11-04 ## Status Accepted ## Context We applied the CQRS style (see [ADR 7. Use CQRS architectural style](007-use-cqrs-architectural-style.md)), now we need to decide how to handle reading (querying) requests. ## Decision We will use 2 layered architecture to handle queries: API layer and Application Service layer. As we applied the CQRS and created a separated read model, querying should be straightforward so 2 layers are enough. The API layer is responsible for Query creation based on HTTP request and the module Application layer is responsible for query handling. ## Consequences - Whole query handling logic is in Application Service layer - Application Service layer is coupled to the database and querying framework - Solution is simple, easy to understand - We don't abstract over database - Performance is better (no object mapping between layers, querying database almost immediately) ================================================ FILE: docs/architecture-decision-log/0010-use-clean-architecture-for-writes.md ================================================ # 10. Use Clean Architecture for writes Date: 2019-07-01 Log date: 2019-11-05 ## Status Accepted ## Context We applied the CQRS style (see [ADR #7](0007-use-cqrs-architectural-style.md)), now we need to decide how to handle writing operations (Commands). ## Decision We will use **Clean Architecture** to handle commands with 4 layers: **API layer**, **Application Service layer**, **Infrastructure layer** and **Domain layer**.
We need to add Domain layer because domain logic will be complex and we want to isolate this logic from other stuff like infrastructure or API. Isolation of domain logic supports testing, maintainability and readability. ## Consequences - Complexity of the whole solution is higher - we need to add two more layers - domain and infrastructure - Complexity of implementation of business logic will be smaller - we can focus only on business concerns on this layer - Complexity of implementation of infrastructure will be smaller - we can focus only on infrastructure concerns on this layer - Business logic will be testable (no references to other layers) - Business logic will be independent of persistence (to some level) - In the Domain layer, we will have the same level of abstraction (close to business) - Application layer will have louse coupling to the Domain layer and Infrastructure layer (depend on abstractions only) - We use one of the most popular application architecture - developers are familiar with it ================================================ FILE: docs/architecture-decision-log/0011-create-rich-domain-models.md ================================================ # 11. Create rich Domain Models Date: 2019-07-01 Log date: 2019-11-05 ## Status Accepted ## Context We need to create Domain Models for all of the modules. Each Domain Model should represent a solution that solves a particular set of Domain problems (implements business logic). ## Possible solutions 1. Create Anemic Domain Model (Data Model) and implement *Transaction Script* pattern together with *Active Record* pattern 2. Put all business logic to database in stored procedures 3. Create a Rich Domain Model ## Decision Solution number 3 - Rich Domain Model
1 - no, because the procedural style of coding will not be enough. We want to focus on behavior, not on the data.
2 - no, keeping business logic in the database is not a good idea in that case, object-oriented programming is better than T-SQL to model our domain and we don't have performance architectural drivers to resign from OOD.
We expect complex business logic with different rules, calculations and processing so we want to get as much as possible from Object-Oriented Design principles like abstraction, encapsulation, polymorphism. We want to mutate the state of our objects only through methods (abstraction) to encapsulate all logic and hide implementation details from the client (the Application Service Layer and Unit Tests).
## Consequences - All objects should be encapsulated (private by default principle) - Encapsulation of objects implies more work in infrastructure (mapping to private fields, collections is harder) - Encapsulation of objects decreases to some level testability of these objects (Object-Oriented Design vs Testable Design) - All public methods of domain objects create Domain Model API - Implementation details of business logic are hidden - Clients of Domain Model are easier to implement - Better object-oriented programming skills are required to implement Rich Domain Model - Is easier to protect business rules/invariants using Rich Domain Model ================================================ FILE: docs/architecture-decision-log/0012-use-domain-driven-design-tactical-patterns.md ================================================ # 12. Use Domain-Driven Design tactical patterns Date: 2019-07-01 Log date: 2019-11-05 ## Status Accepted ## Context We decided to use the Clean Architecture ([ADR #10](0010-use-clean-architecture-for-writes.md)) and create Rich Domain Models ([ADR #11](0011-create-rich-domain-models.md)) for each module. We need to define or use some construction elements / building blocks to implement our architecture and business logic. ## Decision We decided to use **Domain-Driven Design** tactical patterns. They focus on the Domain Model implementation. Especially we will use the following building blocks: - Command - public method on Aggregate (behavior) - Domain Event - the immutable class which represents important fact occurred on a special point of time (behavior) - Entity - class with identity (identity cannot change) with mutable attributes which represents concept from domain - Value Object - immutable class without an identity which represents concept from domain - Aggregate - cluster of domain objects (Entities, Value Objects) with one class entry point (Entity as Aggregate Root) which defines the boundary of transaction/consistency and protects business rules and invariants - Repository - collection-like abstraction to persist and load particular Aggregate - Domain Service - stateless service to execute some business logic which does not belong to any of Entity/Value Object ## Consequences - We need to define entities and value objects - We need to define aggregates boundaries - We need to add repositories for each aggregate - We can invoke only public methods on Aggregate Roots, everything else should be hidden - Developers need to be familiar with DDD tactical patterns ================================================ FILE: docs/architecture-decision-log/0013-protect-business-invariants-using-exceptions.md ================================================ # 13. Protect business invariants using exceptions Date: 2019-07-01 Log date: 2019-11-05 ## Status Accepted ## Context Aggregates should check business invariants. When the invariant is broken, we should stop processing and return an error immediately to the client. ## Possible solutions ### 1. Use exceptions #### Pros - we can stop processing immediately (fail-fast) - popular approach in C# - we don't need to check the result of each method (if-else statements) - we can catch all Business Exceptions in one place and translate them (for example in the API layer to some HTTP response code). #### Cons - indirection - little performance impact - for special cases, we need to add a specific catch. ### 2. Return Result object #### Pros - no indirection - no performance impact - signature method is more descriptive. ### Cons - we need to add checks result of each method (if-else statements) - approach is less-known in the C# world - it needs a library or more coding to support Results ## Decision Solution number 1 - Use exceptions.
Performance cost of throwing an exception is irrelevant, we don't want too many if/else statements in entities, more familiar with exceptions approach. ## Consequences - We need to add special *BusinessException* class to separate business rules validation exceptions from other exceptions - We need to create different business exceptions for each business rule - We will have a small performance impact (throwing exceptions) - We will have generic mechanism which catches *BusinessException* - We will not have a lot of if/else statements in Entities/Value Objects to check method results - Some monitoring tools logs automatically each exception. If we want to use one of this tool we should be aware of this and figure it out proper solution ================================================ FILE: docs/architecture-decision-log/0014-event-driven-communication-between-modules.md ================================================ # 14. Event-driven communication between modules Date: 2019-07-15 Log date: 2019-11-09 ## Status Accepted ## Context Each module should be autonomous. However, communication between them must take place. We have to decide what will be the preferred way of communication and integration between modules. ## Possible solutions ### 1. Direct method call (synchronous) Each Module will expose a set of methods (interface, module API) which can be called by other modules directly. #### Pros - easier implementation - no indirection - more natural in the monolith architecture - supports immediate consistency #### Cons - less autonomy - strong coupling between modules - direct method call is blocking - module has a dependency on another module ### 2. Event-driven (asynchronous) Each module will publish a specific set of events. Other modules can subscribe to specific events. It is the implementation of _Publish/Subscribe_ pattern. #### Pros - more autonomy - coupling is only to middleware/broker of events - no blocking communication - stronger modules boundaries - module does not have a dependency on another module #### Cons - indirection - more complex solution - middleware/broker is needed - does not support immediate consistency ## Decision Solution number 2 - Event-driven (asynchronous)
We want to achieve the maximum level of autonomy and loose coupling between modules. Moreover, we don't want dependencies between modules. We allow direct calls in the future, but this should be an exception, not a rule. ## Consequences - We need to implement the Publish/Subscribe pattern - Solution will be more complex - Modules will have more autonomy - Modules will have coupling to broker/middleware - During modules integration, eventual consistency will occur (asynchronous communication) - Events become Published Language of our Bounded Contexts (modules) - Events structure should be stable as much as possible ================================================ FILE: docs/architecture-decision-log/0015-use-in-memory-events-bus.md ================================================ # 15. Use In-Memory Events Bus Date: 2019-07-15 Log date: 2019-11-09 ## Status Accepted ## Context As we want to base inter-modular communication on asynchronous communication in the form of event-driven architecture, we need some "events bus" to do that. ## Possible solutions ### 1. In Memory Events Bus In memory Publish/Subscribe implementation without any external component. #### Pros - very easy to implement - no network communication needed - performance (it depends) - no need to learn anything - simple solution #### Cons - does not support more advanced integration scenarios, everything needs to be implemented - does not have configuration - does not have other features (who have messaging brokers) ### 2. Message Broker External middleware component. It could be a low-level broker (like RabbitMQ) or a high-level broker (like MassTransit, NServiceBus). #### Pros - only integration with platform code needed - more advanced integration scenarios - richness of configuration - a lot of features #### Cons - complex solution - network communication - new platform learning needed - performance (it depends) ## Decision Solution number 1 - In Memory Events Bus
At that moment we don't see more advanced integration scenarios in our system than simple publish/subscribe scenario. We decided to follow the simplest scenario and if it will be necessary - move to more advanced. ## Consequences - We need to implement Publish/Subscribe in memory - All modules will have dependency to In Memory Events Bus to publish events/subscribe to events - if we ever want to separate a module to another process (microservices architecture), we will need to switch to middleware ================================================ FILE: docs/architecture-decision-log/0016-create-ioc-container-per-module.md ================================================ # 16. Create an IoC Container per module Date: 2019-07-15 Log date: 2019-11-09 ## Status Accepted ## Context For each module, when we process particular Command or Query, we need to resolve a graph of objects. We need to decide how dependencies of objects will be resolved. ## Possible solutions ### 1. One IoC Container for whole application One IoC container located in the host project. #### Pros - standard approach - dependencies configured in one place #### Cons - couples host application with all of projects, libraries - modules autonomy decreases - strong coupling ### 2. IoC Container per module Multiple IoC containers per modules. #### Pros - module autonomy - loose coupling - host application has dependency only to Application Service Layer #### Cons - duplicated code - non-standard approach ## Decision Solution number 2 - IoC Container per module
IoC Container per module supports the autonomy of the module and louse coupling so this is a more important aspect for us than duplicated code in some places. ## Consequences - Create and maintain an IoC Container for each module - Implementation is not standard, but still acceptable easy - We can add dependencies to module and other modules are intact ================================================ FILE: docs/architecture-decision-log/0017-implement-archictecture-tests.md ================================================ # 17. Implement Architecture Tests Date: 2019-11-16 ## Status Accepted ## Context In some cases it is not possible to enforce the application architecture, design or established conventions using compiler (compile-time). For this reason, code implementations can diverge from the original design and architecture. We want to minimize this behavior, not only by code review. ## Decision We decided to implement Unit Tests for our architecture.
We will implement tests for each module separately and one tests library for general architecture. We will use _NetArchTest_ library which was created exactly for this purpose. ## Consequences - We will have quick feedback about breaking the design rules - Unit tests for architecture are documenting our architecture to some level - We will have dependency to external library - We need to implement some _"reflection-based"_ code to check some rules, because library does not provide everything what we need - This kind of tests are a bit slower than normal unit tests (because of reflection) - More tests to maintain ================================================ FILE: docs/catalog-of-terms/Aggregate-DDD/README.md ================================================ # Aggregate (DDD) ## Definition *Cluster ENTITES and VALUE OBJECTS into AGGREGATES and define boundaries around each. Choose one ENTITY to be the root of each AGGREGATE, and control all access to the objects inside the boundary through the root. Transient references to internal members can be passed out for use within a single operation only. Because the root controls access, it cannot be blindsided by changes to the internals. This arrangement makes it practical to enforce all invariants for objects in the AGGREGATE and for the AGGREGATE as a whole in any state change.* Source: [Domain-Driven Design: Tackling Complexity in the Heart of Software, Eric Evans](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215) ## Example ### Model ![](http://www.plantuml.com/plantuml/png/FOx13i8m34FlV0LyG9Sxfo7rHx8cTDFQPaeJJV3rT1SkjcpPqfkxePhNSdjiBHKdTYttrUpeJm35SygRhRvuPqtIZ9jDIIhiMR-VXNUeGbvGGvKcPKp3UGaHGSLkh42IEYGqB9A3lCFeQeTNpiePZKEC4V2Vnd4wBfoP6mt_0G00) ### Code ```csharp public class MeetingGroup : Entity, IAggregateRoot { public MeetingGroupId Id { get; private set; } private string _name; private string _description; private MeetingGroupLocation _location; private MemberId _creatorId; private List _members; private DateTime _createDate; private DateTime? _paymentDateTo; internal static MeetingGroup CreateBasedOnProposal( MeetingGroupProposalId meetingGroupProposalId, string name, string description, MeetingGroupLocation location, MemberId creatorId) { return new MeetingGroup(meetingGroupProposalId, name, description, location, creatorId); } private MeetingGroup() { // Only for EF. } private MeetingGroup(MeetingGroupProposalId meetingGroupProposalId, string name, string description, MeetingGroupLocation location, MemberId creatorId) { this.Id = new MeetingGroupId(meetingGroupProposalId.Value); this._name = name; this._description = description; this._creatorId = creatorId; this._location = location; this._createDate = SystemClock.Now; this.AddDomainEvent(new MeetingGroupCreatedDomainEvent(this.Id, creatorId)); this._members = new List(); this._members.Add(MeetingGroupMember.CreateNew(this.Id, this._creatorId, MeetingGroupMemberRole.Organizer)); } public void EditGeneralAttributes(string name, string description, MeetingGroupLocation location) { this._name = name; this._description = description; this._location = location; this.AddDomainEvent(new MeetingGroupGeneralAttributesEditedDomainEvent(this._name, this._description, this._location)); } public void JoinToGroupMember(MemberId memberId) { this.CheckRule(new MeetingGroupMemberCannotBeAddedTwiceRule(_members, memberId)); this._members.Add(MeetingGroupMember.CreateNew(this.Id, memberId, MeetingGroupMemberRole.Member)); } public void LeaveGroup(MemberId memberId) { this.CheckRule(new NotActualGroupMemberCannotLeaveGroupRule(_members, memberId)); var member = this._members.Single(x => x.IsMember(memberId)); member.Leave(); } public void SetExpirationDate(DateTime dateTo) { _paymentDateTo = dateTo; this.AddDomainEvent(new MeetingGroupPaymentInfoUpdatedDomainEvent(this.Id, _paymentDateTo.Value)); } public Meeting CreateMeeting( string title, MeetingTerm term, string description, MeetingLocation location, int? attendeesLimit, int guestsLimit, Term rsvpTerm, MoneyValue eventFee, List hostsMembersIds, MemberId creatorId) { this.CheckRule(new MeetingCanBeOrganizedOnlyByPayedGroupRule(_paymentDateTo)); this.CheckRule(new MeetingHostMustBeAMeetingGroupMemberRule(creatorId, hostsMembersIds, _members)); return Meeting.CreateNew( this.Id, title, term, description, location, MeetingLimits.Create(attendeesLimit, guestsLimit), rsvpTerm, eventFee, hostsMembersIds, creatorId); } internal bool IsMemberOfGroup(MemberId attendeeId) { return _members.Any(x => x.IsMember(attendeeId)); } internal bool IsOrganizer(MemberId memberId) { return _members.Any(x => x.IsOrganizer(memberId)); } } ``` ### Description Classes `MeetingGroup`, `MeetingGroupLocation`, `MeetingGroupId`, `MeetingGroupMember', 'MeetingGroupMemberRole` form the **Aggregate**. `MeetingGroup` acts as the **AggregateRoot** (*Choose one ENTITY to be the root of each AGGREGATE*). AggregateRoot has a global identifier (`Id`) and public methods to change state of the *Aggregate*. The rest is encapsulated (*Because the root controls access, it cannot be blindsided by changes to the internals*). For each public method, the invariants are checked first (`CheckRule`) (*This arrangement makes it practical to enforce all invariants for objects in the AGGREGATE and for the AGGREGATE as a whole in any state change*). ## Additional References - [DDD_Aggregate (Martin Fowler)](https://martinfowler.com/bliki/DDD_Aggregate.html) ================================================ FILE: docs/catalog-of-terms/Aggregate-DDD/aggregate-ddd.puml ================================================ @startuml Aggregate package "Meeting Groups Aggregate" <> { class "MeetingGroupId" << VO >> { } class "MeetingGroup" << Entity, AR >> { -string: _description -DateTime: _createDate -DateTime: _paymentDateTo {static} MeetingGroup CreateBasedOnProposal() Meeting CreateMeeting(..) void SetExpirationDate(DateTime dateTo) void JoinToGroupMember(MemberId memberId) void LeaveGroup(MemberId memberId) void EditGeneralAttributes(...) bool IsMemberOfGroup(MemberId attendeeId) bool IsOrganizer(MemberId memberId) } class "MeetingGroupLocation" << VO >> { string City string CountryCode {static} MeetingGroupLocation Create(...) } class "MeetingGroupMember" << Entity >> { ~DateTime JoinedDate -bool _isActive -DateTime? _leaveDate {static} MeetingGroupMember CreateNew(...) void Leave() ~bool IsMember(MemberId memberId) ~bool IsOrganizer(MemberId memberId) } class "MemberId" << VO >> { } class "MeetingGroupMemberRole" << VO >> { string Value } } class "Member" << Entity, AR >> { } "MeetingGroup" *-- "MeetingGroupId" : of "MeetingGroup" *-- "MeetingGroupLocation" : for "MeetingGroup" "1" *-- "0..*" "MeetingGroupMember" : member of "MeetingGroupMember" *-- "MemberId" : assigned to "MeetingGroupMemberRole" --* "MeetingGroupMember" : is assigned to "Member" o-- "MemberId" : of @enduml ================================================ FILE: docs/catalog-of-terms/Command/README.md ================================================ # Command ## Definition *A command is a request made to do something. A command represents the intention of a system’s user regarding what the system will do to change its state.* *Command characteristics:* - *The result of a command can be either success or failure; the result is an [Event(s)](../Event/)* - *In case of success, state change(s) must have occurred somewhere (otherwise nothing happened)* - *Commands should be named with a verb, in the present tense or infinitive, and a nominal group coming from the domain (entity of aggregate type)* Source: [Open Agile Architecture](https://pubs.opengroup.org/architecture/o-aa-standard/#KLP-EDA-event-command) ## Example ### Model ### Code `Meeting` class in [Domain Model](../Domain-Model/): ```csharp public void Cancel(MemberId cancelMemberId) { this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term)); if (!_isCanceled) { _isCanceled = true; _cancelDate = SystemClock.Now; _cancelMemberId = cancelMemberId; this.AddDomainEvent(new MeetingCanceledDomainEvent(this.Id, _cancelMemberId, _cancelDate.Value)); } } ``` `CancelMeetingCommand` class in [Application Layer](../Application-Layer/) ```csharp public class CancelMeetingCommand : CommandBase { public CancelMeetingCommand(Guid meetingId) { MeetingId = meetingId; } public Guid MeetingId { get; } } internal class CancelMeetingCommandHandler : ICommandHandler { private readonly IMeetingRepository _meetingRepository; private readonly IMemberContext _memberContext; internal CancelMeetingCommandHandler(IMeetingRepository meetingRepository, IMemberContext memberContext) { _meetingRepository = meetingRepository; _memberContext = memberContext; } public async Task Handle(CancelMeetingCommand request, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); meeting.Cancel(_memberContext.MemberId); return Unit.Value; } } ``` ### Description In the example above, we can distinguish between 2 types of Commands: - as an object in the application layer [Parameter Object Pattern](../Parameter-Object-Pattern/) - in the form of a method on the object (in DDD on the [Aggregate](../Aggregate-DDD/) The most important thing is that the Command can be rejected until the state changes. In both the `CommandHandler` (invalid MeetingId) and the `Cancel` method (business rule broken), an exception can be thrown and the Command is then rejected and all uncomitted changes are rolled back (state does not change). ================================================ FILE: docs/catalog-of-terms/Command/command.puml ================================================ @startuml Command class CancelMeetingCommand { MeetingGroupId: Id } note top of CancelMeetingCommand : Command as part of Application Layer class Meeting { void Cancel(MemberId cancelMemberId) } note top of Meeting : Command defined in Domain Model (on an Aggregate) @enduml ================================================ FILE: docs/catalog-of-terms/Decorator-Pattern/README.md ================================================ # Decorator Pattern ## Definition *In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. The decorator pattern is often useful for adhering to the [Single Responsibility Principle](../Single-Responsibility-Principle/), as it allows functionality to be divided between classes with unique areas of concern.* Source: [Wikipedia](https://en.wikipedia.org/wiki/Decorator_pattern) ## Example ### Model ![](http://www.plantuml.com/plantuml/png/bPFDhjCm48NtVehPqGitVO3eghfLMO044eA-W1jF4Wjx9h8dL8YVPyVDJqL9g66HHMRE-Sx46Jz7qK5wxrJbT8pm7b4iDV70986Tmm3V5BpQ6pDrzY981d7paCeVqVCNN7P-g0ctz1tOUqrcwgy2Pibr96Ery3Z8fwGO0om9XbfN26ydmvlqE0nFbk0ubNQ3QMnivk8Z73HLw9mMotJapqW3yMTcved_EtAfpMSilpiRSq-UINh7JPDCj-pNM76udEdJyVQ4Lc5G9CZc0IvKOa5mM0jmdM6NPUehW0yOQWu-WXlbZt3g1GnZf1UI-bMhgK5e-GmZ0hZ3HC2ea0nS4fLghK50tybNyEXF6A9IAxjQ63vJiRlkJ0cMXl3_4wkvD6l-lXGbnFGQbuxbFrkQL7RNYhwxdzuEmgZck3niLEPu-T6smJQjRB_l_ho09LZVEVH84Y4_sB-ZrFs5Wss6aFE_N0UOKO1HFIEPthjV) ### Code ```csharp 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) { return await _decorated.Handle(command, cancellationToken); } using ( LogContext.Push( new RequestLogEnricher(_executionContextAccessor), new CommandLogEnricher(command))) { try { this._logger.Information( "Executing command {Command}", command.GetType().Name); var result = await _decorated.Handle(command, cancellationToken); this._logger.Information("Command {Command} processed successful", command.GetType().Name); return result; } 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))); } } } } ``` ### Description The Logging decorator logs execution, arguments and processing of each Command. This way each log inside a processor has the log context of the processing command. A unique trade of a decorator is that it does both: * It implements `ICommandHandler` (known also as the *component*). * It accepts an implementation of `ICommandHandler` (known also as the *concrete component*). Usually that is done via [Dependency Injection](../Dependency-Injection/). The decorator builds on top of existing functionality provided by the injected `ICommandHandler`, but it does not change the behavior of it. --- Decorator should not be confused with [Strategy](../Strategy-Pattern/)!!! *A decorator lets you change the skin of an object, while Strategy lets you change the guts.* ================================================ FILE: docs/catalog-of-terms/Decorator-Pattern/decorator-pattern.puml ================================================ @startuml class MeetingsController { +AddMeetingAttendee() +RemoveMeetingAttendee() } class AddMeetingAttendeeCommand class RemoveMeetingAttendeeCommand class Mediator { +Send() } interface ICommandHandler { +Handle(TCommand, CancellationToken) } ~class AddMeetingAttendeeCommandHandler { +Handle(AddMeetingAttendeeCommand, CancellationToken) } ~class RemoveMeetingAttendeeCommandHandler { +Handle(RemoveMeetingAttendeeCommand, CancellationToken) } ~class LoggingCommandHandlerDecorator { +Handle(T, CancellationToken) -Log() } note left of LoggingCommandHandlerDecorator::Log Performs logging, but doesn't change how Handle operates end note hide empty members MeetingsController -down-> Mediator: informs MeetingsController -down-> Mediator: informs Mediator -down-> AddMeetingAttendeeCommand: sends Mediator -down-> RemoveMeetingAttendeeCommand: sends AddMeetingAttendeeCommandHandler -up-> AddMeetingAttendeeCommand: handles RemoveMeetingAttendeeCommandHandler -up-> RemoveMeetingAttendeeCommand: handles AddMeetingAttendeeCommandHandler .right.|> ICommandHandler: implements RemoveMeetingAttendeeCommandHandler .right.|> ICommandHandler: implements LoggingCommandHandlerDecorator ..|> ICommandHandler: implements LoggingCommandHandlerDecorator *..|> ICommandHandler: decorates @enduml ================================================ FILE: docs/catalog-of-terms/Dependency-Injection/README.md ================================================ # Dependency Injection ## Definition *Dependency Injection is a technique in which an object receives other objects that it depends on. These other objects are called dependencies.* Source: [Wikipedia](https://en.wikipedia.org/wiki/Dependency_injection) ## Example ### Model ![](http://www.plantuml.com/plantuml/png/HSun3i8m30NGdLF00LBlJ1rOE4PgcpOqiIl7KLLEJpeWbl-bfp_yiNeqRoLVRaamD-9c-RguR_KEO74VvkHBcrfbGnLdyG6rm3hRvvXuXQBKShHGL3JtQTZF828eiJeRa685Z1wppa5VeLkfyE2DXLZm24zvCtfI0VfZ-k6mdUV6xhs_) ### Code ```csharp internal class CancelMeetingCommandHandler : ICommandHandler { private readonly IMeetingRepository _meetingRepository; private readonly IMemberContext _memberContext; internal CancelMeetingCommandHandler(IMeetingRepository meetingRepository, IMemberContext memberContext) { _meetingRepository = meetingRepository; _memberContext = memberContext; } public async Task Handle(CancelMeetingCommand request, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); meeting.Cancel(_memberContext.MemberId); return Unit.Value; } } ``` ### Description A `CancelMeetingCommandHandler` needs two collaborators (dependencies) to fulfill its job - repository of meetings (`IMeetingRepository`) and information about member context (`IMemberContext`). It doesn't instance implementation of these interfaces itself - they are provided (injected) via construction (*Constructor Injection*). ================================================ FILE: docs/catalog-of-terms/Dependency-Injection/dependency-injection.puml ================================================ @startuml Dependency Injection class "CancelMeetingCommandHandler" { CancelMeetingCommandHandler(IMeetingRepository meetingRepository, IMemberContext memberContext) } interface "IMeetingRepository" { } interface "IMemberContext" { } "CancelMeetingCommandHandler" -> "IMeetingRepository" : uses "CancelMeetingCommandHandler" -> "IMemberContext" : uses @enduml ================================================ FILE: docs/catalog-of-terms/Domain-Event/README.md ================================================ # Domain Event ## Definition *An event is something that has happened in the past. A **domain event** is, something that happened in the domain that you want other parts of the same domain (in-process) to be aware of. The notified parts usually react somehow to the events.* Source: [Domain events: design and implementation](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation) ## Example ### Model ![](http://www.plantuml.com/plantuml/png/ZL31Ii0m3BttAtgN_S3mC9mUTfemy1xR28Ks7Kag3lNVZMC7BHwyb9UyrvV7cqI1jPNiGWOHlxLd2PnsJPKUuIX8EZE2Ohol1H8zlDh6lpj_yuToYROtZ6oeKo2d6kSQqOYvDb8-hcbJq2O6dY2tasxCIE5mdrUe7wVlGD0bKkGN2EYNFjLvU0tXsoAkP1Rkb-RsOnXwlz6dicSiDehhkFF3_rePFRufKXGtsMkLVW40) ### Code ```csharp public class SubscriptionPaymentCreatedDomainEvent : DomainEventBase { public SubscriptionPaymentCreatedDomainEvent( Guid subscriptionPaymentId, Guid payerId, string subscriptionPeriodCode, string countryCode, string status, decimal value, string currency) { SubscriptionPaymentId = subscriptionPaymentId; PayerId = payerId; SubscriptionPeriodCode = subscriptionPeriodCode; CountryCode = countryCode; Status = status; Value = value; Currency = currency; } public Guid SubscriptionPaymentId { get; } public Guid PayerId { get; } public string SubscriptionPeriodCode { get; } public string CountryCode { get; } public string Status { get; } public decimal Value { get; } public string Currency { get; } } public class DomainEventBase : IDomainEvent { public Guid Id { get; } public DateTime OccurredOn { get; } public DomainEventBase() { this.Id = Guid.NewGuid(); this.OccurredOn = DateTime.UtcNow; } } ``` ### Description A `SubscriptionPaymentCreatedDomainEvent` gets fired within the `SubscriptionPayment` aggregate root. This happens whenever a `Member` who is also a `Payer`, issues a command to buy a `Subscription`. All properties are `get` only, because an event is something that has happened in the past, and you can not change the past. Event details: * `SubscriptionPaymentId` - Auto generated unique Id for the subscription payment. * `PayerId` - The Id of the payer wanting to buy a subscription. * `SubscriptionPeriodCode` - The period of validity for this subscription: * 1 Month * 6 Months * Custom * `CountryCode` - The code of the country that the payer is issuing from. * `Status` - Automatically set to *WaitingForPayment*. * `Value` - The amount to be paid for the chosen subscription period. * `Currecy` - The currency of choice of the payer. --- Business domain events extend `DomainEventBase` which in-turn implements the `IDomainEvent` interface. Base event details: * `Id` - Auto generated unique Id for the event itself. * `OccurredOn` - The point-in-time when the event happened. ================================================ FILE: docs/catalog-of-terms/Domain-Event/domain-event.puml ================================================ @startuml class SubscriptionPaymentCreatedDomainEvent { +SubscriptionPaymentId +PayerId +SubscriptionPeriodCode +CountryCode +Status +Value +Currency } class DomainEventBase interface IDomainEvent { +Id +OccurredOn } IDomainEvent <|-- DomainEventBase: implements DomainEventBase <|-- SubscriptionPaymentCreatedDomainEvent: extends @enduml ================================================ FILE: docs/catalog-of-terms/Entity-DDD/README.md ================================================ # Entity (DDD) ## Definition *When an object is distinguished by its identity, rather than its attributes, make this primary to its definition in the model. Keep the class definition simple and focused on life cycle continuity and identity. Define a means of distinguishing each object regardless of its form or history.* Source: [Domain-Driven Design: Tackling Complexity in the Heart of Software, Eric Evans](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215) ## Example ### Model ![](http://www.plantuml.com/plantuml/png/7OunieCm34JxVugV0nZrgIZ8GOob08DjCV9g67Bwu3IxM-oRUMD3D7Z9Vu-jfkmiRlb_1Oxs9B9u3ik6rMTlOaahf698McXVx7FDibDHzXmj5AsQxsiuUp0pbTWWHgofKOg8MPUWxm2nqkXLiU4AqpIH_6P7XgEBZ5BvxYy0) ### Code ```csharp public class MeetingGroup : Entity, IAggregateRoot { public MeetingGroupId Id { get; private set; } private string _name; private string _description; private MeetingGroupLocation _location; private MemberId _creatorId; private List _members; private DateTime _createDate; private DateTime? _paymentDateTo; internal static MeetingGroup CreateBasedOnProposal( MeetingGroupProposalId meetingGroupProposalId, string name, string description, MeetingGroupLocation location, MemberId creatorId) { return new MeetingGroup(meetingGroupProposalId, name, description, location, creatorId); } private MeetingGroup() { // Only for EF. } private MeetingGroup(MeetingGroupProposalId meetingGroupProposalId, string name, string description, MeetingGroupLocation location, MemberId creatorId) { this.Id = new MeetingGroupId(meetingGroupProposalId.Value); this._name = name; this._description = description; this._creatorId = creatorId; this._location = location; this._createDate = SystemClock.Now; this.AddDomainEvent(new MeetingGroupCreatedDomainEvent(this.Id, creatorId)); this._members = new List(); this._members.Add(MeetingGroupMember.CreateNew(this.Id, this._creatorId, MeetingGroupMemberRole.Organizer)); } public void EditGeneralAttributes(string name, string description, MeetingGroupLocation location) { this._name = name; this._description = description; this._location = location; this.AddDomainEvent(new MeetingGroupGeneralAttributesEditedDomainEvent(this._name, this._description, this._location)); } public void JoinToGroupMember(MemberId memberId) { this.CheckRule(new MeetingGroupMemberCannotBeAddedTwiceRule(_members, memberId)); this._members.Add(MeetingGroupMember.CreateNew(this.Id, memberId, MeetingGroupMemberRole.Member)); } public void LeaveGroup(MemberId memberId) { this.CheckRule(new NotActualGroupMemberCannotLeaveGroupRule(_members, memberId)); var member = this._members.Single(x => x.IsMember(memberId)); member.Leave(); } public void SetExpirationDate(DateTime dateTo) { _paymentDateTo = dateTo; this.AddDomainEvent(new MeetingGroupPaymentInfoUpdatedDomainEvent(this.Id, _paymentDateTo.Value)); } public Meeting CreateMeeting( string title, MeetingTerm term, string description, MeetingLocation location, int? attendeesLimit, int guestsLimit, Term rsvpTerm, MoneyValue eventFee, List hostsMembersIds, MemberId creatorId) { this.CheckRule(new MeetingCanBeOrganizedOnlyByPayedGroupRule(_paymentDateTo)); this.CheckRule(new MeetingHostMustBeAMeetingGroupMemberRule(creatorId, hostsMembersIds, _members)); return Meeting.CreateNew( this.Id, title, term, description, location, MeetingLimits.Create(attendeesLimit, guestsLimit), rsvpTerm, eventFee, hostsMembersIds, creatorId); } internal bool IsMemberOfGroup(MemberId attendeeId) { return _members.Any(x => x.IsMember(attendeeId)); } internal bool IsOrganizer(MemberId memberId) { return _members.Any(x => x.IsOrganizer(memberId)); } } ``` ### Description A *Meeting Group* is something we want to follow in time (it has a life cycle). For this reason, it has its unique identifier (`Id`). Is should be fully encapsulated - you can only mutate its state via exposed *behavior* (no setters). ================================================ FILE: docs/catalog-of-terms/Entity-DDD/entity-ddd.puml ================================================ @startuml Entity class "MeetingGroup" << Entity >> { MeetingGroupId: Id -string: _description -DateTime: _createDate -DateTime: _paymentDateTo {static} MeetingGroup CreateBasedOnProposal() Meeting CreateMeeting(..) void SetExpirationDate(DateTime dateTo) void JoinToGroupMember(MemberId memberId) void LeaveGroup(MemberId memberId) void EditGeneralAttributes(...) bool IsMemberOfGroup(MemberId attendeeId) bool IsOrganizer(MemberId memberId) } @enduml ================================================ FILE: docs/catalog-of-terms/Event/README.md ================================================ # Event ## Definition *An event is something that has happened in the past.* Source: [Domain events: design and implementation](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation) ## Example For examples, see specific kind of events: - [Domain Event](../Domain-Event/) - [Integration Event](../Integration-Event/) ## Related - [Event Driven Architecture](../Event-Driven-Architecture/) - [Event Storming](../Event-Storming/) - [Event Sourcing](../Event-Sourcing/) ================================================ FILE: docs/catalog-of-terms/Event-Driven-Architecture/README.md ================================================ # Event Driven Architecture TODO ================================================ FILE: docs/catalog-of-terms/Event-Sourcing/README.md ================================================ # Event Sourcing TODO ================================================ FILE: docs/catalog-of-terms/Event-Storming/README.md ================================================ # Event Storming TODO ================================================ FILE: docs/catalog-of-terms/Integration-Event/README.md ================================================ # Integration Event TODO ================================================ FILE: docs/catalog-of-terms/README.md ================================================ # Catalog of terms - Act/Arrange/Assert - Actor (Event Storming) - API - Application Layer - [Aggregate (DDD)](Aggregate-DDD/) - Architecture Decision Record (ADR) - Architecture Test - Asynchronous Communication - Audit Log/Trail - Authentication - Authorization - Bounded Context (DDD) - C4 Model - Chain Of Command Pattern - [Command](Command/) - Composition Root - Continuous Integration - Contract - CQRS - Database Change Management - [Decorator Pattern](Decorator-Pattern/) - [Dependency Injection](Dependency-Injection/) - Dependency Inversion - Diagram as text - Domain Centric Architecture - [Domain Event](Domain-Event/) - Domain Layer - Domain Model - Domain Primitive - Domain Services (DDD) - Don't Repeat Yourself Principle - Encapsulation - [Entity (DDD)](Entity-DDD/) - [Event](Event/) - Eventual Consistency - [Event Driven Architecture](Event-Driven-Architecture/) - [Event Sourcing](Event-Sourcing/) - [Event Storming](Event-Storming/) - Events Stream - External System (Event Storming) - Facade Pattern - Factory Pattern - Given When Then - Layered Architecture - Mediator Pattern - Message (Messaging) - Mock - Modularity - Module - Monolith - Idempotency - Immediate Consistency - Immutability - Infrastructure Layer - [Integration Event](Integration-Event/) - Interface - Interface Segregation Principle - Inversion Of Control - Integration Test - Outbox Pattern (aka Store And Forward) - Parameter Object Pattern - Persistence Ignorance - POCO - Policy (EventStorming) - Projection (EventSourcing) - Pure Function - Rich Domain Model - Role-based Access Control - Query - Read Model - Repositories (DDD) - Single Responsibility Principle - [Strategy Pattern](Strategy-Pattern/) - Stub - Synchronous Communication - Transaction (Database) - Ubiquitous Language (DDD) - Unit Of Work Pattern - Unit Test - Write Model - [ValueObject (DDD)](ValueObject-DDD/) ================================================ FILE: docs/catalog-of-terms/Strategy-Pattern/README.md ================================================ # Strategy Pattern ## Definition *The strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.* Source: [Wikipedia](https://en.wikipedia.org/wiki/Strategy_pattern) ## Example ### Model ![](http://www.plantuml.com/plantuml/png/jLDDQnin4BtFhnZIYzFQsxin9lM6f8KU38PUYooDNL5z66cMPd7wtwlrHifs3Oqfs63GpBpvUBFpxYABm8qrS13ofzWJtZoIew3b3RwxF_tm2DA86B4scXmd4sSelMDwOlWDETWx-cZa89ZsBU07ZCIR5tEI_RTTGFd9RPUlKsBO2KcOSNZiulH4ic7gGQM93CIKWPykHgxEaGbREA-QTjDiempwmDgxS-uZGEsj5KvzJdz3eQp4aUoYdRKEMj9N7Vb1IFQXhUf0Wgcu9w_mmTWbt9VyVaYsTllDO9zzdObcid6A1J1Ox2FngKvgqJWERUqLJJ4EnbzJq5vDKNP9QRZHT_Yo_hig7l-tR25shmD9_YPCGm_1syBpgfskKJoUqaXTbKhgzqChGh87Rj6ItLA802y2d2d_oysUbrbpaBNtVZOh6e8on-8vkS-KyqPy1U0y4nhQCVfTRZMVAu-0jJ0cucAxp8AkYh0M7xTB8AUmImU0Vmkdfx9ylNl8hvxD-19Xw2ZJltLTbsTVc73v5OpMM63pURwCuJh7Ug_A-LHLDLhjNNerrlm1) ### Code ```csharp internal class BuySubscriptionCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; private readonly IPayerContext _payerContext; private readonly ISqlConnectionFactory _sqlConnectionFactory; internal BuySubscriptionCommandHandler( IAggregateStore aggregateStore, IPayerContext payerContext, ISqlConnectionFactory sqlConnectionFactory) { _aggregateStore = aggregateStore; _payerContext = payerContext; _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(BuySubscriptionCommand command, CancellationToken cancellationToken) { var priceList = await PriceListFactory.CreatePriceList(_sqlConnectionFactory.GetOpenConnection()); var subscription = SubscriptionPayment.Buy( _payerContext.PayerId, SubscriptionPeriod.Of(command.SubscriptionTypeCode), command.CountryCode, MoneyValue.Of(command.Value, command.Currency), priceList); _aggregateStore.AppendChanges(subscription); return subscription.Id; } } public static class PriceListFactory { public static async Task CreatePriceList(IDbConnection connection) { var priceListItemList = await GetPriceListItems(connection); var priceListItems = priceListItemList .Select(x => new PriceListItemData( x.CountryCode, SubscriptionPeriod.Of(x.SubscriptionPeriodCode), MoneyValue.Of(x.MoneyValue, x.MoneyCurrency), PriceListItemCategory.Of(x.CategoryCode))) .ToList(); // This is place for selecting pricing strategy based on provided data and the system state. IPricingStrategy pricingStrategy = new DirectValueFromPriceListPricingStrategy(priceListItems); return PriceList.Create( priceListItems, pricingStrategy); } public static async Task> GetPriceListItems(IDbConnection connection) { var priceListItems = await connection.QueryAsync("SELECT " + $"[PriceListItem].[CountryCode] AS [{nameof(PriceListItemDto.CountryCode)}], " + $"[PriceListItem].[SubscriptionPeriodCode] AS [{nameof(PriceListItemDto.SubscriptionPeriodCode)}], " + $"[PriceListItem].[MoneyValue] AS [{nameof(PriceListItemDto.MoneyValue)}], " + $"[PriceListItem].[MoneyCurrency] AS [{nameof(PriceListItemDto.MoneyCurrency)}], " + $"[PriceListItem].[CategoryCode] AS [{nameof(PriceListItemDto.CategoryCode)}] " + "FROM [payments].[PriceListItems] AS [PriceListItem] " + "WHERE [PriceListItem].[IsActive] = 1"); var priceListItemList = priceListItems.AsList(); return priceListItemList; } } public class PriceList : ValueObject { private readonly List _items; private readonly IPricingStrategy _pricingStrategy; private PriceList( List items, IPricingStrategy pricingStrategy) { _items = items; _pricingStrategy = pricingStrategy; } public static PriceList Create( List items, IPricingStrategy pricingStrategy) { return new PriceList(items, pricingStrategy); } public MoneyValue GetPrice( string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category) { CheckRule(new PriceForSubscriptionMustBeDefinedRule(countryCode, subscriptionPeriod, _items, category)); return _pricingStrategy.GetPrice(countryCode, subscriptionPeriod, category); } } public interface IPricingStrategy { MoneyValue GetPrice( string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category); } public class DiscountedValueFromPriceListPricingStrategy : IPricingStrategy { private readonly List _items; private readonly MoneyValue _discountValue; public DiscountedValueFromPriceListPricingStrategy( List items, MoneyValue discountValue) { _items = items; _discountValue = discountValue; } public MoneyValue GetPrice(string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category) { var priceListItem = _items.Single(x => x.CountryCode == countryCode && x.SubscriptionPeriod == subscriptionPeriod && x.Category == category); return priceListItem.Value - _discountValue; } } public class DirectValuePricingStrategy : IPricingStrategy { private readonly MoneyValue _directValue; public DirectValuePricingStrategy(MoneyValue directValue) { _directValue = directValue; } public MoneyValue GetPrice(string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category) { return _directValue; } } public class DirectValueFromPriceListPricingStrategy : IPricingStrategy { private readonly List _items; public DirectValueFromPriceListPricingStrategy(List items) { _items = items; } public MoneyValue GetPrice( string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category) { var priceListItem = _items.Single(x => x.CountryCode == countryCode && x.SubscriptionPeriod == subscriptionPeriod && x.Category == category); return priceListItem.Value; } } ``` ### Description Let's introduce the concepts of the strategy pattern, so we can understand how the above example fits this pattern. * **Client** - The calling code. * **Context** - An object which maintains a reference to one of the *concrete strategies* and communicates with the *client*. * **Strategy interface** - An interface or abstract class that the *client* can use to set a concrete strategy at run-time, through the *context*. * **Concrete strategies** - One or more implementations of the *strategy interface*. --- If we have a close look at our example of buying a `Subscription`, we can notice the elements of the strategy pattern. * `BuySubscriptionCommandHandler` is the calling code! Also the handler, indirectly via the `PriceListFactory` sets the current *strategy* of `PriceList`, so their combined interaction represents the **Client**. * `PriceList` is the object which maintains a reference to a pricing strategy, so it represents the **Context**. * `IPricingStrategy` represents the **Strategy interface**. * `DiscountedValueFromPriceListPricingStrategy`, `DirectValueFromPriceListPricingStrategy` and `DirectValuePricingStrategy` are the implementations of `IPricingStrategy` so they represent the **Concrete strategies**. --- The interaction of the `BuySubscriptionCommandHandler` and `PriceListFactory` is a good example of leveraging multiple design patterns. Check out [Factory Pattern](../Factory-Pattern/) to learn more. Strategy should not be confused with [Decorator](../Decorator-Pattern/)!!! *A strategy lets you change the guts of an object, while decorator lets you change the skin.* ================================================ FILE: docs/catalog-of-terms/Strategy-Pattern/strategy-pattern.puml ================================================ @startuml package "Generic" #DDDDDD { class Client { - context } class Context { - strategy + setStrategy(strategy) + do() } interface Strategy { + execute() } class ConcreteStrategyA { + execute() } class ConcreteStrategyB { + execute() } } package "BuySubscription" #DDDDDD { class BuySubscriptionCommandHandler { - connection - PriceListFactory.CreatePriceList(connection) } class PriceList { - _pricingStrategy + Create(items, pricingStrategy) + GetPrice(countryCode, subscriptionPeriod, category) } interface IPricingStrategy { + GetPrice(countryCode, subscriptionPeriod, category) } class DirectValueFromPriceListPricingStrategy { + GetPrice(countryCode, subscriptionPeriod, category) } class DirectValuePricingStrategy { + GetPrice(countryCode, subscriptionPeriod, category) } class DiscountedValueFromPriceListPricingStrategy { + GetPrice(countryCode, subscriptionPeriod, category) } } hide empty members Client -down-|> Context Context *-- Strategy Strategy <|-- ConcreteStrategyA Strategy <|-- ConcreteStrategyB note left of Context::do Calls strategy.execute() end note BuySubscriptionCommandHandler -down-> PriceList PriceList *-- IPricingStrategy IPricingStrategy <|-- DirectValueFromPriceListPricingStrategy IPricingStrategy <|-- DirectValuePricingStrategy IPricingStrategy <|-- DiscountedValueFromPriceListPricingStrategy note left of PriceList::GetPrice Calls _pricingStrategy.GetPrice(...) end note @enduml ================================================ FILE: docs/catalog-of-terms/ValueObject-DDD/README.md ================================================ # ValueObject (DDD) ## Definition *When you care only about the attributes of an element of the model, classify it as a VALUE OBJECT. Make it express the meaning of the attributes it conveys and give it related functionality. Treat the VALUE OBJECT as immutable. Don't give it any identity and avoid the design complexities necessary to maintain ENTITIES.* Source: [Domain-Driven Design: Tackling Complexity in the Heart of Software, Eric Evans](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215) ## Example ### Model ![](http://www.plantuml.com/plantuml/png/7OwnSiCm34FtVaNx0JBtJXwyT-jEYupjc19z51bVylMniW27Ty0TnkPe7aM-VhQQ9OZ3v7jrFzelWE4vB9klCKTZorgTgmzP2-oBlPupxr2KGj1IqQfoLTFPXOYWO7Cs8CqDCZgABablwMAbmJzAyDzyv-nfcYPuz9pq0_fwEFgdaIjT_WO0) ### Code ```csharp public class MoneyValue : ValueObject { public decimal Value { get; } public string Currency { get; } private MoneyValue(decimal value, string currency) { this.Value = value; this.Currency = currency; } public static MoneyValue Of(decimal value, string currency) { CheckRule(new ValueOfMoneyMustNotBeNegativeRule(value)); return new MoneyValue(value, currency); } public static bool operator >(decimal left, MoneyValue right) => left > right.Value; public static bool operator <(decimal left, MoneyValue right) => left < right.Value; public static bool operator >=(decimal left, MoneyValue right) => left >= right.Value; public static bool operator <=(decimal left, MoneyValue right) => left <= right.Value; public static bool operator >(MoneyValue left, decimal right) => left.Value > right; public static bool operator <(MoneyValue left, decimal right) => left.Value < right; public static bool operator >=(MoneyValue left, decimal right) => left.Value >= right; public static bool operator <=(MoneyValue left, decimal right) => left.Value <= right; } ``` ### Description A *Money Value* class represents concept of money. In our Domain, we don't want to follow money in time (it does not have a life cycle). It does not have an identity either. Whole object is **immutable** (`Value` and `Currency` defined as readonly). The comparison is done by comparing attribute values, not identifiers (see `ValueObject` abstract class). ================================================ FILE: docs/catalog-of-terms/ValueObject-DDD/value-object-ddd.puml ================================================ @startuml ValueObject class "MeetingGroup" << ValueObject >> { decimal: Value {readonly} string: Currency {readonly} } @enduml ================================================ FILE: docs/mutation-tests-reports/mutation-report.html ================================================ Your browser doesn't support custom elements. Please use a latest version of an evergreen browser (Firefox, Chrome, Safari, Opera, etc). ================================================ FILE: runIntegrationTests.cmd ================================================ @ECHO OFF SETLOCAL SET CONTAINER_ID= FOR /f %%i IN ('docker ps -q -f name^=myMeetings-integration-db') DO SET CONTAINER_ID=%%i IF "%CONTAINER_ID%"=="" ( ECHO "not found" ) ELSE ( docker rm --force myMeetings-integration-db ) docker run --rm --name myMeetings-integration-db -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=61cD4gE6!" -e "MSSQL_PID=Express" -p 1439:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu TIMEOUT 30 docker cp ./src/Database/CompanyName.MyMeetings.Database/Scripts/CreateDatabase_Linux.sql myMeetings-integration-db:/ docker exec -i myMeetings-integration-db sh -c "/opt/mssql-tools/bin/sqlcmd -d master -i /CreateDatabase_Linux.sql -U sa -P 61cD4gE6!" dotnet build src/ --configuration Release --no-restore SET ASPNETCORE_MyMeetings_IntegrationTests_ConnectionString=Server=localhost,1439;Database=MyMeetings;User=sa;Password=61cD4gE6! dotnet "src/Database/DatabaseMigrator/bin/Release/netcoreapp3.1/DatabaseMigrator.dll" %ASPNETCORE_MyMeetings_IntegrationTests_ConnectionString% "src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations" dotnet test --configuration Release --no-build --verbosity normal src/Modules/Administration/Tests/IntegrationTests/CompanyName.MyMeetings.Modules.Administration.IntegrationTests.csproj dotnet test --configuration Release --no-build --verbosity normal src/Modules/Payments/Tests/IntegrationTests/CompanyName.MyMeetings.Modules.Payments.IntegrationTests.csproj dotnet test --configuration Release --no-build --verbosity normal src/Modules/UserAccess/Tests/IntegrationTests/CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.csproj dotnet test --configuration Release --no-build --verbosity normal src/Modules/Meetings/Tests/IntegrationTests/CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.csproj dotnet test --configuration Release --no-build --verbosity normal src/Tests/IntegrationTests/CompanyName.MyMeetings.IntegrationTests.csproj ================================================ FILE: src/.dockerignore ================================================ **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/azds.yaml **/bin **/charts **/docker-compose* **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md ================================================ FILE: src/.editorconfig ================================================ [*.cs] # SA1309: Field names should not begin with underscore dotnet_diagnostic.SA1309.severity = none # SA1600: Elements should be documented dotnet_diagnostic.SA1600.severity = none # SA1633: File should have header dotnet_diagnostic.SA1633.severity = none # SA1200: Using directives should be placed correctly dotnet_diagnostic.SA1200.severity = none # SA1402: File may only contain a single type dotnet_diagnostic.SA1402.severity = suggestion # SA1101: Prefix local calls with this dotnet_diagnostic.SA1101.severity = none # SA0001: All diagnostics of XML documentation comments has been disabled due to the current project configuration. dotnet_diagnostic.SA0001.severity = none # SA1201: Elements should appear in the correct order dotnet_diagnostic.SA1201.severity = none # SA1204: Static elements should appear before instance elements dotnet_diagnostic.SA1204.severity = none # SA1413: Use trailing comma in multi-line initializers dotnet_diagnostic.SA1413.severity = none # SA1623: Property summary documentation should match accessors dotnet_diagnostic.SA1623.severity = none ================================================ FILE: src/API/CompanyName.MyMeetings.API/CompanyName.MyMeetings.API.csproj ================================================  true InProcess 2b9855d3-f073-44d2-aa45-b15e896794b9 Linux ..\.. bin\Debug\CompanyName.MyMeetings.API.xml ================================================ FILE: src/API/CompanyName.MyMeetings.API/Configuration/Authorization/AttributeAuthorizationHandler.cs ================================================ using Microsoft.AspNetCore.Authorization; namespace CompanyName.MyMeetings.API.Configuration.Authorization { public abstract class AttributeAuthorizationHandler : AuthorizationHandler where TRequirement : IAuthorizationRequirement where TAttribute : Attribute { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement) { var endpoint = (context.Resource as HttpContext).GetEndpoint() as RouteEndpoint; var attribute = endpoint?.Metadata.GetMetadata(); return HandleRequirementAsync(context, requirement, attribute); } protected abstract Task HandleRequirementAsync( AuthorizationHandlerContext context, TRequirement requirement, TAttribute attribute); } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Configuration/Authorization/AuthorizationChecker.cs ================================================ using System.Reflection; using System.Text; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Configuration.Authorization { public static class AuthorizationChecker { public static void CheckAllEndpoints() { var assembly = typeof(Startup).Assembly; var allControllerTypes = assembly.GetTypes().Where(x => x.IsSubclassOf(typeof(ControllerBase))); List notProtectedActionMethods = []; foreach (var controllerType in allControllerTypes) { var controllerHasPermissionAttribute = controllerType.GetCustomAttribute(); if (controllerHasPermissionAttribute != null) { continue; } var actionMethods = controllerType.GetMethods() .Where(x => x.IsPublic && x.DeclaringType == controllerType) .ToList(); foreach (var publicMethod in actionMethods) { var hasPermissionAttribute = publicMethod.GetCustomAttribute(); if (hasPermissionAttribute == null) { var noPermissionRequired = publicMethod.GetCustomAttribute(); if (noPermissionRequired == null) { notProtectedActionMethods.Add($"{controllerType.Name}.{publicMethod.Name}"); } } } } if (notProtectedActionMethods.Any()) { var errorBuilder = new StringBuilder(); errorBuilder.AppendLine("Invalid authorization configuration: "); foreach (var notProtectedActionMethod in notProtectedActionMethods) { errorBuilder.AppendLine($"Method {notProtectedActionMethod} is not protected. "); } throw new ApplicationException(errorBuilder.ToString()); } } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Configuration/Authorization/HasPermissionAttribute.cs ================================================ using Microsoft.AspNetCore.Authorization; namespace CompanyName.MyMeetings.API.Configuration.Authorization { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] internal class HasPermissionAttribute : AuthorizeAttribute { internal const string HasPermissionPolicyName = "HasPermission"; public HasPermissionAttribute(string name) : base(HasPermissionPolicyName) { Name = name; } public string Name { get; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Configuration/Authorization/HasPermissionAuthorizationHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.UserAccess.Application.Authorization.GetUserPermissions; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using Microsoft.AspNetCore.Authorization; namespace CompanyName.MyMeetings.API.Configuration.Authorization { internal class HasPermissionAuthorizationHandler : AttributeAuthorizationHandler< HasPermissionAuthorizationRequirement, HasPermissionAttribute> { private readonly IExecutionContextAccessor _executionContextAccessor; private readonly IUserAccessModule _userAccessModule; public HasPermissionAuthorizationHandler( IExecutionContextAccessor executionContextAccessor, IUserAccessModule userAccessModule) { _executionContextAccessor = executionContextAccessor; _userAccessModule = userAccessModule; } protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, HasPermissionAuthorizationRequirement requirement, HasPermissionAttribute attribute) { var permissions = await _userAccessModule.ExecuteQueryAsync(new GetUserPermissionsQuery(_executionContextAccessor.UserId)); if (!await AuthorizeAsync(attribute.Name, permissions)) { context.Fail(); return; } context.Succeed(requirement); } private Task AuthorizeAsync(string permission, List permissions) { #if !DEBUG return Task.FromResult(true); #endif #pragma warning disable CS0162 // Unreachable code detected return Task.FromResult(permissions.Any(x => x.Code == permission)); #pragma warning restore CS0162 // Unreachable code detected } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Configuration/Authorization/HasPermissionAuthorizationRequirement.cs ================================================ using Microsoft.AspNetCore.Authorization; namespace CompanyName.MyMeetings.API.Configuration.Authorization { public class HasPermissionAuthorizationRequirement : IAuthorizationRequirement { } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Configuration/Authorization/NoPermissionRequiredAttribute.cs ================================================ namespace CompanyName.MyMeetings.API.Configuration.Authorization { [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class NoPermissionRequiredAttribute : Attribute { } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Configuration/ExecutionContext/CorrelationMiddleware.cs ================================================ namespace CompanyName.MyMeetings.API.Configuration.ExecutionContext { internal class CorrelationMiddleware { internal const string CorrelationHeaderKey = "CorrelationId"; private readonly RequestDelegate _next; public CorrelationMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { var correlationId = Guid.NewGuid(); context.Request?.Headers.Append(CorrelationHeaderKey, correlationId.ToString()); await _next.Invoke(context); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Configuration/ExecutionContext/ExecutionContextAccessor.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; namespace CompanyName.MyMeetings.API.Configuration.ExecutionContext { public class ExecutionContextAccessor : IExecutionContextAccessor { private readonly IHttpContextAccessor _httpContextAccessor; public ExecutionContextAccessor(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public Guid UserId { get { if (_httpContextAccessor .HttpContext? .User? .Claims? .SingleOrDefault(x => x.Type == "sub")? .Value != null) { return Guid.Parse(_httpContextAccessor.HttpContext.User.Claims.Single( x => x.Type == "sub").Value); } throw new ApplicationException("User context is not available"); } } public Guid CorrelationId { get { if (IsAvailable && _httpContextAccessor.HttpContext.Request.Headers.Keys.Any( x => x == CorrelationMiddleware.CorrelationHeaderKey)) { return Guid.Parse( _httpContextAccessor.HttpContext.Request.Headers[CorrelationMiddleware.CorrelationHeaderKey]); } throw new ApplicationException("Http context and correlation id is not available"); } } public bool IsAvailable => _httpContextAccessor.HttpContext != null; } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Configuration/Extensions/SwaggerExtensions.cs ================================================ using System.Reflection; using Microsoft.OpenApi.Models; namespace CompanyName.MyMeetings.API.Configuration.Extensions { internal static class SwaggerExtensions { internal static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services) { services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "MyMeetings API", Version = "v1", Description = "MyMeetings API for modular monolith .NET application." }); options.CustomSchemaIds(t => t.ToString()); var baseDirectory = AppDomain.CurrentDomain.BaseDirectory; var commentsFileName = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var commentsFile = Path.Combine(baseDirectory, commentsFileName); options.IncludeXmlComments(commentsFile); options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }, Scheme = "oauth2", Name = "Bearer", In = ParameterLocation.Header }, new List() } }); }); return services; } internal static IApplicationBuilder UseSwaggerDocumentation(this IApplicationBuilder app) { app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyMeetings API"); }); return app; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Configuration/Validation/BusinessRuleValidationExceptionProblemDetails.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Configuration.Validation { public class BusinessRuleValidationExceptionProblemDetails : ProblemDetails { public BusinessRuleValidationExceptionProblemDetails(BusinessRuleValidationException exception) { Title = "Business rule broken"; Status = StatusCodes.Status409Conflict; Detail = exception.Message; Type = "https://somedomain/business-rule-validation-error"; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Configuration/Validation/InvalidCommandProblemDetails.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Configuration.Validation { public class InvalidCommandProblemDetails : ProblemDetails { public InvalidCommandProblemDetails(InvalidCommandException exception) { Title = "Command validation error"; Status = StatusCodes.Status400BadRequest; Type = "https://somedomain/validation-error"; Errors = exception.Errors; } public List Errors { get; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Administration/AdministrationAutofacModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using CompanyName.MyMeetings.Modules.Administration.Infrastructure; namespace CompanyName.MyMeetings.API.Modules.Administration { internal class AdministrationAutofacModule : Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType() .As() .InstancePerLifetimeScope(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Administration/AdministrationPermissions.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Administration { public class AdministrationPermissions { public const string AcceptMeetingGroupProposal = "AcceptMeetingGroupProposal"; } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Administration/MeetingGroupProposals/MeetingGroupProposalsController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.AcceptMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.GetMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.GetMeetingGroupProposals; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Administration.MeetingGroupProposals { [Route("api/administration/meetingGroupProposals")] [ApiController] public class MeetingGroupProposalsController : ControllerBase { private readonly IAdministrationModule _administrationModule; public MeetingGroupProposalsController(IAdministrationModule administrationModule) { _administrationModule = administrationModule; } [HttpGet("")] [HasPermission(AdministrationPermissions.AcceptMeetingGroupProposal)] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetMeetingGroupProposals() { var meetingGroupProposals = await _administrationModule.ExecuteQueryAsync(new GetMeetingGroupProposalsQuery()); return Ok(meetingGroupProposals); } [HttpPatch("{meetingGroupProposalId}/accept")] [HasPermission(AdministrationPermissions.AcceptMeetingGroupProposal)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task AcceptMeetingGroupProposal(Guid meetingGroupProposalId) { await _administrationModule.ExecuteCommandAsync( new AcceptMeetingGroupProposalCommand(meetingGroupProposalId)); return Ok(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/Countries/CountriesController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.Countries; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Meetings.Countries { [Route("api/meetings/countries")] [ApiController] public class CountriesController : ControllerBase { private readonly IMeetingsModule _meetingsModule; public CountriesController(IMeetingsModule meetingsModule) { _meetingsModule = meetingsModule; } [HttpGet("")] [HasPermission(MeetingsPermissions.GetMeetingGroupProposals)] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetAllCountries(int? page, int? perPage) { var countries = await _meetingsModule.ExecuteQueryAsync( new GetAllCountriesQuery()); return Ok(countries); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/MeetingCommentingConfiguration/MeetingCommentingConfigurationController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.DisableMeetingCommentingConfiguration; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.EnableMeetingCommentingConfiguration; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Meetings.MeetingCommentingConfiguration { [Route("api/meetings/meetings/{meetingId}/configuration/commenting")] [ApiController] public class MeetingCommentingConfigurationController : ControllerBase { private readonly IMeetingsModule _meetingsModule; public MeetingCommentingConfigurationController(IMeetingsModule meetingsModule) { _meetingsModule = meetingsModule; } [HttpPatch("disable")] [HasPermission(MeetingsPermissions.DisableMeetingCommenting)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task DisableCommenting(Guid meetingId) { await _meetingsModule.ExecuteCommandAsync(new DisableMeetingCommentingConfigurationCommand(meetingId)); return Ok(); } [HttpPatch("enable")] [HasPermission(MeetingsPermissions.EnableMeetingCommenting)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task EnableCommenting(Guid meetingId) { await _meetingsModule.ExecuteCommandAsync(new EnableMeetingCommentingConfigurationCommand(meetingId)); return Ok(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/MeetingComments/AddMeetingCommentRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Meetings.MeetingComments { public class AddMeetingCommentRequest { public Guid MeetingId { get; set; } public string Comment { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/MeetingComments/EditMeetingCommentRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Meetings.MeetingComments { public class EditMeetingCommentRequest { public string EditedComment { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/MeetingComments/MeetingCommentsController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingComment; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingCommentLike; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingCommentReply; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.EditMeetingComment; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.RemoveMeetingComment; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.RemoveMeetingCommentLike; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Meetings.MeetingComments { [Route("api/meetings/[controller]")] [ApiController] public class MeetingCommentsController : ControllerBase { private readonly IMeetingsModule _meetingModule; public MeetingCommentsController(IMeetingsModule meetingModule) { _meetingModule = meetingModule; } [HttpPost] [HasPermission(MeetingsPermissions.AddMeetingComment)] [ProducesResponseType(typeof(Guid), StatusCodes.Status200OK)] public async Task AddComment([FromBody] AddMeetingCommentRequest request) { var commentId = await _meetingModule.ExecuteCommandAsync(new AddMeetingCommentCommand( request.MeetingId, request.Comment)); return Ok(commentId); } [HttpPut("{meetingCommentId}")] [HasPermission(MeetingsPermissions.EditMeetingComment)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task EditComment( [FromRoute] Guid meetingCommentId, [FromBody] EditMeetingCommentRequest request) { await _meetingModule.ExecuteCommandAsync(new EditMeetingCommentCommand( meetingCommentId, request.EditedComment)); return Ok(); } [HttpDelete("{meetingCommentId}")] [HasPermission(MeetingsPermissions.RemoveMeetingComment)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task DeleteComment([FromRoute] Guid meetingCommentId, [FromQuery] string reason) { await _meetingModule.ExecuteCommandAsync( new RemoveMeetingCommentCommand(meetingCommentId, reason)); return Ok(); } [HttpPost("{meetingCommentId}/replies")] [HasPermission(MeetingsPermissions.AddMeetingCommentReply)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task AddReply([FromRoute] Guid meetingCommentId, [FromBody] string reply) { await _meetingModule.ExecuteCommandAsync(new AddReplyToMeetingCommentCommand(meetingCommentId, reply)); return Ok(); } [HttpPost("{meetingCommentId}/likes")] [HasPermission(MeetingsPermissions.LikeMeetingComment)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task LikeComment([FromRoute] Guid meetingCommentId) { await _meetingModule.ExecuteCommandAsync( new AddMeetingCommentLikeCommand(meetingCommentId)); return Ok(); } [HttpDelete("{meetingCommentId}/likes")] [HasPermission(MeetingsPermissions.UnlikeMeetingComment)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task UnlikeComment([FromRoute] Guid meetingCommentId) { await _meetingModule.ExecuteCommandAsync( new RemoveMeetingCommentLikeCommand(meetingCommentId)); return Ok(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/MeetingGroupProposals/MeetingGroupProposalsController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetAllMeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetMemberMeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.ProposeMeetingGroup; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Meetings.MeetingGroupProposals { [Route("api/meetings/[controller]")] [ApiController] public class MeetingGroupProposalsController : ControllerBase { private readonly IMeetingsModule _meetingsModule; public MeetingGroupProposalsController(IMeetingsModule meetingsModule) { _meetingsModule = meetingsModule; } [HttpGet("")] [HasPermission(MeetingsPermissions.GetMeetingGroupProposals)] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetMemberMeetingGroupProposals() { var meetingGroupProposals = await _meetingsModule.ExecuteQueryAsync( new GetMemberMeetingGroupProposalsQuery()); return Ok(meetingGroupProposals); } [HttpGet("all")] [HasPermission(MeetingsPermissions.GetMeetingGroupProposals)] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetAllMeetingGroupProposals(int? page, int? perPage) { var meetingGroupProposals = await _meetingsModule.ExecuteQueryAsync( new GetAllMeetingGroupProposalsQuery(page, perPage)); return Ok(meetingGroupProposals); } [HttpPost("")] [HasPermission(MeetingsPermissions.ProposeMeetingGroup)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task ProposeMeetingGroup(ProposeMeetingGroupRequest request) { await _meetingsModule.ExecuteCommandAsync( new ProposeMeetingGroupCommand( request.Name, request.Description, request.LocationCity, request.LocationCountryCode)); return Ok(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/MeetingGroupProposals/ProposeMeetingGroupRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Meetings.MeetingGroupProposals { public class ProposeMeetingGroupRequest { public string Name { get; set; } public string Description { get; set; } public string LocationCity { get; set; } public string LocationCountryCode { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/MeetingGroups/CreateNewMeetingGroupRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Meetings.MeetingGroups { public class CreateNewMeetingGroupRequest { public string Name { get; set; } public string Description { get; set; } public string LocationCity { get; set; } public string LocationCountry { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/MeetingGroups/EditMeetingGroupGeneralAttributesRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Meetings.MeetingGroups { public class EditMeetingGroupGeneralAttributesRequest { public string Name { get; set; } public string Description { get; set; } public string LocationCity { get; set; } public string LocationCountry { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/MeetingGroups/MeetingGroupsController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.EditMeetingGroupGeneralAttributes; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetAllMeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetAuthenticationMemberMeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetMeetingGroupDetails; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.JoinToGroup; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.LeaveMeetingGroup; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Meetings.MeetingGroups { [Route("api/meetings/[controller]")] [ApiController] public class MeetingGroupsController : ControllerBase { private readonly IMeetingsModule _meetingsModule; public MeetingGroupsController(IMeetingsModule meetingsModule) { _meetingsModule = meetingsModule; } [HttpGet("")] [HasPermission(MeetingsPermissions.GetAuthenticatedMemberMeetingGroups)] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetAuthenticatedMemberMeetingGroups() { var meetingGroups = await _meetingsModule.ExecuteQueryAsync( new GetAuthenticationMemberMeetingGroupsQuery()); return Ok(meetingGroups); } [HttpGet("{meetingGroupId}")] [HasPermission(MeetingsPermissions.GetMeetingGroupDetails)] [ProducesResponseType(typeof(MeetingGroupDetailsDto), StatusCodes.Status200OK)] public async Task GetMeetingGroupDetails(Guid meetingGroupId) { var meetingGroupDetails = await _meetingsModule.ExecuteQueryAsync( new GetMeetingGroupDetailsQuery(meetingGroupId)); return Ok(meetingGroupDetails); } [HttpGet("all")] [HasPermission(MeetingsPermissions.GetAllMeetingGroups)] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetAllMeetingGroups() { var meetingGroups = await _meetingsModule.ExecuteQueryAsync(new GetAllMeetingGroupsQuery()); return Ok(meetingGroups); } [HttpPut("{meetingGroupId}")] [HasPermission(MeetingsPermissions.EditMeetingGroupGeneralAttributes)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task EditMeetingGroupGeneralAttributes( [FromRoute] Guid meetingGroupId, [FromBody] EditMeetingGroupGeneralAttributesRequest request) { await _meetingsModule.ExecuteCommandAsync(new EditMeetingGroupGeneralAttributesCommand( meetingGroupId, request.Name, request.Description, request.LocationCity, request.LocationCountry)); return Ok(); } [HttpPost("{meetingGroupId}/members")] [HasPermission(MeetingsPermissions.JoinToGroup)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task JoinToGroup(Guid meetingGroupId) { await _meetingsModule.ExecuteCommandAsync(new JoinToGroupCommand(meetingGroupId)); return Ok(); } [HttpDelete("{meetingGroupId}/members")] [HasPermission(MeetingsPermissions.LeaveMeetingGroup)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task LeaveMeetingGroup(Guid meetingGroupId) { await _meetingsModule.ExecuteCommandAsync(new LeaveMeetingGroupCommand(meetingGroupId)); return Ok(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/Meetings/AddMeetingAttendeeRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Meetings.Meetings { public class AddMeetingAttendeeRequest { public int GuestsNumber { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/Meetings/ChangeMeetingMainAttributesRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Meetings.Meetings { public class ChangeMeetingMainAttributesRequest { public Guid MeetingId { get; set; } public string Title { get; set; } public DateTime TermStartDate { get; set; } public DateTime TermEndDate { get; set; } public string Description { get; set; } public string MeetingLocationName { get; set; } public string MeetingLocationAddress { get; set; } public string MeetingLocationPostalCode { get; set; } public string MeetingLocationCity { get; set; } public int? AttendeesLimit { get; set; } public int GuestsLimit { get; set; } public DateTime? RSVPTermStartDate { get; set; } public DateTime? RSVPTermEndDate { get; set; } public decimal? EventFeeValue { get; set; } public string EventFeeCurrency { get; set; } public List HostMemberIds { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/Meetings/CreateMeetingRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Meetings.Meetings { public class CreateMeetingRequest { public Guid MeetingGroupId { get; set; } public string Title { get; set; } public DateTime TermStartDate { get; set; } public DateTime TermEndDate { get; set; } public string Description { get; set; } public string MeetingLocationName { get; set; } public string MeetingLocationAddress { get; set; } public string MeetingLocationPostalCode { get; set; } public string MeetingLocationCity { get; set; } public int? AttendeesLimit { get; set; } public int GuestsLimit { get; set; } public DateTime? RSVPTermStartDate { get; set; } public DateTime? RSVPTermEndDate { get; set; } public decimal? EventFeeValue { get; set; } public string EventFeeCurrency { get; set; } public List HostMemberIds { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/Meetings/MeetingsController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.AddMeetingAttendee; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.AddMeetingNotAttendee; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.CancelMeeting; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.ChangeMeetingMainAttributes; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.ChangeNotAttendeeDecision; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.CreateMeeting; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetAuthenticatedMemberMeetings; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetMeetingAttendees; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetMeetingDetails; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.RemoveMeetingAttendee; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SetMeetingAttendeeRole; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SetMeetingHostRole; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SignOffMemberFromWaitlist; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SignUpMemberToWaitlist; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Meetings.Meetings { [Route("api/meetings/meetings")] [ApiController] public class MeetingsController : ControllerBase { private readonly IMeetingsModule _meetingsModule; public MeetingsController(IMeetingsModule meetingsModule) { _meetingsModule = meetingsModule; } [HttpGet("")] [HasPermission(MeetingsPermissions.GetAuthenticatedMemberMeetings)] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetAuthenticatedMemberMeetings() { var meetings = await _meetingsModule.ExecuteQueryAsync(new GetAuthenticatedMemberMeetingsQuery()); return Ok(meetings); } [HttpGet("{meetingId}")] [HasPermission(MeetingsPermissions.GetMeetingDetails)] [ProducesResponseType(typeof(MeetingDetailsDto), StatusCodes.Status200OK)] public async Task GetMeetingDetails(Guid meetingId) { var meetingDetails = await _meetingsModule.ExecuteQueryAsync(new GetMeetingDetailsQuery(meetingId)); return Ok(meetingDetails); } [HttpPost("")] [HasPermission(MeetingsPermissions.CreateNewMeeting)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task CreateNewMeeting([FromBody] CreateMeetingRequest request) { await _meetingsModule.ExecuteCommandAsync(new CreateMeetingCommand( request.MeetingGroupId, request.Title, request.TermStartDate, request.TermEndDate, request.Description, request.MeetingLocationName, request.MeetingLocationAddress, request.MeetingLocationPostalCode, request.MeetingLocationCity, request.AttendeesLimit, request.GuestsLimit, request.RSVPTermStartDate, request.RSVPTermEndDate, request.EventFeeValue, request.EventFeeCurrency, request.HostMemberIds)); return Ok(); } [HttpPut("{meetingId}")] [HasPermission(MeetingsPermissions.EditMeeting)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task EditMeeting( [FromRoute] Guid meetingId, [FromBody] ChangeMeetingMainAttributesRequest mainAttributesRequest) { await _meetingsModule.ExecuteCommandAsync(new ChangeMeetingMainAttributesCommand( meetingId, mainAttributesRequest.Title, mainAttributesRequest.TermStartDate, mainAttributesRequest.TermEndDate, mainAttributesRequest.Description, mainAttributesRequest.MeetingLocationName, mainAttributesRequest.MeetingLocationAddress, mainAttributesRequest.MeetingLocationPostalCode, mainAttributesRequest.MeetingLocationCity, mainAttributesRequest.AttendeesLimit, mainAttributesRequest.GuestsLimit, mainAttributesRequest.RSVPTermStartDate, mainAttributesRequest.RSVPTermEndDate, mainAttributesRequest.EventFeeValue, mainAttributesRequest.EventFeeCurrency)); return Ok(); } [HttpGet("{meetingId}/attendees")] [HasPermission(MeetingsPermissions.GetMeetingAttendees)] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetMeetingAttendees(Guid meetingId) { var meetingAttendees = await _meetingsModule.ExecuteQueryAsync(new GetMeetingAttendeesQuery(meetingId)); return Ok(meetingAttendees); } [HttpPost("{meetingId}/attendees")] [HasPermission(MeetingsPermissions.AddMeetingAttendee)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task AddMeetingAttendee( [FromRoute] Guid meetingId, [FromBody] AddMeetingAttendeeRequest attendeeRequest) { await _meetingsModule.ExecuteCommandAsync(new AddMeetingAttendeeCommand( meetingId, attendeeRequest.GuestsNumber)); return Ok(); } [HttpDelete("{meetingId}/attendees/{attendeeId}")] [HasPermission(MeetingsPermissions.RemoveMeetingAttendee)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task RemoveMeetingAttendee( Guid meetingId, Guid attendeeId, RemoveMeetingAttendeeRequest request) { await _meetingsModule.ExecuteCommandAsync( new RemoveMeetingAttendeeCommand(meetingId, attendeeId, request.RemovingReason)); return Ok(); } [HttpPost("{meetingId}/notAttendees")] [HasPermission(MeetingsPermissions.AddNotAttendee)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task AddNotAttendee(Guid meetingId) { await _meetingsModule.ExecuteCommandAsync(new AddMeetingNotAttendeeCommand(meetingId)); return Ok(); } [HttpDelete("{meetingId}/notAttendees")] [HasPermission(MeetingsPermissions.ChangeNotAttendeeDecision)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task ChangeNotAttendeeDecision(Guid meetingId) { await _meetingsModule.ExecuteCommandAsync(new ChangeNotAttendeeDecisionCommand(meetingId)); return Ok(); } [HttpPost("{meetingId}/waitlistMembers")] [HasPermission(MeetingsPermissions.SignUpMemberToWaitlist)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task SignUpMemberToWaitlist(Guid meetingId) { await _meetingsModule.ExecuteCommandAsync(new SignUpMemberToWaitlistCommand(meetingId)); return Ok(); } [HttpDelete("{meetingId}/waitlistMembers")] [HasPermission(MeetingsPermissions.SignOffMemberFromWaitlist)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task SignOffMemberFromWaitlist(Guid meetingId) { await _meetingsModule.ExecuteCommandAsync(new SignOffMemberFromWaitlistCommand(meetingId)); return Ok(); } [HttpPost("{meetingId}/hosts")] [HasPermission(MeetingsPermissions.SetMeetingHostRole)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task SetMeetingHostRole(Guid meetingId, SetMeetingHostRequest request) { await _meetingsModule.ExecuteCommandAsync(new SetMeetingHostRoleCommand(request.AttendeeId, meetingId)); return Ok(); } [HttpPost("{meetingId}/attendees/attendeeRole")] [HasPermission(MeetingsPermissions.SetMeetingAttendeeRole)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task SetMeetingAttendeeRole(Guid meetingId, SetMeetingHostRequest request) { await _meetingsModule.ExecuteCommandAsync(new SetMeetingAttendeeRoleCommand(request.AttendeeId, meetingId)); return Ok(); } [HttpPatch("{meetingId}/cancel")] [HasPermission(MeetingsPermissions.CancelMeeting)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task CancelMeeting(Guid meetingId) { await _meetingsModule.ExecuteCommandAsync(new CancelMeetingCommand(meetingId)); return Ok(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/Meetings/RemoveMeetingAttendeeRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Meetings.Meetings { public class RemoveMeetingAttendeeRequest { public string RemovingReason { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/Meetings/SetMeetingAttendeeRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Meetings.Meetings { public class SetMeetingAttendeeRequest { public Guid AttendeeId { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/Meetings/SetMeetingHostRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Meetings.Meetings { public class SetMeetingHostRequest { public Guid AttendeeId { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/MeetingsAutofacModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure; namespace CompanyName.MyMeetings.API.Modules.Meetings { public class MeetingsAutofacModule : Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType() .As() .InstancePerLifetimeScope(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Meetings/MeetingsPermissions.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Meetings { public class MeetingsPermissions { public const string GetMeetingGroupProposals = "GetMeetingGroupProposals"; public const string ProposeMeetingGroup = "ProposeMeetingGroup"; public const string CreateNewMeeting = "CreateNewMeeting"; public const string EditMeeting = "EditMeeting"; public const string AddMeetingAttendee = "AddMeetingAttendee"; public const string RemoveMeetingAttendee = "RemoveMeetingAttendee"; public const string AddNotAttendee = "AddNotAttendee"; public const string ChangeNotAttendeeDecision = "ChangeNotAttendeeDecision"; public const string SignUpMemberToWaitlist = "SignUpMemberToWaitlist"; public const string SignOffMemberFromWaitlist = "SignOffMemberFromWaitlist"; public const string SetMeetingHostRole = "SetMeetingHostRole"; public const string SetMeetingAttendeeRole = "SetMeetingAttendeeRole"; public const string CancelMeeting = "CancelMeeting"; public const string GetAllMeetingGroups = "GetAllMeetingGroups"; public const string EditMeetingGroupGeneralAttributes = "EditMeetingGroupGeneralAttributes"; public const string JoinToGroup = "JoinToGroup"; public const string LeaveMeetingGroup = "LeaveMeetingGroup"; public const string AddMeetingComment = "AddMeetingComment"; public const string EditMeetingComment = "EditMeetingComment"; public const string RemoveMeetingComment = "RemoveMeetingComment"; public const string AddMeetingCommentReply = "AddMeetingCommentReply"; public const string LikeMeetingComment = "LikeMeetingComment"; public const string UnlikeMeetingComment = "UnlikeMeetingComment"; public const string EnableMeetingCommenting = "EnableMeetingCommenting"; public const string DisableMeetingCommenting = "DisableMeetingCommenting"; public const string GetAuthenticatedMemberMeetingGroups = "GetAuthenticatedMemberMeetingGroups"; public const string GetMeetingGroupDetails = "GetMeetingGroupDetails"; public const string GetMeetingDetails = "GetMeetingDetails"; public const string GetAuthenticatedMemberMeetings = "GetAuthenticatedMemberMeetings"; public const string GetMeetingAttendees = "GetMeetingAttendees"; } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/MeetingFees/CreateMeetingFeePaymentRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Payments.MeetingFees { public class CreateMeetingFeePaymentRequest { public Guid MeetingFeeId { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/MeetingFees/MeetingFeePaymentsController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.CreateMeetingFeePayment; using CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeePaymentAsPaid; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Payments.MeetingFees { [Route("api/payments/meetingFeePayments")] [ApiController] public class MeetingFeePaymentsController : ControllerBase { private readonly IPaymentsModule _meetingsModule; public MeetingFeePaymentsController(IPaymentsModule meetingsModule) { _meetingsModule = meetingsModule; } [HttpPost] [HasPermission(PaymentsPermissions.RegisterPayment)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task CreateMeetingFeePayment(CreateMeetingFeePaymentRequest request) { await _meetingsModule.ExecuteCommandAsync(new CreateMeetingFeePaymentCommand(request.MeetingFeeId)); return Ok(); } [HttpPut] [Route("{meetingFeePaymentId}/purchased")] [HasPermission(PaymentsPermissions.RegisterPayment)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task RegisterMeetingFeePayment( Guid meetingFeePaymentId) { await _meetingsModule.ExecuteCommandAsync(new MarkMeetingFeePaymentAsPaidCommand(meetingFeePaymentId)); return Ok(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/MeetingFees/RegisterMeetingFeePaymentRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Payments.MeetingFees { public class RegisterMeetingFeePaymentRequest { public Guid PaymentId { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/Payers/PayersController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetPayerSubscription; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionDetails; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Payments.Payers { [Route("api/payments/payers")] [ApiController] public class PayersController : ControllerBase { private readonly IPaymentsModule _paymentsModule; public PayersController(IPaymentsModule paymentsModule) { _paymentsModule = paymentsModule; } [HttpGet("authenticated/subscription")] [HasPermission(PaymentsPermissions.GetAuthenticatedPayerSubscription)] [ProducesResponseType(typeof(SubscriptionDetailsDto), StatusCodes.Status200OK)] public async Task GetAuthenticatedPayerSubscription() { var subscription = await _paymentsModule.ExecuteQueryAsync(new GetAuthenticatedPayerSubscriptionQuery()); return Ok(subscription); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/PaymentsAutofacModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Infrastructure; namespace CompanyName.MyMeetings.API.Modules.Payments { public class PaymentsAutofacModule : Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType() .As() .InstancePerLifetimeScope(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/PaymentsPermissions.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Payments { public class PaymentsPermissions { public const string RegisterPayment = "RegisterPayment"; public const string BuySubscription = "BuySubscription"; public const string RenewSubscription = "RenewSubscription"; public const string CreatePriceListItem = "CreatePriceListItem"; public const string ActivatePriceListItem = "ActivatePriceListItem"; public const string DeactivatePriceListItem = "DeactivatePriceListItem"; public const string ChangePriceListItemAttributes = "ChangePriceListItemAttributes"; public const string GetAuthenticatedPayerSubscription = "GetAuthenticatedPayerSubscription"; public const string GetPriceListItem = "GetPriceListItem"; } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/PriceListItems/ChangePriceListItemAttributesRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Payments.PriceListItems { public class ChangePriceListItemAttributesRequest { public Guid PriceListItemId { get; set; } public string CountryCode { get; set; } public string SubscriptionPeriodCode { get; set; } public string CategoryCode { get; set; } public decimal PriceValue { get; set; } public string PriceCurrency { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/PriceListItems/CreatePriceListItemRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Payments.PriceListItems { public class CreatePriceListItemRequest { public string CountryCode { get; set; } public string SubscriptionPeriodCode { get; set; } public string CategoryCode { get; set; } public decimal PriceValue { get; set; } public string PriceCurrency { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/PriceListItems/GetPriceListItemRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Payments.PriceListItems { public class GetPriceListItemRequest { public string CountryCode { get; set; } public string CategoryCode { get; set; } public string PeriodTypeCode { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/PriceListItems/PriceListItemsController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.ActivatePriceListItem; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.ChangePriceListItemAttributes; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.CreatePriceListItem; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.DeactivatePriceListItem; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.GetPriceListItem; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Payments.PriceListItems { [ApiController] [Route("api/payments/priceListItems")] public class PriceListItemsController : ControllerBase { private readonly IPaymentsModule _paymentsModule; public PriceListItemsController(IPaymentsModule paymentsModule) { _paymentsModule = paymentsModule; } [HttpGet] [HasPermission(PaymentsPermissions.GetPriceListItem)] [ProducesResponseType(typeof(PriceListItemMoneyValueDto), StatusCodes.Status200OK)] public async Task GetPriceListItem([FromQuery] GetPriceListItemRequest request) { var priceListItem = await _paymentsModule.ExecuteQueryAsync(new GetPriceListItemQuery( request.CountryCode, request.CategoryCode, request.PeriodTypeCode)); return Ok(priceListItem); } [HttpPost] [HasPermission(PaymentsPermissions.CreatePriceListItem)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task CreatePriceListItem([FromBody] CreatePriceListItemRequest request) { await _paymentsModule.ExecuteCommandAsync(new CreatePriceListItemCommand( request.SubscriptionPeriodCode, request.CategoryCode, request.CountryCode, request.PriceValue, request.PriceCurrency)); return Ok(); } [HttpPatch("{priceListItemId}/activate")] [HasPermission(PaymentsPermissions.ActivatePriceListItem)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task ActivatePriceListItem([FromRoute] Guid priceListItemId) { await _paymentsModule.ExecuteCommandAsync(new ActivatePriceListItemCommand(priceListItemId)); return Ok(); } [HttpPatch("{priceListItemId}/deactivate")] [HasPermission(PaymentsPermissions.DeactivatePriceListItem)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task DeactivatePriceListItem([FromRoute] Guid priceListItemId) { await _paymentsModule.ExecuteCommandAsync(new DeactivatePriceListItemCommand(priceListItemId)); return Ok(); } [HttpPut] [HasPermission(PaymentsPermissions.ChangePriceListItemAttributes)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task ChangePriceListItemAttributes( [FromBody] ChangePriceListItemAttributesRequest request) { await _paymentsModule.ExecuteCommandAsync(new ChangePriceListItemAttributesCommand( request.PriceListItemId, request.CountryCode, request.SubscriptionPeriodCode, request.CategoryCode, request.PriceValue, request.PriceCurrency)); return Ok(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/RegisterSubscriptionRenewalPaymentRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Payments { public class RegisterSubscriptionRenewalPaymentRequest { public Guid PaymentId { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/SubscriptionRenewalsController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionRenewalPaymentAsPaid; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Payments { [Route("api/payments/subscriptionRenewals")] [ApiController] public class SubscriptionRenewalsController : ControllerBase { private readonly IPaymentsModule _paymentsModule; public SubscriptionRenewalsController(IPaymentsModule paymentsModule) { _paymentsModule = paymentsModule; } [HttpPost] [HasPermission(PaymentsPermissions.RegisterPayment)] public async Task RegisterSubscriptionPayment(RegisterSubscriptionRenewalPaymentRequest request) { await _paymentsModule.ExecuteCommandAsync( new MarkSubscriptionRenewalPaymentAsPaidCommand(request.PaymentId)); return Ok(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/Subscriptions/BuySubscriptionRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Payments.Subscriptions { public class BuySubscriptionRequest { public string SubscriptionTypeCode { get; set; } public string CountryCode { get; set; } public decimal Value { get; set; } public string Currency { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/Subscriptions/RegisterSubscriptionPaymentRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Payments.Subscriptions { public class RegisterSubscriptionPaymentRequest { public Guid PaymentId { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/Subscriptions/RenewSubscriptionRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.Payments.Subscriptions { public class RenewSubscriptionRequest { public string SubscriptionTypeCode { get; set; } public string CountryCode { get; set; } public decimal Value { get; set; } public string Currency { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/Subscriptions/SubscriptionPaymentsController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionPaymentAsPaid; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Payments.Subscriptions { [Route("api/payments/subscriptionPayments")] [ApiController] public class SubscriptionPaymentsController : ControllerBase { private readonly IPaymentsModule _meetingsModule; public SubscriptionPaymentsController(IPaymentsModule meetingsModule) { _meetingsModule = meetingsModule; } [HttpPost] [HasPermission(PaymentsPermissions.RegisterPayment)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task RegisterSubscriptionPayment(RegisterSubscriptionPaymentRequest request) { await _meetingsModule.ExecuteCommandAsync(new MarkSubscriptionPaymentAsPaidCommand(request.PaymentId)); return Ok(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/Payments/Subscriptions/SubscriptionsController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.BuySubscription; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.BuySubscriptionRenewal; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.Payments.Subscriptions { [Route("api/payments/subscriptions")] [ApiController] public class SubscriptionsController : ControllerBase { private readonly IPaymentsModule _meetingsModule; public SubscriptionsController(IPaymentsModule meetingsModule) { _meetingsModule = meetingsModule; } [HttpPost("")] [HasPermission(PaymentsPermissions.BuySubscription)] public async Task BuySubscription(BuySubscriptionRequest request) { var paymentId = await _meetingsModule.ExecuteCommandAsync( new BuySubscriptionCommand( request.SubscriptionTypeCode, request.CountryCode, request.Value, request.Currency)); return Ok(paymentId); } [HttpPost("{subscriptionId}/renewals")] [HasPermission(PaymentsPermissions.RenewSubscription)] public async Task RenewSubscription( Guid subscriptionId, RenewSubscriptionRequest request) { await _meetingsModule.ExecuteCommandAsync( new BuySubscriptionRenewalCommand( subscriptionId, request.SubscriptionTypeCode, request.CountryCode, request.Value, request.Currency)); return Accepted(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/UserAccess/AuthenticatedUserController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.UserAccess.Application.Authorization.GetAuthenticatedUserPermissions; using CompanyName.MyMeetings.Modules.UserAccess.Application.Authorization.GetUserPermissions; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetAuthenticatedUser; using CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetUser; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.UserAccess { [Route("api/userAccess/authenticatedUser")] [ApiController] public class AuthenticatedUserController : ControllerBase { private readonly IUserAccessModule _userAccessModule; public AuthenticatedUserController(IUserAccessModule userAccessModule) { _userAccessModule = userAccessModule; } [NoPermissionRequired] [HttpGet("")] [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] public async Task GetAuthenticatedUser() { var user = await _userAccessModule.ExecuteQueryAsync(new GetAuthenticatedUserQuery()); return Ok(user); } [NoPermissionRequired] [HttpGet("permissions")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetAuthenticatedUserPermissions() { var permissions = await _userAccessModule.ExecuteQueryAsync(new GetAuthenticatedUserPermissionsQuery()); return Ok(permissions); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/UserAccess/EmailsController.cs ================================================ using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using CompanyName.MyMeetings.Modules.UserAccess.Application.Emails; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.UserAccess { [Route("api/userAccess/emails")] [ApiController] public class EmailsController : ControllerBase { private readonly IUserAccessModule _userAccessModule; public EmailsController(IUserAccessModule userAccessModule) { _userAccessModule = userAccessModule; } [NoPermissionRequired] [AllowAnonymous] [HttpGet("")] public async Task GetEmails() { var allEmails = await _userAccessModule.ExecuteQueryAsync(new GetAllEmailsQuery()); return Ok(allEmails); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/UserAccess/RegisterNewUserRequest.cs ================================================ namespace CompanyName.MyMeetings.API.Modules.UserAccess { public class RegisterNewUserRequest { public string Login { get; set; } public string Password { get; set; } public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string ConfirmLink { get; set; } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/UserAccess/UserAccessAutofacModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure; namespace CompanyName.MyMeetings.API.Modules.UserAccess { public class UserAccessAutofacModule : Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType() .As() .InstancePerLifetimeScope(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Modules/UserAccess/UserRegistrationsController.cs ================================================ 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 Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace CompanyName.MyMeetings.API.Modules.UserAccess { [Route("userAccess/[controller]")] [ApiController] public class UserRegistrationsController : ControllerBase { private readonly IRegistrationsModule _registrationsModule; public UserRegistrationsController(IRegistrationsModule registrationsModule) { _registrationsModule = registrationsModule; } [NoPermissionRequired] [AllowAnonymous] [HttpPost("")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task RegisterNewUser(RegisterNewUserRequest request) { await _registrationsModule.ExecuteCommandAsync(new RegisterNewUserCommand( request.Login, request.Password, request.Email, request.FirstName, request.LastName, request.ConfirmLink)); return Ok(); } [NoPermissionRequired] [AllowAnonymous] [HttpPatch("{userRegistrationId}/confirm")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task ConfirmRegistration(Guid userRegistrationId) { await _registrationsModule.ExecuteCommandAsync(new ConfirmUserRegistrationCommand(userRegistrationId)); return Ok(); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Program.cs ================================================ using Autofac.Extensions.DependencyInjection; namespace CompanyName.MyMeetings.API { public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IHostBuilder CreateWebHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureWebHostDefaults( webBuilder => { webBuilder.UseStartup(); }); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:57869", "sslPort": 44368 } }, "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "api/values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "DevWorkshops.Meetings.API": { "commandName": "Project", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:5000" }, "Docker": { "commandName": "Docker", "launchBrowser": true, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/api/values", "publishAllPorts": true, "useSSL": true } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/Startup.cs ================================================ using Autofac; using Autofac.Extensions.DependencyInjection; using CompanyName.MyMeetings.API.Configuration.Authorization; using CompanyName.MyMeetings.API.Configuration.ExecutionContext; using CompanyName.MyMeetings.API.Configuration.Extensions; using CompanyName.MyMeetings.API.Configuration.Validation; using CompanyName.MyMeetings.API.Modules.Administration; using CompanyName.MyMeetings.API.Modules.Meetings; using CompanyName.MyMeetings.API.Modules.Payments; using CompanyName.MyMeetings.API.Modules.UserAccess; using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; 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; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Server.HttpSys; using Serilog; using Serilog.Formatting.Compact; using ILogger = Serilog.ILogger; namespace CompanyName.MyMeetings.API { public class Startup { private const string MeetingsConnectionString = "MeetingsConnectionString"; private static ILogger _logger; private static ILogger _loggerForApi; private readonly IConfiguration _configuration; public Startup(IWebHostEnvironment env) { ConfigureLogger(); _configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{env.EnvironmentName}.json") .AddUserSecrets() .AddEnvironmentVariables("Meetings_") .Build(); _loggerForApi.Information("Connection string:" + _configuration.GetConnectionString(MeetingsConnectionString)); AuthorizationChecker.CheckAllEndpoints(); } public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSwaggerDocumentation(); services.ConfigureIdentityService(); services.AddSingleton(); services.AddSingleton(); services.AddProblemDetails(x => { x.Map(ex => new InvalidCommandProblemDetails(ex)); x.Map(ex => new BusinessRuleValidationExceptionProblemDetails(ex)); }); services.AddAuthorization(options => { options.AddPolicy(HasPermissionAttribute.HasPermissionPolicyName, policyBuilder => { policyBuilder.Requirements.Add(new HasPermissionAuthorizationRequirement()); policyBuilder.AddAuthenticationSchemes("Bearer"); }); }); services.AddScoped(); } public void ConfigureContainer(ContainerBuilder containerBuilder) { containerBuilder.RegisterModule(new MeetingsAutofacModule()); containerBuilder.RegisterModule(new AdministrationAutofacModule()); containerBuilder.RegisterModule(new UserAccessAutofacModule()); containerBuilder.RegisterModule(new PaymentsAutofacModule()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider) { var container = app.ApplicationServices.GetAutofacRoot(); app.UseCors(builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()); InitializeModules(container); app.UseMiddleware(); app.UseSwaggerDocumentation(); app.AddIdentityService(); if (env.IsDevelopment()) { app.UseProblemDetails(); } else { // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseRouting(); // app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } private static void ConfigureLogger() { _logger = new LoggerConfiguration() .Enrich.FromLogContext() .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{Module}] [{Context}] {Message:lj}{NewLine}{Exception}") .WriteTo.File(new CompactJsonFormatter(), "logs/logs") .CreateLogger(); _loggerForApi = _logger.ForContext("Module", "API"); _loggerForApi.Information("Logger configured"); } private void InitializeModules(ILifetimeScope container) { var httpContextAccessor = container.Resolve(); var executionContextAccessor = new ExecutionContextAccessor(httpContextAccessor); var emailsConfiguration = new EmailsConfiguration(_configuration["EmailsConfiguration:FromEmail"]); MeetingsStartup.Initialize( _configuration.GetConnectionString(MeetingsConnectionString), executionContextAccessor, _logger, emailsConfiguration, null); AdministrationStartup.Initialize( _configuration.GetConnectionString(MeetingsConnectionString), executionContextAccessor, _logger, null); UserAccessStartup.Initialize( _configuration.GetConnectionString(MeetingsConnectionString), executionContextAccessor, _logger, emailsConfiguration, _configuration["Security:TextEncryptionKey"], null, null); PaymentsStartup.Initialize( _configuration.GetConnectionString(MeetingsConnectionString), executionContextAccessor, _logger, emailsConfiguration, null); RegistrationsStartup.Initialize( _configuration.GetConnectionString(MeetingsConnectionString), executionContextAccessor, _logger, emailsConfiguration, _configuration["Security:TextEncryptionKey"], null, null); } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/appsettings.Production.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Trace", "System": "Information", "Microsoft": "None" } }, "AllowedHosts": "*", "EmailsConfiguration": { "FromEmail": "no-reply@mymeetings.com" }, "Security": { /* NOTE! This is sensitive data and should be stored in secure way (not here). Added only for demo purpose. */ "TextEncryptionKey": "E546C8DF278CK5990069B522" }, "ConnectionStrings": { "MeetingsConnectionString": "YourConnectioString" } } ================================================ FILE: src/API/CompanyName.MyMeetings.API/entrypoint.sh ================================================ #!/bin/bash sleep 30 ; dotnet CompanyName.MyMeetings.API.dll ================================================ FILE: src/API/CompanyName.MyMeetings.API/tempkey.rsa ================================================ {"KeyId":"6962297f9adb377dadb20e11b9d82f6c","Parameters":{"D":"k4wzTPOMjoB3/gKcu/IGMXrLgZlmPR7NbtUb22Dkw8xhUMVGBV2fE7zEEZyR+jkvKTQbzL+vOA9pmAOxtgZk3AnB1c3ntreZjB3tjYdshglRq7I1VhVDdxJ6e8jwcXqdUhUMiXdYScdkA/jKUO9Ls1XVKA4iXl7fJ3nMhLNgsCXAzuS824XvDGEGJ6FZxjCNfGtTIgoZPFTyJvkHAqQ+Ll72BYdb0vS3gY7wNlAYScHdfBOhmeirvkN4Z9uXp1E/m+QGGlfgm7Eqdt0d4AjMQ8BeNR3wrMB4B5QN6ssP0ENVqIDSLxnq4NLL74fiqgBcFlu3nqSRcqgPB0tvMah2aQ==","DP":"fTAwFVELf2DVCALpwParCuCChyFRgBWbgHanv4oRstuHHe2hgbasAKnXF+dO00D6k3Kvh5xFKlHr+e3W4hFI7nGM47DfVOX0L+DhsC2sa4qCHpMNU0EpJdrnASpPWWfBAiQPae+VC4WbzJQy1LOoH8Juqm5FqG8evZJ7otMzB9s=","DQ":"hBz7Jz9TQoKsV//0KfXMlRFqO3v1qVe4A8b7knrTCL/0IjaD1edZvx3G6H6zkKXZkkXHVTYnJHMxIRm3Ds8C6EYwt9jfNtodQundv9w8xTKNJTemLKLtp9IuBBbL3X4dwZdriiI4woDRhe+ZD5fP25ElqbBlDC4ra1/N6KJUREU=","Exponent":"AQAB","InverseQ":"MrVycQYUEcmT6ddtkiXV4L3Qg+1iXgIbFF5OAtgXkD+lI+rclgUnFTYG3qgUhMn/ZtV/HhpW2FavUKCsmyv2Lmd1nVHe4LjgAjpKu9NlkripI/ROw8SculZEisI7P2lcAVI5kUSAQumiQg7kQcDqrTnrTI/T5m5LuygcGoYo3kA=","Modulus":"nCbBW8+F6FuisseukkmknunroSMyJ8oi/yRyqWWBiVwiWYkvT88SfASyBLciDPXtV0PXsGkmxGyD4z/cQa5O5xU/gRHS3BKvxmuBribI+6JGBF1ukXoMooGIKEunDArgKb8O1bWDX7MyxU8sxyok73qOvtIqEb+ENKd1bFnP4PH/Yi3ZgHyTGcKfdRuB80WJQf8g9p1SFecg4Hhrh+j8XGeQow46bHgoKHelGSFlC6MFt12k0HISfty1AnHxrUP1IT5YpEWIDHNiexqJgc5Q2Bw9Tworux3sR6hQUh588NWSPw/9nhWT/l6AGifp7nJqiIEtgvrhWeVtUXJgZkWgnQ==","P":"xSulQHBrGbZPrSqKiPBORAdK+ZHqg0VD6i9pCbzjDBWYGRS/ekbF/JQR81p/Wrm/K6hi1+eeDElZPK2e4WfyqQqVeDs6QQMjXjpSA/65WWdTDjNL7So2fYSUV70dwawo/EWtY1JHmBGEcKKDbeNGMwrP44iGQhE7wsGLQFB1NM8=","Q":"yr34862ggc3OsYi8JFy83fmwiurGQc25+0U1UtiT0jhadSXcg+XEHjWUKTpHVJEIF+DKV+lwXTEtF5lsa1BAl5hHDwgcY19TJkaF4QIsfNMp0/s5iWNRdRhWKoqp9QGKxTOF9jpCeJgj0MUwLcFLZQ+UvWnqLv3yjsJ+De69xtM="}} ================================================ FILE: src/API/RequestExamples/Authentication.http ================================================ ### Authenticate Member POST {{baseUrl}}/connect/token Content-Type: application/x-www-form-urlencoded grant_type=password&username=testMember@mail.com&password=testMemberPass&client_id=ro.client&client_secret=secret ### Authenticate Admin POST {{baseUrl}}/connect/token Content-Type: application/x-www-form-urlencoded grant_type=password&username=testAdmin@mail.com&password=testAdminPass&client_id=ro.client&client_secret=secret ================================================ FILE: src/API/RequestExamples/Users.http ================================================ ### Register a new user POST {{baseUrl}}/userAccess/UserRegistrations Content-Type: application/json { "Login": "login", "Password": "password", "Email": "email@mail.com", "FirstName": "John", "LastName": "Doe", "ConfirmLink": "Abc" } ### User registration confirmation PATCH {{baseUrl}}/userAccess/UserRegistrations/e80985c5-bf97-4bb3-b178-9423d70ef87b/confirm ================================================ FILE: src/API/RequestExamples/http-client.env.json ================================================ { "dev": { "baseUrl": "http://localhost:5000" } } ================================================ FILE: src/BuildingBlocks/Application/CompanyName.MyMeetings.BuildingBlocks.Application.csproj ================================================  ================================================ FILE: src/BuildingBlocks/Application/Data/ISqlConnectionFactory.cs ================================================ using System.Data; namespace CompanyName.MyMeetings.BuildingBlocks.Application.Data { public interface ISqlConnectionFactory { IDbConnection GetOpenConnection(); IDbConnection CreateNewConnection(); string GetConnectionString(); } } ================================================ FILE: src/BuildingBlocks/Application/Emails/EmailMessage.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Application.Emails { public struct EmailMessage { public string To { get; } public string Subject { get; } public string Content { get; } public EmailMessage( string to, string subject, string content) { this.To = to; this.Subject = subject; this.Content = content; } } } ================================================ FILE: src/BuildingBlocks/Application/Emails/IEmailSender.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Application.Emails { public interface IEmailSender { Task SendEmail(EmailMessage message); } } ================================================ FILE: src/BuildingBlocks/Application/Events/DomainNotificationBase.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.BuildingBlocks.Application.Events { public class DomainNotificationBase : IDomainEventNotification where T : IDomainEvent { public T DomainEvent { get; } public Guid Id { get; } public DomainNotificationBase(T domainEvent, Guid id) { this.Id = id; this.DomainEvent = domainEvent; } } } ================================================ FILE: src/BuildingBlocks/Application/Events/IDomainEventNotification.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.BuildingBlocks.Application.Events { public interface IDomainEventNotification : IDomainEventNotification { TEventType DomainEvent { get; } } public interface IDomainEventNotification : INotification { Guid Id { get; } } } ================================================ FILE: src/BuildingBlocks/Application/IExecutionContextAccessor.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Application { public interface IExecutionContextAccessor { Guid UserId { get; } Guid CorrelationId { get; } bool IsAvailable { get; } } } ================================================ FILE: src/BuildingBlocks/Application/InvalidCommandException.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Application { public class InvalidCommandException : Exception { public List Errors { get; } public InvalidCommandException(List errors) { this.Errors = errors; } } } ================================================ FILE: src/BuildingBlocks/Application/Outbox/IOutbox.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Application.Outbox { public interface IOutbox { void Add(OutboxMessage message); Task Save(); } } ================================================ FILE: src/BuildingBlocks/Application/Outbox/OutboxMessage.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Application.Outbox { public class OutboxMessage { public Guid Id { get; set; } public DateTime OccurredOn { get; set; } public string Type { get; set; } public string Data { get; set; } public DateTime? ProcessedDate { get; set; } public OutboxMessage(Guid id, DateTime occurredOn, string type, string data) { this.Id = id; this.OccurredOn = occurredOn; this.Type = type; this.Data = data; } private OutboxMessage() { } } } ================================================ FILE: src/BuildingBlocks/Application/Queries/IPagedQuery.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Application.Queries { public interface IPagedQuery { /// /// Page number. If null then default is 1. /// int? Page { get; } /// /// Records number per page (page size). /// int? PerPage { get; } } } ================================================ FILE: src/BuildingBlocks/Application/Queries/PageData.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Application.Queries { public struct PageData { public int Offset { get; } public int Next { get; } public PageData(int offset, int next) { this.Offset = offset; this.Next = next; } } } ================================================ FILE: src/BuildingBlocks/Application/Queries/PagedQueryHelper.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Application.Queries { public static class PagedQueryHelper { public const string Offset = "Offset"; public const string Next = "Next"; public static PageData GetPageData(IPagedQuery query) { int offset; if (!query.Page.HasValue || !query.PerPage.HasValue) { offset = 0; } else { offset = (query.Page.Value - 1) * query.PerPage.Value; } int next; if (!query.PerPage.HasValue) { next = int.MaxValue; } else { next = query.PerPage.Value; } return new PageData(offset, next); } public static string AppendPageStatement(string sql) { return $"{sql} " + $"OFFSET @{Offset} ROWS FETCH NEXT @{Next} ROWS ONLY; "; } } } ================================================ FILE: src/BuildingBlocks/Domain/BusinessRuleValidationException.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Domain { public class BusinessRuleValidationException : Exception { public IBusinessRule BrokenRule { get; } public string Details { get; } public BusinessRuleValidationException(IBusinessRule brokenRule) : base(brokenRule.Message) { BrokenRule = brokenRule; this.Details = brokenRule.Message; } public override string ToString() { return $"{BrokenRule.GetType().FullName}: {BrokenRule.Message}"; } } } ================================================ FILE: src/BuildingBlocks/Domain/CompanyName.MyMeetings.BuildingBlocks.Domain.csproj ================================================ ================================================ FILE: src/BuildingBlocks/Domain/DomainEventBase.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Domain { public class DomainEventBase : IDomainEvent { public Guid Id { get; } public DateTime OccurredOn { get; } public DomainEventBase() { this.Id = Guid.NewGuid(); this.OccurredOn = DateTime.UtcNow; } } } ================================================ FILE: src/BuildingBlocks/Domain/Entity.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Domain { public abstract class Entity { private List _domainEvents; /// /// Domain events occurred. /// public IReadOnlyCollection DomainEvents => _domainEvents?.AsReadOnly(); public void ClearDomainEvents() { _domainEvents?.Clear(); } /// /// Add domain event. /// /// Domain event. protected void AddDomainEvent(IDomainEvent domainEvent) { _domainEvents ??= []; this._domainEvents.Add(domainEvent); } protected void CheckRule(IBusinessRule rule) { if (rule.IsBroken()) { throw new BusinessRuleValidationException(rule); } } } } ================================================ FILE: src/BuildingBlocks/Domain/IAggregateRoot.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Domain { public interface IAggregateRoot { } } ================================================ FILE: src/BuildingBlocks/Domain/IBusinessRule.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Domain { public interface IBusinessRule { bool IsBroken(); string Message { get; } } } ================================================ FILE: src/BuildingBlocks/Domain/IDomainEvent.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.BuildingBlocks.Domain { public interface IDomainEvent : INotification { Guid Id { get; } DateTime OccurredOn { get; } } } ================================================ FILE: src/BuildingBlocks/Domain/IgnoreMemberAttribute.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Domain { [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class IgnoreMemberAttribute : Attribute { } } ================================================ FILE: src/BuildingBlocks/Domain/TypedIdValueBase.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Domain { public abstract class TypedIdValueBase : IEquatable { public Guid Value { get; } protected TypedIdValueBase(Guid value) { if (value == Guid.Empty) { throw new InvalidOperationException("Id value cannot be empty!"); } Value = value; } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } return obj is TypedIdValueBase other && Equals(other); } public override int GetHashCode() { return Value.GetHashCode(); } public bool Equals(TypedIdValueBase other) { return this.Value == other?.Value; } public static bool operator ==(TypedIdValueBase obj1, TypedIdValueBase obj2) { if (object.Equals(obj1, null)) { if (object.Equals(obj2, null)) { return true; } return false; } return obj1.Equals(obj2); } public static bool operator !=(TypedIdValueBase x, TypedIdValueBase y) { return !(x == y); } } } ================================================ FILE: src/BuildingBlocks/Domain/ValueObject.cs ================================================ using System.Reflection; namespace CompanyName.MyMeetings.BuildingBlocks.Domain { public abstract class ValueObject : IEquatable { private List _properties; private List _fields; public static bool operator ==(ValueObject obj1, ValueObject obj2) { if (object.Equals(obj1, null)) { if (object.Equals(obj2, null)) { return true; } return false; } return obj1.Equals(obj2); } public static bool operator !=(ValueObject obj1, ValueObject obj2) { return !(obj1 == obj2); } public bool Equals(ValueObject obj) { return Equals(obj as object); } public override bool Equals(object obj) { if (obj == null || GetType() != obj.GetType()) { return false; } return GetProperties().All(p => PropertiesAreEqual(obj, p)) && GetFields().All(f => FieldsAreEqual(obj, f)); } public override int GetHashCode() { unchecked { int hash = 17; foreach (var prop in GetProperties()) { var value = prop.GetValue(this, null); hash = HashValue(hash, value); } foreach (var field in GetFields()) { var value = field.GetValue(this); hash = HashValue(hash, value); } return hash; } } protected static void CheckRule(IBusinessRule rule) { if (rule.IsBroken()) { throw new BusinessRuleValidationException(rule); } } private bool PropertiesAreEqual(object obj, PropertyInfo p) { return object.Equals(p.GetValue(this, null), p.GetValue(obj, null)); } private bool FieldsAreEqual(object obj, FieldInfo f) { return object.Equals(f.GetValue(this), f.GetValue(obj)); } private IEnumerable GetProperties() { if (this._properties == null) { this._properties = GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public) .Where(p => p.GetCustomAttribute(typeof(IgnoreMemberAttribute)) == null) .ToList(); // Not available in Core // !Attribute.IsDefined(p, typeof(IgnoreMemberAttribute))).ToList(); } return this._properties; } private IEnumerable GetFields() { if (this._fields == null) { this._fields = GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(p => p.GetCustomAttribute(typeof(IgnoreMemberAttribute)) == null) .ToList(); } return this._fields; } private int HashValue(int seed, object value) { var currentHash = value?.GetHashCode() ?? 0; return (seed * 23) + currentHash; } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/BiDictionary.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure { public class BiDictionary { private readonly IDictionary _firstToSecond = new Dictionary(); private readonly IDictionary _secondToFirst = new Dictionary(); public void Add(TFirst first, TSecond second) { if (_firstToSecond.ContainsKey(first) || _secondToFirst.ContainsKey(second)) { throw new ArgumentException("Duplicate first or second"); } _firstToSecond.Add(first, second); _secondToFirst.Add(second, first); } public bool TryGetByFirst(TFirst first, out TSecond second) { return _firstToSecond.TryGetValue(first, out second); } public bool TryGetBySecond(TSecond second, out TFirst first) { return _secondToFirst.TryGetValue(second, out first); } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/CompanyName.MyMeetings.BuildingBlocks.Infrastructure.csproj ================================================  ================================================ FILE: src/BuildingBlocks/Infrastructure/DomainEventsDispatching/DomainEventsAccessor.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching { public class DomainEventsAccessor : IDomainEventsAccessor { private readonly DbContext _dbContext; public DomainEventsAccessor(DbContext dbContext) { _dbContext = dbContext; } public IReadOnlyCollection GetAllDomainEvents() { var domainEntities = this._dbContext.ChangeTracker .Entries() .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()).ToList(); return domainEntities .SelectMany(x => x.Entity.DomainEvents) .ToList(); } public void ClearAllDomainEvents() { var domainEntities = this._dbContext.ChangeTracker .Entries() .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()).ToList(); domainEntities .ForEach(entity => entity.Entity.ClearDomainEvents()); } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/DomainEventsDispatching/DomainEventsDispatcher.cs ================================================ using Autofac; using Autofac.Core; using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Serialization; using MediatR; using Newtonsoft.Json; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching { public class DomainEventsDispatcher : IDomainEventsDispatcher { private readonly IMediator _mediator; private readonly ILifetimeScope _scope; private readonly IOutbox _outbox; private readonly IDomainEventsAccessor _domainEventsProvider; private readonly IDomainNotificationsMapper _domainNotificationsMapper; public DomainEventsDispatcher( IMediator mediator, ILifetimeScope scope, IOutbox outbox, IDomainEventsAccessor domainEventsProvider, IDomainNotificationsMapper domainNotificationsMapper) { _mediator = mediator; _scope = scope; _outbox = outbox; _domainEventsProvider = domainEventsProvider; _domainNotificationsMapper = domainNotificationsMapper; } public async Task DispatchEventsAsync() { var domainEvents = _domainEventsProvider.GetAllDomainEvents(); List> domainEventNotifications = []; foreach (var domainEvent in domainEvents) { Type domainEvenNotificationType = typeof(IDomainEventNotification<>); var domainNotificationWithGenericType = domainEvenNotificationType.MakeGenericType(domainEvent.GetType()); var domainNotification = _scope.ResolveOptional(domainNotificationWithGenericType, new List { new NamedParameter("domainEvent", domainEvent), new NamedParameter("id", domainEvent.Id) }); if (domainNotification != null) { domainEventNotifications.Add(domainNotification as IDomainEventNotification); } } _domainEventsProvider.ClearAllDomainEvents(); foreach (var domainEvent in domainEvents) { await _mediator.Publish(domainEvent); } foreach (var domainEventNotification in domainEventNotifications) { var type = _domainNotificationsMapper.GetName(domainEventNotification.GetType()); var data = JsonConvert.SerializeObject(domainEventNotification, new JsonSerializerSettings { ContractResolver = new AllPropertiesContractResolver() }); var outboxMessage = new OutboxMessage( domainEventNotification.Id, domainEventNotification.DomainEvent.OccurredOn, type, data); _outbox.Add(outboxMessage); } } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/DomainEventsDispatching/DomainEventsDispatcherNotificationHandlerDecorator.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching { public class DomainEventsDispatcherNotificationHandlerDecorator : INotificationHandler where T : INotification { private readonly INotificationHandler _decorated; private readonly IDomainEventsDispatcher _domainEventsDispatcher; public DomainEventsDispatcherNotificationHandlerDecorator( IDomainEventsDispatcher domainEventsDispatcher, INotificationHandler decorated) { _domainEventsDispatcher = domainEventsDispatcher; _decorated = decorated; } public async Task Handle(T notification, CancellationToken cancellationToken) { await this._decorated.Handle(notification, cancellationToken); await this._domainEventsDispatcher.DispatchEventsAsync(); } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/DomainEventsDispatching/DomainNotificationsMapper.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching { public class DomainNotificationsMapper : IDomainNotificationsMapper { private readonly BiDictionary _domainNotificationsMap; public DomainNotificationsMapper(BiDictionary domainNotificationsMap) { _domainNotificationsMap = domainNotificationsMap; } public string GetName(Type type) { return _domainNotificationsMap.TryGetBySecond(type, out var name) ? name : null; } public Type GetType(string name) { return _domainNotificationsMap.TryGetByFirst(name, out var type) ? type : null; } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/DomainEventsDispatching/IDomainEventsAccessor.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching { public interface IDomainEventsAccessor { IReadOnlyCollection GetAllDomainEvents(); void ClearAllDomainEvents(); } } ================================================ FILE: src/BuildingBlocks/Infrastructure/DomainEventsDispatching/IDomainEventsDispatcher.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching { public interface IDomainEventsDispatcher { Task DispatchEventsAsync(); } } ================================================ FILE: src/BuildingBlocks/Infrastructure/DomainEventsDispatching/IDomainNotificationsMapper.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching { public interface IDomainNotificationsMapper { string GetName(Type type); Type GetType(string name); } } ================================================ FILE: src/BuildingBlocks/Infrastructure/DomainEventsDispatching/UnitOfWorkCommandHandlerDecorator.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching { public class UnitOfWorkCommandHandlerDecorator : IRequestHandler where T : IRequest { private readonly IRequestHandler _decorated; private readonly IUnitOfWork _unitOfWork; public UnitOfWorkCommandHandlerDecorator( IRequestHandler decorated, IUnitOfWork unitOfWork) { _decorated = decorated; _unitOfWork = unitOfWork; } public async Task Handle(T command, CancellationToken cancellationToken) { await this._decorated.Handle(command, cancellationToken); await this._unitOfWork.CommitAsync(cancellationToken); } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/Emails/EmailSender.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using Dapper; using Serilog; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails { public class EmailSender : IEmailSender { private readonly ILogger _logger; private readonly EmailsConfiguration _configuration; private readonly ISqlConnectionFactory _sqlConnectionFactory; public EmailSender( ILogger logger, EmailsConfiguration configuration, ISqlConnectionFactory sqlConnectionFactory) { _logger = logger; _configuration = configuration; _sqlConnectionFactory = sqlConnectionFactory; } public async Task SendEmail(EmailMessage message) { var sqlConnection = _sqlConnectionFactory.GetOpenConnection(); await sqlConnection.ExecuteScalarAsync( "INSERT INTO [app].[Emails] ([Id], [From], [To], [Subject], [Content], [Date]) " + "VALUES (@Id, @From, @To, @Subject, @Content, @Date) ", new { Id = Guid.NewGuid(), From = _configuration.FromEmail, message.To, message.Subject, message.Content, Date = DateTime.UtcNow }); _logger.Information( "Email sent. From: {From}, To: {To}, Subject: {Subject}, Content: {Content}.", _configuration.FromEmail, message.To, message.Subject, message.Content); } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/Emails/EmailsConfiguration.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails { public class EmailsConfiguration { public EmailsConfiguration(string fromEmail) { FromEmail = fromEmail; } public string FromEmail { get; } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/EventBus/IEventsBus.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus { public interface IEventsBus : IDisposable { Task Publish(T @event) where T : IntegrationEvent; void Subscribe(IIntegrationEventHandler handler) where T : IntegrationEvent; void StartConsuming(); } } ================================================ FILE: src/BuildingBlocks/Infrastructure/EventBus/IIntegrationEventHandler.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus { public interface IIntegrationEventHandler : IIntegrationEventHandler where TIntegrationEvent : IntegrationEvent { Task Handle(TIntegrationEvent @event); } public interface IIntegrationEventHandler { } } ================================================ FILE: src/BuildingBlocks/Infrastructure/EventBus/InMemoryEventBus.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus { public sealed class InMemoryEventBus { static InMemoryEventBus() { } private InMemoryEventBus() { _handlersDictionary = new Dictionary>(); } public static InMemoryEventBus Instance { get; } = new InMemoryEventBus(); private readonly IDictionary> _handlersDictionary; public void Subscribe(IIntegrationEventHandler handler) where T : IntegrationEvent { var eventType = typeof(T).FullName; if (eventType != null) { if (_handlersDictionary.ContainsKey(eventType)) { var handlers = _handlersDictionary[eventType]; handlers.Add(handler); } else { _handlersDictionary.Add(eventType, [handler]); } } } public async Task Publish(T @event) where T : IntegrationEvent { var eventType = @event.GetType().FullName; if (eventType == null) { return; } List integrationEventHandlers = _handlersDictionary[eventType]; foreach (var integrationEventHandler in integrationEventHandlers) { if (integrationEventHandler is IIntegrationEventHandler handler) { await handler.Handle(@event); } } } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/EventBus/InMemoryEventBusClient.cs ================================================ using Serilog; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus { public class InMemoryEventBusClient : IEventsBus { private readonly ILogger _logger; public InMemoryEventBusClient(ILogger logger) { _logger = logger; } public void Dispose() { } public async Task Publish(T @event) where T : IntegrationEvent { _logger.Information("Publishing {Event}", @event.GetType().FullName); await InMemoryEventBus.Instance.Publish(@event); } public void Subscribe(IIntegrationEventHandler handler) where T : IntegrationEvent { InMemoryEventBus.Instance.Subscribe(handler); } public void StartConsuming() { } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/EventBus/IntegrationEvent.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus { public abstract class IntegrationEvent : INotification { public Guid Id { get; } public DateTime OccurredOn { get; } protected IntegrationEvent(Guid id, DateTime occurredOn) { this.Id = id; this.OccurredOn = occurredOn; } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/IUnitOfWork.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure { public interface IUnitOfWork { Task CommitAsync( CancellationToken cancellationToken = default, Guid? internalCommandId = null); } } ================================================ FILE: src/BuildingBlocks/Infrastructure/Inbox/InboxMessage.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Inbox { public class InboxMessage { public Guid Id { get; set; } public DateTime OccurredOn { get; set; } public string Type { get; set; } public string Data { get; set; } public DateTime? ProcessedDate { get; set; } public InboxMessage(DateTime occurredOn, string type, string data) { this.Id = Guid.NewGuid(); this.OccurredOn = occurredOn; this.Type = type; this.Data = data; } private InboxMessage() { } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/InternalCommands/IInternalCommandsMapper.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands { public interface IInternalCommandsMapper { string GetName(Type type); Type GetType(string name); } } ================================================ FILE: src/BuildingBlocks/Infrastructure/InternalCommands/InternalCommand.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands { public class InternalCommand { public Guid Id { get; set; } public string Type { get; set; } public string Data { get; set; } public DateTime? ProcessedDate { get; set; } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/InternalCommands/InternalCommandsMapper.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands { public class InternalCommandsMapper : IInternalCommandsMapper { private readonly BiDictionary _internalCommandsMap; public InternalCommandsMapper(BiDictionary internalCommandsMap) { _internalCommandsMap = internalCommandsMap; } public string GetName(Type type) { return _internalCommandsMap.TryGetBySecond(type, out var name) ? name : null; } public Type GetType(string name) { return _internalCommandsMap.TryGetByFirst(name, out var type) ? type : null; } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/Serialization/AllPropertiesContractResolver.cs ================================================ using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Serialization { public class AllPropertiesContractResolver : DefaultContractResolver { protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) { var properties = type.GetProperties( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Select(p => this.CreateProperty(p, memberSerialization)) .ToList(); properties.ForEach(p => { p.Writable = true; p.Readable = true; }); return properties; } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/ServiceProviderWrapper.cs ================================================ using Autofac; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure { public class ServiceProviderWrapper : IServiceProvider { private readonly ILifetimeScope lifeTimeScope; public ServiceProviderWrapper(ILifetimeScope lifeTimeScope) { this.lifeTimeScope = lifeTimeScope; } #nullable enable public object? GetService(Type serviceType) => this.lifeTimeScope.ResolveOptional(serviceType); } } ================================================ FILE: src/BuildingBlocks/Infrastructure/SqlConnectionFactory.cs ================================================ using System.Data; using System.Data.SqlClient; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure { public class SqlConnectionFactory : ISqlConnectionFactory, IDisposable { private readonly string _connectionString; private IDbConnection _connection; public SqlConnectionFactory(string connectionString) { this._connectionString = connectionString; } public IDbConnection GetOpenConnection() { if (this._connection == null || this._connection.State != ConnectionState.Open) { this._connection = new SqlConnection(_connectionString); this._connection.Open(); } return this._connection; } public IDbConnection CreateNewConnection() { var connection = new SqlConnection(_connectionString); connection.Open(); return connection; } public string GetConnectionString() { return _connectionString; } public void Dispose() { if (this._connection != null && this._connection.State == ConnectionState.Open) { this._connection.Dispose(); } } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/StronglyTypedIdValueConverterSelector.cs ================================================ using System.Collections.Concurrent; using CompanyName.MyMeetings.BuildingBlocks.Domain; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 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/. /// public class StronglyTypedIdValueConverterSelector : ValueConverterSelector { private readonly ConcurrentDictionary<(Type ModelClrType, Type ProviderClrType), ValueConverterInfo> _converters = new ConcurrentDictionary<(Type ModelClrType, Type ProviderClrType), ValueConverterInfo>(); public StronglyTypedIdValueConverterSelector(ValueConverterSelectorDependencies dependencies) : base(dependencies) { } public override IEnumerable Select(Type modelClrType, Type providerClrType = null) { var baseConverters = base.Select(modelClrType, providerClrType); foreach (var converter in baseConverters) { yield return converter; } var underlyingModelType = UnwrapNullableType(modelClrType); var underlyingProviderType = UnwrapNullableType(providerClrType); if (underlyingProviderType is null || underlyingProviderType == typeof(Guid)) { var isTypedIdValue = typeof(TypedIdValueBase).IsAssignableFrom(underlyingModelType); if (isTypedIdValue) { var converterType = typeof(TypedIdValueConverter<>).MakeGenericType(underlyingModelType); yield return _converters.GetOrAdd((underlyingModelType, typeof(Guid)), _ => { return new ValueConverterInfo( modelClrType: modelClrType, providerClrType: typeof(Guid), factory: valueConverterInfo => (ValueConverter)Activator.CreateInstance(converterType, valueConverterInfo.MappingHints)); }); } } } private static Type UnwrapNullableType(Type type) { if (type is null) { return null; } return Nullable.GetUnderlyingType(type) ?? type; } } } ================================================ FILE: src/BuildingBlocks/Infrastructure/TypedIdValueConverter.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure { public class TypedIdValueConverter : ValueConverter where TTypedIdValue : TypedIdValueBase { public TypedIdValueConverter(ConverterMappingHints mappingHints = null) : base(id => id.Value, value => Create(value), mappingHints) { } private static TTypedIdValue Create(Guid id) => Activator.CreateInstance(typeof(TTypedIdValue), id) as TTypedIdValue; } } ================================================ FILE: src/BuildingBlocks/Infrastructure/UnitOfWork.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.BuildingBlocks.Infrastructure { public class UnitOfWork : IUnitOfWork { private readonly DbContext _context; private readonly IDomainEventsDispatcher _domainEventsDispatcher; public UnitOfWork( DbContext context, IDomainEventsDispatcher domainEventsDispatcher) { this._context = context; this._domainEventsDispatcher = domainEventsDispatcher; } public async Task CommitAsync( CancellationToken cancellationToken = default, Guid? internalCommandId = null) { await this._domainEventsDispatcher.DispatchEventsAsync(); return await _context.SaveChangesAsync(cancellationToken); } } } ================================================ FILE: src/BuildingBlocks/Tests/Application.UnitTests/CompanyName.MyMeetings.BuildingBlocks.Application.UnitTests.csproj ================================================  ================================================ FILE: src/BuildingBlocks/Tests/Application.UnitTests/Queries/PagedQueryHelperTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Queries; using NUnit.Framework; namespace CompanyName.MyMeetings.BuildingBlocks.Application.UnitTests.Queries { [TestFixture] public class PagedQueryHelperTests { [TestCase(1, 5, 0, 5)] [TestCase(3, 10, 20, 10)] [TestCase(null, 20, 0, 20)] [TestCase(5, null, 0, int.MaxValue)] [TestCase(null, null, 0, int.MaxValue)] public void PagedQueryHelper_GetPageData_Test(int? page, int? perPage, int offset, int next) { IPagedQuery query = new TestQuery(page, perPage); var pageData = PagedQueryHelper.GetPageData(query); Assert.That(pageData, Is.EqualTo(new PageData(offset, next))); } private class TestQuery : IPagedQuery { public TestQuery(int? page, int? perPage) { Page = page; PerPage = perPage; } public int? Page { get; } public int? PerPage { get; } } } } ================================================ FILE: src/BuildingBlocks/Tests/IntegrationTests/CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.csproj ================================================ ================================================ FILE: src/BuildingBlocks/Tests/IntegrationTests/EnvironmentVariablesProvider.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.IntegrationTests { public static class EnvironmentVariablesProvider { public static string GetVariable(string variableName) { var environmentVariable = Environment.GetEnvironmentVariable(variableName); if (!string.IsNullOrEmpty(environmentVariable)) { return environmentVariable; } environmentVariable = Environment.GetEnvironmentVariable(variableName, EnvironmentVariableTarget.User); if (!string.IsNullOrEmpty(environmentVariable)) { return environmentVariable; } return Environment.GetEnvironmentVariable(variableName, EnvironmentVariableTarget.Machine); } } } ================================================ FILE: src/BuildingBlocks/Tests/IntegrationTests/Probing/AssertErrorException.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing { public class AssertErrorException : Exception { public AssertErrorException(string message) : base(message) { } } } ================================================ FILE: src/BuildingBlocks/Tests/IntegrationTests/Probing/IProbe.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing { public interface IProbe { bool IsSatisfied(); Task SampleAsync(); string DescribeFailureTo(); } public interface IProbe { bool IsSatisfied(T sample); Task GetSampleAsync(); string DescribeFailureTo(); } } ================================================ FILE: src/BuildingBlocks/Tests/IntegrationTests/Probing/Poller.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing { public class Poller { private readonly int _timeoutMillis; private readonly int _pollDelayMillis; public Poller(int timeoutMillis) { _timeoutMillis = timeoutMillis; _pollDelayMillis = 1000; } public async Task CheckAsync(IProbe probe) { var timeout = new Timeout(_timeoutMillis); while (!probe.IsSatisfied()) { if (timeout.HasTimedOut()) { throw new AssertErrorException(DescribeFailureOf(probe)); } await Task.Delay(_pollDelayMillis); await probe.SampleAsync(); } } public async Task GetAsync(IProbe probe) where T : class { var timeout = new Timeout(_timeoutMillis); T sample = null; while (!probe.IsSatisfied(sample)) { if (timeout.HasTimedOut()) { throw new AssertErrorException(DescribeFailureOf(probe)); } await Task.Delay(_pollDelayMillis); sample = await probe.GetSampleAsync(); } return sample; } private static string DescribeFailureOf(IProbe probe) { return probe.DescribeFailureTo(); } private static string DescribeFailureOf(IProbe probe) { return probe.DescribeFailureTo(); } } } ================================================ FILE: src/BuildingBlocks/Tests/IntegrationTests/Probing/Timeout.cs ================================================ namespace CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing { public class Timeout { private readonly DateTime _endTime; public Timeout(int duration) { this._endTime = DateTime.Now.AddMilliseconds(duration); } public bool HasTimedOut() { return DateTime.Now > _endTime; } } } ================================================ FILE: src/CompanyName.MyMeetings.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.8.34330.188 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.API", "API\CompanyName.MyMeetings.API\CompanyName.MyMeetings.API.csproj", "{49D08B64-AC8E-4607-820F-8A0B989CFD33}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildingBlocks", "BuildingBlocks", "{E91D4BE3-61FE-441C-A227-29850D414216}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{BCE1EE3C-ADB1-48CC-9FD1-C7324D886964}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meetings", "Meetings", "{9CD43CAC-C149-41E1-9654-157D578143B7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UserAccess", "UserAccess", "{AE6D0618-60E1-40B2-A46F-664A19C9503C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{BC9DDFD1-FB81-4996-812A-68BEBCA33A97}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.UserAccess.Application", "Modules\UserAccess\Application\CompanyName.MyMeetings.Modules.UserAccess.Application.csproj", "{F34C6504-590B-480A-A239-F230CDFF8CED}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Meetings.Application", "Modules\Meetings\Application\CompanyName.MyMeetings.Modules.Meetings.Application.csproj", "{5C2C1630-8A7A-451F-81A8-8547C939F5FD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Meetings.Domain", "Modules\Meetings\Domain\CompanyName.MyMeetings.Modules.Meetings.Domain.csproj", "{F71CFDA8-0770-4761-896A-FB0098113F87}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Meetings.Infrastructure", "Modules\Meetings\Infrastructure\CompanyName.MyMeetings.Modules.Meetings.Infrastructure.csproj", "{93F3D023-37BD-4C5C-9442-DE2631CD4954}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.BuildingBlocks.Domain", "BuildingBlocks\Domain\CompanyName.MyMeetings.BuildingBlocks.Domain.csproj", "{CA91F71E-8067-4E98-A4D4-E17F5880DE39}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.BuildingBlocks.Infrastructure", "BuildingBlocks\Infrastructure\CompanyName.MyMeetings.BuildingBlocks.Infrastructure.csproj", "{2EEAFD26-A0A9-4F91-BEB7-B0F8ED85ACBA}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Administration", "Administration", "{5F398170-87FD-4368-9930-FAAAD2D9FDCC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Administration.Domain", "Modules\Administration\Domain\CompanyName.MyMeetings.Modules.Administration.Domain.csproj", "{590517F8-686B-460A-97B3-0A09A8BA443C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Administration.Application", "Modules\Administration\Application\CompanyName.MyMeetings.Modules.Administration.Application.csproj", "{6F039231-8745-4171-B5CA-8688E4683CFC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Administration.Infrastructure", "Modules\Administration\Infrastructure\CompanyName.MyMeetings.Modules.Administration.Infrastructure.csproj", "{CD765A37-EB16-4E35-AA03-6E706D70E8A0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents", "Modules\Meetings\IntegrationEvents\CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents.csproj", "{3ED61776-83A0-426C-9B3A-3AB755DA01EF}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Database", "Database", "{C733D087-7051-4E35-BCDB-081252A108E5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Administration.IntegrationEvents", "Modules\Administration\IntegrationEvents\CompanyName.MyMeetings.Modules.Administration.IntegrationEvents.csproj", "{396817BD-94A7-4559-A7F1-2FAB8009BCF8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.UserAccess.Infrastructure", "Modules\UserAccess\Infrastructure\CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.csproj", "{55E1C531-2B9B-49B1-BDFE-9DE322E7FE00}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.UserAccess.Domain", "Modules\UserAccess\Domain\CompanyName.MyMeetings.Modules.UserAccess.Domain.csproj", "{F364B0C4-1882-46A1-9B08-22587BEF05A2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents", "Modules\UserAccess\IntegrationEvents\CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents.csproj", "{0C31EA31-6A10-47D0-82E5-6D224E1CE532}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Payments", "Payments", "{13E4D721-2E96-497E-9657-503D09468F9F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Payments.Application", "Modules\Payments\Application\CompanyName.MyMeetings.Modules.Payments.Application.csproj", "{6E013582-9E44-4D7E-8CFE-5F6FB6757B03}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Payments.Domain", "Modules\Payments\Domain\CompanyName.MyMeetings.Modules.Payments.Domain.csproj", "{C457929B-91BE-48CC-A714-217D9CABD3CC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Payments.Infrastructure", "Modules\Payments\Infrastructure\CompanyName.MyMeetings.Modules.Payments.Infrastructure.csproj", "{1F4EAF3D-F5B8-473B-871F-606191D4AB9F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Payments.IntegrationEvents", "Modules\Payments\IntegrationEvents\CompanyName.MyMeetings.Modules.Payments.IntegrationEvents.csproj", "{8DF88C85-594D-45D7-B12E-6B641D497853}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.BuildingBlocks.Application", "BuildingBlocks\Application\CompanyName.MyMeetings.BuildingBlocks.Application.csproj", "{9EAA687B-951E-4D89-8857-99151FF1BCD7}" EndProject Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "CompanyName.MyMeetings.Database", "Database\CompanyName.MyMeetings.Database\CompanyName.MyMeetings.Database.sqlproj", "{43DBBB02-CA43-42AD-BE21-04AC867BA168}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0FF699EF-8156-43CB-8D18-8EA28F30E9EE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.UserAccess.Domain.UnitTests", "Modules\UserAccess\Tests\UnitTests\CompanyName.MyMeetings.Modules.UserAccess.Domain.UnitTests.csproj", "{0BC12804-A858-427B-88E2-F9CDE9E97986}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{53E4F002-E708-45F7-8444-19EB8977B5C9}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests", "Modules\Payments\Tests\UnitTests\CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.csproj", "{602768DD-F063-469D-9CD3-95CB02F6E441}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{F544D6DA-740A-4313-9542-D989666EA9DE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Administration.Domain.UnitTests", "Modules\Administration\Tests\UnitTests\CompanyName.MyMeetings.Modules.Administration.Domain.UnitTests.csproj", "{AC73F391-BC1F-4B7F-AF49-BD87223E2BB0}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{237A249C-B589-49B8-9B84-CA504DE5D137}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests", "Modules\Meetings\Tests\UnitTests\CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.csproj", "{E13970C6-87EE-40B1-854A-3053C95BC4C9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{8B08A9EE-CE27-4CC3-ACB3-3BD9628E5479}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.ArchTests", "Tests\ArchTests\CompanyName.MyMeetings.ArchTests.csproj", "{88081DDB-2FBD-4F7D-830F-24F5F6B78294}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Administration.ArchTests", "Modules\Administration\Tests\ArchTests\CompanyName.MyMeetings.Modules.Administration.ArchTests.csproj", "{F2B164AC-99AC-4906-A133-85D6BB8432B4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Meetings.ArchTests", "Modules\Meetings\Tests\ArchTests\CompanyName.MyMeetings.Modules.Meetings.ArchTests.csproj", "{475AF987-1458-447E-8504-4F1040B56A2C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Payments.ArchTests", "Modules\Payments\Tests\ArchTests\CompanyName.MyMeetings.Modules.Payments.ArchTests.csproj", "{1885C71E-1624-4673-B0BC-BF4035CFFE72}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.UserAccess.ArchTests", "Modules\UserAccess\Tests\ArchTests\CompanyName.MyMeetings.Modules.UserAccess.ArchTests.csproj", "{F8EE61DA-0E8B-4074-A0AF-433244EC1FB8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests", "Modules\UserAccess\Tests\IntegrationTests\CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.csproj", "{396A6C3F-74BA-4D85-8BF3-1182F9DE9D95}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Payments.IntegrationTests", "Modules\Payments\Tests\IntegrationTests\CompanyName.MyMeetings.Modules.Payments.IntegrationTests.csproj", "{B448FDC3-5F85-47EE-9F4A-2654E8CC67E1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Administration.IntegrationTests", "Modules\Administration\Tests\IntegrationTests\CompanyName.MyMeetings.Modules.Administration.IntegrationTests.csproj", "{3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.IntegrationTests", "Tests\IntegrationTests\CompanyName.MyMeetings.IntegrationTests.csproj", "{586DB9FA-CBBF-4867-A57D-E2359925D09A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Meetings.IntegrationTests", "Modules\Meetings\Tests\IntegrationTests\CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.csproj", "{785E93F2-3415-4215-BD85-1CED429CF260}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B1D398A1-7005-4792-8D7F-CD9E5F3C29B7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.BuildingBlocks.Application.UnitTests", "BuildingBlocks\Tests\Application.UnitTests\CompanyName.MyMeetings.BuildingBlocks.Application.UnitTests.csproj", "{32915CF6-62D9-4121-8650-6279D5154B4B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.BuildingBlocks.IntegrationTests", "BuildingBlocks\Tests\IntegrationTests\CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.csproj", "{8CCB9013-92CE-4A26-B649-0997F5444161}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CI", "CI", "{63502820-A1A1-442D-A30E-4F43E722D1B0}" ProjectSection(SolutionItems) = preProject ..\.github\workflows\buildPipeline.yml = ..\.github\workflows\buildPipeline.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{8172C84D-A01E-4B56-8A41-498E5DB9E395}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets Directory.Packages.props = Directory.Packages.props global.json = global.json ..\README.md = ..\README.md stylecop.json = stylecop.json EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseMigrator", "Database\DatabaseMigrator\DatabaseMigrator.csproj", "{9189FC86-116F-49D4-900E-DEC434136C6D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "..\build\_build.csproj", "{866A329C-E942-46D1-AF37-BB617F7F4AC2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Database.Build", "Database\CompanyName.MyMeetings.Database.Build\CompanyName.MyMeetings.Database.Build.csproj", "{165E76B9-DB0C-49B7-B3DC-52DFBEA55A79}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RequestExamples", "RequestExamples", "{00B904C6-D29A-4F26-B7AD-116C701DB73F}" ProjectSection(SolutionItems) = preProject API\RequestExamples\Authentication.http = API\RequestExamples\Authentication.http API\RequestExamples\http-client.env.json = API\RequestExamples\http-client.env.json API\RequestExamples\Users.http = API\RequestExamples\Users.http EndProjectSection 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 Production|Any CPU = Production|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {49D08B64-AC8E-4607-820F-8A0B989CFD33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {49D08B64-AC8E-4607-820F-8A0B989CFD33}.Debug|Any CPU.Build.0 = Debug|Any CPU {49D08B64-AC8E-4607-820F-8A0B989CFD33}.Production|Any CPU.ActiveCfg = Production|Any CPU {49D08B64-AC8E-4607-820F-8A0B989CFD33}.Production|Any CPU.Build.0 = Production|Any CPU {49D08B64-AC8E-4607-820F-8A0B989CFD33}.Release|Any CPU.ActiveCfg = Release|Any CPU {49D08B64-AC8E-4607-820F-8A0B989CFD33}.Release|Any CPU.Build.0 = Release|Any CPU {F34C6504-590B-480A-A239-F230CDFF8CED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F34C6504-590B-480A-A239-F230CDFF8CED}.Debug|Any CPU.Build.0 = Debug|Any CPU {F34C6504-590B-480A-A239-F230CDFF8CED}.Production|Any CPU.ActiveCfg = Production|Any CPU {F34C6504-590B-480A-A239-F230CDFF8CED}.Production|Any CPU.Build.0 = Production|Any CPU {F34C6504-590B-480A-A239-F230CDFF8CED}.Release|Any CPU.ActiveCfg = Release|Any CPU {F34C6504-590B-480A-A239-F230CDFF8CED}.Release|Any CPU.Build.0 = Release|Any CPU {5C2C1630-8A7A-451F-81A8-8547C939F5FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5C2C1630-8A7A-451F-81A8-8547C939F5FD}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C2C1630-8A7A-451F-81A8-8547C939F5FD}.Production|Any CPU.ActiveCfg = Production|Any CPU {5C2C1630-8A7A-451F-81A8-8547C939F5FD}.Production|Any CPU.Build.0 = Production|Any CPU {5C2C1630-8A7A-451F-81A8-8547C939F5FD}.Release|Any CPU.ActiveCfg = Release|Any CPU {5C2C1630-8A7A-451F-81A8-8547C939F5FD}.Release|Any CPU.Build.0 = Release|Any CPU {F71CFDA8-0770-4761-896A-FB0098113F87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F71CFDA8-0770-4761-896A-FB0098113F87}.Debug|Any CPU.Build.0 = Debug|Any CPU {F71CFDA8-0770-4761-896A-FB0098113F87}.Production|Any CPU.ActiveCfg = Production|Any CPU {F71CFDA8-0770-4761-896A-FB0098113F87}.Production|Any CPU.Build.0 = Production|Any CPU {F71CFDA8-0770-4761-896A-FB0098113F87}.Release|Any CPU.ActiveCfg = Release|Any CPU {F71CFDA8-0770-4761-896A-FB0098113F87}.Release|Any CPU.Build.0 = Release|Any CPU {93F3D023-37BD-4C5C-9442-DE2631CD4954}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {93F3D023-37BD-4C5C-9442-DE2631CD4954}.Debug|Any CPU.Build.0 = Debug|Any CPU {93F3D023-37BD-4C5C-9442-DE2631CD4954}.Production|Any CPU.ActiveCfg = Production|Any CPU {93F3D023-37BD-4C5C-9442-DE2631CD4954}.Production|Any CPU.Build.0 = Production|Any CPU {93F3D023-37BD-4C5C-9442-DE2631CD4954}.Release|Any CPU.ActiveCfg = Release|Any CPU {93F3D023-37BD-4C5C-9442-DE2631CD4954}.Release|Any CPU.Build.0 = Release|Any CPU {CA91F71E-8067-4E98-A4D4-E17F5880DE39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CA91F71E-8067-4E98-A4D4-E17F5880DE39}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA91F71E-8067-4E98-A4D4-E17F5880DE39}.Production|Any CPU.ActiveCfg = Production|Any CPU {CA91F71E-8067-4E98-A4D4-E17F5880DE39}.Production|Any CPU.Build.0 = Production|Any CPU {CA91F71E-8067-4E98-A4D4-E17F5880DE39}.Release|Any CPU.ActiveCfg = Release|Any CPU {CA91F71E-8067-4E98-A4D4-E17F5880DE39}.Release|Any CPU.Build.0 = Release|Any CPU {2EEAFD26-A0A9-4F91-BEB7-B0F8ED85ACBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2EEAFD26-A0A9-4F91-BEB7-B0F8ED85ACBA}.Debug|Any CPU.Build.0 = Debug|Any CPU {2EEAFD26-A0A9-4F91-BEB7-B0F8ED85ACBA}.Production|Any CPU.ActiveCfg = Production|Any CPU {2EEAFD26-A0A9-4F91-BEB7-B0F8ED85ACBA}.Production|Any CPU.Build.0 = Production|Any CPU {2EEAFD26-A0A9-4F91-BEB7-B0F8ED85ACBA}.Release|Any CPU.ActiveCfg = Release|Any CPU {2EEAFD26-A0A9-4F91-BEB7-B0F8ED85ACBA}.Release|Any CPU.Build.0 = Release|Any CPU {590517F8-686B-460A-97B3-0A09A8BA443C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {590517F8-686B-460A-97B3-0A09A8BA443C}.Debug|Any CPU.Build.0 = Debug|Any CPU {590517F8-686B-460A-97B3-0A09A8BA443C}.Production|Any CPU.ActiveCfg = Production|Any CPU {590517F8-686B-460A-97B3-0A09A8BA443C}.Production|Any CPU.Build.0 = Production|Any CPU {590517F8-686B-460A-97B3-0A09A8BA443C}.Release|Any CPU.ActiveCfg = Release|Any CPU {590517F8-686B-460A-97B3-0A09A8BA443C}.Release|Any CPU.Build.0 = Release|Any CPU {6F039231-8745-4171-B5CA-8688E4683CFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6F039231-8745-4171-B5CA-8688E4683CFC}.Debug|Any CPU.Build.0 = Debug|Any CPU {6F039231-8745-4171-B5CA-8688E4683CFC}.Production|Any CPU.ActiveCfg = Production|Any CPU {6F039231-8745-4171-B5CA-8688E4683CFC}.Production|Any CPU.Build.0 = Production|Any CPU {6F039231-8745-4171-B5CA-8688E4683CFC}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F039231-8745-4171-B5CA-8688E4683CFC}.Release|Any CPU.Build.0 = Release|Any CPU {CD765A37-EB16-4E35-AA03-6E706D70E8A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD765A37-EB16-4E35-AA03-6E706D70E8A0}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD765A37-EB16-4E35-AA03-6E706D70E8A0}.Production|Any CPU.ActiveCfg = Production|Any CPU {CD765A37-EB16-4E35-AA03-6E706D70E8A0}.Production|Any CPU.Build.0 = Production|Any CPU {CD765A37-EB16-4E35-AA03-6E706D70E8A0}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD765A37-EB16-4E35-AA03-6E706D70E8A0}.Release|Any CPU.Build.0 = Release|Any CPU {3ED61776-83A0-426C-9B3A-3AB755DA01EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3ED61776-83A0-426C-9B3A-3AB755DA01EF}.Debug|Any CPU.Build.0 = Debug|Any CPU {3ED61776-83A0-426C-9B3A-3AB755DA01EF}.Production|Any CPU.ActiveCfg = Production|Any CPU {3ED61776-83A0-426C-9B3A-3AB755DA01EF}.Production|Any CPU.Build.0 = Production|Any CPU {3ED61776-83A0-426C-9B3A-3AB755DA01EF}.Release|Any CPU.ActiveCfg = Release|Any CPU {3ED61776-83A0-426C-9B3A-3AB755DA01EF}.Release|Any CPU.Build.0 = Release|Any CPU {396817BD-94A7-4559-A7F1-2FAB8009BCF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {396817BD-94A7-4559-A7F1-2FAB8009BCF8}.Debug|Any CPU.Build.0 = Debug|Any CPU {396817BD-94A7-4559-A7F1-2FAB8009BCF8}.Production|Any CPU.ActiveCfg = Production|Any CPU {396817BD-94A7-4559-A7F1-2FAB8009BCF8}.Production|Any CPU.Build.0 = Production|Any CPU {396817BD-94A7-4559-A7F1-2FAB8009BCF8}.Release|Any CPU.ActiveCfg = Release|Any CPU {396817BD-94A7-4559-A7F1-2FAB8009BCF8}.Release|Any CPU.Build.0 = Release|Any CPU {55E1C531-2B9B-49B1-BDFE-9DE322E7FE00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {55E1C531-2B9B-49B1-BDFE-9DE322E7FE00}.Debug|Any CPU.Build.0 = Debug|Any CPU {55E1C531-2B9B-49B1-BDFE-9DE322E7FE00}.Production|Any CPU.ActiveCfg = Production|Any CPU {55E1C531-2B9B-49B1-BDFE-9DE322E7FE00}.Production|Any CPU.Build.0 = Production|Any CPU {55E1C531-2B9B-49B1-BDFE-9DE322E7FE00}.Release|Any CPU.ActiveCfg = Release|Any CPU {55E1C531-2B9B-49B1-BDFE-9DE322E7FE00}.Release|Any CPU.Build.0 = Release|Any CPU {F364B0C4-1882-46A1-9B08-22587BEF05A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F364B0C4-1882-46A1-9B08-22587BEF05A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {F364B0C4-1882-46A1-9B08-22587BEF05A2}.Production|Any CPU.ActiveCfg = Production|Any CPU {F364B0C4-1882-46A1-9B08-22587BEF05A2}.Production|Any CPU.Build.0 = Production|Any CPU {F364B0C4-1882-46A1-9B08-22587BEF05A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {F364B0C4-1882-46A1-9B08-22587BEF05A2}.Release|Any CPU.Build.0 = Release|Any CPU {0C31EA31-6A10-47D0-82E5-6D224E1CE532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0C31EA31-6A10-47D0-82E5-6D224E1CE532}.Debug|Any CPU.Build.0 = Debug|Any CPU {0C31EA31-6A10-47D0-82E5-6D224E1CE532}.Production|Any CPU.ActiveCfg = Production|Any CPU {0C31EA31-6A10-47D0-82E5-6D224E1CE532}.Production|Any CPU.Build.0 = Production|Any CPU {0C31EA31-6A10-47D0-82E5-6D224E1CE532}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C31EA31-6A10-47D0-82E5-6D224E1CE532}.Release|Any CPU.Build.0 = Release|Any CPU {6E013582-9E44-4D7E-8CFE-5F6FB6757B03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6E013582-9E44-4D7E-8CFE-5F6FB6757B03}.Debug|Any CPU.Build.0 = Debug|Any CPU {6E013582-9E44-4D7E-8CFE-5F6FB6757B03}.Production|Any CPU.ActiveCfg = Production|Any CPU {6E013582-9E44-4D7E-8CFE-5F6FB6757B03}.Production|Any CPU.Build.0 = Production|Any CPU {6E013582-9E44-4D7E-8CFE-5F6FB6757B03}.Release|Any CPU.ActiveCfg = Release|Any CPU {6E013582-9E44-4D7E-8CFE-5F6FB6757B03}.Release|Any CPU.Build.0 = Release|Any CPU {C457929B-91BE-48CC-A714-217D9CABD3CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C457929B-91BE-48CC-A714-217D9CABD3CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {C457929B-91BE-48CC-A714-217D9CABD3CC}.Production|Any CPU.ActiveCfg = Production|Any CPU {C457929B-91BE-48CC-A714-217D9CABD3CC}.Production|Any CPU.Build.0 = Production|Any CPU {C457929B-91BE-48CC-A714-217D9CABD3CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {C457929B-91BE-48CC-A714-217D9CABD3CC}.Release|Any CPU.Build.0 = Release|Any CPU {1F4EAF3D-F5B8-473B-871F-606191D4AB9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1F4EAF3D-F5B8-473B-871F-606191D4AB9F}.Debug|Any CPU.Build.0 = Debug|Any CPU {1F4EAF3D-F5B8-473B-871F-606191D4AB9F}.Production|Any CPU.ActiveCfg = Production|Any CPU {1F4EAF3D-F5B8-473B-871F-606191D4AB9F}.Production|Any CPU.Build.0 = Production|Any CPU {1F4EAF3D-F5B8-473B-871F-606191D4AB9F}.Release|Any CPU.ActiveCfg = Release|Any CPU {1F4EAF3D-F5B8-473B-871F-606191D4AB9F}.Release|Any CPU.Build.0 = Release|Any CPU {8DF88C85-594D-45D7-B12E-6B641D497853}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8DF88C85-594D-45D7-B12E-6B641D497853}.Debug|Any CPU.Build.0 = Debug|Any CPU {8DF88C85-594D-45D7-B12E-6B641D497853}.Production|Any CPU.ActiveCfg = Production|Any CPU {8DF88C85-594D-45D7-B12E-6B641D497853}.Production|Any CPU.Build.0 = Production|Any CPU {8DF88C85-594D-45D7-B12E-6B641D497853}.Release|Any CPU.ActiveCfg = Release|Any CPU {8DF88C85-594D-45D7-B12E-6B641D497853}.Release|Any CPU.Build.0 = Release|Any CPU {9EAA687B-951E-4D89-8857-99151FF1BCD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9EAA687B-951E-4D89-8857-99151FF1BCD7}.Debug|Any CPU.Build.0 = Debug|Any CPU {9EAA687B-951E-4D89-8857-99151FF1BCD7}.Production|Any CPU.ActiveCfg = Production|Any CPU {9EAA687B-951E-4D89-8857-99151FF1BCD7}.Production|Any CPU.Build.0 = Production|Any CPU {9EAA687B-951E-4D89-8857-99151FF1BCD7}.Release|Any CPU.ActiveCfg = Release|Any CPU {9EAA687B-951E-4D89-8857-99151FF1BCD7}.Release|Any CPU.Build.0 = Release|Any CPU {43DBBB02-CA43-42AD-BE21-04AC867BA168}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {43DBBB02-CA43-42AD-BE21-04AC867BA168}.Debug|Any CPU.Deploy.0 = Debug|Any CPU {43DBBB02-CA43-42AD-BE21-04AC867BA168}.Production|Any CPU.ActiveCfg = Production|Any CPU {43DBBB02-CA43-42AD-BE21-04AC867BA168}.Production|Any CPU.Deploy.0 = Production|Any CPU {43DBBB02-CA43-42AD-BE21-04AC867BA168}.Release|Any CPU.ActiveCfg = Release|Any CPU {0BC12804-A858-427B-88E2-F9CDE9E97986}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0BC12804-A858-427B-88E2-F9CDE9E97986}.Debug|Any CPU.Build.0 = Debug|Any CPU {0BC12804-A858-427B-88E2-F9CDE9E97986}.Production|Any CPU.ActiveCfg = Production|Any CPU {0BC12804-A858-427B-88E2-F9CDE9E97986}.Production|Any CPU.Build.0 = Production|Any CPU {0BC12804-A858-427B-88E2-F9CDE9E97986}.Release|Any CPU.ActiveCfg = Release|Any CPU {0BC12804-A858-427B-88E2-F9CDE9E97986}.Release|Any CPU.Build.0 = Release|Any CPU {602768DD-F063-469D-9CD3-95CB02F6E441}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {602768DD-F063-469D-9CD3-95CB02F6E441}.Debug|Any CPU.Build.0 = Debug|Any CPU {602768DD-F063-469D-9CD3-95CB02F6E441}.Production|Any CPU.ActiveCfg = Production|Any CPU {602768DD-F063-469D-9CD3-95CB02F6E441}.Production|Any CPU.Build.0 = Production|Any CPU {602768DD-F063-469D-9CD3-95CB02F6E441}.Release|Any CPU.ActiveCfg = Release|Any CPU {602768DD-F063-469D-9CD3-95CB02F6E441}.Release|Any CPU.Build.0 = Release|Any CPU {AC73F391-BC1F-4B7F-AF49-BD87223E2BB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AC73F391-BC1F-4B7F-AF49-BD87223E2BB0}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC73F391-BC1F-4B7F-AF49-BD87223E2BB0}.Production|Any CPU.ActiveCfg = Production|Any CPU {AC73F391-BC1F-4B7F-AF49-BD87223E2BB0}.Production|Any CPU.Build.0 = Production|Any CPU {AC73F391-BC1F-4B7F-AF49-BD87223E2BB0}.Release|Any CPU.ActiveCfg = Release|Any CPU {AC73F391-BC1F-4B7F-AF49-BD87223E2BB0}.Release|Any CPU.Build.0 = Release|Any CPU {E13970C6-87EE-40B1-854A-3053C95BC4C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E13970C6-87EE-40B1-854A-3053C95BC4C9}.Debug|Any CPU.Build.0 = Debug|Any CPU {E13970C6-87EE-40B1-854A-3053C95BC4C9}.Production|Any CPU.ActiveCfg = Production|Any CPU {E13970C6-87EE-40B1-854A-3053C95BC4C9}.Production|Any CPU.Build.0 = Production|Any CPU {E13970C6-87EE-40B1-854A-3053C95BC4C9}.Release|Any CPU.ActiveCfg = Release|Any CPU {E13970C6-87EE-40B1-854A-3053C95BC4C9}.Release|Any CPU.Build.0 = Release|Any CPU {88081DDB-2FBD-4F7D-830F-24F5F6B78294}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {88081DDB-2FBD-4F7D-830F-24F5F6B78294}.Debug|Any CPU.Build.0 = Debug|Any CPU {88081DDB-2FBD-4F7D-830F-24F5F6B78294}.Production|Any CPU.ActiveCfg = Production|Any CPU {88081DDB-2FBD-4F7D-830F-24F5F6B78294}.Production|Any CPU.Build.0 = Production|Any CPU {88081DDB-2FBD-4F7D-830F-24F5F6B78294}.Release|Any CPU.ActiveCfg = Release|Any CPU {88081DDB-2FBD-4F7D-830F-24F5F6B78294}.Release|Any CPU.Build.0 = Release|Any CPU {F2B164AC-99AC-4906-A133-85D6BB8432B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F2B164AC-99AC-4906-A133-85D6BB8432B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {F2B164AC-99AC-4906-A133-85D6BB8432B4}.Production|Any CPU.ActiveCfg = Production|Any CPU {F2B164AC-99AC-4906-A133-85D6BB8432B4}.Production|Any CPU.Build.0 = Production|Any CPU {F2B164AC-99AC-4906-A133-85D6BB8432B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {F2B164AC-99AC-4906-A133-85D6BB8432B4}.Release|Any CPU.Build.0 = Release|Any CPU {475AF987-1458-447E-8504-4F1040B56A2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {475AF987-1458-447E-8504-4F1040B56A2C}.Debug|Any CPU.Build.0 = Debug|Any CPU {475AF987-1458-447E-8504-4F1040B56A2C}.Production|Any CPU.ActiveCfg = Production|Any CPU {475AF987-1458-447E-8504-4F1040B56A2C}.Production|Any CPU.Build.0 = Production|Any CPU {475AF987-1458-447E-8504-4F1040B56A2C}.Release|Any CPU.ActiveCfg = Release|Any CPU {475AF987-1458-447E-8504-4F1040B56A2C}.Release|Any CPU.Build.0 = Release|Any CPU {1885C71E-1624-4673-B0BC-BF4035CFFE72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1885C71E-1624-4673-B0BC-BF4035CFFE72}.Debug|Any CPU.Build.0 = Debug|Any CPU {1885C71E-1624-4673-B0BC-BF4035CFFE72}.Production|Any CPU.ActiveCfg = Production|Any CPU {1885C71E-1624-4673-B0BC-BF4035CFFE72}.Production|Any CPU.Build.0 = Production|Any CPU {1885C71E-1624-4673-B0BC-BF4035CFFE72}.Release|Any CPU.ActiveCfg = Release|Any CPU {1885C71E-1624-4673-B0BC-BF4035CFFE72}.Release|Any CPU.Build.0 = Release|Any CPU {F8EE61DA-0E8B-4074-A0AF-433244EC1FB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F8EE61DA-0E8B-4074-A0AF-433244EC1FB8}.Debug|Any CPU.Build.0 = Debug|Any CPU {F8EE61DA-0E8B-4074-A0AF-433244EC1FB8}.Production|Any CPU.ActiveCfg = Production|Any CPU {F8EE61DA-0E8B-4074-A0AF-433244EC1FB8}.Production|Any CPU.Build.0 = Production|Any CPU {F8EE61DA-0E8B-4074-A0AF-433244EC1FB8}.Release|Any CPU.ActiveCfg = Release|Any CPU {F8EE61DA-0E8B-4074-A0AF-433244EC1FB8}.Release|Any CPU.Build.0 = Release|Any CPU {396A6C3F-74BA-4D85-8BF3-1182F9DE9D95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {396A6C3F-74BA-4D85-8BF3-1182F9DE9D95}.Debug|Any CPU.Build.0 = Debug|Any CPU {396A6C3F-74BA-4D85-8BF3-1182F9DE9D95}.Production|Any CPU.ActiveCfg = Production|Any CPU {396A6C3F-74BA-4D85-8BF3-1182F9DE9D95}.Production|Any CPU.Build.0 = Production|Any CPU {396A6C3F-74BA-4D85-8BF3-1182F9DE9D95}.Release|Any CPU.ActiveCfg = Release|Any CPU {396A6C3F-74BA-4D85-8BF3-1182F9DE9D95}.Release|Any CPU.Build.0 = Release|Any CPU {B448FDC3-5F85-47EE-9F4A-2654E8CC67E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B448FDC3-5F85-47EE-9F4A-2654E8CC67E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {B448FDC3-5F85-47EE-9F4A-2654E8CC67E1}.Production|Any CPU.ActiveCfg = Production|Any CPU {B448FDC3-5F85-47EE-9F4A-2654E8CC67E1}.Production|Any CPU.Build.0 = Production|Any CPU {B448FDC3-5F85-47EE-9F4A-2654E8CC67E1}.Release|Any CPU.ActiveCfg = Release|Any CPU {B448FDC3-5F85-47EE-9F4A-2654E8CC67E1}.Release|Any CPU.Build.0 = Release|Any CPU {3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F}.Production|Any CPU.ActiveCfg = Production|Any CPU {3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F}.Production|Any CPU.Build.0 = Production|Any CPU {3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F}.Release|Any CPU.Build.0 = Release|Any CPU {586DB9FA-CBBF-4867-A57D-E2359925D09A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {586DB9FA-CBBF-4867-A57D-E2359925D09A}.Debug|Any CPU.Build.0 = Debug|Any CPU {586DB9FA-CBBF-4867-A57D-E2359925D09A}.Production|Any CPU.ActiveCfg = Production|Any CPU {586DB9FA-CBBF-4867-A57D-E2359925D09A}.Production|Any CPU.Build.0 = Production|Any CPU {586DB9FA-CBBF-4867-A57D-E2359925D09A}.Release|Any CPU.ActiveCfg = Release|Any CPU {586DB9FA-CBBF-4867-A57D-E2359925D09A}.Release|Any CPU.Build.0 = Release|Any CPU {785E93F2-3415-4215-BD85-1CED429CF260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {785E93F2-3415-4215-BD85-1CED429CF260}.Debug|Any CPU.Build.0 = Debug|Any CPU {785E93F2-3415-4215-BD85-1CED429CF260}.Production|Any CPU.ActiveCfg = Production|Any CPU {785E93F2-3415-4215-BD85-1CED429CF260}.Production|Any CPU.Build.0 = Production|Any CPU {785E93F2-3415-4215-BD85-1CED429CF260}.Release|Any CPU.ActiveCfg = Release|Any CPU {785E93F2-3415-4215-BD85-1CED429CF260}.Release|Any CPU.Build.0 = Release|Any CPU {32915CF6-62D9-4121-8650-6279D5154B4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {32915CF6-62D9-4121-8650-6279D5154B4B}.Debug|Any CPU.Build.0 = Debug|Any CPU {32915CF6-62D9-4121-8650-6279D5154B4B}.Production|Any CPU.ActiveCfg = Production|Any CPU {32915CF6-62D9-4121-8650-6279D5154B4B}.Production|Any CPU.Build.0 = Production|Any CPU {32915CF6-62D9-4121-8650-6279D5154B4B}.Release|Any CPU.ActiveCfg = Release|Any CPU {32915CF6-62D9-4121-8650-6279D5154B4B}.Release|Any CPU.Build.0 = Release|Any CPU {8CCB9013-92CE-4A26-B649-0997F5444161}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8CCB9013-92CE-4A26-B649-0997F5444161}.Debug|Any CPU.Build.0 = Debug|Any CPU {8CCB9013-92CE-4A26-B649-0997F5444161}.Production|Any CPU.ActiveCfg = Production|Any CPU {8CCB9013-92CE-4A26-B649-0997F5444161}.Production|Any CPU.Build.0 = Production|Any CPU {8CCB9013-92CE-4A26-B649-0997F5444161}.Release|Any CPU.ActiveCfg = Release|Any CPU {8CCB9013-92CE-4A26-B649-0997F5444161}.Release|Any CPU.Build.0 = Release|Any CPU {9189FC86-116F-49D4-900E-DEC434136C6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9189FC86-116F-49D4-900E-DEC434136C6D}.Debug|Any CPU.Build.0 = Debug|Any CPU {9189FC86-116F-49D4-900E-DEC434136C6D}.Production|Any CPU.ActiveCfg = Production|Any CPU {9189FC86-116F-49D4-900E-DEC434136C6D}.Production|Any CPU.Build.0 = Production|Any CPU {9189FC86-116F-49D4-900E-DEC434136C6D}.Release|Any CPU.ActiveCfg = Release|Any CPU {9189FC86-116F-49D4-900E-DEC434136C6D}.Release|Any CPU.Build.0 = Release|Any CPU {866A329C-E942-46D1-AF37-BB617F7F4AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {866A329C-E942-46D1-AF37-BB617F7F4AC2}.Production|Any CPU.ActiveCfg = Release|Any CPU {866A329C-E942-46D1-AF37-BB617F7F4AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU {165E76B9-DB0C-49B7-B3DC-52DFBEA55A79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {165E76B9-DB0C-49B7-B3DC-52DFBEA55A79}.Debug|Any CPU.Build.0 = Debug|Any CPU {165E76B9-DB0C-49B7-B3DC-52DFBEA55A79}.Production|Any CPU.ActiveCfg = Debug|Any CPU {165E76B9-DB0C-49B7-B3DC-52DFBEA55A79}.Production|Any CPU.Build.0 = Debug|Any CPU {165E76B9-DB0C-49B7-B3DC-52DFBEA55A79}.Release|Any CPU.ActiveCfg = Release|Any CPU {165E76B9-DB0C-49B7-B3DC-52DFBEA55A79}.Release|Any CPU.Build.0 = Release|Any CPU {1853847F-9988-43A1-B3E1-DDBE4B2F3365}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1853847F-9988-43A1-B3E1-DDBE4B2F3365}.Debug|Any CPU.Build.0 = Debug|Any CPU {1853847F-9988-43A1-B3E1-DDBE4B2F3365}.Production|Any CPU.ActiveCfg = Debug|Any CPU {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 EndGlobalSection GlobalSection(NestedProjects) = preSolution {49D08B64-AC8E-4607-820F-8A0B989CFD33} = {BC9DDFD1-FB81-4996-812A-68BEBCA33A97} {9CD43CAC-C149-41E1-9654-157D578143B7} = {BCE1EE3C-ADB1-48CC-9FD1-C7324D886964} {AE6D0618-60E1-40B2-A46F-664A19C9503C} = {BCE1EE3C-ADB1-48CC-9FD1-C7324D886964} {F34C6504-590B-480A-A239-F230CDFF8CED} = {AE6D0618-60E1-40B2-A46F-664A19C9503C} {5C2C1630-8A7A-451F-81A8-8547C939F5FD} = {9CD43CAC-C149-41E1-9654-157D578143B7} {F71CFDA8-0770-4761-896A-FB0098113F87} = {9CD43CAC-C149-41E1-9654-157D578143B7} {93F3D023-37BD-4C5C-9442-DE2631CD4954} = {9CD43CAC-C149-41E1-9654-157D578143B7} {CA91F71E-8067-4E98-A4D4-E17F5880DE39} = {E91D4BE3-61FE-441C-A227-29850D414216} {2EEAFD26-A0A9-4F91-BEB7-B0F8ED85ACBA} = {E91D4BE3-61FE-441C-A227-29850D414216} {5F398170-87FD-4368-9930-FAAAD2D9FDCC} = {BCE1EE3C-ADB1-48CC-9FD1-C7324D886964} {590517F8-686B-460A-97B3-0A09A8BA443C} = {5F398170-87FD-4368-9930-FAAAD2D9FDCC} {6F039231-8745-4171-B5CA-8688E4683CFC} = {5F398170-87FD-4368-9930-FAAAD2D9FDCC} {CD765A37-EB16-4E35-AA03-6E706D70E8A0} = {5F398170-87FD-4368-9930-FAAAD2D9FDCC} {3ED61776-83A0-426C-9B3A-3AB755DA01EF} = {9CD43CAC-C149-41E1-9654-157D578143B7} {396817BD-94A7-4559-A7F1-2FAB8009BCF8} = {5F398170-87FD-4368-9930-FAAAD2D9FDCC} {55E1C531-2B9B-49B1-BDFE-9DE322E7FE00} = {AE6D0618-60E1-40B2-A46F-664A19C9503C} {F364B0C4-1882-46A1-9B08-22587BEF05A2} = {AE6D0618-60E1-40B2-A46F-664A19C9503C} {0C31EA31-6A10-47D0-82E5-6D224E1CE532} = {AE6D0618-60E1-40B2-A46F-664A19C9503C} {13E4D721-2E96-497E-9657-503D09468F9F} = {BCE1EE3C-ADB1-48CC-9FD1-C7324D886964} {6E013582-9E44-4D7E-8CFE-5F6FB6757B03} = {13E4D721-2E96-497E-9657-503D09468F9F} {C457929B-91BE-48CC-A714-217D9CABD3CC} = {13E4D721-2E96-497E-9657-503D09468F9F} {1F4EAF3D-F5B8-473B-871F-606191D4AB9F} = {13E4D721-2E96-497E-9657-503D09468F9F} {8DF88C85-594D-45D7-B12E-6B641D497853} = {13E4D721-2E96-497E-9657-503D09468F9F} {9EAA687B-951E-4D89-8857-99151FF1BCD7} = {E91D4BE3-61FE-441C-A227-29850D414216} {43DBBB02-CA43-42AD-BE21-04AC867BA168} = {C733D087-7051-4E35-BCDB-081252A108E5} {0FF699EF-8156-43CB-8D18-8EA28F30E9EE} = {AE6D0618-60E1-40B2-A46F-664A19C9503C} {0BC12804-A858-427B-88E2-F9CDE9E97986} = {0FF699EF-8156-43CB-8D18-8EA28F30E9EE} {53E4F002-E708-45F7-8444-19EB8977B5C9} = {13E4D721-2E96-497E-9657-503D09468F9F} {602768DD-F063-469D-9CD3-95CB02F6E441} = {53E4F002-E708-45F7-8444-19EB8977B5C9} {F544D6DA-740A-4313-9542-D989666EA9DE} = {5F398170-87FD-4368-9930-FAAAD2D9FDCC} {AC73F391-BC1F-4B7F-AF49-BD87223E2BB0} = {F544D6DA-740A-4313-9542-D989666EA9DE} {237A249C-B589-49B8-9B84-CA504DE5D137} = {9CD43CAC-C149-41E1-9654-157D578143B7} {E13970C6-87EE-40B1-854A-3053C95BC4C9} = {237A249C-B589-49B8-9B84-CA504DE5D137} {88081DDB-2FBD-4F7D-830F-24F5F6B78294} = {8B08A9EE-CE27-4CC3-ACB3-3BD9628E5479} {F2B164AC-99AC-4906-A133-85D6BB8432B4} = {F544D6DA-740A-4313-9542-D989666EA9DE} {475AF987-1458-447E-8504-4F1040B56A2C} = {237A249C-B589-49B8-9B84-CA504DE5D137} {1885C71E-1624-4673-B0BC-BF4035CFFE72} = {53E4F002-E708-45F7-8444-19EB8977B5C9} {F8EE61DA-0E8B-4074-A0AF-433244EC1FB8} = {0FF699EF-8156-43CB-8D18-8EA28F30E9EE} {396A6C3F-74BA-4D85-8BF3-1182F9DE9D95} = {0FF699EF-8156-43CB-8D18-8EA28F30E9EE} {B448FDC3-5F85-47EE-9F4A-2654E8CC67E1} = {53E4F002-E708-45F7-8444-19EB8977B5C9} {3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F} = {F544D6DA-740A-4313-9542-D989666EA9DE} {586DB9FA-CBBF-4867-A57D-E2359925D09A} = {8B08A9EE-CE27-4CC3-ACB3-3BD9628E5479} {785E93F2-3415-4215-BD85-1CED429CF260} = {237A249C-B589-49B8-9B84-CA504DE5D137} {B1D398A1-7005-4792-8D7F-CD9E5F3C29B7} = {E91D4BE3-61FE-441C-A227-29850D414216} {32915CF6-62D9-4121-8650-6279D5154B4B} = {B1D398A1-7005-4792-8D7F-CD9E5F3C29B7} {8CCB9013-92CE-4A26-B649-0997F5444161} = {B1D398A1-7005-4792-8D7F-CD9E5F3C29B7} {9189FC86-116F-49D4-900E-DEC434136C6D} = {C733D087-7051-4E35-BCDB-081252A108E5} {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} EndGlobalSection EndGlobal ================================================ FILE: src/Database/.dockerignore ================================================ **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/azds.yaml **/bin **/charts **/docker-compose* **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md ================================================ FILE: src/Database/ClearDatabase.sql ================================================ DELETE FROM [administration].[MeetingGroupProposals] GO DELETE FROM [administration].[OutboxMessages] GO DELETE FROM [administration].[InboxMessages] GO DELETE FROM [administration].[InternalCommands] GO DELETE FROM [meetings].[InboxMessages] GO DELETE FROM [meetings].[OutboxMessages] GO DELETE FROM [meetings].[InternalCommands] GO DELETE FROM [meetings].[MeetingGroupProposals] GO DELETE FROM [meetings].[MeetingGroupMembers] GO DELETE FROM [meetings].[MeetingAttendees] GO DELETE FROM [meetings].[MeetingGroups] GO DROP TABLE [administration].[MeetingGroupProposals] GO DROP TABLE [administration].[OutboxMessages] GO DROP TABLE [administration].[InboxMessages] GO DROP TABLE [administration].[InternalCommands] GO DROP TABLE [meetings].[InboxMessages] GO DROP TABLE [meetings].[OutboxMessages] GO DROP TABLE [meetings].[InternalCommands] GO DROP TABLE [meetings].[MeetingGroupProposals] GO DROP TABLE [meetings].[MeetingGroupMembers] GO DROP TABLE [meetings].[MeetingAttendees] GO DROP TABLE [meetings].[MeetingGroups] GO DROP TABLE [meetings].[Meetings] GO DROP VIEW [meetings].[v_MeetingGroups] GO DROP TABLE [administration].[Members] GO DROP VIEW [administration].[v_MeetingGroupProposals] GO DROP VIEW [administration].[v_Members] GO DROP SCHEMA [administration] GO DROP TABLE [meetings].[MeetingNotAttendees] GO DROP TABLE [meetings].[MeetingWaitlistMembers] GO DROP TABLE [meetings].[Members] GO DROP VIEW [meetings].[v_Meetings] GO DROP VIEW [meetings].[v_Members] GO DROP VIEW [meetings].[v_MeetingGroupProposals] GO DROP TABLE payments.InboxMessages DROP TABLE payments.InternalCommands GO DROP TABLE payments.OutboxMessages GO DROP TABLE payments.Payers GO DROP TABLE users.InboxMessages GO DROP TABLE users.InternalCommands GO DROP TABLE users.OutboxMessages GO DROP TABLE users.[RolesToPermissions] GO DROP TABLE users.[Permissions] GO DROP TABLE users.[Users] GO DROP TABLE users.[UserRoles] GO DROP TABLE users.[UserRegistrations] GO DROP VIEW users.[v_UserPermissions] GO DROP VIEW users.[v_UserRegistrations] GO DROP VIEW users.[v_UserRoles] GO DROP VIEW users.[v_Users] GO DROP SCHEMA [users] GO DROP TABLE payments.[Messages] GO DROP TABLE payments.Streams GO DROP TYPE payments.NewStreamMessages GO DROP TABLE payments.SubscriptionDetails GO DROP TABLE payments.SubscriptionCheckpoints GO DROP TABLE payments.PriceListItems GO DROP TABLE payments.SubscriptionPayments GO DROP TABLE [meetings].[MemberSubscriptions] GO DROP SCHEMA [payments] GO DROP VIEW meetings.v_MeetingGroupMembers DROP SCHEMA [meetings] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/CompanyName.MyMeetings.Database.sqlproj ================================================  Debug AnyCPU CompanyName.MyMeetings.Database 2.0 4.1 {43dbbb02-ca43-42ad-be21-04ac867ba168} Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider Database CompanyName.MyMeetings.Database CompanyName.MyMeetings.Database 1033, CI BySchemaAndSchemaType True v4.8 CS Properties False True True $(MSBuildProjectName).sql False pdbonly true false true prompt 4 bin\Debug\ $(MSBuildProjectName).sql false true full false true true prompt 4 True 11.0 True 11.0 bin\Release\ $(MSBuildProjectName).sql False pdbonly true false true prompt 4 DoNotCopy ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/ClearDatabase.sql ================================================ DELETE FROM [administration].[InboxMessages] DELETE FROM [administration].[InternalCommands] DELETE FROM [administration].[OutboxMessages] DELETE FROM [administration].[MeetingGroupProposals] DELETE FROM [administration].[Members] DELETE FROM [meetings].[InboxMessages] DELETE FROM [meetings].[InternalCommands] DELETE FROM [meetings].[OutboxMessages] DELETE FROM [meetings].[MeetingAttendees] DELETE FROM [meetings].[MeetingGroupMembers] DELETE FROM [meetings].[MeetingGroupProposals] DELETE FROM [meetings].[MeetingGroups] DELETE FROM [meetings].[MeetingNotAttendees] DELETE FROM [meetings].[Meetings] DELETE FROM [meetings].[MeetingWaitlistMembers] DELETE FROM [meetings].[MeetingMemberCommentLikes] DELETE FROM [meetings].[Members] DELETE FROM [meetings].[MeetingComments] DELETE FROM [payments].[InboxMessages] DELETE FROM [payments].[InternalCommands] DELETE FROM [payments].[OutboxMessages] DELETE FROM payments.[Messages] DBCC CHECKIDENT ('payments.Messages', RESEED, 0); DELETE FROM payments.Streams DBCC CHECKIDENT ('payments.Streams', RESEED, 0); DELETE FROM payments.SubscriptionDetails DELETE FROM [payments].[SubscriptionCheckpoints] DELETE FROM [payments].PriceListItems DELETE FROM [payments].SubscriptionPayments DELETE FROM [payments].MeetingFees DELETE FROM [payments].[Payers] DELETE FROM [users].[InboxMessages] DELETE FROM [users].[InternalCommands] DELETE FROM [users].[OutboxMessages] DELETE FROM [users].[Users] DELETE FROM [users].[RolesToPermissions] DELETE FROM [users].[UserRoles] DELETE FROM [users].[Permissions] DELETE FROM [registrations].[UserRegistrations] ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/CreateDatabase.sql ================================================ CREATE DATABASE [MyMeetings] CONTAINMENT = NONE ON PRIMARY ( NAME = N'MyMeetings', FILENAME = N'/var/opt/mssql/data/MyMeetings.mdf' , SIZE = 8192KB , FILEGROWTH = 65536KB ) LOG ON ( NAME = N'MyMeetings_log', FILENAME = N'/var/opt/mssql/data/MyMeetings_log.ldf' , SIZE = 8192KB , FILEGROWTH = 65536KB ) GO CREATE SCHEMA app AUTHORIZATION dbo GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/CreateDatabase_Linux.sql ================================================ CREATE DATABASE [MyMeetings] CONTAINMENT = NONE GO USE [MyMeetings] GO CREATE SCHEMA app AUTHORIZATION dbo GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/CreateDatabase_Windows.sql ================================================ CREATE DATABASE [MyMeetings] CONTAINMENT = NONE GO USE [MyMeetings] GO CREATE SCHEMA app AUTHORIZATION dbo GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/CreateStructure.sql ================================================  PRINT N'Creating [administration]...'; GO CREATE SCHEMA [administration] AUTHORIZATION [dbo]; GO PRINT N'Creating [meetings]...'; GO CREATE SCHEMA [meetings] AUTHORIZATION [dbo]; GO PRINT N'Creating [payments]...'; GO CREATE SCHEMA [payments] AUTHORIZATION [dbo]; GO PRINT N'Creating [users]...'; GO CREATE SCHEMA [users] AUTHORIZATION [dbo]; GO PRINT N'Creating [registrations]...'; GO CREATE SCHEMA [registrations] AUTHORIZATION [dbo]; GO PRINT N'Creating [payments].[NewStreamMessages]...'; GO CREATE TYPE [payments].[NewStreamMessages] AS TABLE ( [StreamVersion] INT IDENTITY (0, 1) NOT NULL, [Id] UNIQUEIDENTIFIER NOT NULL, [Created] DATETIME DEFAULT (GETUTCDATE()) NOT NULL, [Type] NVARCHAR (128) NOT NULL, [JsonData] NVARCHAR (MAX) NULL, [JsonMetadata] NVARCHAR (MAX) NULL); GO PRINT N'Creating [administration].[InternalCommands]...'; GO CREATE TABLE [administration].[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_administration_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [administration].[InboxMessages]...'; GO CREATE TABLE [administration].[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_administration_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [administration].[OutboxMessages]...'; GO CREATE TABLE [administration].[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_administration_OutboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [administration].[Members]...'; GO CREATE TABLE [administration].[Members] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_administration_Members_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [administration].[MeetingGroupProposals]...'; GO CREATE TABLE [administration].[MeetingGroupProposals] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR (255) NOT NULL, [Description] VARCHAR (200) NULL, [LocationCity] NVARCHAR (50) NOT NULL, [LocationCountryCode] NVARCHAR (3) NOT NULL, [ProposalUserId] UNIQUEIDENTIFIER NOT NULL, [ProposalDate] DATETIME NOT NULL, [StatusCode] NVARCHAR (50) NOT NULL, [DecisionDate] DATETIME NULL, [DecisionUserId] UNIQUEIDENTIFIER NULL, [DecisionCode] NVARCHAR (50) NULL, [DecisionRejectReason] NVARCHAR (250) NULL, CONSTRAINT [PK_administration_MeetingGroupProposals_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MeetingWaitlistMembers]...'; GO CREATE TABLE [meetings].[MeetingWaitlistMembers] ( [MeetingId] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [SignUpDate] DATETIME2 (7) NOT NULL, [IsSignedOff] BIT NOT NULL, [SignOffDate] DATETIME2 (7) NULL, [IsMovedToAttendees] BIT NOT NULL, [MovedToAttendeesDate] DATETIME2 (7) NULL, CONSTRAINT [PK_meetings_MeetingWaitlistMembers_MeetingId_MemberId_SignUpDate] PRIMARY KEY CLUSTERED ([MeetingId] ASC, [MemberId] ASC, [SignUpDate] ASC) ); GO PRINT N'Creating [meetings].[MeetingNotAttendees]...'; GO CREATE TABLE [meetings].[MeetingNotAttendees] ( [MeetingId] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [DecisionDate] DATETIME2 (7) NOT NULL, [DecisionChanged] BIT NOT NULL, [DecisionChangeDate] DATETIME2 (7) NULL, CONSTRAINT [PK_meetings_MeetingNotAttendees_Id] PRIMARY KEY CLUSTERED ([MeetingId] ASC, [MemberId] ASC, [DecisionDate] ASC) ); GO PRINT N'Creating [meetings].[Meetings]...'; GO CREATE TABLE [meetings].[Meetings] ( [Id] UNIQUEIDENTIFIER NOT NULL, [MeetingGroupId] UNIQUEIDENTIFIER NOT NULL, [CreatorId] UNIQUEIDENTIFIER NOT NULL, [CreateDate] DATETIME2 (7) NOT NULL, [Title] NVARCHAR (200) NOT NULL, [Description] NVARCHAR (4000) NOT NULL, [TermStartDate] DATETIME NOT NULL, [TermEndDate] DATETIME NOT NULL, [LocationName] NVARCHAR (200) NOT NULL, [LocationAddress] NVARCHAR (200) NOT NULL, [LocationPostalCode] NVARCHAR (200) NULL, [LocationCity] NVARCHAR (50) NOT NULL, [AttendeesLimit] INT NULL, [GuestsLimit] INT NOT NULL, [RSVPTermStartDate] DATETIME NULL, [RSVPTermEndDate] DATETIME NULL, [EventFeeValue] DECIMAL (5) NULL, [EventFeeCurrency] VARCHAR (3) NULL, [ChangeDate] DATETIME2 (7) NULL, [ChangeMemberId] UNIQUEIDENTIFIER NULL, [CancelDate] DATETIME NULL, [CancelMemberId] UNIQUEIDENTIFIER NULL, [IsCanceled] BIT NOT NULL, CONSTRAINT [PK_meetings_Meetings_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MeetingGroupMembers]...'; GO CREATE TABLE [meetings].[MeetingGroupMembers] ( [MeetingGroupId] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [JoinedDate] DATETIME2 (7) NOT NULL, [RoleCode] VARCHAR (50) NOT NULL, [IsActive] BIT NOT NULL, [LeaveDate] DATETIME NULL, CONSTRAINT [PK_meetings_MeetingGroupMembers_MeetingGroupId_MemberId_JoinedDate] PRIMARY KEY CLUSTERED ([MeetingGroupId] ASC, [MemberId] ASC, [JoinedDate] ASC) ); GO PRINT N'Creating [meetings].[MeetingGroups]...'; GO CREATE TABLE [meetings].[MeetingGroups] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR (255) NOT NULL, [Description] VARCHAR (200) NULL, [LocationCity] NVARCHAR (50) NOT NULL, [LocationCountryCode] NVARCHAR (3) NOT NULL, [CreatorId] UNIQUEIDENTIFIER NOT NULL, [CreateDate] DATETIME NOT NULL, [PaymentDateTo] DATE NULL, CONSTRAINT [PK_meetings_MeetingGroups_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[Members]...'; GO CREATE TABLE [meetings].[Members] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_meetings_Members_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MeetingGroupProposals]...'; GO CREATE TABLE [meetings].[MeetingGroupProposals] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR (255) NOT NULL, [Description] VARCHAR (200) NULL, [LocationCity] NVARCHAR (50) NOT NULL, [LocationCountryCode] NVARCHAR (3) NOT NULL, [ProposalUserId] UNIQUEIDENTIFIER NOT NULL, [ProposalDate] DATETIME NOT NULL, [StatusCode] NVARCHAR (50) NOT NULL, CONSTRAINT [PK_meetings_MeetingGroupProposals_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[OutboxMessages]...'; GO CREATE TABLE [meetings].[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_meetings_OutboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[InternalCommands]...'; GO CREATE TABLE [meetings].[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_meetings_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[InboxMessages]...'; GO CREATE TABLE [meetings].[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_meetings_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MemberSubscriptions]...'; GO CREATE TABLE [meetings].[MemberSubscriptions] ( [Id] UNIQUEIDENTIFIER NOT NULL, [ExpirationDate] DATETIME NOT NULL, CONSTRAINT [PK_meetings_MemberSubscriptions_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MeetingAttendees]...'; GO CREATE TABLE [meetings].[MeetingAttendees] ( [MeetingId] UNIQUEIDENTIFIER NOT NULL, [AttendeeId] UNIQUEIDENTIFIER NOT NULL, [DecisionDate] DATETIME2 (7) NOT NULL, [RoleCode] VARCHAR (50) NULL, [GuestsNumber] INT NULL, [DecisionChanged] BIT NOT NULL, [DecisionChangeDate] DATETIME2 (7) NULL, [IsRemoved] BIT NOT NULL, [RemovingMemberId] UNIQUEIDENTIFIER NULL, [RemovingReason] NVARCHAR (500) NULL, [RemovedDate] DATETIME2 (7) NULL, [BecameNotAttendeeDate] DATETIME2 (7) NULL, [FeeValue] DECIMAL (5) NULL, [FeeCurrency] VARCHAR (3) NULL, [IsFeePaid] BIT NOT NULL, CONSTRAINT [PK_meetings_MeetingAttendees_Id] PRIMARY KEY CLUSTERED ([MeetingId] ASC, [AttendeeId] ASC, [DecisionDate] ASC) ); GO PRINT N'Creating [meetings].[MeetingComments]...'; GO CREATE TABLE meetings.MeetingComments ( [Id] UNIQUEIDENTIFIER NOT NULL, [MeetingId] UNIQUEIDENTIFIER NOT NULL, [AuthorId] UNIQUEIDENTIFIER NOT NULL, [InReplyToCommentId] UNIQUEIDENTIFIER NULL, [Comment] VARCHAR(300) NULL, [IsRemoved] BIT NOT NULL, [RemovedByReason] VARCHAR(300) NULL, [CreateDate] DATETIME NOT NULL, [EditDate] DATETIME NULL, [LikesCount] INT NOT NULL, CONSTRAINT [PK_meetings_MeetingComments_Id] PRIMARY KEY ([Id] ASC) ) GO PRINT N'Creating [meetings].[MeetingMemberCommentLikes]...'; CREATE TABLE [meetings].[MeetingMemberCommentLikes] ( [Id] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [MeetingCommentId] UNIQUEIDENTIFIER NOT NULL, CONSTRAINT [PK_meetings_MeetingMemberCommentLikes_Id] PRIMARY KEY ([Id] ASC), CONSTRAINT [FK_meetings_MeetingMemberCommentLikes_Members] FOREIGN KEY ([MemberId]) REFERENCES meetings.Members ([Id]), CONSTRAINT [FK_meetings_MeetingMemberCommentLikes_MeetingComments] FOREIGN KEY ([MeetingCommentId]) REFERENCES meetings.MeetingComments ([Id]) ) GO PRINT N'Creating [payments].[Payers]...'; GO CREATE TABLE [payments].[Payers] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_payments_Payers_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[OutboxMessages]...'; GO CREATE TABLE [payments].[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_payments_OutboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[InternalCommands]...'; GO CREATE TABLE [payments].[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_payments_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[InboxMessages]...'; GO CREATE TABLE [payments].[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_payments_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[Messages]...'; GO CREATE TABLE [payments].[Messages] ( [StreamIdInternal] INT NOT NULL, [StreamVersion] INT NOT NULL, [Position] BIGINT IDENTITY (0, 1) NOT NULL, [Id] UNIQUEIDENTIFIER NOT NULL, [Created] DATETIME NOT NULL, [Type] NVARCHAR (128) NOT NULL, [JsonData] NVARCHAR (MAX) NOT NULL, [JsonMetadata] NVARCHAR (MAX) NULL, CONSTRAINT [PK_Events] PRIMARY KEY NONCLUSTERED ([Position] ASC) ); GO PRINT N'Creating [payments].[Messages].[IX_Messages_Position]...'; GO CREATE UNIQUE NONCLUSTERED INDEX [IX_Messages_Position] ON [payments].[Messages]([Position] ASC); GO PRINT N'Creating [payments].[Messages].[IX_Messages_StreamIdInternal_Id]...'; GO CREATE UNIQUE NONCLUSTERED INDEX [IX_Messages_StreamIdInternal_Id] ON [payments].[Messages]([StreamIdInternal] ASC, [Id] ASC); GO PRINT N'Creating [payments].[Messages].[IX_Messages_StreamIdInternal_Revision]...'; GO CREATE UNIQUE NONCLUSTERED INDEX [IX_Messages_StreamIdInternal_Revision] ON [payments].[Messages]([StreamIdInternal] ASC, [StreamVersion] ASC); GO PRINT N'Creating [payments].[Messages].[IX_Messages_StreamIdInternal_Created]...'; GO CREATE NONCLUSTERED INDEX [IX_Messages_StreamIdInternal_Created] ON [payments].[Messages]([StreamIdInternal] ASC, [Created] ASC); GO PRINT N'Creating [payments].[MeetingFees]...'; GO CREATE TABLE [payments].[MeetingFees] ( [MeetingFeeId] UNIQUEIDENTIFIER NOT NULL, [PayerId] UNIQUEIDENTIFIER NOT NULL, [MeetingId] UNIQUEIDENTIFIER NOT NULL, [FeeValue] DECIMAL (18, 2) NOT NULL, [FeeCurrency] VARCHAR (50) NOT NULL, [Status] VARCHAR (50) NOT NULL, CONSTRAINT [PK_payments_MeetingFees_MeetingFeeId] PRIMARY KEY CLUSTERED ([MeetingFeeId] ASC) ); GO PRINT N'Creating [payments].[PriceListItems]...'; GO CREATE TABLE [payments].[PriceListItems] ( [Id] UNIQUEIDENTIFIER NOT NULL, [SubscriptionPeriodCode] VARCHAR (50) NOT NULL, [CategoryCode] VARCHAR (50) NOT NULL, [CountryCode] VARCHAR (50) NOT NULL, [MoneyValue] DECIMAL (18, 2) NOT NULL, [MoneyCurrency] VARCHAR (50) NOT NULL, [IsActive] BIT NOT NULL ); GO PRINT N'Creating [payments].[SubscriptionPayments]...'; GO CREATE TABLE [payments].[SubscriptionPayments] ( [PaymentId] UNIQUEIDENTIFIER NOT NULL, [PayerId] UNIQUEIDENTIFIER NOT NULL, [Type] VARCHAR (50) NOT NULL, [Status] VARCHAR (50) NOT NULL, [Period] VARCHAR (50) NOT NULL, [Date] DATETIME NOT NULL, [SubscriptionId] UNIQUEIDENTIFIER NULL, [MoneyValue] DECIMAL (18, 2) NOT NULL, [MoneyCurrency] VARCHAR (50) NOT NULL ); GO PRINT N'Creating [payments].[SubscriptionCheckpoints]...'; GO CREATE TABLE [payments].[SubscriptionCheckpoints] ( [Code] VARCHAR (50) NOT NULL, [Position] BIGINT NOT NULL ); GO PRINT N'Creating [payments].[SubscriptionDetails]...'; GO CREATE TABLE [payments].[SubscriptionDetails] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Period] VARCHAR (50) NOT NULL, [Status] VARCHAR (50) NOT NULL, [CountryCode] VARCHAR (50) NOT NULL, [ExpirationDate] DATETIME NOT NULL, CONSTRAINT [PK_payments_SubscriptionDetails_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[Streams]...'; GO CREATE TABLE [payments].[Streams] ( [Id] CHAR (42) NOT NULL, [IdOriginal] NVARCHAR (1000) NOT NULL, [IdInternal] INT IDENTITY (1, 1) NOT NULL, [Version] INT NOT NULL, [Position] BIGINT NOT NULL, CONSTRAINT [PK_Streams] PRIMARY KEY CLUSTERED ([IdInternal] ASC) ); GO PRINT N'Creating [payments].[Streams].[IX_Streams_Id]...'; GO CREATE UNIQUE NONCLUSTERED INDEX [IX_Streams_Id] ON [payments].[Streams]([Id] ASC); GO PRINT N'Creating [users].[InboxMessages]...'; GO CREATE TABLE [users].[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_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]...'; GO CREATE TABLE [users].[UserRoles] ( [UserId] UNIQUEIDENTIFIER NOT NULL, [RoleCode] NVARCHAR (50) NULL ); GO PRINT N'Creating [registrations].[UserRegistrations]...'; GO CREATE TABLE [registrations].[UserRegistrations] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [Password] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, [StatusCode] VARCHAR (50) NOT NULL, [RegisterDate] DATETIME NOT NULL, [ConfirmedDate] DATETIME NULL, CONSTRAINT [PK_registrations_UserRegistrations_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [users].[Users]...'; GO CREATE TABLE [users].[Users] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [Password] NVARCHAR (255) NOT NULL, [IsActive] BIT NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_users_Users_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [users].[RolesToPermissions]...'; GO CREATE TABLE [users].[RolesToPermissions] ( [RoleCode] VARCHAR (50) NOT NULL, [PermissionCode] VARCHAR (50) NOT NULL, CONSTRAINT [PK_RolesToPermissions_RoleCode_PermissionCode] PRIMARY KEY CLUSTERED ([RoleCode] ASC, [PermissionCode] ASC) ); GO PRINT N'Creating [users].[Permissions]...'; GO CREATE TABLE [users].[Permissions] ( [Code] VARCHAR (50) NOT NULL, [Name] VARCHAR (100) NOT NULL, [Description] VARCHAR (255) NULL, CONSTRAINT [PK_users_Permissions_Code] PRIMARY KEY CLUSTERED ([Code] ASC) ); GO PRINT N'Creating [users].[InternalCommands]...'; GO CREATE TABLE [users].[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_users_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); 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]...'; GO CREATE TABLE [users].[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) ); 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]...'; GO ALTER TABLE [payments].[Streams] ADD CONSTRAINT [DF_payments_Streams_Version] DEFAULT (-1) FOR [Version]; GO PRINT N'Creating [payments].[DF_payments_Streams_Position]...'; GO ALTER TABLE [payments].[Streams] ADD CONSTRAINT [DF_payments_Streams_Position] DEFAULT (-1) FOR [Position]; GO PRINT N'Creating [payments].[FK_Events_Streams]...'; GO ALTER TABLE [payments].[Messages] WITH NOCHECK ADD CONSTRAINT [FK_Events_Streams] FOREIGN KEY ([StreamIdInternal]) REFERENCES [payments].[Streams] ([IdInternal]); GO PRINT N'Creating [administration].[v_MeetingGroupProposals]...'; GO CREATE VIEW [administration].[v_MeetingGroupProposals] AS SELECT [MeetingGroupProposal].[Id], [MeetingGroupProposal].[Name], [MeetingGroupProposal].[Description], [MeetingGroupProposal].[LocationCity], [MeetingGroupProposal].[LocationCountryCode], [MeetingGroupProposal].[ProposalUserId], [MeetingGroupProposal].[ProposalDate], [MeetingGroupProposal].[StatusCode], [MeetingGroupProposal].[DecisionDate], [MeetingGroupProposal].[DecisionUserId], [MeetingGroupProposal].[DecisionCode], [MeetingGroupProposal].[DecisionRejectReason] FROM [administration].[MeetingGroupProposals] AS [MeetingGroupProposal] GO PRINT N'Creating [administration].[v_Members]...'; GO CREATE VIEW [administration].[v_Members] AS SELECT [Member].[Id], [Member].[Login], [Member].[Email], [Member].[FirstName], [Member].[LastName], [Member].[Name] FROM [administration].[Members] AS [Member] GO PRINT N'Creating [meetings].[v_MeetingGroups]...'; GO CREATE VIEW [meetings].[v_MeetingGroups] AS SELECT [MeetingGroup].Id, [MeetingGroup].[Name], [MeetingGroup].[Description], [MeetingGroup].[LocationCountryCode], [MeetingGroup].[LocationCity] FROM meetings.MeetingGroups AS [MeetingGroup] GO PRINT N'Creating [meetings].[v_Meetings]...'; GO CREATE VIEW [meetings].[v_Meetings] AS SELECT Meeting.[Id], Meeting.[Title], Meeting.[Description], Meeting.LocationAddress, Meeting.LocationCity, Meeting.LocationPostalCode, Meeting.TermStartDate, Meeting.TermEndDate FROM meetings.Meetings AS [Meeting] GO PRINT N'Creating [meetings].[v_Members]...'; GO CREATE VIEW [meetings].[v_Members] AS SELECT [Member].Id, [Member].[Name], [Member].[Login], [Member].[Email] FROM meetings.Members AS [Member] GO PRINT N'Creating [meetings].[v_MeetingGroupMembers]...'; GO CREATE VIEW [meetings].[v_MeetingGroupMembers] AS SELECT [MeetingGroupMember].MeetingGroupId, [MeetingGroupMember].MemberId, [MeetingGroupMember].RoleCode FROM meetings.MeetingGroupMembers AS [MeetingGroupMember] GO PRINT N'Creating [meetings].[v_MeetingGroupProposals]...'; GO CREATE VIEW [meetings].[v_MeetingGroupProposals] AS SELECT [MeetingGroupProposal].[Id], [MeetingGroupProposal].[Name], [MeetingGroupProposal].[Description], [MeetingGroupProposal].[LocationCity], [MeetingGroupProposal].[LocationCountryCode], [MeetingGroupProposal].[ProposalUserId], [MeetingGroupProposal].[ProposalDate], [MeetingGroupProposal].[StatusCode] FROM [meetings].[MeetingGroupProposals] AS [MeetingGroupProposal] GO PRINT N'Creating [users].[v_UserRoles]...'; GO CREATE VIEW [users].[v_UserRoles] AS SELECT [UserRole].[UserId], [UserRole].[RoleCode] FROM [users].[UserRoles] AS [UserRole] GO PRINT N'Creating [users].[v_Users]...'; GO CREATE VIEW [users].[v_Users] AS SELECT [User].[Id], [User].[IsActive], [User].[Login], [User].[Password], [User].[Email], [User].[Name] FROM [users].[Users] AS [User] GO PRINT N'Creating [registrations].[v_UserRegistrations]...'; GO CREATE VIEW [users].[v_UserRegistrations] AS SELECT [UserRegistration].[Id], [UserRegistration].[Login], [UserRegistration].[Email], [UserRegistration].[FirstName], [UserRegistration].[LastName], [UserRegistration].[Name], [UserRegistration].[StatusCode] FROM [registrations].[UserRegistrations] AS [UserRegistration] GO PRINT N'Creating [registrations].[v_UserPermissions]...'; GO CREATE VIEW [users].[v_UserPermissions] AS SELECT DISTINCT [UserRole].UserId, [RolesToPermission].PermissionCode FROM [users].UserRoles AS [UserRole] INNER JOIN [users].RolesToPermissions AS [RolesToPermission] ON [UserRole].RoleCode = [RolesToPermission].RoleCode GO PRINT N'Checking existing data against newly created constraints'; GO ALTER TABLE [payments].[Messages] WITH CHECK CHECK CONSTRAINT [FK_Events_Streams]; GO PRINT N'Update complete.'; GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0001_initial_structure.sql ================================================  PRINT N'Creating [administration]...'; GO CREATE SCHEMA [administration] AUTHORIZATION [dbo]; GO PRINT N'Creating [meetings]...'; GO CREATE SCHEMA [meetings] AUTHORIZATION [dbo]; GO PRINT N'Creating [payments]...'; GO CREATE SCHEMA [payments] AUTHORIZATION [dbo]; GO PRINT N'Creating [users]...'; GO CREATE SCHEMA [users] AUTHORIZATION [dbo]; GO PRINT N'Creating [registrations]...'; GO CREATE SCHEMA [registrations] AUTHORIZATION [dbo]; GO PRINT N'Creating [payments].[NewStreamMessages]...'; GO CREATE TYPE [payments].[NewStreamMessages] AS TABLE ( [StreamVersion] INT IDENTITY (0, 1) NOT NULL, [Id] UNIQUEIDENTIFIER NOT NULL, [Created] DATETIME DEFAULT (GETUTCDATE()) NOT NULL, [Type] NVARCHAR (128) NOT NULL, [JsonData] NVARCHAR (MAX) NULL, [JsonMetadata] NVARCHAR (MAX) NULL); GO PRINT N'Creating [administration].[InternalCommands]...'; GO CREATE TABLE [administration].[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_administration_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [administration].[InboxMessages]...'; GO CREATE TABLE [administration].[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_administration_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [administration].[OutboxMessages]...'; GO CREATE TABLE [administration].[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_administration_OutboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [administration].[Members]...'; GO CREATE TABLE [administration].[Members] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_administration_Members_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [administration].[MeetingGroupProposals]...'; GO CREATE TABLE [administration].[MeetingGroupProposals] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR (255) NOT NULL, [Description] VARCHAR (200) NULL, [LocationCity] NVARCHAR (50) NOT NULL, [LocationCountryCode] NVARCHAR (3) NOT NULL, [ProposalUserId] UNIQUEIDENTIFIER NOT NULL, [ProposalDate] DATETIME NOT NULL, [StatusCode] NVARCHAR (50) NOT NULL, [DecisionDate] DATETIME NULL, [DecisionUserId] UNIQUEIDENTIFIER NULL, [DecisionCode] NVARCHAR (50) NULL, [DecisionRejectReason] NVARCHAR (250) NULL, CONSTRAINT [PK_administration_MeetingGroupProposals_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MeetingWaitlistMembers]...'; GO CREATE TABLE [meetings].[MeetingWaitlistMembers] ( [MeetingId] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [SignUpDate] DATETIME2 (7) NOT NULL, [IsSignedOff] BIT NOT NULL, [SignOffDate] DATETIME2 (7) NULL, [IsMovedToAttendees] BIT NOT NULL, [MovedToAttendeesDate] DATETIME2 (7) NULL, CONSTRAINT [PK_meetings_MeetingWaitlistMembers_MeetingId_MemberId_SignUpDate] PRIMARY KEY CLUSTERED ([MeetingId] ASC, [MemberId] ASC, [SignUpDate] ASC) ); GO PRINT N'Creating [meetings].[MeetingNotAttendees]...'; GO CREATE TABLE [meetings].[MeetingNotAttendees] ( [MeetingId] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [DecisionDate] DATETIME2 (7) NOT NULL, [DecisionChanged] BIT NOT NULL, [DecisionChangeDate] DATETIME2 (7) NULL, CONSTRAINT [PK_meetings_MeetingNotAttendees_Id] PRIMARY KEY CLUSTERED ([MeetingId] ASC, [MemberId] ASC, [DecisionDate] ASC) ); GO PRINT N'Creating [meetings].[Meetings]...'; GO CREATE TABLE [meetings].[Meetings] ( [Id] UNIQUEIDENTIFIER NOT NULL, [MeetingGroupId] UNIQUEIDENTIFIER NOT NULL, [CreatorId] UNIQUEIDENTIFIER NOT NULL, [CreateDate] DATETIME2 (7) NOT NULL, [Title] NVARCHAR (200) NOT NULL, [Description] NVARCHAR (4000) NOT NULL, [TermStartDate] DATETIME NOT NULL, [TermEndDate] DATETIME NOT NULL, [LocationName] NVARCHAR (200) NOT NULL, [LocationAddress] NVARCHAR (200) NOT NULL, [LocationPostalCode] NVARCHAR (200) NULL, [LocationCity] NVARCHAR (50) NOT NULL, [AttendeesLimit] INT NULL, [GuestsLimit] INT NOT NULL, [RSVPTermStartDate] DATETIME NULL, [RSVPTermEndDate] DATETIME NULL, [EventFeeValue] DECIMAL (5) NULL, [EventFeeCurrency] VARCHAR (3) NULL, [ChangeDate] DATETIME2 (7) NULL, [ChangeMemberId] UNIQUEIDENTIFIER NULL, [CancelDate] DATETIME NULL, [CancelMemberId] UNIQUEIDENTIFIER NULL, [IsCanceled] BIT NOT NULL, CONSTRAINT [PK_meetings_Meetings_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MeetingGroupMembers]...'; GO CREATE TABLE [meetings].[MeetingGroupMembers] ( [MeetingGroupId] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [JoinedDate] DATETIME2 (7) NOT NULL, [RoleCode] VARCHAR (50) NOT NULL, [IsActive] BIT NOT NULL, [LeaveDate] DATETIME NULL, CONSTRAINT [PK_meetings_MeetingGroupMembers_MeetingGroupId_MemberId_JoinedDate] PRIMARY KEY CLUSTERED ([MeetingGroupId] ASC, [MemberId] ASC, [JoinedDate] ASC) ); GO PRINT N'Creating [meetings].[MeetingGroups]...'; GO CREATE TABLE [meetings].[MeetingGroups] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR (255) NOT NULL, [Description] VARCHAR (200) NULL, [LocationCity] NVARCHAR (50) NOT NULL, [LocationCountryCode] NVARCHAR (3) NOT NULL, [CreatorId] UNIQUEIDENTIFIER NOT NULL, [CreateDate] DATETIME NOT NULL, [PaymentDateTo] DATE NULL, CONSTRAINT [PK_meetings_MeetingGroups_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[Members]...'; GO CREATE TABLE [meetings].[Members] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_meetings_Members_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MeetingGroupProposals]...'; GO CREATE TABLE [meetings].[MeetingGroupProposals] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR (255) NOT NULL, [Description] VARCHAR (200) NULL, [LocationCity] NVARCHAR (50) NOT NULL, [LocationCountryCode] NVARCHAR (3) NOT NULL, [ProposalUserId] UNIQUEIDENTIFIER NOT NULL, [ProposalDate] DATETIME NOT NULL, [StatusCode] NVARCHAR (50) NOT NULL, CONSTRAINT [PK_meetings_MeetingGroupProposals_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[OutboxMessages]...'; GO CREATE TABLE [meetings].[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_meetings_OutboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[InternalCommands]...'; GO CREATE TABLE [meetings].[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_meetings_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[InboxMessages]...'; GO CREATE TABLE [meetings].[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_meetings_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MemberSubscriptions]...'; GO CREATE TABLE [meetings].[MemberSubscriptions] ( [Id] UNIQUEIDENTIFIER NOT NULL, [ExpirationDate] DATETIME NOT NULL, CONSTRAINT [PK_meetings_MemberSubscriptions_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MeetingAttendees]...'; GO CREATE TABLE [meetings].[MeetingAttendees] ( [MeetingId] UNIQUEIDENTIFIER NOT NULL, [AttendeeId] UNIQUEIDENTIFIER NOT NULL, [DecisionDate] DATETIME2 (7) NOT NULL, [RoleCode] VARCHAR (50) NULL, [GuestsNumber] INT NULL, [DecisionChanged] BIT NOT NULL, [DecisionChangeDate] DATETIME2 (7) NULL, [IsRemoved] BIT NOT NULL, [RemovingMemberId] UNIQUEIDENTIFIER NULL, [RemovingReason] NVARCHAR (500) NULL, [RemovedDate] DATETIME2 (7) NULL, [BecameNotAttendeeDate] DATETIME2 (7) NULL, [FeeValue] DECIMAL (5) NULL, [FeeCurrency] VARCHAR (3) NULL, [IsFeePaid] BIT NOT NULL, CONSTRAINT [PK_meetings_MeetingAttendees_Id] PRIMARY KEY CLUSTERED ([MeetingId] ASC, [AttendeeId] ASC, [DecisionDate] ASC) ); GO PRINT N'Creating [meetings].[MeetingComments]...'; GO CREATE TABLE meetings.MeetingComments ( [Id] UNIQUEIDENTIFIER NOT NULL, [MeetingId] UNIQUEIDENTIFIER NOT NULL, [AuthorId] UNIQUEIDENTIFIER NOT NULL, [InReplyToCommentId] UNIQUEIDENTIFIER NULL, [Comment] VARCHAR(300) NULL, [IsRemoved] BIT NOT NULL, [RemovedByReason] VARCHAR(300) NULL, [CreateDate] DATETIME NOT NULL, [EditDate] DATE NULL, CONSTRAINT [PK_meetings_MeetingComments_Id] PRIMARY KEY ([Id] ASC) ) GO PRINT N'Creating [payments].[Payers]...'; GO CREATE TABLE [payments].[Payers] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_payments_Payers_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[OutboxMessages]...'; GO CREATE TABLE [payments].[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_payments_OutboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[InternalCommands]...'; GO CREATE TABLE [payments].[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_payments_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[InboxMessages]...'; GO CREATE TABLE [payments].[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_payments_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[MeetingFees]...'; GO CREATE TABLE [payments].[MeetingFees] ( [MeetingFeeId] UNIQUEIDENTIFIER NOT NULL, [PayerId] UNIQUEIDENTIFIER NOT NULL, [MeetingId] UNIQUEIDENTIFIER NOT NULL, [FeeValue] DECIMAL (18, 2) NOT NULL, [FeeCurrency] VARCHAR (50) NOT NULL, [Status] VARCHAR (50) NOT NULL, CONSTRAINT [PK_payments_MeetingFees_MeetingFeeId] PRIMARY KEY CLUSTERED ([MeetingFeeId] ASC) ); GO PRINT N'Creating [payments].[PriceListItems]...'; GO CREATE TABLE [payments].[PriceListItems] ( [Id] UNIQUEIDENTIFIER NOT NULL, [SubscriptionPeriodCode] VARCHAR (50) NOT NULL, [CategoryCode] VARCHAR (50) NOT NULL, [CountryCode] VARCHAR (50) NOT NULL, [MoneyValue] DECIMAL (18, 2) NOT NULL, [MoneyCurrency] VARCHAR (50) NOT NULL, [IsActive] BIT NOT NULL ); GO PRINT N'Creating [payments].[SubscriptionPayments]...'; GO CREATE TABLE [payments].[SubscriptionPayments] ( [PaymentId] UNIQUEIDENTIFIER NOT NULL, [PayerId] UNIQUEIDENTIFIER NOT NULL, [Type] VARCHAR (50) NOT NULL, [Status] VARCHAR (50) NOT NULL, [Period] VARCHAR (50) NOT NULL, [Date] DATETIME NOT NULL, [SubscriptionId] UNIQUEIDENTIFIER NULL, [MoneyValue] DECIMAL (18, 2) NOT NULL, [MoneyCurrency] VARCHAR (50) NOT NULL ); GO PRINT N'Creating [payments].[SubscriptionCheckpoints]...'; GO CREATE TABLE [payments].[SubscriptionCheckpoints] ( [Code] VARCHAR (50) NOT NULL, [Position] BIGINT NOT NULL ); GO PRINT N'Creating [payments].[SubscriptionDetails]...'; GO CREATE TABLE [payments].[SubscriptionDetails] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Period] VARCHAR (50) NOT NULL, [Status] VARCHAR (50) NOT NULL, [CountryCode] VARCHAR (50) NOT NULL, [ExpirationDate] DATETIME NOT NULL, CONSTRAINT [PK_payments_SubscriptionDetails_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[Streams]...'; GO CREATE TABLE [payments].[Streams] ( Id CHAR(42) NOT NULL, IdOriginal NVARCHAR(1000) NOT NULL, IdInternal INT IDENTITY(1,1) NOT NULL, [Version] INT NOT NULL, Position BIGINT NOT NULL, MaxAge INT DEFAULT(NULL), MaxCount INT DEFAULT(NULL), IdOriginalReversed AS REVERSE(IdOriginal) CONSTRAINT PK_Streams PRIMARY KEY CLUSTERED (IdInternal) ); GO PRINT N'Creating [payments].[DF_payments_Streams_Version]...'; GO ALTER TABLE [payments].[Streams] ADD CONSTRAINT [DF_payments_Streams_Version] DEFAULT (-1) FOR [Version]; GO PRINT N'Creating [payments].[DF_payments_Streams_Position]...'; GO ALTER TABLE [payments].[Streams] ADD CONSTRAINT [DF_payments_Streams_Position] DEFAULT (-1) FOR [Position]; GO PRINT N'Creating [payments].[Streams].[IX_Streams_Id]...'; GO CREATE UNIQUE NONCLUSTERED INDEX [IX_Streams_Id] ON [payments].[Streams]([Id] ASC); CREATE NONCLUSTERED INDEX IX_Streams_IdOriginal ON [payments].Streams (IdOriginal); CREATE NONCLUSTERED INDEX IX_Streams_IdOriginalReversed ON [payments].Streams (IdOriginalReversed); GO PRINT N'Creating [payments].[Messages]...'; GO CREATE TABLE [payments].[Messages] ( [StreamIdInternal] INT NOT NULL, [StreamVersion] INT NOT NULL, [Position] BIGINT IDENTITY (0, 1) NOT NULL, [Id] UNIQUEIDENTIFIER NOT NULL, [Created] DATETIME NOT NULL, [Type] NVARCHAR (128) NOT NULL, [JsonData] NVARCHAR (MAX) NOT NULL, [JsonMetadata] NVARCHAR (MAX) NULL, CONSTRAINT [PK_Events] PRIMARY KEY NONCLUSTERED ([Position] ASC), CONSTRAINT FK_Events_Streams FOREIGN KEY (StreamIdInternal) REFERENCES payments.Streams(IdInternal) ); GO PRINT N'Creating [payments].[Messages] Index...'; IF NOT EXISTS( SELECT * FROM sys.indexes WHERE name='IX_Messages_StreamIdInternal_Id' AND object_id = OBJECT_ID('payments.Messages')) BEGIN CREATE UNIQUE NONCLUSTERED INDEX IX_Messages_StreamIdInternal_Id ON payments.Messages (StreamIdInternal, Id); END IF NOT EXISTS( SELECT * FROM sys.indexes WHERE name='IX_Messages_StreamIdInternal_Revision' AND object_id = OBJECT_ID('payments.Messages')) BEGIN CREATE UNIQUE NONCLUSTERED INDEX IX_Messages_StreamIdInternal_Revision ON payments.Messages (StreamIdInternal, StreamVersion); END IF NOT EXISTS( SELECT * FROM sys.indexes WHERE name='IX_Messages_StreamIdInternal_Created' AND object_id = OBJECT_ID('payments.Messages')) BEGIN CREATE NONCLUSTERED INDEX IX_Messages_StreamIdInternal_Created ON payments.Messages (StreamIdInternal, Created); END IF NOT EXISTS( SELECT * FROM sys.table_types tt JOIN sys.schemas s ON tt.schema_id = s.schema_id WHERE s.name + '.' + tt.name='payments.NewStreamMessages') BEGIN CREATE TYPE payments.NewStreamMessages AS TABLE ( StreamVersion INT IDENTITY(0,1) NOT NULL, Id UNIQUEIDENTIFIER NOT NULL, Created DATETIME DEFAULT(GETUTCDATE()) NOT NULL, [Type] NVARCHAR(128) NOT NULL, JsonData NVARCHAR(max) NULL, JsonMetadata NVARCHAR(max) NULL ); END BEGIN IF NOT EXISTS (SELECT NULL FROM SYS.EXTENDED_PROPERTIES WHERE [major_id] = OBJECT_ID('payments.Streams') AND [name] = N'version' AND [minor_id] = 0) EXEC sys.sp_addextendedproperty @name = N'version', @value = N'3', @level0type = N'SCHEMA', @level0name = 'payments', @level1type = N'TABLE', @level1name = 'Streams'; END GO PRINT N'Creating [users].[InboxMessages]...'; GO CREATE TABLE [users].[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_users_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [users].[UserRoles]...'; GO CREATE TABLE [users].[UserRoles] ( [UserId] UNIQUEIDENTIFIER NOT NULL, [RoleCode] NVARCHAR (50) NULL ); GO PRINT N'Creating [registrations].[UserRegistrations]...'; GO CREATE TABLE [registrations].[UserRegistrations] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [Password] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, [StatusCode] VARCHAR (50) NOT NULL, [RegisterDate] DATETIME NOT NULL, [ConfirmedDate] DATETIME NULL, CONSTRAINT [PK_registrations_UserRegistrations_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [users].[Users]...'; GO CREATE TABLE [users].[Users] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [Password] NVARCHAR (255) NOT NULL, [IsActive] BIT NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_users_Users_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [users].[RolesToPermissions]...'; GO CREATE TABLE [users].[RolesToPermissions] ( [RoleCode] VARCHAR (50) NOT NULL, [PermissionCode] VARCHAR (50) NOT NULL, CONSTRAINT [PK_RolesToPermissions_RoleCode_PermissionCode] PRIMARY KEY CLUSTERED ([RoleCode] ASC, [PermissionCode] ASC) ); GO PRINT N'Creating [users].[Permissions]...'; GO CREATE TABLE [users].[Permissions] ( [Code] VARCHAR (50) NOT NULL, [Name] VARCHAR (100) NOT NULL, [Description] VARCHAR (255) NULL, CONSTRAINT [PK_users_Permissions_Code] PRIMARY KEY CLUSTERED ([Code] ASC) ); GO PRINT N'Creating [users].[InternalCommands]...'; GO CREATE TABLE [users].[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_users_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [users].[OutboxMessages]...'; GO CREATE TABLE [users].[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 [administration].[v_MeetingGroupProposals]...'; GO CREATE VIEW [administration].[v_MeetingGroupProposals] AS SELECT [MeetingGroupProposal].[Id], [MeetingGroupProposal].[Name], [MeetingGroupProposal].[Description], [MeetingGroupProposal].[LocationCity], [MeetingGroupProposal].[LocationCountryCode], [MeetingGroupProposal].[ProposalUserId], [MeetingGroupProposal].[ProposalDate], [MeetingGroupProposal].[StatusCode], [MeetingGroupProposal].[DecisionDate], [MeetingGroupProposal].[DecisionUserId], [MeetingGroupProposal].[DecisionCode], [MeetingGroupProposal].[DecisionRejectReason] FROM [administration].[MeetingGroupProposals] AS [MeetingGroupProposal] GO PRINT N'Creating [administration].[v_Members]...'; GO CREATE VIEW [administration].[v_Members] AS SELECT [Member].[Id], [Member].[Login], [Member].[Email], [Member].[FirstName], [Member].[LastName], [Member].[Name] FROM [administration].[Members] AS [Member] GO PRINT N'Creating [meetings].[v_MeetingGroups]...'; GO CREATE VIEW [meetings].[v_MeetingGroups] AS SELECT [MeetingGroup].Id, [MeetingGroup].[Name], [MeetingGroup].[Description], [MeetingGroup].[LocationCountryCode], [MeetingGroup].[LocationCity] FROM meetings.MeetingGroups AS [MeetingGroup] GO PRINT N'Creating [meetings].[v_Meetings]...'; GO CREATE VIEW [meetings].[v_Meetings] AS SELECT Meeting.[Id], Meeting.[Title], Meeting.[Description], Meeting.LocationAddress, Meeting.LocationCity, Meeting.LocationPostalCode, Meeting.TermStartDate, Meeting.TermEndDate FROM meetings.Meetings AS [Meeting] GO PRINT N'Creating [meetings].[v_Members]...'; GO CREATE VIEW [meetings].[v_Members] AS SELECT [Member].Id, [Member].[Name], [Member].[Login], [Member].[Email] FROM meetings.Members AS [Member] GO PRINT N'Creating [meetings].[v_MeetingGroupMembers]...'; GO CREATE VIEW [meetings].[v_MeetingGroupMembers] AS SELECT [MeetingGroupMember].MeetingGroupId, [MeetingGroupMember].MemberId, [MeetingGroupMember].RoleCode FROM meetings.MeetingGroupMembers AS [MeetingGroupMember] GO PRINT N'Creating [meetings].[v_MeetingGroupProposals]...'; GO CREATE VIEW [meetings].[v_MeetingGroupProposals] AS SELECT [MeetingGroupProposal].[Id], [MeetingGroupProposal].[Name], [MeetingGroupProposal].[Description], [MeetingGroupProposal].[LocationCity], [MeetingGroupProposal].[LocationCountryCode], [MeetingGroupProposal].[ProposalUserId], [MeetingGroupProposal].[ProposalDate], [MeetingGroupProposal].[StatusCode] FROM [meetings].[MeetingGroupProposals] AS [MeetingGroupProposal] GO PRINT N'Creating [users].[v_UserRoles]...'; GO CREATE VIEW [users].[v_UserRoles] AS SELECT [UserRole].[UserId], [UserRole].[RoleCode] FROM [users].[UserRoles] AS [UserRole] GO PRINT N'Creating [users].[v_Users]...'; GO CREATE VIEW [users].[v_Users] AS SELECT [User].[Id], [User].[IsActive], [User].[Login], [User].[Password], [User].[Email], [User].[Name] FROM [users].[Users] AS [User] GO PRINT N'Creating [registrations].[v_UserRegistrations]...'; GO CREATE VIEW [users].[v_UserRegistrations] AS SELECT [UserRegistration].[Id], [UserRegistration].[Login], [UserRegistration].[Email], [UserRegistration].[FirstName], [UserRegistration].[LastName], [UserRegistration].[Name], [UserRegistration].[StatusCode] FROM [registrations].[UserRegistrations] AS [UserRegistration] GO PRINT N'Creating [users].[v_UserPermissions]...'; GO CREATE VIEW [users].[v_UserPermissions] AS SELECT DISTINCT [UserRole].UserId, [RolesToPermission].PermissionCode FROM [users].UserRoles AS [UserRole] INNER JOIN [users].RolesToPermissions AS [RolesToPermission] ON [UserRole].RoleCode = [RolesToPermission].RoleCode GO PRINT N'Checking existing data against newly created constraints'; GO ALTER TABLE [payments].[Messages] WITH CHECK CHECK CONSTRAINT [FK_Events_Streams]; GO PRINT N'Update complete.'; GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0002_change_meeting_comments_edit_date_type_and_add_meeting_comments_view.sql ================================================ ALTER TABLE [meetings].[MeetingComments] ALTER COLUMN [EditDate] DATETIME GO CREATE VIEW [meetings].[v_MeetingComments] AS SELECT [MeetingComments].Id, [MeetingComments].MeetingId, [MeetingComments].AuthorId, [MeetingComments].InReplyToCommentId, [MeetingComments].Comment, [MeetingComments].CreateDate, [MeetingComments].EditDate FROM [meetings].[MeetingComments] AS [MeetingComments] WHERE [MeetingComments].IsRemoved = 0 GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0003_add_meetings_countries_table.sql ================================================ CREATE TABLE [meetings].[Countries] ( [Code] CHAR(2) NOT NULL, [Name] NVARCHAR(50) NOT NULL CONSTRAINT [PK_meetings_Countries_Code] PRIMARY KEY ([Code] ASC) ) GO CREATE VIEW [meetings].[v_Countries] AS SELECT [Country].[Code], [Country].[Name] FROM meetings.Countries AS [Country] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0004_add_meeting_commenting_configurations_table.sql ================================================ CREATE TABLE meetings.MeetingCommentingConfigurations ( [Id] UNIQUEIDENTIFIER NOT NULL, [MeetingId] UNIQUEIDENTIFIER NOT NULL, [IsCommentingEnabled] BIT NOT NULL, CONSTRAINT [PK_meetings_MeetingCommentingConfigurations_Id] PRIMARY KEY ([Id] ASC), CONSTRAINT [FK_meetings_MeetingCommentingConfigurations_Meetings] FOREIGN KEY ([MeetingId]) REFERENCES meetings.Meetings ([Id]) ); GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0005_add_payer_id_to_subcription_details_view.sql ================================================ DELETE FROM payments.SubscriptionDetails GO ALTER TABLE payments.SubscriptionDetails ADD [PayerId] UNIQUEIDENTIFIER NOT NULL GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0006_add_member_meeting_groups_view.sql ================================================ CREATE VIEW [meetings].[v_MemberMeetingGroups] AS SELECT [MeetingGroup].Id, [MeetingGroup].[Name], [MeetingGroup].[Description], [MeetingGroup].[LocationCountryCode], [MeetingGroup].[LocationCity], [MeetingGroupMember].[MemberId], [MeetingGroupMember].[RoleCode], [MeetingGroupMember].[IsActive], [MeetingGroupMember].[JoinedDate] FROM meetings.MeetingGroups AS [MeetingGroup] INNER JOIN [meetings].[MeetingGroupMembers] AS [MeetingGroupMember] ON [MeetingGroup].[Id] = [MeetingGroupMember].[MeetingGroupId] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0007_add_meeting_attendees_view.sql ================================================ CREATE VIEW [meetings].[v_MeetingAttendees] AS SELECT [MeetingAttendee].[MeetingId], [MeetingAttendee].[AttendeeId], [MeetingAttendee].[DecisionDate], [MeetingAttendee].[RoleCode], [MeetingAttendee].[GuestsNumber], [Member].[FirstName], [Member].[LastName] FROM [meetings].[MeetingAttendees] AS [MeetingAttendee] INNER JOIN [meetings].[Members] AS [Member] ON [MeetingAttendee].[AttendeeId] = [Member].[Id] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0008_add_meeting_details_view.sql ================================================ CREATE VIEW [meetings].[v_MeetingDetails] AS SELECT [Meeting].[Id], [Meeting].[MeetingGroupId], [Meeting].[Title], [Meeting].[TermStartDate], [Meeting].[TermEndDate], [Meeting].[Description], [Meeting].[LocationName], [Meeting].[LocationAddress], [Meeting].[LocationPostalCode], [Meeting].[LocationCity], [Meeting].[AttendeesLimit], [Meeting].[GuestsLimit], [Meeting].[RSVPTermStartDate], [Meeting].[RSVPTermEndDate], [Meeting].[EventFeeValue], [Meeting].[EventFeeCurrency] FROM [meetings].[Meetings] AS [Meeting] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0009_add_mock_emails_table.sql ================================================ CREATE TABLE [app].[Emails] ( [Id] UNIQUEIDENTIFIER NOT NULL, [From] NVARCHAR(255) NOT NULL, [To] NVARCHAR(255) NOT NULL, [Subject] NVARCHAR(255) NOT NULL, [Content] NVARCHAR(MAX) NOT NULL, [Date] DATETIME NOT NULL, CONSTRAINT [PK_app_Emails_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ) ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0010_add_member_meetings_view.sql ================================================ CREATE VIEW [meetings].[v_MemberMeetings] AS SELECT [Meeting].[Id], [Meeting].[Title], [Meeting].[Description], [Meeting].[LocationAddress], [Meeting].[LocationCity], [Meeting].[LocationPostalCode], [Meeting].[TermStartDate], [Meeting].[TermEndDate], [MeetingAttendee].[AttendeeId], [MeetingAttendee].[IsRemoved], [MeetingAttendee].[RoleCode] FROM [meetings].[Meetings] AS [Meeting] INNER JOIN [meetings].[MeetingAttendees] AS [MeetingAttendee] ON [Meeting].[Id] = [MeetingAttendee].[MeetingId] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0011_add_likes_count_to_meeting_comments_table.sql ================================================ ALTER TABLE [meetings].[MeetingComments] ADD [LikesCount] INT DEFAULT 0 GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0012_add_likes_count_to_meeting_comments_view.sql ================================================ ALTER VIEW [meetings].[v_MeetingComments] AS SELECT [MeetingComments].Id, [MeetingComments].MeetingId, [MeetingComments].AuthorId, [MeetingComments].InReplyToCommentId, [MeetingComments].Comment, [MeetingComments].CreateDate, [MeetingComments].EditDate, [MeetingComments].LikesCount FROM [meetings].[MeetingComments] AS [MeetingComments] WHERE [MeetingComments].IsRemoved = 0 GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0013_add_meeting_member_comment_likes_table.sql ================================================ CREATE TABLE [meetings].[MeetingMemberCommentLikes] ( [Id] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [MeetingCommentId] UNIQUEIDENTIFIER NOT NULL, CONSTRAINT [PK_meetings_MeetingMemberCommentLikes_Id] PRIMARY KEY ([Id] ASC), CONSTRAINT [FK_meetings_MeetingMemberCommentLikes_Members] FOREIGN KEY ([MemberId]) REFERENCES meetings.Members ([Id]), CONSTRAINT [FK_meetings_MeetingMemberCommentLikes_MeetingComments] FOREIGN KEY ([MeetingCommentId]) REFERENCES meetings.MeetingComments ([Id]) ) ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Migrations/1_0_0_0/0014_add_missing_tables_for_registrations.sql ================================================ CREATE TABLE [registrations].[OutboxMessages] ( [Id] UNIQUEIDENTIFIER NOT NULL, [OccurredOn] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, CONSTRAINT [PK_registrations_OutboxMessages_Id] PRIMARY KEY ([Id] ASC) ) GO CREATE TABLE [registrations].[InternalCommands] ( [Id] UNIQUEIDENTIFIER NOT NULL, [EnqueueDate] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, [Error] NVARCHAR(MAX) NULL, CONSTRAINT [PK_registrations_InternalCommands_Id] PRIMARY KEY ([Id] ASC) ) GO CREATE TABLE [registrations].[InboxMessages] ( [Id] UNIQUEIDENTIFIER NOT NULL, [OccurredOn] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, CONSTRAINT [PK_registrations_InboxMessages_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/SeedDatabase.sql ================================================ -- Add Test Member INSERT INTO registrations.UserRegistrations VALUES ( '2EBFECFC-ED13-43B8-B516-6AC89D51C510', 'testMember@mail.com', 'testMember@mail.com', 'AHKrg4VuA82NvKXNoaPx0odIMnw1eRQV9IqmCHJ1jemv4sWFD3jsw6dabc1xerzgLQ==', -- testMemberPass 'John', 'Doe', 'John Doe', 'Confirmed', GETDATE(), GETDATE() ) INSERT INTO users.Users VALUES ( '2EBFECFC-ED13-43B8-B516-6AC89D51C510', 'testMember@mail.com', 'testMember@mail.com', 'AHKrg4VuA82NvKXNoaPx0odIMnw1eRQV9IqmCHJ1jemv4sWFD3jsw6dabc1xerzgLQ==', -- testMemberPass 1, 'John', 'Doe', 'John Doe' ) INSERT INTO meetings.Members VALUES ( '2EBFECFC-ED13-43B8-B516-6AC89D51C510', 'testMember@mail.com', 'testMember@mail.com', 'John', 'Doe', 'John Doe' ) INSERT INTO payments.Payers VALUES ( '2EBFECFC-ED13-43B8-B516-6AC89D51C510', 'testMember@mail.com', 'testMember@mail.com', 'John', 'Doe', 'John Doe' ) INSERT INTO users.UserRoles VALUES ('2EBFECFC-ED13-43B8-B516-6AC89D51C510', 'Member') -- Add Test Administrator INSERT INTO registrations.UserRegistrations VALUES ( '4065630E-4A4C-4F01-9142-0BACF6B8C64D', 'testAdmin@mail.com', 'testAdmin@mail.com', 'AK0qplH5peUHwnCVuzW9zy0JGZTTG6/Ji88twX+nw9JdTUwqa3Wol1K4m5aCG9pE2A==', -- testAdminPass 'Jane', 'Doe', 'Jane Doe', 'Confirmed', GETDATE(), GETDATE() ) INSERT INTO users.Users VALUES ( '4065630E-4A4C-4F01-9142-0BACF6B8C64D', 'testAdmin@mail.com', 'testAdmin@mail.com', 'AJtxxwNDtxRI7lq268ienCfsBwvo9y3RmKkP4JMpRinWqdgbnFgzjLX9LqEt2YcUgA==', -- testAdminPass 1, 'Jane', 'Doe', 'Jane Doe' ) INSERT INTO users.UserRoles VALUES ('4065630E-4A4C-4F01-9142-0BACF6B8C64D', 'Administrator') -- Roles to Permissions INSERT INTO users.[Permissions] ([Code], [Name]) VALUES -- Meetings ('GetMeetingGroupProposals', 'GetMeetingGroupProposals'), ('ProposeMeetingGroup', 'ProposeMeetingGroup'), ('CreateNewMeeting','CreateNewMeeting'), ('EditMeeting','EditMeeting'), ('AddMeetingAttendee','AddMeetingAttendee'), ('RemoveMeetingAttendee','RemoveMeetingAttendee'), ('AddNotAttendee','AddNotAttendee'), ('ChangeNotAttendeeDecision','ChangeNotAttendeeDecision'), ('SignUpMemberToWaitlist','SignUpMemberToWaitlist'), ('SignOffMemberFromWaitlist','SignOffMemberFromWaitlist'), ('SetMeetingHostRole','SetMeetingHostRole'), ('SetMeetingAttendeeRole','SetMeetingAttendeeRole'), ('CancelMeeting','CancelMeeting'), ('GetAllMeetingGroups','GetAllMeetingGroups'), ('EditMeetingGroupGeneralAttributes','EditMeetingGroupGeneralAttributes'), ('JoinToGroup','JoinToGroup'), ('LeaveMeetingGroup','LeaveMeetingGroup'), ('AddMeetingComment','AddMeetingComment'), ('EditMeetingComment','EditMeetingComment'), ('RemoveMeetingComment','RemoveMeetingComment'), ('AddMeetingCommentReply','AddMeetingCommentReply'), ('LikeMeetingComment','LikeMeetingComment'), ('UnlikeMeetingComment','UnlikeMeetingComment'), ('EnableMeetingCommenting','EnableMeetingCommenting'), ('DisableMeetingCommenting','DisableMeetingCommenting'), ('MyMeetingGroupsView','MyMeetingGroupsView'), ('AllMeetingGroupsView','AllMeetingGroupsView'), ('SubscriptionView','SubscriptionView'), ('EmailsView','EmailsView'), ('MyMeetingsView','MyMeetingsView'), ('GetAuthenticatedMemberMeetings','GetAuthenticatedMemberMeetings'), -- Administration ('AcceptMeetingGroupProposal','AcceptMeetingGroupProposal'), ('AdministrationsView','AdministrationsView'), -- Payments ('RegisterPayment','RegisterPayment'), ('BuySubscription','BuySubscription'), ('RenewSubscription','RenewSubscription'), ('CreatePriceListItem','CreatePriceListItem'), ('ActivatePriceListItem','ActivatePriceListItem'), ('DeactivatePriceListItem','DeactivatePriceListItem'), ('ChangePriceListItemAttributes','ChangePriceListItemAttributes'), ('GetAuthenticatedPayerSubscription','GetAuthenticatedPayerSubscription'), ('GetPriceListItem','GetPriceListItem') -- Meetings INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetMeetingGroupProposals') INSERT INTO users.RolesToPermissions VALUES ('Member', 'ProposeMeetingGroup') INSERT INTO users.RolesToPermissions VALUES ('Member', 'CreateNewMeeting') INSERT INTO users.RolesToPermissions VALUES ('Member', 'EditMeeting') INSERT INTO users.RolesToPermissions VALUES ('Member', 'AddMeetingAttendee') INSERT INTO users.RolesToPermissions VALUES ('Member', 'RemoveMeetingAttendee') INSERT INTO users.RolesToPermissions VALUES ('Member', 'AddNotAttendee') INSERT INTO users.RolesToPermissions VALUES ('Member', 'ChangeNotAttendeeDecision') INSERT INTO users.RolesToPermissions VALUES ('Member', 'SignUpMemberToWaitlist') INSERT INTO users.RolesToPermissions VALUES ('Member', 'SignOffMemberFromWaitlist') INSERT INTO users.RolesToPermissions VALUES ('Member', 'SetMeetingHostRole') INSERT INTO users.RolesToPermissions VALUES ('Member', 'SetMeetingAttendeeRole') INSERT INTO users.RolesToPermissions VALUES ('Member', 'CancelMeeting') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetAllMeetingGroups') INSERT INTO users.RolesToPermissions VALUES ('Member', 'EditMeetingGroupGeneralAttributes') INSERT INTO users.RolesToPermissions VALUES ('Member', 'JoinToGroup') INSERT INTO users.RolesToPermissions VALUES ('Member', 'LeaveMeetingGroup') INSERT INTO users.RolesToPermissions VALUES ('Member', 'AddMeetingComment') INSERT INTO users.RolesToPermissions VALUES ('Member', 'EditMeetingComment') INSERT INTO users.RolesToPermissions VALUES ('Member', 'RemoveMeetingComment') INSERT INTO users.RolesToPermissions VALUES ('Member', 'AddMeetingCommentReply') INSERT INTO users.RolesToPermissions VALUES ('Member', 'LikeMeetingComment') INSERT INTO users.RolesToPermissions VALUES ('Member', 'UnlikeMeetingComment') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetAuthenticatedMemberMeetingGroups') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetMeetingGroupDetails') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetMeetingDetails') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetMeetingAttendees') INSERT INTO users.RolesToPermissions VALUES ('Member', 'MyMeetingsGroupsView') INSERT INTO users.RolesToPermissions VALUES ('Member', 'SubscriptionView') INSERT INTO users.RolesToPermissions VALUES ('Member', 'EmailsView') INSERT INTO users.RolesToPermissions VALUES ('Member', 'AllMeetingGroupsView') INSERT INTO users.RolesToPermissions VALUES ('Member', 'MyMeetingsView') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetAuthenticatedMemberMeetings') -- Administration INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'AcceptMeetingGroupProposal') INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'AdministrationsView') -- Payments INSERT INTO users.RolesToPermissions VALUES ('Member', 'RegisterPayment') INSERT INTO users.RolesToPermissions VALUES ('Member', 'BuySubscription') INSERT INTO users.RolesToPermissions VALUES ('Member', 'RenewSubscription') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetAuthenticatedPayerSubscription') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetPriceListItem') INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'CreatePriceListItem') INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'ActivatePriceListItem') INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'DeactivatePriceListItem') INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'ChangePriceListItemAttributes') INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'GetPriceListItem') ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Scripts/Seeds/0001_SeedCountries.sql ================================================ INSERT INTO [meetings].[Countries] VALUES ('AD', 'Andorra') INSERT INTO [meetings].[Countries] VALUES ('AE', 'United Arab Emirates') INSERT INTO [meetings].[Countries] VALUES ('AF', 'Afghanistan') INSERT INTO [meetings].[Countries] VALUES ('AG', 'Antigua and Barbuda') INSERT INTO [meetings].[Countries] VALUES ('AI', 'Anguilla') INSERT INTO [meetings].[Countries] VALUES ('AL', 'Albania') INSERT INTO [meetings].[Countries] VALUES ('AM', 'Armenia') INSERT INTO [meetings].[Countries] VALUES ('AN', 'Netherlands Antilles') INSERT INTO [meetings].[Countries] VALUES ('AO', 'Angola') INSERT INTO [meetings].[Countries] VALUES ('AQ', 'Antarctica') INSERT INTO [meetings].[Countries] VALUES ('AR', 'Argentina') INSERT INTO [meetings].[Countries] VALUES ('AS', 'American Samoa') INSERT INTO [meetings].[Countries] VALUES ('AT', 'Austria') INSERT INTO [meetings].[Countries] VALUES ('AU', 'Australia') INSERT INTO [meetings].[Countries] VALUES ('AW', 'Aruba') INSERT INTO [meetings].[Countries] VALUES ('AX', 'Aland Islands') INSERT INTO [meetings].[Countries] VALUES ('AZ', 'Azerbaijan') INSERT INTO [meetings].[Countries] VALUES ('BA', 'Bosnia and Herzegovina') INSERT INTO [meetings].[Countries] VALUES ('BB', 'Barbados') INSERT INTO [meetings].[Countries] VALUES ('BD', 'Bangladesh') INSERT INTO [meetings].[Countries] VALUES ('BE', 'Belgium') INSERT INTO [meetings].[Countries] VALUES ('BF', 'Burkina Faso') INSERT INTO [meetings].[Countries] VALUES ('BG', 'Bulgaria') INSERT INTO [meetings].[Countries] VALUES ('BH', 'Bahrain') INSERT INTO [meetings].[Countries] VALUES ('BI', 'Burundi') INSERT INTO [meetings].[Countries] VALUES ('BJ', 'Benin') INSERT INTO [meetings].[Countries] VALUES ('BM', 'Bermuda') INSERT INTO [meetings].[Countries] VALUES ('BN', 'Brunei Darussalam') INSERT INTO [meetings].[Countries] VALUES ('BO', 'Bolivia') INSERT INTO [meetings].[Countries] VALUES ('BR', 'Brazil') INSERT INTO [meetings].[Countries] VALUES ('BS', 'Bahamas') INSERT INTO [meetings].[Countries] VALUES ('BT', 'Bhutan') INSERT INTO [meetings].[Countries] VALUES ('BV', 'Bouvet Island') INSERT INTO [meetings].[Countries] VALUES ('BW', 'Botswana') INSERT INTO [meetings].[Countries] VALUES ('BY', 'Belarus') INSERT INTO [meetings].[Countries] VALUES ('BZ', 'Belize') INSERT INTO [meetings].[Countries] VALUES ('CA', 'Canada') INSERT INTO [meetings].[Countries] VALUES ('CC', 'Cocos (Keeling) Islands') INSERT INTO [meetings].[Countries] VALUES ('CD', 'Congo, The Democratic Republic of the') INSERT INTO [meetings].[Countries] VALUES ('CF', 'Central African Republic') INSERT INTO [meetings].[Countries] VALUES ('CG', 'Congo') INSERT INTO [meetings].[Countries] VALUES ('CH', 'Switzerland') INSERT INTO [meetings].[Countries] VALUES ('CI', 'Cote DIvoire') INSERT INTO [meetings].[Countries] VALUES ('CK', 'Cook Islands') INSERT INTO [meetings].[Countries] VALUES ('CL', 'Chile') INSERT INTO [meetings].[Countries] VALUES ('CM', 'Cameroon') INSERT INTO [meetings].[Countries] VALUES ('CN', 'China') INSERT INTO [meetings].[Countries] VALUES ('CO', 'Colombia') INSERT INTO [meetings].[Countries] VALUES ('CR', 'Costa Rica') INSERT INTO [meetings].[Countries] VALUES ('CU', 'Cuba') INSERT INTO [meetings].[Countries] VALUES ('CV', 'Cape Verde') INSERT INTO [meetings].[Countries] VALUES ('CX', 'Christmas Island') INSERT INTO [meetings].[Countries] VALUES ('CY', 'Cyprus') INSERT INTO [meetings].[Countries] VALUES ('CZ', 'Czech Republic') INSERT INTO [meetings].[Countries] VALUES ('DE', 'Germany') INSERT INTO [meetings].[Countries] VALUES ('DJ', 'Djibouti') INSERT INTO [meetings].[Countries] VALUES ('DK', 'Denmark') INSERT INTO [meetings].[Countries] VALUES ('DM', 'Dominica') INSERT INTO [meetings].[Countries] VALUES ('DO', 'Dominican Republic') INSERT INTO [meetings].[Countries] VALUES ('DZ', 'Algeria') INSERT INTO [meetings].[Countries] VALUES ('EC', 'Ecuador') INSERT INTO [meetings].[Countries] VALUES ('EE', 'Estonia') INSERT INTO [meetings].[Countries] VALUES ('EG', 'Egypt') INSERT INTO [meetings].[Countries] VALUES ('EH', 'Western Sahara') INSERT INTO [meetings].[Countries] VALUES ('ER', 'Eritrea') INSERT INTO [meetings].[Countries] VALUES ('ES', 'Spain') INSERT INTO [meetings].[Countries] VALUES ('ET', 'Ethiopia') INSERT INTO [meetings].[Countries] VALUES ('FI', 'Finland') INSERT INTO [meetings].[Countries] VALUES ('FJ', 'Fiji') INSERT INTO [meetings].[Countries] VALUES ('FK', 'Falkland Islands (Malvinas)') INSERT INTO [meetings].[Countries] VALUES ('FM', 'Micronesia, Federated States of') INSERT INTO [meetings].[Countries] VALUES ('FO', 'Faroe Islands') INSERT INTO [meetings].[Countries] VALUES ('FR', 'France') INSERT INTO [meetings].[Countries] VALUES ('GA', 'Gabon') INSERT INTO [meetings].[Countries] VALUES ('GB', 'United Kingdom') INSERT INTO [meetings].[Countries] VALUES ('GD', 'Grenada') INSERT INTO [meetings].[Countries] VALUES ('GE', 'Georgia') INSERT INTO [meetings].[Countries] VALUES ('GF', 'French Guiana') INSERT INTO [meetings].[Countries] VALUES ('GG', 'Guernsey') INSERT INTO [meetings].[Countries] VALUES ('GH', 'Ghana') INSERT INTO [meetings].[Countries] VALUES ('GI', 'Gibraltar') INSERT INTO [meetings].[Countries] VALUES ('GL', 'Greenland') INSERT INTO [meetings].[Countries] VALUES ('GM', 'Gambia') INSERT INTO [meetings].[Countries] VALUES ('GN', 'Guinea') INSERT INTO [meetings].[Countries] VALUES ('GP', 'Guadeloupe') INSERT INTO [meetings].[Countries] VALUES ('GQ', 'Equatorial Guinea') INSERT INTO [meetings].[Countries] VALUES ('GR', 'Greece') INSERT INTO [meetings].[Countries] VALUES ('GS', 'South Georgia and the South Sandwich Islands') INSERT INTO [meetings].[Countries] VALUES ('GT', 'Guatemala') INSERT INTO [meetings].[Countries] VALUES ('GU', 'Guam') INSERT INTO [meetings].[Countries] VALUES ('GW', 'Guinea-Bissau') INSERT INTO [meetings].[Countries] VALUES ('GY', 'Guyana') INSERT INTO [meetings].[Countries] VALUES ('HK', 'Hong Kong') INSERT INTO [meetings].[Countries] VALUES ('HM', 'Heard Island and Mcdonald Islands') INSERT INTO [meetings].[Countries] VALUES ('HN', 'Honduras') INSERT INTO [meetings].[Countries] VALUES ('HR', 'Croatia') INSERT INTO [meetings].[Countries] VALUES ('HT', 'Haiti') INSERT INTO [meetings].[Countries] VALUES ('HU', 'Hungary') INSERT INTO [meetings].[Countries] VALUES ('ID', 'Indonesia') INSERT INTO [meetings].[Countries] VALUES ('IE', 'Ireland') INSERT INTO [meetings].[Countries] VALUES ('IL', 'Israel') INSERT INTO [meetings].[Countries] VALUES ('IM', 'Isle of Man') INSERT INTO [meetings].[Countries] VALUES ('IN', 'India') INSERT INTO [meetings].[Countries] VALUES ('IO', 'British Indian Ocean Territory') INSERT INTO [meetings].[Countries] VALUES ('IQ', 'Iraq') INSERT INTO [meetings].[Countries] VALUES ('IR', 'Iran, Islamic Republic Of') INSERT INTO [meetings].[Countries] VALUES ('IS', 'Iceland') INSERT INTO [meetings].[Countries] VALUES ('IT', 'Italy') INSERT INTO [meetings].[Countries] VALUES ('JE', 'Jersey') INSERT INTO [meetings].[Countries] VALUES ('JM', 'Jamaica') INSERT INTO [meetings].[Countries] VALUES ('JO', 'Jordan') INSERT INTO [meetings].[Countries] VALUES ('JP', 'Japan') INSERT INTO [meetings].[Countries] VALUES ('KE', 'Kenya') INSERT INTO [meetings].[Countries] VALUES ('KG', 'Kyrgyzstan') INSERT INTO [meetings].[Countries] VALUES ('KH', 'Cambodia') INSERT INTO [meetings].[Countries] VALUES ('KI', 'Kiribati') INSERT INTO [meetings].[Countries] VALUES ('KM', 'Comoros') INSERT INTO [meetings].[Countries] VALUES ('KN', 'Saint Kitts and Nevis') INSERT INTO [meetings].[Countries] VALUES ('KP', 'Korea, Democratic PeopleS Republic of') INSERT INTO [meetings].[Countries] VALUES ('KR', 'Korea, Republic of') INSERT INTO [meetings].[Countries] VALUES ('KW', 'Kuwait') INSERT INTO [meetings].[Countries] VALUES ('KY', 'Cayman Islands') INSERT INTO [meetings].[Countries] VALUES ('KZ', 'Kazakhstan') INSERT INTO [meetings].[Countries] VALUES ('LA', 'Lao PeopleS Democratic Republic') INSERT INTO [meetings].[Countries] VALUES ('LB', 'Lebanon') INSERT INTO [meetings].[Countries] VALUES ('LC', 'Saint Lucia') INSERT INTO [meetings].[Countries] VALUES ('LI', 'Liechtenstein') INSERT INTO [meetings].[Countries] VALUES ('LK', 'Sri Lanka') INSERT INTO [meetings].[Countries] VALUES ('LR', 'Liberia') INSERT INTO [meetings].[Countries] VALUES ('LS', 'Lesotho') INSERT INTO [meetings].[Countries] VALUES ('LT', 'Lithuania') INSERT INTO [meetings].[Countries] VALUES ('LU', 'Luxembourg') INSERT INTO [meetings].[Countries] VALUES ('LV', 'Latvia') INSERT INTO [meetings].[Countries] VALUES ('LY', 'Libyan Arab Jamahiriya') INSERT INTO [meetings].[Countries] VALUES ('MA', 'Morocco') INSERT INTO [meetings].[Countries] VALUES ('MC', 'Monaco') INSERT INTO [meetings].[Countries] VALUES ('MD', 'Moldova, Republic of') INSERT INTO [meetings].[Countries] VALUES ('ME', 'Montenegro') INSERT INTO [meetings].[Countries] VALUES ('MG', 'Madagascar') INSERT INTO [meetings].[Countries] VALUES ('MH', 'Marshall Islands') INSERT INTO [meetings].[Countries] VALUES ('MK', 'Macedonia, The Former Yugoslav Republic of') INSERT INTO [meetings].[Countries] VALUES ('ML', 'Mali') INSERT INTO [meetings].[Countries] VALUES ('MM', 'Myanmar') INSERT INTO [meetings].[Countries] VALUES ('MN', 'Mongolia') INSERT INTO [meetings].[Countries] VALUES ('MO', 'Macao') INSERT INTO [meetings].[Countries] VALUES ('MP', 'Northern Mariana Islands') INSERT INTO [meetings].[Countries] VALUES ('MQ', 'Martinique') INSERT INTO [meetings].[Countries] VALUES ('MR', 'Mauritania') INSERT INTO [meetings].[Countries] VALUES ('MS', 'Montserrat') INSERT INTO [meetings].[Countries] VALUES ('MT', 'Malta') INSERT INTO [meetings].[Countries] VALUES ('MU', 'Mauritius') INSERT INTO [meetings].[Countries] VALUES ('MV', 'Maldives') INSERT INTO [meetings].[Countries] VALUES ('MW', 'Malawi') INSERT INTO [meetings].[Countries] VALUES ('MX', 'Mexico') INSERT INTO [meetings].[Countries] VALUES ('MY', 'Malaysia') INSERT INTO [meetings].[Countries] VALUES ('MZ', 'Mozambique') INSERT INTO [meetings].[Countries] VALUES ('NA', 'Namibia') INSERT INTO [meetings].[Countries] VALUES ('NC', 'New Caledonia') INSERT INTO [meetings].[Countries] VALUES ('NE', 'Niger') INSERT INTO [meetings].[Countries] VALUES ('NF', 'Norfolk Island') INSERT INTO [meetings].[Countries] VALUES ('NG', 'Nigeria') INSERT INTO [meetings].[Countries] VALUES ('NI', 'Nicaragua') INSERT INTO [meetings].[Countries] VALUES ('NL', 'Netherlands') INSERT INTO [meetings].[Countries] VALUES ('NO', 'Norway') INSERT INTO [meetings].[Countries] VALUES ('NP', 'Nepal') INSERT INTO [meetings].[Countries] VALUES ('NR', 'Nauru') INSERT INTO [meetings].[Countries] VALUES ('NU', 'Niue') INSERT INTO [meetings].[Countries] VALUES ('NZ', 'New Zealand') INSERT INTO [meetings].[Countries] VALUES ('OM', 'Oman') INSERT INTO [meetings].[Countries] VALUES ('PA', 'Panama') INSERT INTO [meetings].[Countries] VALUES ('PE', 'Peru') INSERT INTO [meetings].[Countries] VALUES ('PF', 'French Polynesia') INSERT INTO [meetings].[Countries] VALUES ('PG', 'Papua New Guinea') INSERT INTO [meetings].[Countries] VALUES ('PH', 'Philippines') INSERT INTO [meetings].[Countries] VALUES ('PK', 'Pakistan') INSERT INTO [meetings].[Countries] VALUES ('PL', 'Poland') INSERT INTO [meetings].[Countries] VALUES ('PM', 'Saint Pierre and Miquelon') INSERT INTO [meetings].[Countries] VALUES ('PN', 'Pitcairn') INSERT INTO [meetings].[Countries] VALUES ('PR', 'Puerto Rico') INSERT INTO [meetings].[Countries] VALUES ('PS', 'Palestinian Territory, Occupied') INSERT INTO [meetings].[Countries] VALUES ('PT', 'Portugal') INSERT INTO [meetings].[Countries] VALUES ('PW', 'Palau') INSERT INTO [meetings].[Countries] VALUES ('PY', 'Paraguay') INSERT INTO [meetings].[Countries] VALUES ('QA', 'Qatar') INSERT INTO [meetings].[Countries] VALUES ('RE', 'Reunion') INSERT INTO [meetings].[Countries] VALUES ('RO', 'Romania') INSERT INTO [meetings].[Countries] VALUES ('RS', 'Serbia') INSERT INTO [meetings].[Countries] VALUES ('RU', 'Russian Federation') INSERT INTO [meetings].[Countries] VALUES ('RW', 'RWANDA') INSERT INTO [meetings].[Countries] VALUES ('SA', 'Saudi Arabia') INSERT INTO [meetings].[Countries] VALUES ('SB', 'Solomon Islands') INSERT INTO [meetings].[Countries] VALUES ('SC', 'Seychelles') INSERT INTO [meetings].[Countries] VALUES ('SD', 'Sudan') INSERT INTO [meetings].[Countries] VALUES ('SE', 'Sweden') INSERT INTO [meetings].[Countries] VALUES ('SG', 'Singapore') INSERT INTO [meetings].[Countries] VALUES ('SH', 'Saint Helena') INSERT INTO [meetings].[Countries] VALUES ('SI', 'Slovenia') INSERT INTO [meetings].[Countries] VALUES ('SJ', 'Svalbard and Jan Mayen') INSERT INTO [meetings].[Countries] VALUES ('SK', 'Slovakia') INSERT INTO [meetings].[Countries] VALUES ('SL', 'Sierra Leone') INSERT INTO [meetings].[Countries] VALUES ('SM', 'San Marino') INSERT INTO [meetings].[Countries] VALUES ('SN', 'Senegal') INSERT INTO [meetings].[Countries] VALUES ('SO', 'Somalia') INSERT INTO [meetings].[Countries] VALUES ('SR', 'Suriname') INSERT INTO [meetings].[Countries] VALUES ('ST', 'Sao Tome and Principe') INSERT INTO [meetings].[Countries] VALUES ('SV', 'El Salvador') INSERT INTO [meetings].[Countries] VALUES ('SY', 'Syrian Arab Republic') INSERT INTO [meetings].[Countries] VALUES ('SZ', 'Swaziland') INSERT INTO [meetings].[Countries] VALUES ('TC', 'Turks and Caicos Islands') INSERT INTO [meetings].[Countries] VALUES ('TD', 'Chad') INSERT INTO [meetings].[Countries] VALUES ('TF', 'French Southern Territories') INSERT INTO [meetings].[Countries] VALUES ('TG', 'Togo') INSERT INTO [meetings].[Countries] VALUES ('TH', 'Thailand') INSERT INTO [meetings].[Countries] VALUES ('TJ', 'Tajikistan') INSERT INTO [meetings].[Countries] VALUES ('TK', 'Tokelau') INSERT INTO [meetings].[Countries] VALUES ('TL', 'Timor-Leste') INSERT INTO [meetings].[Countries] VALUES ('TM', 'Turkmenistan') INSERT INTO [meetings].[Countries] VALUES ('TN', 'Tunisia') INSERT INTO [meetings].[Countries] VALUES ('TO', 'Tonga') INSERT INTO [meetings].[Countries] VALUES ('TR', 'Turkey') INSERT INTO [meetings].[Countries] VALUES ('TT', 'Trinidad and Tobago') INSERT INTO [meetings].[Countries] VALUES ('TV', 'Tuvalu') INSERT INTO [meetings].[Countries] VALUES ('TW', 'Taiwan, Province of China') INSERT INTO [meetings].[Countries] VALUES ('TZ', 'Tanzania, United Republic of') INSERT INTO [meetings].[Countries] VALUES ('UA', 'Ukraine') INSERT INTO [meetings].[Countries] VALUES ('UG', 'Uganda') INSERT INTO [meetings].[Countries] VALUES ('UM', 'United States Minor Outlying Islands') INSERT INTO [meetings].[Countries] VALUES ('US', 'United States') INSERT INTO [meetings].[Countries] VALUES ('UY', 'Uruguay') INSERT INTO [meetings].[Countries] VALUES ('UZ', 'Uzbekistan') INSERT INTO [meetings].[Countries] VALUES ('VA', 'Holy See (Vatican City State)') INSERT INTO [meetings].[Countries] VALUES ('VC', 'Saint Vincent and the Grenadines') INSERT INTO [meetings].[Countries] VALUES ('VE', 'Venezuela') INSERT INTO [meetings].[Countries] VALUES ('VG', 'Virgin Islands, British') INSERT INTO [meetings].[Countries] VALUES ('VI', 'Virgin Islands, U.S.') INSERT INTO [meetings].[Countries] VALUES ('VN', 'Viet Nam') INSERT INTO [meetings].[Countries] VALUES ('VU', 'Vanuatu') INSERT INTO [meetings].[Countries] VALUES ('WF', 'Wallis and Futuna') INSERT INTO [meetings].[Countries] VALUES ('WS', 'Samoa') INSERT INTO [meetings].[Countries] VALUES ('YE', 'Yemen') INSERT INTO [meetings].[Countries] VALUES ('YT', 'Mayotte') INSERT INTO [meetings].[Countries] VALUES ('ZA', 'South Africa') INSERT INTO [meetings].[Countries] VALUES ('ZM', 'Zambia') INSERT INTO [meetings].[Countries] VALUES ('ZW', 'Zimbabwe') ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/Security/Schemas.sql ================================================ CREATE SCHEMA app AUTHORIZATION dbo GO CREATE SCHEMA administration AUTHORIZATION dbo GO CREATE SCHEMA meetings AUTHORIZATION dbo GO CREATE SCHEMA users AUTHORIZATION dbo GO CREATE SCHEMA registrations AUTHORIZATION dbo GO CREATE SCHEMA payments AUTHORIZATION dbo GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/administration/Tables/InboxMessages.sql ================================================ CREATE TABLE [administration].InboxMessages ( [Id] UNIQUEIDENTIFIER NOT NULL, [OccurredOn] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, CONSTRAINT [PK_administration_InboxMessages_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/administration/Tables/InternalCommands.sql ================================================ CREATE TABLE [administration].InternalCommands ( [Id] UNIQUEIDENTIFIER NOT NULL, [EnqueueDate] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, [Error] NVARCHAR(MAX) NULL, CONSTRAINT [PK_administration_InternalCommands_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/administration/Tables/MeetingGroupProposals.sql ================================================ CREATE TABLE [administration].[MeetingGroupProposals] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR(255) NOT NULL, [Description] VARCHAR(200) NULL, [LocationCity] NVARCHAR(50) NOT NULL, [LocationCountryCode] NVARCHAR(3) NOT NULL, [ProposalUserId] UNIQUEIDENTIFIER NOT NULL, [ProposalDate] DATETIME NOT NULL, [StatusCode] NVARCHAR(50) NOT NULL, [DecisionDate] DATETIME NULL, [DecisionUserId] UNIQUEIDENTIFIER NULL, [DecisionCode] NVARCHAR(50) NULL, [DecisionRejectReason] NVARCHAR(250) NULL, CONSTRAINT [PK_administration_MeetingGroupProposals_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/administration/Tables/Members.sql ================================================ CREATE TABLE [administration].[Members] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR(100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR(50) NOT NULL, [LastName] NVARCHAR(50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_administration_Members_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/administration/Tables/OutboxMessages.sql ================================================ CREATE TABLE [administration].OutboxMessages ( [Id] UNIQUEIDENTIFIER NOT NULL, [OccurredOn] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, CONSTRAINT [PK_administration_OutboxMessages_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/administration/Views/v_MeetingGroupProposals.sql ================================================ CREATE VIEW [administration].[v_MeetingGroupProposals] AS SELECT [MeetingGroupProposal].[Id], [MeetingGroupProposal].[Name], [MeetingGroupProposal].[Description], [MeetingGroupProposal].[LocationCity], [MeetingGroupProposal].[LocationCountryCode], [MeetingGroupProposal].[ProposalUserId], [MeetingGroupProposal].[ProposalDate], [MeetingGroupProposal].[StatusCode], [MeetingGroupProposal].[DecisionDate], [MeetingGroupProposal].[DecisionUserId], [MeetingGroupProposal].[DecisionCode], [MeetingGroupProposal].[DecisionRejectReason] FROM [administration].[MeetingGroupProposals] AS [MeetingGroupProposal] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/administration/Views/v_Members.sql ================================================ CREATE VIEW [administration].[v_Members] AS SELECT [Member].[Id], [Member].[Login], [Member].[Email], [Member].[FirstName], [Member].[LastName], [Member].[Name] FROM [administration].[Members] AS [Member] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/app/Tables/Emails.sql ================================================ CREATE TABLE [app].[Emails] ( [Id] UNIQUEIDENTIFIER NOT NULL, [From] NVARCHAR(255) NOT NULL, [To] NVARCHAR(255) NOT NULL, [Subject] NVARCHAR(255) NOT NULL, [Content] NVARCHAR(MAX) NOT NULL, [Date] DATETIME NOT NULL, CONSTRAINT [PK_app_Emails_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ) ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/app/Tables/MigrationsJournal.sql ================================================ CREATE TABLE [app].[MigrationsJournal]( [Id] [int] IDENTITY(1, 1) NOT NULL, [ScriptName] NVARCHAR(255) NOT NULL, [Applied] DATETIME NOT NULL, CONSTRAINT [PK_app_MigrationsJournal_Id] PRIMARY KEY CLUSTERED ( [Id] ASC )) ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/Countries.sql ================================================ CREATE TABLE [meetings].[Countries] ( [Code] CHAR(2) NOT NULL, [Name] NVARCHAR(50) NOT NULL CONSTRAINT [PK_meetings_Countries_Code] PRIMARY KEY ([Code] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/InboxMessages.sql ================================================ CREATE TABLE [meetings].InboxMessages ( [Id] UNIQUEIDENTIFIER NOT NULL, [OccurredOn] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, CONSTRAINT [PK_meetings_InboxMessages_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/InternalCommands.sql ================================================ CREATE TABLE [meetings].InternalCommands ( [Id] UNIQUEIDENTIFIER NOT NULL, [EnqueueDate] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, [Error] NVARCHAR(MAX) NULL, CONSTRAINT [PK_meetings_InternalCommands_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/MeetingAttendees.sql ================================================ CREATE TABLE meetings.MeetingAttendees ( [MeetingId] UNIQUEIDENTIFIER NOT NULL, [AttendeeId] UNIQUEIDENTIFIER NOT NULL, [DecisionDate] DATETIME2 NOT NULL, [RoleCode] VARCHAR(50) NULL, [GuestsNumber] INT NULL, [DecisionChanged] BIT NOT NULL, [DecisionChangeDate] DATETIME2 NULL, [IsRemoved] BIT NOT NULL, [RemovingMemberId] UNIQUEIDENTIFIER NULL, [RemovingReason] NVARCHAR(500) NULL, [RemovedDate] DATETIME2 NULL, [BecameNotAttendeeDate] DATETIME2 NULL, [FeeValue] DECIMAL(5, 0) NULL, [FeeCurrency] VARCHAR(3) NULL, [IsFeePaid] BIT NOT NULL, CONSTRAINT [PK_meetings_MeetingAttendees_Id] PRIMARY KEY ([MeetingId] ASC, [AttendeeId] ASC, [DecisionDate] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/MeetingCommentingConfigurations.sql ================================================ CREATE TABLE meetings.MeetingCommentingConfigurations ( [Id] UNIQUEIDENTIFIER NOT NULL, [MeetingId] UNIQUEIDENTIFIER NOT NULL, [IsCommentingEnabled] BIT NOT NULL, CONSTRAINT [PK_meetings_MeetingCommentingConfigurations_Id] PRIMARY KEY ([Id] ASC), CONSTRAINT [FK_meetings_MeetingCommentingConfigurations_Meetings] FOREIGN KEY ([MeetingId]) REFERENCES meetings.Meetings ([Id]) ); GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/MeetingComments.sql ================================================ CREATE TABLE meetings.MeetingComments ( [Id] UNIQUEIDENTIFIER NOT NULL, [MeetingId] UNIQUEIDENTIFIER NOT NULL, [AuthorId] UNIQUEIDENTIFIER NOT NULL, [InReplyToCommentId] UNIQUEIDENTIFIER NULL, [Comment] VARCHAR(300) NULL, [IsRemoved] BIT NOT NULL, [RemovedByReason] VARCHAR(300) NULL, [CreateDate] DATETIME NOT NULL, [EditDate] DATETIME NULL, [LikesCount] INT NOT NULL, CONSTRAINT [PK_meetings_MeetingComments_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/MeetingGroupMembers.sql ================================================ CREATE TABLE meetings.MeetingGroupMembers ( [MeetingGroupId] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [JoinedDate] DATETIME2 NOT NULL, [RoleCode] VARCHAR(50) NOT NULL, [IsActive] BIT NOT NULL, [LeaveDate] DATETIME NULL, CONSTRAINT [PK_meetings_MeetingGroupMembers_MeetingGroupId_MemberId_JoinedDate] PRIMARY KEY ([MeetingGroupId] ASC, [MemberId] ASC, [JoinedDate] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/MeetingGroupProposals.sql ================================================ CREATE TABLE [meetings].[MeetingGroupProposals] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR(255) NOT NULL, [Description] VARCHAR(200) NULL, [LocationCity] NVARCHAR(50) NOT NULL, [LocationCountryCode] NVARCHAR(3) NOT NULL, [ProposalUserId] UNIQUEIDENTIFIER NOT NULL, [ProposalDate] DATETIME NOT NULL, [StatusCode] NVARCHAR(50) NOT NULL CONSTRAINT [PK_meetings_MeetingGroupProposals_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/MeetingGroups.sql ================================================ CREATE TABLE meetings.MeetingGroups ( [Id] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR(255) NOT NULL, [Description] VARCHAR(200) NULL, [LocationCity] NVARCHAR(50) NOT NULL, [LocationCountryCode] NVARCHAR(3) NOT NULL, [CreatorId] UNIQUEIDENTIFIER NOT NULL, [CreateDate] DATETIME NOT NULL, [PaymentDateTo] DATE NULL, CONSTRAINT [PK_meetings_MeetingGroups_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/MeetingMemberCommentLikes.sql ================================================ CREATE TABLE [meetings].[MeetingMemberCommentLikes] ( [Id] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [MeetingCommentId] UNIQUEIDENTIFIER NOT NULL, CONSTRAINT [PK_meetings_MeetingMemberCommentLikes_Id] PRIMARY KEY ([Id] ASC), CONSTRAINT [FK_meetings_MeetingMemberCommentLikes_Members] FOREIGN KEY ([MemberId]) REFERENCES meetings.Members ([Id]), CONSTRAINT [FK_meetings_MeetingMemberCommentLikes_MeetingComments] FOREIGN KEY ([MeetingCommentId]) REFERENCES meetings.MeetingComments ([Id]) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/MeetingNotAttendees.sql ================================================ CREATE TABLE meetings.MeetingNotAttendees ( [MeetingId] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [DecisionDate] DATETIME2 NOT NULL, [DecisionChanged] BIT NOT NULL, [DecisionChangeDate] DATETIME2 NULL, CONSTRAINT [PK_meetings_MeetingNotAttendees_Id] PRIMARY KEY ([MeetingId] ASC, [MemberId] ASC, [DecisionDate] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/MeetingWaitlistMembers.sql ================================================ CREATE TABLE meetings.MeetingWaitlistMembers ( [MeetingId] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [SignUpDate] DATETIME2 NOT NULL, [IsSignedOff] BIT NOT NULL, [SignOffDate] DATETIME2 NULL, [IsMovedToAttendees] BIT NOT NULL, [MovedToAttendeesDate] DATETIME2 NULL, CONSTRAINT [PK_meetings_MeetingWaitlistMembers_MeetingId_MemberId_SignUpDate] PRIMARY KEY ([MeetingId] ASC, [MemberId] ASC, [SignUpDate] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/Meetings.sql ================================================ CREATE TABLE meetings.Meetings ( [Id] UNIQUEIDENTIFIER NOT NULL, [MeetingGroupId] UNIQUEIDENTIFIER NOT NULL, [CreatorId] UNIQUEIDENTIFIER NOT NULL, [CreateDate] DATETIME2 NOT NULL, [Title] NVARCHAR(200) NOT NULL, [Description] NVARCHAR(4000) NOT NULL, [TermStartDate] DATETIME NOT NULL, [TermEndDate] DATETIME NOT NULL, [LocationName] NVARCHAR(200) NOT NULL, [LocationAddress] NVARCHAR(200) NOT NULL, [LocationPostalCode] NVARCHAR(200) NULL, [LocationCity] NVARCHAR(50) NOT NULL, [AttendeesLimit] INT NULL, [GuestsLimit] INT NOT NULL, [RSVPTermStartDate] DATETIME NULL, [RSVPTermEndDate] DATETIME NULL, [EventFeeValue] DECIMAL(5, 0) NULL, [EventFeeCurrency] VARCHAR(3) NULL, [ChangeDate] DATETIME2 NULL, [ChangeMemberId] UNIQUEIDENTIFIER NULL, [CancelDate] DATETIME NULL, [CancelMemberId] UNIQUEIDENTIFIER NULL, [IsCanceled] BIT NOT NULL, CONSTRAINT [PK_meetings_Meetings_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/MemberSubscriptions.sql ================================================ CREATE TABLE [meetings].[MemberSubscriptions] ( [Id] UNIQUEIDENTIFIER NOT NULL, [ExpirationDate] DATETIME NOT NULL, CONSTRAINT [PK_meetings_MemberSubscriptions_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/Members.sql ================================================ CREATE TABLE [meetings].[Members] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR(100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR(50) NOT NULL, [LastName] NVARCHAR(50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_meetings_Members_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Tables/OutboxMessages.sql ================================================ CREATE TABLE [meetings].OutboxMessages ( [Id] UNIQUEIDENTIFIER NOT NULL, [OccurredOn] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, CONSTRAINT [PK_meetings_OutboxMessages_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Views/v_Countries.sql ================================================ CREATE VIEW [meetings].[v_Countriess] AS SELECT [Country].[Code], [Country].[Name] FROM meetings.Countries AS [Country] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Views/v_MeetingAttendees.sql ================================================ CREATE VIEW [meetings].[v_MeetingAttendees] AS SELECT [MeetingAttendee].[MeetingId], [MeetingAttendee].[AttendeeId], [MeetingAttendee].[DecisionDate], [MeetingAttendee].[RoleCode], [MeetingAttendee].[GuestsNumber], [Member].[FirstName], [Member].[LastName] FROM [meetings].[MeetingAttendees] AS [MeetingAttendee] INNER JOIN [meetings].[Members] AS [Member] ON [MeetingAttendee].[AttendeeId] = [Member].[Id] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Views/v_MeetingComments.sql ================================================ CREATE VIEW [meetings].[v_MeetingComments] AS SELECT [MeetingComments].Id, [MeetingComments].MeetingId, [MeetingComments].AuthorId, [MeetingComments].InReplyToCommentId, [MeetingComments].Comment, [MeetingComments].CreateDate, [MeetingComments].EditDate FROM [meetings].[MeetingComments] AS [MeetingComments] WHERE [MeetingComments].IsRemoved = 0 GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Views/v_MeetingDetails.sql ================================================ CREATE VIEW [meetings].[v_MeetingDetails] AS SELECT [Meeting].[Id], [Meeting].[MeetingGroupId], [Meeting].[Title], [Meeting].[TermStartDate], [Meeting].[TermEndDate], [Meeting].[Description], [Meeting].[LocationName], [Meeting].[LocationAddress], [Meeting].[LocationPostalCode], [Meeting].[LocationCity], [Meeting].[AttendeesLimit], [Meeting].[GuestsLimit], [Meeting].[RSVPTermStartDate], [Meeting].[RSVPTermEndDate], [Meeting].[EventFeeValue], [Meeting].[EventFeeCurrency] FROM [meetings].[Meetings] AS [Meeting] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Views/v_MeetingGroupMembers.sql ================================================ CREATE VIEW [meetings].[v_MeetingGroupMembers] AS SELECT [MeetingGroupMember].MeetingGroupId, [MeetingGroupMember].MemberId, [MeetingGroupMember].RoleCode FROM meetings.MeetingGroupMembers AS [MeetingGroupMember] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Views/v_MeetingGroupProposals.sql ================================================ CREATE VIEW [meetings].[v_MeetingGroupProposals] AS SELECT [MeetingGroupProposal].[Id], [MeetingGroupProposal].[Name], [MeetingGroupProposal].[Description], [MeetingGroupProposal].[LocationCity], [MeetingGroupProposal].[LocationCountryCode], [MeetingGroupProposal].[ProposalUserId], [MeetingGroupProposal].[ProposalDate], [MeetingGroupProposal].[StatusCode] FROM [meetings].[MeetingGroupProposals] AS [MeetingGroupProposal] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Views/v_MeetingGroups.sql ================================================  CREATE VIEW [meetings].[v_MeetingGroups] AS SELECT [MeetingGroup].Id, [MeetingGroup].[Name], [MeetingGroup].[Description], [MeetingGroup].[LocationCountryCode], [MeetingGroup].[LocationCity] FROM meetings.MeetingGroups AS [MeetingGroup] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Views/v_Meetings.sql ================================================  CREATE VIEW [meetings].[v_Meetings] AS SELECT Meeting.[Id], Meeting.[Title], Meeting.[Description], Meeting.LocationAddress, Meeting.LocationCity, Meeting.LocationPostalCode, Meeting.TermStartDate, Meeting.TermEndDate FROM meetings.Meetings AS [Meeting] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Views/v_MemberMeetingGroups.sql ================================================ CREATE VIEW [meetings].[v_MemberMeetingGroups] AS SELECT [MeetingGroup].Id, [MeetingGroup].[Name], [MeetingGroup].[Description], [MeetingGroup].[LocationCountryCode], [MeetingGroup].[LocationCity], [MeetingGroupMember].[MemberId], [MeetingGroupMember].[RoleCode], [MeetingGroupMember].[IsActive], [MeetingGroupMember].[JoinedDate] FROM meetings.MeetingGroups AS [MeetingGroup] INNER JOIN [meetings].[MeetingGroupMembers] AS [MeetingGroupMember] ON [MeetingGroup].[Id] = [MeetingGroupMember].[MeetingGroupId] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Views/v_MemberMeetings.sql ================================================ CREATE VIEW [meetings].[v_MemberMeetings] AS SELECT [Meeting].[Id], [Meeting].[Title], [Meeting].[Description], [Meeting].[LocationAddress], [Meeting].[LocationCity], [Meeting].[LocationPostalCode], [Meeting].[TermStartDate], [Meeting].[TermEndDate], [MeetingAttendee].[AttendeeId], [MeetingAttendee].[IsRemoved], [MeetingAttendee].[RoleCode] FROM [meetings].[Meetings] AS [Meeting] INNER JOIN [meetings].[MeetingAttendees] AS [MeetingAttendee] ON [Meeting].[Id] = [MeetingAttendee].[MeetingId] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/meetings/Views/v_Members.sql ================================================ CREATE VIEW [meetings].[v_Members] AS SELECT [Member].Id, [Member].[Name], [Member].[Login], [Member].[Email] FROM meetings.Members AS [Member] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/payments/Tables/InboxMessages.sql ================================================ CREATE TABLE [payments].InboxMessages ( [Id] UNIQUEIDENTIFIER NOT NULL, [OccurredOn] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, CONSTRAINT [PK_payments_InboxMessages_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/payments/Tables/InternalCommands.sql ================================================ CREATE TABLE [payments].InternalCommands ( [Id] UNIQUEIDENTIFIER NOT NULL, [EnqueueDate] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, [Error] NVARCHAR(MAX) NULL, CONSTRAINT [PK_payments_InternalCommands_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/payments/Tables/MeetingFees.sql ================================================ CREATE TABLE [payments].[MeetingFees] ( [MeetingFeeId] UNIQUEIDENTIFIER NOT NULL, [PayerId] UNIQUEIDENTIFIER NOT NULL, [MeetingId] UNIQUEIDENTIFIER NOT NULL, [FeeValue] DECIMAL(18, 2) NOT NULL, [FeeCurrency] VARCHAR(50) NOT NULL, [Status] VARCHAR(50) NOT NULL, CONSTRAINT [PK_payments_MeetingFees_MeetingFeeId] PRIMARY KEY ([MeetingFeeId] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/payments/Tables/Messages.sql ================================================ CREATE TABLE payments.[Messages] ( StreamIdInternal INT NOT NULL, StreamVersion INT NOT NULL, Position BIGINT IDENTITY(0,1) NOT NULL, Id UNIQUEIDENTIFIER NOT NULL, Created DATETIME NOT NULL, [Type] NVARCHAR(128) NOT NULL, JsonData NVARCHAR(max) NOT NULL, JsonMetadata NVARCHAR(max) , CONSTRAINT PK_Events PRIMARY KEY NONCLUSTERED (Position), CONSTRAINT FK_Events_Streams FOREIGN KEY (StreamIdInternal) REFERENCES payments.Streams(IdInternal) ); GO CREATE UNIQUE NONCLUSTERED INDEX IX_Messages_Position ON payments.Messages (Position); GO CREATE UNIQUE NONCLUSTERED INDEX IX_Messages_StreamIdInternal_Id ON payments.Messages (StreamIdInternal, Id); GO CREATE UNIQUE NONCLUSTERED INDEX IX_Messages_StreamIdInternal_Revision ON payments.Messages (StreamIdInternal, StreamVersion); GO CREATE NONCLUSTERED INDEX IX_Messages_StreamIdInternal_Created ON payments.Messages (StreamIdInternal, Created); GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/payments/Tables/OutboxMessages.sql ================================================ CREATE TABLE [payments].OutboxMessages ( [Id] UNIQUEIDENTIFIER NOT NULL, [OccurredOn] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, CONSTRAINT [PK_payments_OutboxMessages_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/payments/Tables/Payers.sql ================================================ CREATE TABLE [payments].[Payers] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR(100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR(50) NOT NULL, [LastName] NVARCHAR(50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_payments_Payers_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/payments/Tables/PriceListItems.sql ================================================ CREATE TABLE payments.PriceListItems ( [Id] UNIQUEIDENTIFIER NOT NULL, [SubscriptionPeriodCode] VARCHAR(50) NOT NULL, [CategoryCode] VARCHAR(50) NOT NULL, [CountryCode] VARCHAR(50) NOT NULL, [MoneyValue] DECIMAL(18, 2) NOT NULL, [MoneyCurrency] VARCHAR(50) NOT NULL, [IsActive] BIT NOT NULL ) ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/payments/Tables/Streams.sql ================================================ CREATE TABLE payments.Streams ( Id CHAR(42) NOT NULL, IdOriginal NVARCHAR(1000) NOT NULL, IdInternal INT IDENTITY(1,1) NOT NULL, [Version] INT CONSTRAINT DF_payments_Streams_Version DEFAULT(-1) NOT NULL, Position BIGINT CONSTRAINT DF_payments_Streams_Position DEFAULT(-1) NOT NULL, CONSTRAINT PK_Streams PRIMARY KEY CLUSTERED (IdInternal) ); GO CREATE UNIQUE NONCLUSTERED INDEX IX_Streams_Id ON payments.Streams (Id); GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/payments/Tables/SubscriptionCheckpoints.sql ================================================ CREATE TABLE payments.SubscriptionCheckpoints ( [Code] VARCHAR(50) NOT NULL, [Position] BIGINT NOT NULL ) ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/payments/Tables/SubscriptionDetails.sql ================================================ CREATE TABLE payments.SubscriptionDetails ( [Id] UNIQUEIDENTIFIER NOT NULL, [PayerId] UNIQUEIDENTIFIER NOT NULL, [Period] VARCHAR(50) NOT NULL, [Status] VARCHAR(50) NOT NULL, [CountryCode] VARCHAR(50) NOT NULL, [ExpirationDate] DATETIME NOT NULL, CONSTRAINT [PK_payments_SubscriptionDetails_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ) ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/payments/Tables/SubscriptionPayments.sql ================================================ CREATE TABLE payments.SubscriptionPayments ( [PaymentId] UNIQUEIDENTIFIER NOT NULL, [PayerId] UNIQUEIDENTIFIER NOT NULL, [Type] VARCHAR(50) NOT NULL, [Status] VARCHAR(50) NOT NULL, [Period] VARCHAR(50) NOT NULL, [Date] DATETIME NOT NULL, [SubscriptionId] UNIQUEIDENTIFIER NULL, [MoneyValue] DECIMAL(18, 2) NOT NULL, [MoneyCurrency] VARCHAR(50) NOT NULL ) ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/payments/Types/NewStreamMessages.sql ================================================ CREATE TYPE payments.NewStreamMessages AS TABLE ( StreamVersion INT IDENTITY(0,1) NOT NULL, Id UNIQUEIDENTIFIER NOT NULL, Created DATETIME DEFAULT(GETUTCDATE()) NOT NULL, [Type] NVARCHAR(128) NOT NULL, JsonData NVARCHAR(max) NULL, JsonMetadata NVARCHAR(max) NULL ); GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Tables/InboxMessages.sql ================================================ CREATE TABLE [registrations].InboxMessages ( [Id] UNIQUEIDENTIFIER NOT NULL, [OccurredOn] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, CONSTRAINT [PK_registrations_InboxMessages_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Tables/InternalCommands.sql ================================================ CREATE TABLE [registrations].InternalCommands ( [Id] UNIQUEIDENTIFIER NOT NULL, [EnqueueDate] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, [Error] NVARCHAR(MAX) NULL, CONSTRAINT [PK_registrations_InternalCommands_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Tables/OutboxMessages.sql ================================================ CREATE TABLE [registrations].OutboxMessages ( [Id] UNIQUEIDENTIFIER NOT NULL, [OccurredOn] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, CONSTRAINT [PK_registrations_OutboxMessages_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Tables/UserRegistrations.sql ================================================ CREATE TABLE [registrations].[UserRegistrations] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR(100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [Password] NVARCHAR(255) NOT NULL, [FirstName] NVARCHAR(50) NOT NULL, [LastName] NVARCHAR(50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, [StatusCode] VARCHAR(50) NOT NULL, [RegisterDate] DATETIME NOT NULL, [ConfirmedDate] DATETIME NULL, CONSTRAINT [PK_registrations_UserRegistrations_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/registrations/Views/v_UserRegistrations.sql ================================================ CREATE VIEW [registrations].[v_UserRegistrations] AS SELECT [UserRegistration].[Id], [UserRegistration].[Login], [UserRegistration].[Email], [UserRegistration].[FirstName], [UserRegistration].[LastName], [UserRegistration].[Name], [UserRegistration].[StatusCode], [UserRegistration].[Password] FROM [registrations].[UserRegistrations] AS [UserRegistration] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/users/Tables/InboxMessages.sql ================================================ CREATE TABLE [users].InboxMessages ( [Id] UNIQUEIDENTIFIER NOT NULL, [OccurredOn] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, CONSTRAINT [PK_users_InboxMessages_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/users/Tables/InternalCommands.sql ================================================ CREATE TABLE [users].InternalCommands ( [Id] UNIQUEIDENTIFIER NOT NULL, [EnqueueDate] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, [Error] NVARCHAR(MAX) NULL, CONSTRAINT [PK_users_InternalCommands_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/users/Tables/OutboxMessages.sql ================================================ CREATE TABLE [users].OutboxMessages ( [Id] UNIQUEIDENTIFIER NOT NULL, [OccurredOn] DATETIME2 NOT NULL, [Type] VARCHAR(255) NOT NULL, [Data] VARCHAR(MAX) NOT NULL, [ProcessedDate] DATETIME2 NULL, CONSTRAINT [PK_users_OutboxMessages_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/users/Tables/Permissions.sql ================================================ CREATE TABLE [users].[Permissions] ( [Code] VARCHAR(50) NOT NULL, [Name] VARCHAR(100) NOT NULL, [Description] [varchar](255) NULL, CONSTRAINT [PK_users_Permissions_Code] PRIMARY KEY ([Code] ASC) ) ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/users/Tables/RolesToPermissions.sql ================================================ CREATE TABLE [users].[RolesToPermissions] ( [RoleCode] VARCHAR(50) NOT NULL, [PermissionCode] VARCHAR(50) NOT NULL, CONSTRAINT [PK_RolesToPermissions_RoleCode_PermissionCode] PRIMARY KEY (RoleCode ASC, PermissionCode ASC) ) ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/users/Tables/UserRoles.sql ================================================ CREATE TABLE [users].[UserRoles] ( [UserId] UNIQUEIDENTIFIER NOT NULL, [RoleCode] NVARCHAR(50) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/users/Tables/Users.sql ================================================ CREATE TABLE [users].[Users] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR(100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [Password] NVARCHAR(255) NOT NULL, [IsActive] BIT NOT NULL, [FirstName] NVARCHAR(50) NOT NULL, [LastName] NVARCHAR(50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_users_Users_Id] PRIMARY KEY ([Id] ASC) ) GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/users/Views/v_UserPermissions.sql ================================================ CREATE VIEW [users].[v_UserPermissions] AS SELECT DISTINCT [UserRole].UserId, [RolesToPermission].PermissionCode FROM [users].UserRoles AS [UserRole] INNER JOIN [users].RolesToPermissions AS [RolesToPermission] ON [UserRole].RoleCode = [RolesToPermission].RoleCode GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/users/Views/v_UserRoles.sql ================================================ CREATE VIEW [users].[v_UserRoles] AS SELECT [UserRole].[UserId], [UserRole].[RoleCode] FROM [users].[UserRoles] AS [UserRole] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database/Structure/users/Views/v_Users.sql ================================================ CREATE VIEW [users].[v_Users] AS SELECT [User].[Id], [User].[IsActive], [User].[Login], [User].[Password], [User].[Email], [User].[Name] FROM [users].[Users] AS [User] GO ================================================ FILE: src/Database/CompanyName.MyMeetings.Database.Build/CompanyName.MyMeetings.Database.Build.csproj ================================================ Sql130 false ================================================ FILE: src/Database/DatabaseMigrator/.dockerignore ================================================ **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/azds.yaml **/bin **/charts **/docker-compose* **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md ================================================ FILE: src/Database/DatabaseMigrator/DatabaseMigrator.csproj ================================================  Exe ================================================ FILE: src/Database/DatabaseMigrator/Program.cs ================================================ using DbUp; using DbUp.ScriptProviders; using Serilog; using Serilog.Formatting.Compact; namespace DatabaseMigrator { public class Program { public static int Main(string[] args) { var logsPath = "logs\\migration-logs"; ILogger logger = new LoggerConfiguration() .WriteTo.Console() .WriteTo.File(new CompactJsonFormatter(), logsPath) .CreateLogger(); logger.Information("Logger configured. Starting migration..."); if (args.Length != 2) { logger.Error("Invalid arguments. Execution: DatabaseMigrator [connectionString] [pathToScripts]."); logger.Information("Migration stopped"); return -1; } var connectionString = args[0]; var scriptsPath = args[1]; if (!Directory.Exists(scriptsPath)) { logger.Information($"Directory {scriptsPath} does not exist"); return -1; } var serilogUpgradeLog = new SerilogUpgradeLog(logger); var upgrader = DeployChanges.To .SqlDatabase(connectionString) .WithScriptsFromFileSystem(scriptsPath, new FileSystemScriptOptions { IncludeSubDirectories = true }) .LogTo(serilogUpgradeLog) .JournalToSqlTable("app", "MigrationsJournal") .Build(); var result = upgrader.PerformUpgrade(); if (!result.Successful) { logger.Information("Migration failed"); return -1; } logger.Information("Migration successful"); return 0; } } } ================================================ FILE: src/Database/DatabaseMigrator/SerilogUpgradeLog.cs ================================================ using DbUp.Engine.Output; using Serilog; namespace DatabaseMigrator { internal class SerilogUpgradeLog : IUpgradeLog { private readonly ILogger _logger; public SerilogUpgradeLog(ILogger logger) { _logger = logger; } public void WriteInformation(string format, params object[] args) { _logger.Information(format, args); } public void WriteError(string format, params object[] args) { _logger.Error(format, args); } public void WriteWarning(string format, params object[] args) { _logger.Warning(format, args); } } } ================================================ FILE: src/Database/Dockerfile ================================================ FROM mcr.microsoft.com/mssql/server:2022-latest COPY ./CompanyName.MyMeetings.Database/Scripts/CreateDatabase_Linux.sql /scripts/ ENV ACCEPT_EULA=Y ENV SA_PASSWORD=Test@12345 ENV MSSQL_PID=Express ENV MSSQL_TCP_PORT=1433 ADD entrypoint.sh / ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] ================================================ FILE: src/Database/Dockerfile_DatabaseMigrator ================================================ FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["./Database/DatabaseMigrator/DatabaseMigrator.csproj", "DatabaseMigrator.csproj"] COPY ["Directory.Packages.props", "Directory.Packages.props"] COPY ["Directory.Build.targets", "Directory.Build.targets"] COPY ["Directory.Build.props", "Directory.Build.props"] COPY ["stylecop.json", "stylecop.json"] COPY [".editorconfig", ".editorconfig"] RUN dotnet restore "DatabaseMigrator.csproj" COPY ./Database/ . WORKDIR "/src" RUN dotnet build "DatabaseMigrator.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "DatabaseMigrator.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ADD ./Database/entrypoint_DatabaseMigrator.sh / ADD ./Database/CompanyName.MyMeetings.Database/Scripts/Migrations /migrations/ # Copy wait-for-it.sh into our image COPY ./Database/wait-for-it.sh wait-for-it.sh # Make it executable, in Linux RUN chmod +x wait-for-it.sh #ENTRYPOINT ["/bin/bash", "/entrypoint_DatabaseMigrator.sh"] ================================================ FILE: src/Database/InitializeDatabase.sql ================================================  GO PRINT N'Creating [administration]...'; GO CREATE SCHEMA [administration] AUTHORIZATION [dbo]; GO PRINT N'Creating [meetings]...'; GO CREATE SCHEMA [meetings] AUTHORIZATION [dbo]; GO PRINT N'Creating [payments]...'; GO CREATE SCHEMA [payments] AUTHORIZATION [dbo]; GO PRINT N'Creating [users]...'; GO CREATE SCHEMA [users] AUTHORIZATION [dbo]; GO PRINT N'Creating [administration].[InternalCommands]...'; GO CREATE TABLE [administration].[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_administration_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [administration].[InboxMessages]...'; GO CREATE TABLE [administration].[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_administration_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [administration].[OutboxMessages]...'; GO CREATE TABLE [administration].[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_administration_OutboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [administration].[MeetingGroupProposals]...'; GO CREATE TABLE [administration].[MeetingGroupProposals] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR (255) NOT NULL, [Description] VARCHAR (200) NULL, [LocationCity] NVARCHAR (50) NOT NULL, [LocationCountryCode] NVARCHAR (3) NOT NULL, [ProposalUserId] UNIQUEIDENTIFIER NOT NULL, [ProposalDate] DATETIME NOT NULL, [StatusCode] NVARCHAR (50) NOT NULL, [DecisionDate] DATETIME NULL, [DecisionUserId] UNIQUEIDENTIFIER NULL, [DecisionCode] NVARCHAR (50) NULL, [DecisionRejectReason] NVARCHAR (250) NULL, CONSTRAINT [PK_administration_MeetingGroupProposals_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [administration].[Members]...'; GO CREATE TABLE [administration].[Members] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_administration_Members_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MeetingWaitlistMembers]...'; GO CREATE TABLE [meetings].[MeetingWaitlistMembers] ( [MeetingId] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [SignUpDate] DATETIME2 (7) NOT NULL, [IsSignedOff] BIT NOT NULL, [SignOffDate] DATETIME2 (7) NULL, [IsMovedToAttendees] BIT NOT NULL, [MovedToAttendeesDate] DATETIME2 (7) NULL, CONSTRAINT [PK_meetings_MeetingWaitlistMembers_MeetingId_MemberId_SignUpDate] PRIMARY KEY CLUSTERED ([MeetingId] ASC, [MemberId] ASC, [SignUpDate] ASC) ); GO PRINT N'Creating [meetings].[MeetingNotAttendees]...'; GO CREATE TABLE [meetings].[MeetingNotAttendees] ( [MeetingId] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [DecisionDate] DATETIME2 (7) NOT NULL, [DecisionChanged] BIT NOT NULL, [DecisionChangeDate] DATETIME2 (7) NULL, CONSTRAINT [PK_meetings_MeetingNotAttendees_Id] PRIMARY KEY CLUSTERED ([MeetingId] ASC, [MemberId] ASC, [DecisionDate] ASC) ); GO PRINT N'Creating [meetings].[MeetingAttendees]...'; GO CREATE TABLE meetings.MeetingAttendees ( [MeetingId] UNIQUEIDENTIFIER NOT NULL, [AttendeeId] UNIQUEIDENTIFIER NOT NULL, [DecisionDate] DATETIME2 NOT NULL, [RoleCode] VARCHAR(50) NULL, [GuestsNumber] INT NULL, [DecisionChanged] BIT NOT NULL, [DecisionChangeDate] DATETIME2 NULL, [IsRemoved] BIT NOT NULL, [RemovingMemberId] UNIQUEIDENTIFIER NULL, [RemovingReason] NVARCHAR(500) NULL, [RemovedDate] DATETIME2 NULL, [BecameNotAttendeeDate] DATETIME2 NULL, [FeeValue] DECIMAL(5, 0) NULL, [FeeCurrency] VARCHAR(3) NULL, [IsFeePaid] BIT NOT NULL, CONSTRAINT [PK_meetings_MeetingAttendees_Id] PRIMARY KEY ([MeetingId] ASC, [AttendeeId] ASC, [DecisionDate] ASC) ) GO GO PRINT N'Creating [meetings].[Meetings]...'; GO CREATE TABLE [meetings].[Meetings] ( [Id] UNIQUEIDENTIFIER NOT NULL, [MeetingGroupId] UNIQUEIDENTIFIER NOT NULL, [CreatorId] UNIQUEIDENTIFIER NOT NULL, [CreateDate] DATETIME2 (7) NOT NULL, [Title] NVARCHAR (200) NOT NULL, [Description] NVARCHAR (4000) NOT NULL, [TermStartDate] DATETIME NOT NULL, [TermEndDate] DATETIME NOT NULL, [LocationName] NVARCHAR (200) NOT NULL, [LocationAddress] NVARCHAR (200) NOT NULL, [LocationPostalCode] NVARCHAR (200) NULL, [LocationCity] NVARCHAR (50) NOT NULL, [AttendeesLimit] INT NULL, [GuestsLimit] INT NOT NULL, [RSVPTermStartDate] DATETIME NULL, [RSVPTermEndDate] DATETIME NULL, [EventFeeValue] DECIMAL (5) NULL, [EventFeeCurrency] VARCHAR (3) NULL, [ChangeDate] DATETIME2 (7) NULL, [ChangeMemberId] UNIQUEIDENTIFIER NULL, [CancelDate] DATETIME NULL, [CancelMemberId] UNIQUEIDENTIFIER NULL, [IsCanceled] BIT NOT NULL, CONSTRAINT [PK_meetings_Meetings_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MeetingGroupMembers]...'; GO CREATE TABLE [meetings].[MeetingGroupMembers] ( [MeetingGroupId] UNIQUEIDENTIFIER NOT NULL, [MemberId] UNIQUEIDENTIFIER NOT NULL, [JoinedDate] DATETIME2 (7) NOT NULL, [RoleCode] VARCHAR (50) NOT NULL, [IsActive] BIT NOT NULL, [LeaveDate] DATETIME NULL, CONSTRAINT [PK_meetings_MeetingGroupMembers_MeetingGroupId_MemberId_JoinedDate] PRIMARY KEY CLUSTERED ([MeetingGroupId] ASC, [MemberId] ASC, [JoinedDate] ASC) ); GO PRINT N'Creating [meetings].[MeetingGroups]...'; GO CREATE TABLE [meetings].[MeetingGroups] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR (255) NOT NULL, [Description] VARCHAR (200) NULL, [LocationCity] NVARCHAR (50) NOT NULL, [LocationCountryCode] NVARCHAR (3) NOT NULL, [CreatorId] UNIQUEIDENTIFIER NOT NULL, [CreateDate] DATETIME NOT NULL, [PaymentDateTo] DATE NULL, CONSTRAINT [PK_meetings_MeetingGroups_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[Members]...'; GO CREATE TABLE [meetings].[Members] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_meetings_Members_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[MeetingGroupProposals]...'; GO CREATE TABLE [meetings].[MeetingGroupProposals] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Name] NVARCHAR (255) NOT NULL, [Description] VARCHAR (200) NULL, [LocationCity] NVARCHAR (50) NOT NULL, [LocationCountryCode] NVARCHAR (3) NOT NULL, [ProposalUserId] UNIQUEIDENTIFIER NOT NULL, [ProposalDate] DATETIME NOT NULL, [StatusCode] NVARCHAR (50) NOT NULL, CONSTRAINT [PK_meetings_MeetingGroupProposals_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[OutboxMessages]...'; GO CREATE TABLE [meetings].[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_meetings_OutboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[InternalCommands]...'; GO CREATE TABLE [meetings].[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_meetings_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [meetings].[InboxMessages]...'; GO CREATE TABLE [meetings].[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_meetings_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[Payers]...'; GO CREATE TABLE [payments].[Payers] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_payments_Payers_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[OutboxMessages]...'; GO CREATE TABLE [payments].[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_payments_OutboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[InternalCommands]...'; GO CREATE TABLE [payments].[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_payments_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [payments].[InboxMessages]...'; GO CREATE TABLE [payments].[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_payments_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [users].[InboxMessages]...'; GO CREATE TABLE [users].[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_users_InboxMessages_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [users].[UserRoles]...'; GO CREATE TABLE [users].[UserRoles] ( [UserId] UNIQUEIDENTIFIER NOT NULL, [RoleCode] NVARCHAR (50) NULL ); GO PRINT N'Creating [users].[UserRegistrations]...'; GO CREATE TABLE [users].[UserRegistrations] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [Password] NVARCHAR (255) NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, [StatusCode] VARCHAR (50) NOT NULL, [RegisterDate] DATETIME NOT NULL, [ConfirmedDate] DATETIME NULL, CONSTRAINT [PK_users_UserRegistrations_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [users].[Users]...'; GO CREATE TABLE [users].[Users] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Login] NVARCHAR (100) NOT NULL, [Email] NVARCHAR (255) NOT NULL, [Password] NVARCHAR (255) NOT NULL, [IsActive] BIT NOT NULL, [FirstName] NVARCHAR (50) NOT NULL, [LastName] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (255) NOT NULL, CONSTRAINT [PK_users_Users_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [users].[RolesToPermissions]...'; GO CREATE TABLE [users].[RolesToPermissions] ( [RoleCode] VARCHAR (50) NOT NULL, [PermissionCode] VARCHAR (50) NOT NULL, CONSTRAINT [PK_RolesToPermissions_RoleCode_PermissionCode] PRIMARY KEY CLUSTERED ([RoleCode] ASC, [PermissionCode] ASC) ); GO PRINT N'Creating [users].[Permissions]...'; GO CREATE TABLE [users].[Permissions] ( [Code] VARCHAR (50) NOT NULL, [Name] VARCHAR (100) NOT NULL, [Description] VARCHAR (255) NULL, CONSTRAINT [PK_users_Permissions_Code] PRIMARY KEY CLUSTERED ([Code] ASC) ); GO PRINT N'Creating [users].[InternalCommands]...'; GO CREATE TABLE [users].[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_users_InternalCommands_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO PRINT N'Creating [users].[OutboxMessages]...'; GO CREATE TABLE [users].[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 [meetings].[v_MeetingGroups]...'; GO CREATE VIEW [meetings].[v_MeetingGroups] AS SELECT [MeetingGroup].Id, [MeetingGroup].[Name], [MeetingGroup].[Description], [MeetingGroup].[LocationCountryCode], [MeetingGroup].[LocationCity] FROM meetings.MeetingGroups AS [MeetingGroup] GO PRINT N'Creating [meetings].[v_Meetings]...'; GO CREATE VIEW [meetings].[v_Meetings] AS SELECT Meeting.[Id], Meeting.[Title], Meeting.[Description], Meeting.LocationAddress, Meeting.LocationCity, Meeting.LocationPostalCode, Meeting.TermStartDate, Meeting.TermEndDate FROM meetings.Meetings AS [Meeting] GO PRINT N'Creating [meetings].[v_Members]...'; GO CREATE VIEW [meetings].[v_Members] AS SELECT [Member].Id, [Member].[Name], [Member].[Login], [Member].[Email] FROM meetings.Members AS [Member] GO PRINT N'Creating [users].[v_UserRoles]...'; GO CREATE VIEW [users].[v_UserRoles] AS SELECT [UserRole].[UserId], [UserRole].[RoleCode] FROM [users].[UserRoles] AS [UserRole] GO PRINT N'Creating [users].[v_Users]...'; GO CREATE VIEW [users].[v_Users] AS SELECT [User].[Id], [User].[IsActive], [User].[Login], [User].[Password], [User].[Email], [User].[Name] FROM [users].[Users] AS [User] GO PRINT N'Creating [users].[v_UserPermissions]...'; GO CREATE VIEW [users].[v_UserPermissions] AS SELECT DISTINCT [UserRole].UserId, [RolesToPermission].PermissionCode FROM [users].UserRoles AS [UserRole] INNER JOIN [users].RolesToPermissions AS [RolesToPermission] ON [UserRole].RoleCode = [RolesToPermission].RoleCode GO PRINT N'Update complete.'; GO CREATE VIEW [users].[v_UserRegistrations] AS SELECT [UserRegistration].[Id], [UserRegistration].[Login], [UserRegistration].[Email], [UserRegistration].[FirstName], [UserRegistration].[LastName], [UserRegistration].[Name], [UserRegistration].[StatusCode] FROM [users].[UserRegistrations] AS [UserRegistration] GO CREATE VIEW [administration].[v_Members] AS SELECT [Member].[Id], [Member].[Login], [Member].[Email], [Member].[FirstName], [Member].[LastName], [Member].[Name] FROM [administration].[Members] AS [Member] GO CREATE VIEW [administration].[v_MeetingGroupProposals] AS SELECT [MeetingGroupProposal].[Id], [MeetingGroupProposal].[Name], [MeetingGroupProposal].[Description], [MeetingGroupProposal].[LocationCity], [MeetingGroupProposal].[LocationCountryCode], [MeetingGroupProposal].[ProposalUserId], [MeetingGroupProposal].[ProposalDate], [MeetingGroupProposal].[StatusCode], [MeetingGroupProposal].[DecisionDate], [MeetingGroupProposal].[DecisionUserId], [MeetingGroupProposal].[DecisionCode], [MeetingGroupProposal].[DecisionRejectReason] FROM [administration].[MeetingGroupProposals] AS [MeetingGroupProposal] GO CREATE VIEW [meetings].[v_MeetingGroupProposals] AS SELECT [MeetingGroupProposal].[Id], [MeetingGroupProposal].[Name], [MeetingGroupProposal].[Description], [MeetingGroupProposal].[LocationCity], [MeetingGroupProposal].[LocationCountryCode], [MeetingGroupProposal].[ProposalUserId], [MeetingGroupProposal].[ProposalDate], [MeetingGroupProposal].[StatusCode] FROM [meetings].[MeetingGroupProposals] AS [MeetingGroupProposal] GO -- Initialize some data /* SQL Server 2012+*/ DECLARE @DBName sysname; SET @DBName = (SELECT db_name()); DECLARE @SQL varchar(1000); SET @SQL = 'ALTER DATABASE ['+@DBName+'] SET ALLOW_SNAPSHOT_ISOLATION ON; ALTER DATABASE ['+@DBName+'] SET READ_COMMITTED_SNAPSHOT ON;'; exec(@sql) IF OBJECT_ID('payments.Streams', 'U') IS NULL BEGIN CREATE TABLE payments.Streams( Id CHAR(42) NOT NULL, IdOriginal NVARCHAR(1000) NOT NULL, IdInternal INT IDENTITY(1,1) NOT NULL, [Version] INT DEFAULT(-1) NOT NULL, Position BIGINT DEFAULT(-1) NOT NULL, CONSTRAINT PK_Streams PRIMARY KEY CLUSTERED (IdInternal) ); END IF NOT EXISTS( SELECT * FROM sys.indexes WHERE name='IX_Streams_Id' AND object_id = OBJECT_ID('payments.Streams', 'U')) BEGIN CREATE UNIQUE NONCLUSTERED INDEX IX_Streams_Id ON payments.Streams (Id); END IF object_id('payments.Messages', 'U') IS NULL BEGIN CREATE TABLE payments.Messages( StreamIdInternal INT NOT NULL, StreamVersion INT NOT NULL, Position BIGINT IDENTITY(0,1) NOT NULL, Id UNIQUEIDENTIFIER NOT NULL, Created DATETIME NOT NULL, [Type] NVARCHAR(128) NOT NULL, JsonData NVARCHAR(max) NOT NULL, JsonMetadata NVARCHAR(max) , CONSTRAINT PK_Events PRIMARY KEY NONCLUSTERED (Position), CONSTRAINT FK_Events_Streams FOREIGN KEY (StreamIdInternal) REFERENCES payments.Streams(IdInternal) ); END IF NOT EXISTS( SELECT * FROM sys.indexes WHERE name='IX_Messages_Position' AND object_id = OBJECT_ID('payments.Messages')) BEGIN CREATE UNIQUE NONCLUSTERED INDEX IX_Messages_Position ON payments.Messages (Position); END IF NOT EXISTS( SELECT * FROM sys.indexes WHERE name='IX_Messages_StreamIdInternal_Id' AND object_id = OBJECT_ID('payments.Messages')) BEGIN CREATE UNIQUE NONCLUSTERED INDEX IX_Messages_StreamIdInternal_Id ON payments.Messages (StreamIdInternal, Id); END IF NOT EXISTS( SELECT * FROM sys.indexes WHERE name='IX_Messages_StreamIdInternal_Revision' AND object_id = OBJECT_ID('payments.Messages')) BEGIN CREATE UNIQUE NONCLUSTERED INDEX IX_Messages_StreamIdInternal_Revision ON payments.Messages (StreamIdInternal, StreamVersion); END IF NOT EXISTS( SELECT * FROM sys.indexes WHERE name='IX_Messages_StreamIdInternal_Created' AND object_id = OBJECT_ID('payments.Messages')) BEGIN CREATE NONCLUSTERED INDEX IX_Messages_StreamIdInternal_Created ON payments.Messages (StreamIdInternal, Created); END IF NOT EXISTS( SELECT * FROM sys.table_types tt JOIN sys.schemas s ON tt.schema_id = s.schema_id WHERE s.name + '.' + tt.name='payments.NewStreamMessages') BEGIN CREATE TYPE payments.NewStreamMessages AS TABLE ( StreamVersion INT IDENTITY(0,1) NOT NULL, Id UNIQUEIDENTIFIER NOT NULL, Created DATETIME DEFAULT(GETUTCDATE()) NOT NULL, [Type] NVARCHAR(128) NOT NULL, JsonData NVARCHAR(max) NULL, JsonMetadata NVARCHAR(max) NULL ); END BEGIN IF NOT EXISTS (SELECT NULL FROM SYS.EXTENDED_PROPERTIES WHERE [major_id] = OBJECT_ID('payments.Streams') AND [name] = N'version' AND [minor_id] = 0) EXEC sys.sp_addextendedproperty @name = N'version', @value = N'2', @level0type = N'SCHEMA', @level0name = 'payments', @level1type = N'TABLE', @level1name = 'Streams'; END CREATE TABLE payments.SubscriptionDetails ( [Id] UNIQUEIDENTIFIER NOT NULL, [Period] VARCHAR(50) NOT NULL, [Status] VARCHAR(50) NOT NULL, [CountryCode] VARCHAR(50) NOT NULL, [ExpirationDate] DATETIME NOT NULL, CONSTRAINT [PK_payments_SubscriptionDetails_Id] PRIMARY KEY CLUSTERED ([Id] ASC) ) CREATE TABLE payments.SubscriptionCheckpoints ( [Code] VARCHAR(50) NOT NULL, [Position] BIGINT NOT NULL ) CREATE TABLE payments.PriceListItems ( [Id] UNIQUEIDENTIFIER NOT NULL, [SubscriptionPeriodCode] VARCHAR(50) NOT NULL, [CategoryCode] VARCHAR(50) NOT NULL, [CountryCode] VARCHAR(50) NOT NULL, [MoneyValue] DECIMAL(18, 2) NOT NULL, [MoneyCurrency] VARCHAR(50) NOT NULL, [IsActive] BIT NOT NULL ) CREATE TABLE payments.SubscriptionPayments ( [PaymentId] UNIQUEIDENTIFIER NOT NULL, [PayerId] UNIQUEIDENTIFIER NOT NULL, [Type] VARCHAR(50) NOT NULL, [Status] VARCHAR(50) NOT NULL, [Period] VARCHAR(50) NOT NULL, [Date] DATETIME NOT NULL, [SubscriptionId] UNIQUEIDENTIFIER NULL, [MoneyValue] DECIMAL(18, 2) NOT NULL, [MoneyCurrency] VARCHAR(50) NOT NULL ) CREATE TABLE [meetings].[MemberSubscriptions] ( [Id] UNIQUEIDENTIFIER NOT NULL, [ExpirationDate] DATETIME NOT NULL, CONSTRAINT [PK_meetings_MemberSubscriptions_Id] PRIMARY KEY ([Id] ASC) ) GO INSERT INTO payments.PriceListItems VALUES ('d58f0876-efe3-4b4c-b196-a4c3d5fadd24', 'Month', 'New', 'PL', 60, 'PLN', 1) INSERT INTO payments.PriceListItems VALUES ('d48e9951-2ae8-467e-a257-a1f492dbd36d', 'HalfYear', 'New', 'PL', 320, 'PLN', 1) INSERT INTO payments.PriceListItems VALUES ('b7bbe846-c151-48b5-85ef-a5737108640c', 'Month', 'New', 'US', 15, 'USD', 1) INSERT INTO payments.PriceListItems VALUES ('92666bf7-7e86-4784-9c69-e6f3b8bb0ea6', 'HalfYear', 'New', 'US', 80, 'USD', 1) GO INSERT INTO payments.PriceListItems VALUES ('d58f0876-efe3-4b4c-b196-a4c3d5fadd24', 'Month', 'Renewal', 'PL', 60, 'PLN', 1) INSERT INTO payments.PriceListItems VALUES ('d48e9951-2ae8-467e-a257-a1f492dbd36d', 'HalfYear', 'Renewal', 'PL', 320, 'PLN', 1) INSERT INTO payments.PriceListItems VALUES ('b7bbe846-c151-48b5-85ef-a5737108640c', 'Month', 'Renewal', 'US', 15, 'USD', 1) INSERT INTO payments.PriceListItems VALUES ('92666bf7-7e86-4784-9c69-e6f3b8bb0ea6', 'HalfYear', 'Renewal', 'US', 80, 'USD', 1) GO CREATE VIEW [meetings].[v_MeetingGroupMembers] AS SELECT [MeetingGroupMember].MeetingGroupId, [MeetingGroupMember].MemberId, [MeetingGroupMember].RoleCode FROM meetings.MeetingGroupMembers AS [MeetingGroupMember] GO CREATE TABLE [payments].[MeetingFees] ( [MeetingFeeId] UNIQUEIDENTIFIER NOT NULL, [PayerId] UNIQUEIDENTIFIER NOT NULL, [MeetingId] UNIQUEIDENTIFIER NOT NULL, [FeeValue] DECIMAL(18, 2) NOT NULL, [FeeCurrency] VARCHAR(50) NOT NULL, [Status] VARCHAR(50) NOT NULL, CONSTRAINT [PK_payments_MeetingFees_MeetingFeeId] PRIMARY KEY ([MeetingFeeId] ASC) ) GO ================================================ FILE: src/Database/entrypoint.sh ================================================ #!/bin/bash echo 'Starting sql server..' ; # Start SQL Server /opt/mssql/bin/sqlservr & echo 'SQL server started.'; sleep 30 ; echo 'Create database..' ; # Create database /opt/mssql-tools/bin/sqlcmd -d master -i /scripts/CreateDatabase_Linux.sql -U sa -P Test@12345 ; echo 'Database created' ; tail -f /dev/null ================================================ FILE: src/Database/entrypoint_DatabaseMigrator.sh ================================================ # Wait 30 seconds after SQL Server is running for database creation. sleep 30; echo $ASPNETCORE_MyMeetings_IntegrationTests_ConnectionString dotnet DatabaseMigrator.dll $ASPNETCORE_MyMeetings_IntegrationTests_ConnectionString "/migrations" ================================================ FILE: src/Database/wait-for-it.sh ================================================ #!/usr/bin/env bash # Use this script to test if a given TCP host/port are available WAITFORIT_cmdname=${0##*/} echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } usage() { cat << USAGE >&2 Usage: $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] -h HOST | --host=HOST Host or IP under test -p PORT | --port=PORT TCP port under test Alternatively, you specify the host and port as host:port -s | --strict Only execute subcommand if the test succeeds -q | --quiet Don't output any status messages -t TIMEOUT | --timeout=TIMEOUT Timeout in seconds, zero for no timeout -- COMMAND ARGS Execute command with args after the test finishes USAGE exit 1 } wait_for() { if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" else echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" fi WAITFORIT_start_ts=$(date +%s) while : do if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then nc -z $WAITFORIT_HOST $WAITFORIT_PORT WAITFORIT_result=$? else (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 WAITFORIT_result=$? fi if [[ $WAITFORIT_result -eq 0 ]]; then WAITFORIT_end_ts=$(date +%s) echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" break fi sleep 1 done return $WAITFORIT_result } wait_for_wrapper() { # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 if [[ $WAITFORIT_QUIET -eq 1 ]]; then timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & else timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & fi WAITFORIT_PID=$! trap "kill -INT -$WAITFORIT_PID" INT wait $WAITFORIT_PID WAITFORIT_RESULT=$? if [[ $WAITFORIT_RESULT -ne 0 ]]; then echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" fi return $WAITFORIT_RESULT } # process arguments while [[ $# -gt 0 ]] do case "$1" in *:* ) WAITFORIT_hostport=(${1//:/ }) WAITFORIT_HOST=${WAITFORIT_hostport[0]} WAITFORIT_PORT=${WAITFORIT_hostport[1]} shift 1 ;; --child) WAITFORIT_CHILD=1 shift 1 ;; -q | --quiet) WAITFORIT_QUIET=1 shift 1 ;; -s | --strict) WAITFORIT_STRICT=1 shift 1 ;; -h) WAITFORIT_HOST="$2" if [[ $WAITFORIT_HOST == "" ]]; then break; fi shift 2 ;; --host=*) WAITFORIT_HOST="${1#*=}" shift 1 ;; -p) WAITFORIT_PORT="$2" if [[ $WAITFORIT_PORT == "" ]]; then break; fi shift 2 ;; --port=*) WAITFORIT_PORT="${1#*=}" shift 1 ;; -t) WAITFORIT_TIMEOUT="$2" if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi shift 2 ;; --timeout=*) WAITFORIT_TIMEOUT="${1#*=}" shift 1 ;; --) shift WAITFORIT_CLI=("$@") break ;; --help) usage ;; *) echoerr "Unknown argument: $1" usage ;; esac done if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then echoerr "Error: you need to provide a host and port to test." usage fi WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} # Check to see if timeout is from busybox? WAITFORIT_TIMEOUT_PATH=$(type -p timeout) WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) WAITFORIT_BUSYTIMEFLAG="" if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then WAITFORIT_ISBUSY=1 # Check if busybox timeout uses -t flag # (recent Alpine versions don't support -t anymore) if timeout &>/dev/stdout | grep -q -e '-t '; then WAITFORIT_BUSYTIMEFLAG="-t" fi else WAITFORIT_ISBUSY=0 fi if [[ $WAITFORIT_CHILD -gt 0 ]]; then wait_for WAITFORIT_RESULT=$? exit $WAITFORIT_RESULT else if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then wait_for_wrapper WAITFORIT_RESULT=$? else wait_for WAITFORIT_RESULT=$? fi fi if [[ $WAITFORIT_CLI != "" ]]; then if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" exit $WAITFORIT_RESULT fi exec "${WAITFORIT_CLI[@]}" else exit $WAITFORIT_RESULT fi ================================================ FILE: src/Directory.Build.props ================================================  Kamil Grzybek $(Company) Copyright © $(Company) $([System.DateTime]::Now.Year) $(Company)™ $(Company) Projects net8.0 Debug;Release;Production 1591;1701;1702;8032;NU1701;AD0001;NU5128;NU1603 enable false true true PackageReference True True True True True true $([MSBuild]::GetPathOfFileAbove('stylecop.json', $(MSBuildProjectDirectory))) $([MSBuild]::GetPathOfFileAbove('.editorconfig', $(MSBuildProjectDirectory))) ================================================ FILE: src/Directory.Build.targets ================================================  False $(MSBuildThisFileDirectory)\BuildingBlocks\Domain\*.csproj $(MSBuildThisFileDirectory)\BuildingBlocks\Application\*.csproj $(MSBuildThisFileDirectory)\BuildingBlocks\Infrastructure\*.csproj $(MSBuildThisFileDirectory)\BuildingBlocks\Tests\IntegrationTests\*.csproj ================================================ FILE: src/Directory.Packages.props ================================================  ================================================ FILE: src/Dockerfile ================================================ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["API/CompanyName.MyMeetings.API/CompanyName.MyMeetings.API.csproj", "API/CompanyName.MyMeetings.API/"] COPY ["Modules/UserAccess/Application/CompanyName.MyMeetings.Modules.UserAccess.Application.csproj", "Modules/UserAccess/Application/"] COPY ["Modules/UserAccess/IntegrationEvents/CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents.csproj", "Modules/UserAccess/IntegrationEvents/"] COPY ["BuildingBlocks/Infrastructure/CompanyName.MyMeetings.BuildingBlocks.Infrastructure.csproj", "BuildingBlocks/Infrastructure/"] COPY ["BuildingBlocks/Domain/CompanyName.MyMeetings.BuildingBlocks.Domain.csproj", "BuildingBlocks/Domain/"] COPY ["BuildingBlocks/Application/CompanyName.MyMeetings.BuildingBlocks.Application.csproj", "BuildingBlocks/Application/"] COPY ["Modules/UserAccess/Domain/CompanyName.MyMeetings.Modules.UserAccess.Domain.csproj", "Modules/UserAccess/Domain/"] COPY ["Modules/Meetings/IntegrationEvents/CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents.csproj", "Modules/Meetings/IntegrationEvents/"] COPY ["Modules/Meetings/Application/CompanyName.MyMeetings.Modules.Meetings.Application.csproj", "Modules/Meetings/Application/"] COPY ["Modules/Meetings/Domain/CompanyName.MyMeetings.Modules.Meetings.Domain.csproj", "Modules/Meetings/Domain/"] COPY ["Modules/Administration/IntegrationEvents/CompanyName.MyMeetings.Modules.Administration.IntegrationEvents.csproj", "Modules/Administration/IntegrationEvents/"] COPY ["Modules/Payments/IntegrationEvents/CompanyName.MyMeetings.Modules.Payments.IntegrationEvents.csproj", "Modules/Payments/IntegrationEvents/"] COPY ["Modules/UserAccess/Infrastructure/CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.csproj", "Modules/UserAccess/Infrastructure/"] COPY ["Modules/Payments/Application/CompanyName.MyMeetings.Modules.Payments.Application.csproj", "Modules/Payments/Application/"] COPY ["Modules/Payments/Domain/CompanyName.MyMeetings.Modules.Payments.Domain.csproj", "Modules/Payments/Domain/"] COPY ["Modules/Meetings/Infrastructure/CompanyName.MyMeetings.Modules.Meetings.Infrastructure.csproj", "Modules/Meetings/Infrastructure/"] COPY ["Modules/Payments/Infrastructure/CompanyName.MyMeetings.Modules.Payments.Infrastructure.csproj", "Modules/Payments/Infrastructure/"] COPY ["Modules/Administration/Application/CompanyName.MyMeetings.Modules.Administration.Application.csproj", "Modules/Administration/Application/"] COPY ["Modules/Administration/Domain/CompanyName.MyMeetings.Modules.Administration.Domain.csproj", "Modules/Administration/Domain/"] COPY ["Modules/Administration/Infrastructure/CompanyName.MyMeetings.Modules.Administration.Infrastructure.csproj", "Modules/Administration/Infrastructure/"] COPY ["Directory.Packages.props", "Directory.Packages.props"] COPY ["Directory.Build.props", "Directory.Build.props"] COPY ["Directory.Build.targets", "Directory.Build.targets"] RUN dotnet restore "API/CompanyName.MyMeetings.API/CompanyName.MyMeetings.API.csproj" COPY . . WORKDIR "/src/API/CompanyName.MyMeetings.API" RUN dotnet build "CompanyName.MyMeetings.API.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "CompanyName.MyMeetings.API.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ADD entrypoint.sh / ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] ================================================ FILE: src/Modules/Administration/Application/CompanyName.MyMeetings.Modules.Administration.Application.csproj ================================================  ================================================ FILE: src/Modules/Administration/Application/Configuration/Commands/ICommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands { public interface ICommandHandler : IRequestHandler where TCommand : ICommand { } public interface ICommandHandler : IRequestHandler where TCommand : ICommand { } } ================================================ FILE: src/Modules/Administration/Application/Configuration/Commands/ICommandsScheduler.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands { public interface ICommandsScheduler { Task EnqueueAsync(ICommand command); Task EnqueueAsync(ICommand command); } } ================================================ FILE: src/Modules/Administration/Application/Configuration/Commands/InternalCommandBase.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Administration.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; } } } ================================================ FILE: src/Modules/Administration/Application/Configuration/Queries/IQueryHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Queries { public interface IQueryHandler : IRequestHandler where TQuery : IQuery { } } ================================================ FILE: src/Modules/Administration/Application/Contracts/CommandBase.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.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; } } } ================================================ FILE: src/Modules/Administration/Application/Contracts/IAdministrationModule.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.Application.Contracts { public interface IAdministrationModule { Task ExecuteCommandAsync(ICommand command); Task ExecuteCommandAsync(ICommand command); Task ExecuteQueryAsync(IQuery query); } } ================================================ FILE: src/Modules/Administration/Application/Contracts/ICommand.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.Modules.Administration.Application.Contracts { public interface ICommand : IRequest { Guid Id { get; } } public interface ICommand : IRequest { Guid Id { get; } } } ================================================ FILE: src/Modules/Administration/Application/Contracts/IQuery.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.Modules.Administration.Application.Contracts { public interface IQuery : IRequest { } } ================================================ FILE: src/Modules/Administration/Application/Contracts/IRecurringCommand.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.Application.Contracts { public interface IRecurringCommand { } } ================================================ FILE: src/Modules/Administration/Application/Contracts/QueryBase.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.Application.Contracts { public abstract class QueryBase : IQuery { public Guid Id { get; } protected QueryBase() { Id = Guid.NewGuid(); } protected QueryBase(Guid id) { Id = id; } } } ================================================ FILE: src/Modules/Administration/Application/MeetingGroupProposals/AcceptMeetingGroupProposal/AcceptMeetingGroupProposalCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.AcceptMeetingGroupProposal { public class AcceptMeetingGroupProposalCommand : CommandBase { public AcceptMeetingGroupProposalCommand(Guid meetingGroupProposalId) { MeetingGroupProposalId = meetingGroupProposalId; } internal Guid MeetingGroupProposalId { get; } } } ================================================ FILE: src/Modules/Administration/Application/MeetingGroupProposals/AcceptMeetingGroupProposal/AcceptMeetingGroupProposalCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Administration.Domain.Users; namespace CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.AcceptMeetingGroupProposal { internal class AcceptMeetingGroupProposalCommandHandler : ICommandHandler { private readonly IMeetingGroupProposalRepository _meetingGroupProposalRepository; private readonly IUserContext _userContext; internal AcceptMeetingGroupProposalCommandHandler(IMeetingGroupProposalRepository meetingGroupProposalRepository, IUserContext userContext) { _meetingGroupProposalRepository = meetingGroupProposalRepository; _userContext = userContext; } public async Task Handle(AcceptMeetingGroupProposalCommand request, CancellationToken cancellationToken) { var meetingGroupProposal = await _meetingGroupProposalRepository.GetByIdAsync(new MeetingGroupProposalId(request.MeetingGroupProposalId)); meetingGroupProposal.Accept(_userContext.UserId); } } } ================================================ FILE: src/Modules/Administration/Application/MeetingGroupProposals/AcceptMeetingGroupProposal/MeetingGroupProposalAcceptedNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals.Events; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.AcceptMeetingGroupProposal { public class MeetingGroupProposalAcceptedNotification : DomainNotificationBase { [JsonConstructor] public MeetingGroupProposalAcceptedNotification(MeetingGroupProposalAcceptedDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Administration/Application/MeetingGroupProposals/AcceptMeetingGroupProposal/MeetingGroupProposalAcceptedNotificationHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Administration.IntegrationEvents.MeetingGroupProposals; using MediatR; namespace CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.AcceptMeetingGroupProposal { public class MeetingGroupProposalAcceptedNotificationHandler : INotificationHandler { private readonly IEventsBus _eventsBus; public MeetingGroupProposalAcceptedNotificationHandler(IEventsBus eventsBus) { _eventsBus = eventsBus; } public async Task Handle(MeetingGroupProposalAcceptedNotification notification, CancellationToken cancellationToken) { await _eventsBus.Publish(new MeetingGroupProposalAcceptedIntegrationEvent( notification.Id, notification.DomainEvent.OccurredOn, notification.DomainEvent.MeetingGroupProposalId.Value)); } } } ================================================ FILE: src/Modules/Administration/Application/MeetingGroupProposals/GetMeetingGroupProposal/GetMeetingGroupProposalQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.GetMeetingGroupProposal { public class GetMeetingGroupProposalQuery : QueryBase { public GetMeetingGroupProposalQuery(Guid meetingGroupProposalId) { MeetingGroupProposalId = meetingGroupProposalId; } public Guid MeetingGroupProposalId { get; } } } ================================================ FILE: src/Modules/Administration/Application/MeetingGroupProposals/GetMeetingGroupProposal/GetMeetingGroupProposalQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.GetMeetingGroupProposal { internal class GetMeetingGroupProposalQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetMeetingGroupProposalQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(GetMeetingGroupProposalQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MeetingGroupProposal].[Id] AS [{nameof(MeetingGroupProposalDto.Id)}], [MeetingGroupProposal].[Name] AS [{nameof(MeetingGroupProposalDto.Name)}], [MeetingGroupProposal].[ProposalUserId] AS [{nameof(MeetingGroupProposalDto.ProposalUserId)}], [MeetingGroupProposal].[LocationCity] AS [{nameof(MeetingGroupProposalDto.LocationCity)}], [MeetingGroupProposal].[LocationCountryCode] AS [{nameof(MeetingGroupProposalDto.LocationCountryCode)}], [MeetingGroupProposal].[Description] AS [{nameof(MeetingGroupProposalDto.Description)}], [MeetingGroupProposal].[ProposalDate] AS [{nameof(MeetingGroupProposalDto.ProposalDate)}], [MeetingGroupProposal].[StatusCode] AS [{nameof(MeetingGroupProposalDto.StatusCode)}], [MeetingGroupProposal].[DecisionDate] AS [{nameof(MeetingGroupProposalDto.DecisionDate)}], [MeetingGroupProposal].[DecisionUserId] AS [{nameof(MeetingGroupProposalDto.DecisionUserId)}], [MeetingGroupProposal].[DecisionCode] AS [{nameof(MeetingGroupProposalDto.DecisionCode)}], [MeetingGroupProposal].[DecisionRejectReason] AS [{nameof(MeetingGroupProposalDto.DecisionRejectReason)}] FROM [administration].[v_MeetingGroupProposals] AS [MeetingGroupProposal] WHERE [MeetingGroupProposal].[Id] = @MeetingGroupProposalId """; return await connection.QuerySingleAsync(sql, new { query.MeetingGroupProposalId }); } } } ================================================ FILE: src/Modules/Administration/Application/MeetingGroupProposals/GetMeetingGroupProposal/MeetingGroupProposalDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.GetMeetingGroupProposal { public class MeetingGroupProposalDto { public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } public string LocationCity { get; set; } public string LocationCountryCode { get; set; } public Guid ProposalUserId { get; set; } public DateTime ProposalDate { get; set; } public string StatusCode { get; set; } public DateTime? DecisionDate { get; set; } public Guid? DecisionUserId { get; set; } public string DecisionCode { get; set; } public string DecisionRejectReason { get; set; } } } ================================================ FILE: src/Modules/Administration/Application/MeetingGroupProposals/GetMeetingGroupProposals/GetMeetingGroupProposalsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.GetMeetingGroupProposal; namespace CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.GetMeetingGroupProposals { public class GetMeetingGroupProposalsQuery : QueryBase> { public GetMeetingGroupProposalsQuery() { } } } ================================================ FILE: src/Modules/Administration/Application/MeetingGroupProposals/GetMeetingGroupProposals/GetMeetingGroupProposalsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Queries; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.GetMeetingGroupProposal; using Dapper; namespace CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.GetMeetingGroupProposals { internal class GetMeetingGroupProposalsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetMeetingGroupProposalsQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle(GetMeetingGroupProposalsQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MeetingGroupProposal].[Id] AS [{nameof(MeetingGroupProposalDto.Id)}], [MeetingGroupProposal].[Name] AS [{nameof(MeetingGroupProposalDto.Name)}], [MeetingGroupProposal].[ProposalUserId] AS [{nameof(MeetingGroupProposalDto.ProposalUserId)}], [MeetingGroupProposal].[LocationCity] AS [{nameof(MeetingGroupProposalDto.LocationCity)}], [MeetingGroupProposal].[LocationCountryCode] AS [{nameof(MeetingGroupProposalDto.LocationCountryCode)}], [MeetingGroupProposal].[Description] AS [{nameof(MeetingGroupProposalDto.Description)}], [MeetingGroupProposal].[ProposalDate] AS [{nameof(MeetingGroupProposalDto.ProposalDate)}], [MeetingGroupProposal].[StatusCode] AS [{nameof(MeetingGroupProposalDto.StatusCode)}], [MeetingGroupProposal].[DecisionDate] AS [{nameof(MeetingGroupProposalDto.DecisionDate)}], [MeetingGroupProposal].[DecisionUserId] AS [{nameof(MeetingGroupProposalDto.DecisionUserId)}], [MeetingGroupProposal].[DecisionCode] AS [{nameof(MeetingGroupProposalDto.DecisionCode)}], [MeetingGroupProposal].[DecisionRejectReason] AS [{nameof(MeetingGroupProposalDto.DecisionRejectReason)}] FROM [administration].[v_MeetingGroupProposals] AS [MeetingGroupProposal] """; return (await connection.QueryAsync(sql)).AsList(); } } } ================================================ FILE: src/Modules/Administration/Application/MeetingGroupProposals/MeetingGroupProposedIntegrationEventHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.RequestMeetingGroupProposalVerification; using CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals { internal class MeetingGroupProposedIntegrationEventHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; internal MeetingGroupProposedIntegrationEventHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(MeetingGroupProposedIntegrationEvent notification, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync( new RequestMeetingGroupProposalVerificationCommand( Guid.NewGuid(), notification.MeetingGroupProposalId, notification.Name, notification.Description, notification.LocationCity, notification.LocationCountryCode, notification.ProposalUserId, notification.ProposalDate)); } } } ================================================ FILE: src/Modules/Administration/Application/MeetingGroupProposals/RequestMeetingGroupProposalVerification/RequestMeetingGroupProposalVerificationCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.RequestMeetingGroupProposalVerification { public class RequestMeetingGroupProposalVerificationCommand : InternalCommandBase { [JsonConstructor] public RequestMeetingGroupProposalVerificationCommand( Guid id, Guid meetingGroupProposalId, string name, string description, string locationCity, string locationCountryCode, Guid proposalUserId, DateTime proposalDate) : base(id) { this.MeetingGroupProposalId = meetingGroupProposalId; this.Name = name; this.Description = description; this.LocationCity = locationCity; this.LocationCountryCode = locationCountryCode; this.ProposalUserId = proposalUserId; this.ProposalDate = proposalDate; } public Guid MeetingGroupProposalId { get; } public string Name { get; } public string Description { get; } public string LocationCity { get; } public string LocationCountryCode { get; } public Guid ProposalUserId { get; } public DateTime ProposalDate { get; } } } ================================================ FILE: src/Modules/Administration/Application/MeetingGroupProposals/RequestMeetingGroupProposalVerification/RequestMeetingGroupProposalVerificationCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Administration.Domain.Users; namespace CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.RequestMeetingGroupProposalVerification { internal class RequestMeetingGroupProposalVerificationCommandHandler : ICommandHandler { private readonly IMeetingGroupProposalRepository _meetingGroupProposalRepository; public RequestMeetingGroupProposalVerificationCommandHandler(IMeetingGroupProposalRepository meetingGroupProposalRepository) { _meetingGroupProposalRepository = meetingGroupProposalRepository; } public async Task Handle(RequestMeetingGroupProposalVerificationCommand request, CancellationToken cancellationToken) { var meetingGroupProposal = MeetingGroupProposal.CreateToVerify( request.MeetingGroupProposalId, request.Name, request.Description, MeetingGroupLocation.Create(request.LocationCity, request.LocationCountryCode), new UserId(request.ProposalUserId), request.ProposalDate); await _meetingGroupProposalRepository.AddAsync(meetingGroupProposal); return meetingGroupProposal.Id.Value; } } } ================================================ FILE: src/Modules/Administration/Application/Members/CreateMember/CreateMemberCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Administration.Application.Members.CreateMember { public class CreateMemberCommand : InternalCommandBase { [JsonConstructor] public CreateMemberCommand( Guid id, Guid memberId, string login, string email, string firstName, string lastName, string name) : base(id) { Login = login; MemberId = memberId; Email = email; FirstName = firstName; LastName = lastName; Name = name; } internal Guid MemberId { get; } internal string Login { get; } internal string Email { get; } internal string FirstName { get; } internal string LastName { get; } internal string Name { get; } } } ================================================ FILE: src/Modules/Administration/Application/Members/CreateMember/CreateMemberCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Domain.Members; namespace CompanyName.MyMeetings.Modules.Administration.Application.Members.CreateMember { internal class CreateMemberCommandHandler : ICommandHandler { private readonly IMemberRepository _memberRepository; public CreateMemberCommandHandler(IMemberRepository memberRepository) { _memberRepository = memberRepository; } public async Task Handle(CreateMemberCommand request, CancellationToken cancellationToken) { var member = Member.Create( request.MemberId, request.Login, request.Email, request.FirstName, request.LastName, request.Name); await _memberRepository.AddAsync(member); return member.Id.Value; } } } ================================================ FILE: src/Modules/Administration/Application/Members/GetMember/GetMemberQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Administration.Application.Members.GetMember { public class GetMemberQuery : QueryBase { public GetMemberQuery(Guid memberId) { MemberId = memberId; } public Guid MemberId { get; } } } ================================================ FILE: src/Modules/Administration/Application/Members/GetMember/GetMemberQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Administration.Application.Members.GetMember { internal class GetMemberQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetMemberQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(GetMemberQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [Member].[Id] AS [{nameof(MemberDto.Id)}], [Member].[Login] AS [{nameof(MemberDto.Login)}], [Member].[Email] AS [{nameof(MemberDto.Email)}], [Member].[FirstName] AS [{nameof(MemberDto.FirstName)}], [Member].[LastName] AS [{nameof(MemberDto.LastName)}], [Member].[Name] AS [{nameof(MemberDto.Name)}] FROM [administration].[v_Members] AS [Member] WHERE [Member].[Id] = @MemberId """; return await connection.QuerySingleAsync(sql, new { query.MemberId }); } } } ================================================ FILE: src/Modules/Administration/Application/Members/GetMember/MemberDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.Application.Members.GetMember { public class MemberDto { public Guid Id { get; set; } public string Login { get; set; } public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Name { get; set; } } } ================================================ FILE: src/Modules/Administration/Application/Members/NewUserRegisteredIntegrationEventHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Application.Members.CreateMember; using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Administration.Application.Members { internal class NewUserRegisteredIntegrationEventHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; internal NewUserRegisteredIntegrationEventHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(NewUserRegisteredIntegrationEvent notification, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync(new CreateMemberCommand( Guid.NewGuid(), notification.UserId, notification.Login, notification.Email, notification.FirstName, notification.LastName, notification.Name)); } } } ================================================ FILE: src/Modules/Administration/Domain/CompanyName.MyMeetings.Modules.Administration.Domain.csproj ================================================  ================================================ FILE: src/Modules/Administration/Domain/MeetingGroupProposals/Events/MeetingGroupProposalAcceptedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals.Events { public class MeetingGroupProposalAcceptedDomainEvent : DomainEventBase { public MeetingGroupProposalAcceptedDomainEvent(MeetingGroupProposalId meetingGroupProposalId) { MeetingGroupProposalId = meetingGroupProposalId; } public MeetingGroupProposalId MeetingGroupProposalId { get; } } } ================================================ FILE: src/Modules/Administration/Domain/MeetingGroupProposals/Events/MeetingGroupProposalRejectedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals.Events { internal class MeetingGroupProposalRejectedDomainEvent : DomainEventBase { internal MeetingGroupProposalRejectedDomainEvent(MeetingGroupProposalId meetingGroupProposalId) { MeetingGroupProposalId = meetingGroupProposalId; } internal MeetingGroupProposalId MeetingGroupProposalId { get; } } } ================================================ FILE: src/Modules/Administration/Domain/MeetingGroupProposals/Events/MeetingGroupProposalVerificationRequestedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals.Events { public class MeetingGroupProposalVerificationRequestedDomainEvent : DomainEventBase { internal MeetingGroupProposalVerificationRequestedDomainEvent(MeetingGroupProposalId meetingGroupProposalId) { MeetingGroupProposalId = meetingGroupProposalId; } public MeetingGroupProposalId MeetingGroupProposalId { get; } } } ================================================ FILE: src/Modules/Administration/Domain/MeetingGroupProposals/IMeetingGroupProposalRepository.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals { public interface IMeetingGroupProposalRepository { Task AddAsync(MeetingGroupProposal meetingGroupProposal); Task GetByIdAsync(MeetingGroupProposalId meetingGroupProposalId); } } ================================================ FILE: src/Modules/Administration/Domain/MeetingGroupProposals/MeetingGroupLocation.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals { public class MeetingGroupLocation : ValueObject { private MeetingGroupLocation(string city, string countryCode) { City = city; CountryCode = countryCode; } public string City { get; } public string CountryCode { get; } public static MeetingGroupLocation Create(string city, string countryCode) { return new MeetingGroupLocation(city, countryCode); } } } ================================================ FILE: src/Modules/Administration/Domain/MeetingGroupProposals/MeetingGroupProposal.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals.Events; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals.Rules; using CompanyName.MyMeetings.Modules.Administration.Domain.Users; namespace CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals { public class MeetingGroupProposal : Entity, IAggregateRoot { private string _name; private string _description; private MeetingGroupLocation _location; private DateTime _proposalDate; private UserId _proposalUserId; private MeetingGroupProposalStatus _status; private MeetingGroupProposalDecision _decision; private MeetingGroupProposal( MeetingGroupProposalId id, string name, string description, MeetingGroupLocation location, UserId proposalUserId, DateTime proposalDate) { Id = id; _name = name; _description = description; _location = location; _proposalUserId = proposalUserId; _proposalDate = proposalDate; _status = MeetingGroupProposalStatus.ToVerify; _decision = MeetingGroupProposalDecision.NoDecision; this.AddDomainEvent(new MeetingGroupProposalVerificationRequestedDomainEvent(this.Id)); } private MeetingGroupProposal() { _decision = MeetingGroupProposalDecision.NoDecision; } public MeetingGroupProposalId Id { get; private set; } public void Accept(UserId userId) { this.CheckRule(new MeetingGroupProposalCanBeVerifiedOnceRule(_decision)); _decision = MeetingGroupProposalDecision.AcceptDecision(DateTime.UtcNow, userId); _status = _decision.GetStatusForDecision(); this.AddDomainEvent(new MeetingGroupProposalAcceptedDomainEvent(this.Id)); } public void Reject(UserId userId, string rejectReason) { this.CheckRule(new MeetingGroupProposalCanBeVerifiedOnceRule(_decision)); this.CheckRule(new MeetingGroupProposalRejectionMustHaveAReasonRule(rejectReason)); _decision = MeetingGroupProposalDecision.RejectDecision(DateTime.UtcNow, userId, rejectReason); _status = _decision.GetStatusForDecision(); this.AddDomainEvent(new MeetingGroupProposalRejectedDomainEvent(this.Id)); } public static MeetingGroupProposal CreateToVerify( Guid meetingGroupProposalId, string name, string description, MeetingGroupLocation location, UserId proposalUserId, DateTime proposalDate) { var meetingGroupProposal = new MeetingGroupProposal( new MeetingGroupProposalId(meetingGroupProposalId), name, description, location, proposalUserId, proposalDate); return meetingGroupProposal; } } } ================================================ FILE: src/Modules/Administration/Domain/MeetingGroupProposals/MeetingGroupProposalDecision.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Administration.Domain.Users; namespace CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals { public class MeetingGroupProposalDecision : ValueObject { private MeetingGroupProposalDecision(DateTime? date, UserId userId, string code, string rejectReason) { this.Date = date; this.UserId = userId; this.Code = code; this.RejectReason = rejectReason; } public DateTime? Date { get; } public UserId UserId { get; } public string Code { get; } public string RejectReason { get; } internal static MeetingGroupProposalDecision NoDecision => new MeetingGroupProposalDecision(null, null, null, null); private bool IsAccepted => this.Code == "Accept"; private bool IsRejected => this.Code == "Reject"; internal static MeetingGroupProposalDecision AcceptDecision(DateTime date, UserId userId) { return new MeetingGroupProposalDecision(date, userId, "Accept", null); } internal static MeetingGroupProposalDecision RejectDecision(DateTime date, UserId userId, string rejectReason) { return new MeetingGroupProposalDecision(date, userId, "Reject", rejectReason); } internal MeetingGroupProposalStatus GetStatusForDecision() { if (this.IsAccepted) { return MeetingGroupProposalStatus.Verified; } if (this.IsRejected) { return MeetingGroupProposalStatus.Create("Rejected"); } return MeetingGroupProposalStatus.ToVerify; } } } ================================================ FILE: src/Modules/Administration/Domain/MeetingGroupProposals/MeetingGroupProposalId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals { public class MeetingGroupProposalId : TypedIdValueBase { public MeetingGroupProposalId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Administration/Domain/MeetingGroupProposals/MeetingGroupProposalStatus.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals { public class MeetingGroupProposalStatus : ValueObject { private MeetingGroupProposalStatus(string value) { Value = value; } public static MeetingGroupProposalStatus ToVerify => new MeetingGroupProposalStatus("ToVerify"); public static MeetingGroupProposalStatus Verified => new MeetingGroupProposalStatus("Verified"); public string Value { get; } internal static MeetingGroupProposalStatus Create(string value) { return new MeetingGroupProposalStatus(value); } } } ================================================ FILE: src/Modules/Administration/Domain/MeetingGroupProposals/Rules/MeetingGroupProposalCanBeVerifiedOnceRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals.Rules { public class MeetingGroupProposalCanBeVerifiedOnceRule : IBusinessRule { private readonly MeetingGroupProposalDecision _actualDecision; internal MeetingGroupProposalCanBeVerifiedOnceRule(MeetingGroupProposalDecision actualDecision) { _actualDecision = actualDecision; } public string Message => "Meeting group proposal can be verified only once"; public bool IsBroken() => _actualDecision != MeetingGroupProposalDecision.NoDecision; } } ================================================ FILE: src/Modules/Administration/Domain/MeetingGroupProposals/Rules/MeetingGroupProposalRejectionMustHaveAReasonRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals.Rules { public class MeetingGroupProposalRejectionMustHaveAReasonRule : IBusinessRule { private readonly string _reason; internal MeetingGroupProposalRejectionMustHaveAReasonRule(string reason) { _reason = reason; } public string Message => "Meeting group proposal rejection must have a reason"; public bool IsBroken() => string.IsNullOrEmpty(_reason); } } ================================================ FILE: src/Modules/Administration/Domain/Members/Events/MemberCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Administration.Domain.Members.Events { public class MemberCreatedDomainEvent : DomainEventBase { public MemberCreatedDomainEvent(MemberId memberId) { MemberId = memberId; } public MemberId MemberId { get; } } } ================================================ FILE: src/Modules/Administration/Domain/Members/IMemberRepository.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.Domain.Members { public interface IMemberRepository { Task AddAsync(Member member); Task GetByIdAsync(MemberId memberId); } } ================================================ FILE: src/Modules/Administration/Domain/Members/Member.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Administration.Domain.Members.Events; namespace CompanyName.MyMeetings.Modules.Administration.Domain.Members { public class Member : Entity, IAggregateRoot { public MemberId Id { get; private set; } private string _login; private string _email; private string _firstName; private string _lastName; private string _name; private DateTime _createDate; private Member() { // Only for EF. } private Member(Guid id, string login, string email, string firstName, string lastName, string name) { this.Id = new MemberId(id); _login = login; _email = email; _firstName = firstName; _lastName = lastName; _name = name; _createDate = DateTime.UtcNow; this.AddDomainEvent(new MemberCreatedDomainEvent(this.Id)); } public static Member Create(Guid id, string login, string email, string firstName, string lastName, string name) { return new Member(id, login, email, firstName, lastName, name); } } } ================================================ FILE: src/Modules/Administration/Domain/Members/MemberId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Administration.Domain.Members { public class MemberId : TypedIdValueBase { public MemberId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Administration/Domain/Users/IUserContext.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.Domain.Users { public interface IUserContext { UserId UserId { get; } } } ================================================ FILE: src/Modules/Administration/Domain/Users/UserId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Administration.Domain.Users { public class UserId : TypedIdValueBase { public UserId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Administration/Infrastructure/AdministrationContext.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Administration.Domain.Members; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Domain.Members; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.InternalCommands; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Outbox; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure { public class AdministrationContext : DbContext { private readonly ILoggerFactory _loggerFactory; public DbSet InternalCommands { get; set; } internal DbSet MeetingGroupProposals { get; set; } internal DbSet OutboxMessages { get; set; } internal DbSet Members { get; set; } public AdministrationContext(DbContextOptions options, ILoggerFactory loggerFactory) : base(options) { _loggerFactory = loggerFactory; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new MeetingGroupProposalEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new OutboxMessageEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new InternalCommandEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new MemberEntityTypeConfiguration()); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/AdministrationModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing; using MediatR; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure { public class AdministrationModule : IAdministrationModule { 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 = AdministrationCompositionRoot.BeginLifetimeScope()) { var mediator = scope.Resolve(); return await mediator.Send(query); } } } } ================================================ FILE: src/Modules/Administration/Infrastructure/CompanyName.MyMeetings.Modules.Administration.Infrastructure.csproj ================================================  ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/AdministrationCompositionRoot.cs ================================================ using Autofac; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration { internal static class AdministrationCompositionRoot { private static IContainer _container; public static void SetContainer(IContainer container) { _container = container; } public static ILifetimeScope BeginLifetimeScope() { return _container.BeginLifetimeScope(); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/AdministrationStartup.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.AcceptMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.RequestMeetingGroupProposalVerification; using CompanyName.MyMeetings.Modules.Administration.Application.Members.CreateMember; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Authentication; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.DataAccess; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.EventsBus; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Logging; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Mediation; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.InternalCommands; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.Outbox; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Quartz; using Serilog; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration { public class AdministrationStartup { private static IContainer _container; public static void Initialize( string connectionString, IExecutionContextAccessor executionContextAccessor, ILogger logger, IEventsBus eventsBus, long? internalProcessingPoolingInterval = null) { var moduleLogger = logger.ForContext("Module", "Administration"); ConfigureContainer(connectionString, executionContextAccessor, moduleLogger, eventsBus); QuartzStartup.Initialize(moduleLogger, internalProcessingPoolingInterval); EventsBusStartup.Initialize(moduleLogger); } public static void Stop() { QuartzStartup.StopQuartz(); } private static void ConfigureContainer( string connectionString, IExecutionContextAccessor executionContextAccessor, ILogger logger, IEventsBus eventsBus) { var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterModule(new LoggingModule(logger)); 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()); containerBuilder.RegisterModule(new AuthenticationModule()); var domainNotificationsMap = new BiDictionary(); domainNotificationsMap.Add("MeetingGroupProposalAcceptedNotification", typeof(MeetingGroupProposalAcceptedNotification)); containerBuilder.RegisterModule(new OutboxModule(domainNotificationsMap)); BiDictionary internalCommandsMap = new BiDictionary(); internalCommandsMap.Add("CreateMember", typeof(CreateMemberCommand)); internalCommandsMap.Add("RequestMeetingGroupProposalVerification", typeof(RequestMeetingGroupProposalVerificationCommand)); containerBuilder.RegisterModule(new InternalCommandsModule(internalCommandsMap)); containerBuilder.RegisterModule(new QuartzModule()); containerBuilder.RegisterInstance(executionContextAccessor); _container = containerBuilder.Build(); AdministrationCompositionRoot.SetContainer(_container); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/AllConstructorFinder.cs ================================================ using System.Collections.Concurrent; using System.Reflection; using Autofac.Core.Activators.Reflection; namespace CompanyName.MyMeetings.Modules.Administration.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); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Assemblies.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration { internal static class Assemblies { public static readonly Assembly Application = typeof(IAdministrationModule).Assembly; } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Authentication/AuthenticationModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.Administration.Domain.Users; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Users; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Authentication { internal class AuthenticationModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType() .As() .InstancePerLifetimeScope(); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/DataAccess/DataAccessModule.cs ================================================ 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.Administration.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 AdministrationContext(dbContextOptionsBuilder.Options, _loggerFactory); }) .AsSelf() .As() .InstancePerLifetimeScope(); var infrastructureAssembly = typeof(AdministrationContext).Assembly; builder.RegisterAssemblyTypes(infrastructureAssembly) .Where(type => type.Name.EndsWith("Repository")) .AsImplementedInterfaces() .InstancePerLifetimeScope() .FindConstructorsWith(new AllConstructorFinder()); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/EventsBus/EventsBusModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.Administration.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(); } } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents; using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using Serilog; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.EventsBus { internal static class EventsBusStartup { internal static void Initialize( ILogger logger) { SubscribeToIntegrationEvents(logger); } private static void SubscribeToIntegrationEvents(ILogger logger) { var eventBus = AdministrationCompositionRoot.BeginLifetimeScope().Resolve(); SubscribeToIntegrationEvent(eventBus, logger); 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()); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs ================================================ 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.Administration.Infrastructure.Configuration.EventsBus { internal class IntegrationEventGenericHandler : IIntegrationEventHandler where T : IntegrationEvent { public async Task Handle(T @event) { using var scope = AdministrationCompositionRoot.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 [administration].[InboxMessages] (Id, OccurredOn, Type, Data) " + "VALUES (@Id, @OccurredOn, @Type, @Data)"; await connection.ExecuteScalarAsync(sql, new { @event.Id, @event.OccurredOn, type, data }); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Logging/LoggingModule.cs ================================================ using Autofac; using Serilog; namespace CompanyName.MyMeetings.Modules.Administration.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(); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Mediation/MediatorModule.cs ================================================ using System.Reflection; using Autofac; using Autofac.Core; using Autofac.Features.Variance; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Queries; using FluentValidation; using MediatR; using MediatR.Pipeline; namespace CompanyName.MyMeetings.Modules.Administration.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(ICommandHandler<>), typeof(ICommandHandler<,>), typeof(IQueryHandler<,>), typeof(INotificationHandler<>), typeof(IValidator<>), typeof(IRequestPreProcessor<>), typeof(IStreamRequestHandler<,>), typeof(IRequestPostProcessor<,>), typeof(IRequestExceptionHandler<,,>), typeof(IRequestExceptionAction<,>), }; 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; } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/CommandsExecutor.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing { internal static class CommandsExecutor { internal static async Task Execute(ICommand command) { using (var scope = AdministrationCompositionRoot.BeginLifetimeScope()) { var mediator = scope.Resolve(); await mediator.Send(command); } } internal static async Task Execute(ICommand command) { using (var scope = AdministrationCompositionRoot.BeginLifetimeScope()) { var mediator = scope.Resolve(); return await mediator.Send(command); } } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/IRecurringCommand.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing { public interface IRecurringCommand { } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/Inbox/InboxMessageDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.Inbox { public class InboxMessageDto { public Guid Id { get; set; } public string Type { get; set; } public string Data { get; set; } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.Inbox { public class ProcessInboxCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using Dapper; using MediatR; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Administration.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 [administration].[InboxMessages] AS [InboxMessage] WHERE [InboxMessage].[ProcessedDate] IS NULL ORDER BY [InboxMessage].[OccurredOn] """; var messages = await connection.QueryAsync(sql); const string sqlUpdateProcessedDate = """ UPDATE [administration].[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.ExecuteScalarAsync(sqlUpdateProcessedDate, new { Date = DateTime.UtcNow, message.Id }); } } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/Inbox/ProcessInboxJob.cs ================================================ using Quartz; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.Inbox { [DisallowConcurrentExecution] public class ProcessInboxJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ProcessInboxCommand()); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Serialization; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using Dapper; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.InternalCommands { public class CommandsScheduler : ICommandsScheduler { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly IInternalCommandsMapper _internalCommandsMapper; public CommandsScheduler( ISqlConnectionFactory sqlConnectionFactory, IInternalCommandsMapper internalCommandsMapper) { _sqlConnectionFactory = sqlConnectionFactory; _internalCommandsMapper = internalCommandsMapper; } public async Task EnqueueAsync(ICommand command) { var connection = this._sqlConnectionFactory.GetOpenConnection(); const string sqlInsert = "INSERT INTO [administration].[InternalCommands] ([Id], [EnqueueDate] , [Type], [Data]) VALUES " + "(@Id, @EnqueueDate, @Type, @Data)"; await connection.ExecuteAsync(sqlInsert, new { command.Id, EnqueueDate = DateTime.UtcNow, Type = _internalCommandsMapper.GetName(command.GetType()), 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 [administration].[InternalCommands] ([Id], [EnqueueDate] , [Type], [Data]) VALUES " + "(@Id, @EnqueueDate, @Type, @Data)"; await connection.ExecuteAsync(sqlInsert, new { command.Id, EnqueueDate = DateTime.UtcNow, Type = _internalCommandsMapper.GetName(command.GetType()), Data = JsonConvert.SerializeObject(command, new JsonSerializerSettings { ContractResolver = new AllPropertiesContractResolver() }) }); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/InternalCommands/InternalCommandsModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using Module = Autofac.Module; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.InternalCommands { internal class InternalCommandsModule : Module { private readonly BiDictionary _internalCommandsMap; public InternalCommandsModule(BiDictionary internalCommandsMap) { _internalCommandsMap = internalCommandsMap; } protected override void Load(ContainerBuilder builder) { builder.RegisterType() .As() .FindConstructorsWith(new AllConstructorFinder()) .WithParameter("internalCommandsMap", _internalCommandsMap) .SingleInstance(); this.CheckMappings(); } private void CheckMappings() { var internalCommands = Assemblies.Application .GetTypes() .Where(x => x.BaseType != null && ( (x.BaseType.IsGenericType && x.BaseType.GetGenericTypeDefinition() == typeof(InternalCommandBase<>)) || x.BaseType == typeof(InternalCommandBase))) .ToList(); List notMappedInternalCommands = []; foreach (var internalCommand in internalCommands) { _internalCommandsMap.TryGetBySecond(internalCommand, out var name); if (name == null) { notMappedInternalCommands.Add(internalCommand); } } if (notMappedInternalCommands.Any()) { throw new ApplicationException($"Internal Commands {notMappedInternalCommands.Select(x => x.FullName).Aggregate((x, y) => x + "," + y)} not mapped"); } } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.InternalCommands { internal class ProcessInternalCommandsCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using Dapper; using Newtonsoft.Json; using Polly; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.InternalCommands { internal class ProcessInternalCommandsCommandHandler : ICommandHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly IInternalCommandsMapper _internalCommandsMapper; public ProcessInternalCommandsCommandHandler( ISqlConnectionFactory sqlConnectionFactory, IInternalCommandsMapper internalCommandsMapper) { _sqlConnectionFactory = sqlConnectionFactory; _internalCommandsMapper = internalCommandsMapper; } 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 [administration].[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) { const string updateOnErrorSql = "UPDATE [administration].[InternalCommands] " + "SET ProcessedDate = @NowDate, " + "Error = @Error " + "WHERE [Id] = @Id"; await connection.ExecuteScalarAsync( updateOnErrorSql, new { NowDate = DateTime.UtcNow, Error = result.FinalException.ToString(), internalCommand.Id }); } } } private async Task ProcessCommand( InternalCommandDto internalCommand) { var type = _internalCommandsMapper.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; } } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsJob.cs ================================================ using Quartz; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.InternalCommands { [DisallowConcurrentExecution] public class ProcessInternalCommandsJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ProcessInternalCommandsCommand()); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using Serilog; using Serilog.Context; using Serilog.Core; using Serilog.Events; namespace CompanyName.MyMeetings.Modules.Administration.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); return; } 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))); } } } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using Serilog; using Serilog.Context; using Serilog.Core; using Serilog.Events; namespace CompanyName.MyMeetings.Modules.Administration.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) { if (command is IRecurringCommand) { return await _decorated.Handle(command, 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))); } } } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/Outbox/OutboxMessageDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.Outbox { public class OutboxMessageDto { public Guid Id { get; set; } public string Type { get; set; } public string Data { get; set; } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/Outbox/OutboxModule.cs ================================================ 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.Administration.Infrastructure.Outbox; using Module = Autofac.Module; namespace CompanyName.MyMeetings.Modules.Administration.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(); this.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"); } } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.Outbox { public class ProcessOutboxCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using Dapper; using MediatR; using Newtonsoft.Json; using Serilog.Context; using Serilog.Core; using Serilog.Events; namespace CompanyName.MyMeetings.Modules.Administration.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 [administration].[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 [administration].[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()}"))); } } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxJob.cs ================================================ using Quartz; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.Outbox { [DisallowConcurrentExecution] public class ProcessOutboxJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ProcessOutboxCommand()); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/ProcessingModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.InternalCommands; using MediatR; namespace CompanyName.MyMeetings.Modules.Administration.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.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()); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing { internal class UnitOfWorkCommandHandlerDecorator : ICommandHandler where T : ICommand { private readonly AdministrationContext _administrationContext; private readonly ICommandHandler _decorated; private readonly IUnitOfWork _unitOfWork; public UnitOfWorkCommandHandlerDecorator( ICommandHandler decorated, IUnitOfWork unitOfWork, AdministrationContext administrationContext) { _decorated = decorated; _unitOfWork = unitOfWork; _administrationContext = administrationContext; } public async Task Handle(T command, CancellationToken cancellationToken) { await _decorated.Handle(command, cancellationToken); if (command is InternalCommandBase) { InternalCommand internalCommand = await _administrationContext.InternalCommands.FirstOrDefaultAsync(x => x.Id == command.Id, cancellationToken); if (internalCommand != null) { internalCommand.ProcessedDate = DateTime.UtcNow; } } await _unitOfWork.CommitAsync(cancellationToken); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerWithResultDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing { internal class UnitOfWorkCommandHandlerWithResultDecorator : ICommandHandler where T : ICommand { private readonly ICommandHandler _decorated; private readonly IUnitOfWork _unitOfWork; private readonly AdministrationContext _administrationContext; public UnitOfWorkCommandHandlerWithResultDecorator( ICommandHandler decorated, IUnitOfWork unitOfWork, AdministrationContext administrationContext) { _decorated = decorated; _unitOfWork = unitOfWork; _administrationContext = administrationContext; } public async Task Handle(T command, CancellationToken cancellationToken) { var result = await this._decorated.Handle(command, cancellationToken); if (command is InternalCommandBase) { var internalCommand = await _administrationContext.InternalCommands.FirstOrDefaultAsync(x => x.Id == command.Id, cancellationToken: cancellationToken); if (internalCommand != null) { internalCommand.ProcessedDate = DateTime.UtcNow; } } await this._unitOfWork.CommitAsync(cancellationToken); return result; } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/ValidationCommandHandlerDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using FluentValidation; namespace CompanyName.MyMeetings.Modules.Administration.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); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Processing/ValidationCommandHandlerWithResultDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using FluentValidation; namespace CompanyName.MyMeetings.Modules.Administration.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); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Quartz/QuartzModule.cs ================================================ using Autofac; using Quartz; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Quartz { public class QuartzModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { builder.RegisterAssemblyTypes(ThisAssembly) .Where(x => typeof(IJob).IsAssignableFrom(x)).InstancePerDependency(); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Quartz/QuartzStartup.cs ================================================ using System.Collections.Specialized; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.Inbox; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.InternalCommands; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.Outbox; using Quartz; using Quartz.Impl; using Quartz.Logging; using Serilog; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Quartz { internal static class QuartzStartup { private static IScheduler _scheduler; internal static void Initialize(ILogger logger, long? internalProcessingPoolingInterval) { logger.Information("Quartz starting..."); var schedulerConfiguration = new NameValueCollection(); schedulerConfiguration.Add("quartz.scheduler.instanceName", "Administration"); ISchedulerFactory schedulerFactory = new StdSchedulerFactory(schedulerConfiguration); _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(); var processInboxTrigger = TriggerBuilder .Create() .StartNow() .WithCronSchedule("0/2 * * ? * *") .Build(); _scheduler .ScheduleJob(processInboxJob, processInboxTrigger) .GetAwaiter().GetResult(); var processInternalCommandsJob = JobBuilder.Create().Build(); var triggerCommandsProcessing = TriggerBuilder .Create() .StartNow() .WithCronSchedule("0/2 * * ? * *") .Build(); _scheduler.ScheduleJob(processInternalCommandsJob, triggerCommandsProcessing).GetAwaiter().GetResult(); logger.Information("Quartz started."); } internal static void StopQuartz() { _scheduler.Shutdown(); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Quartz/SerilogLogProvider.cs ================================================ using Quartz.Logging; using Serilog; namespace CompanyName.MyMeetings.Modules.Administration.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(); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Configuration/Users/UserContext.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Administration.Domain.Users; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Users { internal class UserContext : IUserContext { private readonly IExecutionContextAccessor _executionContextAccessor; public UserContext(IExecutionContextAccessor executionContextAccessor) { this._executionContextAccessor = executionContextAccessor; } public UserId UserId => new UserId(_executionContextAccessor.UserId); } } ================================================ FILE: src/Modules/Administration/Infrastructure/Domain/MeetingGroupProposals/MeetingGroupProposalEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Administration.Domain.Users; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Domain.MeetingGroupProposals { internal class MeetingGroupProposalEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("MeetingGroupProposals", "administration"); builder.HasKey(x => x.Id); builder.Property("_name").HasColumnName("Name"); builder.Property("_description").HasColumnName("Description"); builder.Property("_proposalUserId").HasColumnName("ProposalUserId"); builder.Property("_proposalDate").HasColumnName("ProposalDate"); builder.OwnsOne("_location", b => { b.Property(p => p.City).HasColumnName("LocationCity"); b.Property(p => p.CountryCode).HasColumnName("LocationCountryCode"); }); builder.OwnsOne("_status", b => { b.Property(p => p.Value).HasColumnName("StatusCode"); }); builder.OwnsOne("_decision", b => { b.Property(p => p.Code).HasColumnName("DecisionCode"); b.Property(p => p.Date).HasColumnName("DecisionDate"); b.Property(p => p.RejectReason).HasColumnName("DecisionRejectReason"); b.Property(p => p.UserId).HasColumnName("DecisionUserId"); }); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Domain/MeetingGroupProposals/MeetingGroupProposalRepository.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Domain.MeetingGroupProposals { internal class MeetingGroupProposalRepository : IMeetingGroupProposalRepository { private readonly AdministrationContext _context; internal MeetingGroupProposalRepository(AdministrationContext context) { _context = context; } public async Task AddAsync(MeetingGroupProposal meetingGroupProposal) { await _context.MeetingGroupProposals.AddAsync(meetingGroupProposal); } public async Task GetByIdAsync(MeetingGroupProposalId meetingGroupProposalId) { return await _context.MeetingGroupProposals.FirstOrDefaultAsync(x => x.Id == meetingGroupProposalId); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Domain/Members/MemberEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Domain.Members; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Domain.Members { internal class MemberEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("Members", "administration"); builder.HasKey(x => x.Id); builder.Property("_login").HasColumnName("Login"); builder.Property("_email").HasColumnName("Email"); builder.Property("_firstName").HasColumnName("FirstName"); builder.Property("_lastName").HasColumnName("LastName"); builder.Property("_name").HasColumnName("Name"); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Domain/Members/MemberRepository.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Domain.Members; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Domain.Members { internal class MemberRepository : IMemberRepository { private readonly AdministrationContext _administrationContext; internal MemberRepository(AdministrationContext meetingsContext) { _administrationContext = meetingsContext; } public async Task AddAsync(Member member) { await _administrationContext.Members.AddAsync(member); } public async Task GetByIdAsync(MemberId memberId) { return await _administrationContext.Members.FirstOrDefaultAsync(x => x.Id == memberId); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/InternalCommands/InternalCommandEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.InternalCommands { internal class InternalCommandEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("InternalCommands", "administration"); builder.HasKey(b => b.Id); builder.Property(b => b.Id).ValueGeneratedNever(); } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Outbox/OutboxAccessor.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Outbox { internal class OutboxAccessor : IOutbox { private readonly AdministrationContext _context; internal OutboxAccessor(AdministrationContext context) { _context = context; } public void Add(OutboxMessage message) { _context.OutboxMessages.Add(message); } public Task Save() { // Save is done automatically using EF Core Change Tracking mechanism during SaveChanges. return Task.CompletedTask; } } } ================================================ FILE: src/Modules/Administration/Infrastructure/Outbox/OutboxMessageEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Outbox { internal class OutboxMessageEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("OutboxMessages", "administration"); builder.HasKey(b => b.Id); builder.Property(b => b.Id).ValueGeneratedNever(); } } } ================================================ FILE: src/Modules/Administration/IntegrationEvents/CompanyName.MyMeetings.Modules.Administration.IntegrationEvents.csproj ================================================  ================================================ FILE: src/Modules/Administration/IntegrationEvents/MeetingGroupProposals/MeetingGroupProposalAcceptedIntegrationEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.Administration.IntegrationEvents.MeetingGroupProposals { public class MeetingGroupProposalAcceptedIntegrationEvent : IntegrationEvent { public MeetingGroupProposalAcceptedIntegrationEvent( Guid id, DateTime occurredOn, Guid meetingGroupProposalId) : base(id, occurredOn) { MeetingGroupProposalId = meetingGroupProposalId; } public Guid MeetingGroupProposalId { get; } } } ================================================ FILE: src/Modules/Administration/Tests/ArchTests/Application/ApplicationTests.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Administration.Application.Configuration.Queries; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using CompanyName.MyMeetings.Modules.Administration.ArchTests.SeedWork; using FluentValidation; using MediatR; using NetArchTest.Rules; using Newtonsoft.Json; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Administration.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); } } } ================================================ FILE: src/Modules/Administration/Tests/ArchTests/CompanyName.MyMeetings.Modules.Administration.ArchTests.csproj ================================================  ================================================ FILE: src/Modules/Administration/Tests/ArchTests/Domain/DomainTests.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Administration.ArchTests.SeedWork; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Administration.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); } } } ================================================ FILE: src/Modules/Administration/Tests/ArchTests/Module/LayersTests.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.ArchTests.SeedWork; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Administration.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); } } } ================================================ FILE: src/Modules/Administration/Tests/ArchTests/SeedWork/TestBase.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.Modules.Administration.Application.Members.CreateMember; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Administration.Infrastructure; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Administration.ArchTests.SeedWork { public abstract class TestBase { protected static Assembly ApplicationAssembly => typeof(CreateMemberCommand).Assembly; protected static Assembly DomainAssembly => typeof(MeetingGroupProposal).Assembly; protected static Assembly InfrastructureAssembly => typeof(AdministrationContext).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); } } } ================================================ FILE: src/Modules/Administration/Tests/IntegrationTests/AssemblyInfo.cs ================================================ using NUnit.Framework; [assembly: NonParallelizable] [assembly: LevelOfParallelism(1)] namespace CompanyName.MyMeetings.Modules.Administration.IntegrationTests { public class AssemblyInfo { } } ================================================ FILE: src/Modules/Administration/Tests/IntegrationTests/CompanyName.MyMeetings.Modules.Administration.IntegrationTests.csproj ================================================  ================================================ FILE: src/Modules/Administration/Tests/IntegrationTests/MeetingGroupProposals/MeetingGroupProposalSampleData.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.IntegrationTests.MeetingGroupProposals { public struct MeetingGroupProposalSampleData { public static Guid MeetingGroupProposalId = Guid.NewGuid(); public static string Name = "Great Meeting"; public static string Description = "Great Meeting description"; public static string LocationCity = "Warsaw"; public static string LocationCountryCode = "PL"; public static Guid ProposalUserId = Guid.NewGuid(); public static DateTime ProposalDate = new DateTime(2020, 1, 1, 10, 20, 00); } } ================================================ FILE: src/Modules/Administration/Tests/IntegrationTests/MeetingGroupProposals/MeetingGroupProposalTests.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.AcceptMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.GetMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.RequestMeetingGroupProposalVerification; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals.Rules; using CompanyName.MyMeetings.Modules.Administration.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Administration.IntegrationTests.MeetingGroupProposals { [TestFixture] public class MeetingGroupProposalTests : TestBase { [Test] public async Task RequestMeetingGroupProposalVerification_Test() { var proposalId = await AdministrationModule.ExecuteCommandAsync(new RequestMeetingGroupProposalVerificationCommand( MeetingGroupProposalSampleData.MeetingGroupProposalId, MeetingGroupProposalSampleData.MeetingGroupProposalId, MeetingGroupProposalSampleData.Name, MeetingGroupProposalSampleData.Description, MeetingGroupProposalSampleData.LocationCity, MeetingGroupProposalSampleData.LocationCountryCode, MeetingGroupProposalSampleData.ProposalUserId, MeetingGroupProposalSampleData.ProposalDate)); var meetingGroupProposal = await AdministrationModule.ExecuteQueryAsync(new GetMeetingGroupProposalQuery(proposalId)); Assert.That(meetingGroupProposal.Id, Is.EqualTo(MeetingGroupProposalSampleData.MeetingGroupProposalId)); Assert.That(meetingGroupProposal.StatusCode, Is.EqualTo(MeetingGroupProposalStatus.ToVerify.Value)); Assert.That(meetingGroupProposal.Name, Is.EqualTo(MeetingGroupProposalSampleData.Name)); Assert.That(meetingGroupProposal.Description, Is.EqualTo(MeetingGroupProposalSampleData.Description)); Assert.That(meetingGroupProposal.LocationCity, Is.EqualTo(MeetingGroupProposalSampleData.LocationCity)); Assert.That(meetingGroupProposal.LocationCountryCode, Is.EqualTo(MeetingGroupProposalSampleData.LocationCountryCode)); Assert.That(meetingGroupProposal.ProposalUserId, Is.EqualTo(MeetingGroupProposalSampleData.ProposalUserId)); Assert.That(meetingGroupProposal.ProposalDate, Is.EqualTo(MeetingGroupProposalSampleData.ProposalDate)); } [Test] public async Task AcceptMeetingGroupProposal_WhenProposalIsNotAccepted_IsSuccessful() { var proposalId = await AdministrationModule.ExecuteCommandAsync(new RequestMeetingGroupProposalVerificationCommand( MeetingGroupProposalSampleData.MeetingGroupProposalId, MeetingGroupProposalSampleData.MeetingGroupProposalId, MeetingGroupProposalSampleData.Name, MeetingGroupProposalSampleData.Description, MeetingGroupProposalSampleData.LocationCity, MeetingGroupProposalSampleData.LocationCountryCode, MeetingGroupProposalSampleData.ProposalUserId, MeetingGroupProposalSampleData.ProposalDate)); await AdministrationModule.ExecuteCommandAsync( new AcceptMeetingGroupProposalCommand(MeetingGroupProposalSampleData.MeetingGroupProposalId)); var meetingGroupProposal = await AdministrationModule.ExecuteQueryAsync(new GetMeetingGroupProposalQuery(proposalId)); Assert.That(meetingGroupProposal.StatusCode, Is.EqualTo(MeetingGroupProposalStatus.Verified.Value)); Assert.That(meetingGroupProposal.DecisionUserId, Is.EqualTo(ExecutionContext.UserId)); var acceptedNotification = await GetLastOutboxMessage(); Assert.That(acceptedNotification.DomainEvent.MeetingGroupProposalId.Value, Is.EqualTo(proposalId)); } [Test] public async Task AcceptMeetingGroupProposal_WhenProposalIsAlreadyAccepted_BreaksMeetingGroupProposalCanBeVerifiedOnceRule() { var proposalId = await AdministrationModule.ExecuteCommandAsync(new RequestMeetingGroupProposalVerificationCommand( MeetingGroupProposalSampleData.MeetingGroupProposalId, MeetingGroupProposalSampleData.MeetingGroupProposalId, MeetingGroupProposalSampleData.Name, MeetingGroupProposalSampleData.Description, MeetingGroupProposalSampleData.LocationCity, MeetingGroupProposalSampleData.LocationCountryCode, MeetingGroupProposalSampleData.ProposalUserId, MeetingGroupProposalSampleData.ProposalDate)); await AdministrationModule.ExecuteCommandAsync( new AcceptMeetingGroupProposalCommand(MeetingGroupProposalSampleData.MeetingGroupProposalId)); AssertBrokenRule(async () => await AdministrationModule.ExecuteCommandAsync( new AcceptMeetingGroupProposalCommand(MeetingGroupProposalSampleData.MeetingGroupProposalId))); } } } ================================================ FILE: src/Modules/Administration/Tests/IntegrationTests/Members/CreateMemberTests.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Members.CreateMember; using CompanyName.MyMeetings.Modules.Administration.Application.Members.GetMember; using CompanyName.MyMeetings.Modules.Administration.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Administration.IntegrationTests.Members { [TestFixture] public class CreateMemberTests : TestBase { [Test] public async Task CreateMember_Test() { var memberId = await AdministrationModule.ExecuteCommandAsync(new CreateMemberCommand( Guid.NewGuid(), MemberSampleData.MemberId, MemberSampleData.Login, MemberSampleData.Email, MemberSampleData.FirstName, MemberSampleData.LastName, MemberSampleData.Name)); var member = await AdministrationModule.ExecuteQueryAsync(new GetMemberQuery(memberId)); Assert.That(member.Id, Is.EqualTo(MemberSampleData.MemberId)); Assert.That(member.LastName, Is.EqualTo(MemberSampleData.LastName)); Assert.That(member.Login, Is.EqualTo(MemberSampleData.Login)); Assert.That(member.Email, Is.EqualTo(MemberSampleData.Email)); Assert.That(member.FirstName, Is.EqualTo(MemberSampleData.FirstName)); Assert.That(member.Name, Is.EqualTo(MemberSampleData.Name)); } } } ================================================ FILE: src/Modules/Administration/Tests/IntegrationTests/Members/MemberSampleData.cs ================================================ namespace CompanyName.MyMeetings.Modules.Administration.IntegrationTests.Members { public struct MemberSampleData { public static Guid MemberId = Guid.Parse("60fa7e87-b0ef-484f-af7a-3ce4f35b0fa7"); 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 Name = "John Doe"; } } ================================================ FILE: src/Modules/Administration/Tests/IntegrationTests/SeedWork/ExecutionContextMock.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; namespace CompanyName.MyMeetings.Modules.Administration.IntegrationTests.SeedWork { public class ExecutionContextMock : IExecutionContextAccessor { public ExecutionContextMock(Guid userId) { UserId = userId; } public Guid UserId { get; } public Guid CorrelationId { get; } public bool IsAvailable { get; } } } ================================================ FILE: src/Modules/Administration/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs ================================================ using System.Data; using System.Reflection; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.AcceptMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Processing.Outbox; using Dapper; using MediatR; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Administration.IntegrationTests.SeedWork { public class OutboxMessagesHelper { public static async Task> GetOutboxMessages(IDbConnection connection) { const string sql = """ SELECT [OutboxMessage].[Id], [OutboxMessage].[Type], [OutboxMessage].[Data] FROM [administration].[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(MeetingGroupProposalAcceptedNotification)).GetType(typeof(T).FullName); return JsonConvert.DeserializeObject(message.Data, type) as T; } } } ================================================ FILE: src/Modules/Administration/Tests/IntegrationTests/SeedWork/TestBase.cs ================================================ using System.Data; using System.Data.SqlClient; using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using CompanyName.MyMeetings.Modules.Administration.Infrastructure; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration; using Dapper; using MediatR; using NSubstitute; using NUnit.Framework; using Serilog; namespace CompanyName.MyMeetings.Modules.Administration.IntegrationTests.SeedWork { public class TestBase { protected string ConnectionString { get; private set; } protected ILogger Logger { get; private set; } protected IAdministrationModule AdministrationModule { get; private set; } protected IEmailSender EmailSender { get; private set; } protected ExecutionContextMock ExecutionContext { 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 = new LoggerConfiguration() .Enrich.FromLogContext() .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{Module}] [{Context}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); EmailSender = Substitute.For(); ExecutionContext = new ExecutionContextMock(Guid.NewGuid()); AdministrationStartup.Initialize( ConnectionString, ExecutionContext, Logger, null); AdministrationModule = new AdministrationModule(); } 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()); } } protected static void AssertBrokenRule(AsyncTestDelegate testDelegate) where TRule : class, IBusinessRule { var message = $"Expected {typeof(TRule).Name} broken rule"; var businessRuleValidationException = Assert.CatchAsync(testDelegate, message); if (businessRuleValidationException != null) { Assert.That(businessRuleValidationException.BrokenRule, Is.TypeOf(), message); } } private static async Task ClearDatabase(IDbConnection connection) { const string sql = "DELETE FROM [administration].[InboxMessages] " + "DELETE FROM [administration].[InternalCommands] " + "DELETE FROM [administration].[OutboxMessages] " + "DELETE FROM [administration].[MeetingGroupProposals] " + "DELETE FROM [administration].[Members] "; await connection.ExecuteScalarAsync(sql); } } } ================================================ FILE: src/Modules/Administration/Tests/UnitTests/CompanyName.MyMeetings.Modules.Administration.Domain.UnitTests.csproj ================================================  ================================================ FILE: src/Modules/Administration/Tests/UnitTests/MeetingGroupProposals/MeetingGroupProposalTests.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals.Events; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals.Rules; using CompanyName.MyMeetings.Modules.Administration.Domain.UnitTests.SeedWork; using CompanyName.MyMeetings.Modules.Administration.Domain.Users; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Administration.Domain.UnitTests.MeetingGroupProposals { [TestFixture] public class MeetingGroupProposalTests : TestBase { [Test] public void CreateProposalToVerify_IsSuccessful() { var meetingGroupProposalId = Guid.NewGuid(); var location = MeetingGroupLocation.Create("Warsaw", "Poland"); var proposalUserId = new UserId(Guid.NewGuid()); var proposalDate = DateTime.Now; var meetingGroupProposal = MeetingGroupProposal.CreateToVerify( meetingGroupProposalId, "meetingName", "meetingDescription", location, proposalUserId, proposalDate); var meetingGroupProposalVerificationRequested = AssertPublishedDomainEvent(meetingGroupProposal); Assert.That(meetingGroupProposalVerificationRequested.MeetingGroupProposalId, Is.EqualTo(new MeetingGroupProposalId(meetingGroupProposalId))); } [Test] public void AcceptProposal_WhenDecisionIsNotMade_IsSuccessful() { var meetingGroupProposalId = Guid.NewGuid(); var location = MeetingGroupLocation.Create("Warsaw", "Poland"); var proposalUserId = new UserId(Guid.NewGuid()); var proposalDate = DateTime.Now; var meetingGroupProposal = MeetingGroupProposal.CreateToVerify( meetingGroupProposalId, "meetingName", "meetingDescription", location, proposalUserId, proposalDate); meetingGroupProposal.Accept(new UserId(Guid.NewGuid())); var meetingGroupProposalAccepted = AssertPublishedDomainEvent(meetingGroupProposal); Assert.That(meetingGroupProposalAccepted.MeetingGroupProposalId, Is.EqualTo(new MeetingGroupProposalId(meetingGroupProposalId))); } [Test] public void AcceptProposal_WhenDecisionIsMade_CanBeVerifiedOnlyOnce() { var meetingGroupProposalId = Guid.NewGuid(); var location = MeetingGroupLocation.Create("Warsaw", "Poland"); var userId = new UserId(Guid.NewGuid()); var proposalUserId = userId; var proposalDate = DateTime.Now; var meetingGroupProposal = MeetingGroupProposal.CreateToVerify( meetingGroupProposalId, "meetingName", "meetingDescription", location, proposalUserId, proposalDate); meetingGroupProposal.Accept(userId); AssertBrokenRule(() => { meetingGroupProposal.Accept(userId); }); } [Test] public void RejectProposal_WhenDecisionIsMade_CanBeVerifiedOnlyOnce() { var meetingGroupProposalId = Guid.NewGuid(); var location = MeetingGroupLocation.Create("Warsaw", "Poland"); var userId = new UserId(Guid.NewGuid()); var proposalUserId = userId; var proposalDate = DateTime.Now; var meetingGroupProposal = MeetingGroupProposal.CreateToVerify( meetingGroupProposalId, "meetingName", "meetingDescription", location, proposalUserId, proposalDate); meetingGroupProposal.Accept(userId); AssertBrokenRule(() => { meetingGroupProposal.Reject(userId, "rejectReason"); }); } [Test] public void RejectProposal_WithoutProvidedReason_CannotBeRejected() { var meetingGroupProposalId = Guid.NewGuid(); var location = MeetingGroupLocation.Create("Warsaw", "Poland"); var userId = new UserId(Guid.NewGuid()); var proposalUserId = userId; var proposalDate = DateTime.Now; var meetingGroupProposal = MeetingGroupProposal.CreateToVerify( meetingGroupProposalId, "meetingName", "meetingDescription", location, proposalUserId, proposalDate); AssertBrokenRule(() => { meetingGroupProposal.Reject(userId, string.Empty); }); } } } ================================================ FILE: src/Modules/Administration/Tests/UnitTests/Members/MemberTests.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Domain.Members; using CompanyName.MyMeetings.Modules.Administration.Domain.Members.Events; using CompanyName.MyMeetings.Modules.Administration.Domain.UnitTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Administration.Domain.UnitTests.Members { [TestFixture] public class MemberTests : TestBase { [Test] public void CreateMember_IsSuccessful() { MemberId memberId = new MemberId(Guid.NewGuid()); var member = Member.Create( memberId.Value, "memberLogin", "memberEmail@mail.com", "John", "Doe", "John Doe"); var memberCreated = AssertPublishedDomainEvent(member); Assert.That(memberCreated.MemberId, Is.EqualTo(memberId)); } } } ================================================ FILE: src/Modules/Administration/Tests/UnitTests/SeedWork/DomainEventsTestHelper.cs ================================================ using System.Collections; using System.Reflection; using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Administration.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; } } } ================================================ FILE: src/Modules/Administration/Tests/UnitTests/SeedWork/TestBase.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Administration.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 List AssertPublishedDomainEvents(Entity aggregate) where T : IDomainEvent { var domainEvents = DomainEventsTestHelper.GetAllDomainEvents(aggregate).OfType().ToList(); if (!domainEvents.Any()) { throw new Exception($"{typeof(T).Name} event not published"); } return domainEvents; } 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); } } } } ================================================ FILE: src/Modules/Meetings/Application/CompanyName.MyMeetings.Modules.Meetings.Application.csproj ================================================  ================================================ FILE: src/Modules/Meetings/Application/Configuration/Commands/ICommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands { public interface ICommandHandler : IRequestHandler where TCommand : ICommand { } public interface ICommandHandler : IRequestHandler where TCommand : ICommand { } } ================================================ FILE: src/Modules/Meetings/Application/Configuration/Commands/ICommandsScheduler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands { public interface ICommandsScheduler { Task EnqueueAsync(ICommand command); Task EnqueueAsync(ICommand command); } } ================================================ FILE: src/Modules/Meetings/Application/Configuration/Commands/InternalCommandBase.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.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; } } } ================================================ FILE: src/Modules/Meetings/Application/Configuration/Queries/IQueryHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries { public interface IQueryHandler : IRequestHandler where TQuery : IQuery { } } ================================================ FILE: src/Modules/Meetings/Application/Contracts/CommandBase.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.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; } } } ================================================ FILE: src/Modules/Meetings/Application/Contracts/ICommand.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Contracts { public interface ICommand : IRequest { Guid Id { get; } } public interface ICommand : IRequest { Guid Id { get; } } } ================================================ FILE: src/Modules/Meetings/Application/Contracts/IMeetingsModule.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.Contracts { public interface IMeetingsModule { Task ExecuteCommandAsync(ICommand command); Task ExecuteCommandAsync(ICommand command); Task ExecuteQueryAsync(IQuery query); } } ================================================ FILE: src/Modules/Meetings/Application/Contracts/IQuery.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Contracts { public interface IQuery : IRequest { } } ================================================ FILE: src/Modules/Meetings/Application/Contracts/IRecurringCommand.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.Contracts { public interface IRecurringCommand { } } ================================================ FILE: src/Modules/Meetings/Application/Contracts/QueryBase.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.Contracts { public abstract class QueryBase : IQuery { public Guid Id { get; } protected QueryBase() { Id = Guid.NewGuid(); } protected QueryBase(Guid id) { Id = id; } } } ================================================ FILE: src/Modules/Meetings/Application/Countries/CountryDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.Countries { public class CountryDto { public string Code { get; set; } public string Name { get; set; } } } ================================================ FILE: src/Modules/Meetings/Application/Countries/GetAllCountriesQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Countries { public class GetAllCountriesQuery : QueryBase> { } } ================================================ FILE: src/Modules/Meetings/Application/Countries/GetAllCountriesQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Countries { internal class GetAllCountriesQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetAllCountriesQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle(GetAllCountriesQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [Country].[Code] AS [{nameof(CountryDto.Code)}], [Country].[Name] AS [{nameof(CountryDto.Name)}] FROM [meetings].[v_Countries] AS [Country] """; return (await connection.QueryAsync(sql)).AsList(); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingCommentingConfigurations/DisableMeetingCommentingConfiguration/DisableMeetingCommentingConfigurationCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.DisableMeetingCommentingConfiguration { public class DisableMeetingCommentingConfigurationCommand : CommandBase { public Guid MeetingId { get; } public DisableMeetingCommentingConfigurationCommand(Guid meetingId) { MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingCommentingConfigurations/DisableMeetingCommentingConfiguration/DisableMeetingCommentingConfigurationCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.DisableMeetingCommentingConfiguration { internal class DisableMeetingCommentingConfigurationCommandHandler : ICommandHandler { private readonly IMeetingCommentingConfigurationRepository _meetingCommentingConfigurationRepository; private readonly IMeetingRepository _meetingRepository; private readonly IMeetingGroupRepository _meetingGroupRepository; private readonly IMemberContext _memberContext; public DisableMeetingCommentingConfigurationCommandHandler( IMeetingCommentingConfigurationRepository meetingCommentingConfigurationRepository, IMeetingGroupRepository meetingGroupRepository, IMeetingRepository meetingRepository, IMemberContext memberContext) { _meetingCommentingConfigurationRepository = meetingCommentingConfigurationRepository; _meetingGroupRepository = meetingGroupRepository; _meetingRepository = meetingRepository; _memberContext = memberContext; } public async Task Handle(DisableMeetingCommentingConfigurationCommand command, CancellationToken cancellationToken) { var meetingCommentingConfiguration = await _meetingCommentingConfigurationRepository.GetByMeetingIdAsync(new MeetingId(command.MeetingId)); if (meetingCommentingConfiguration == null) { throw new InvalidCommandException(["Meeting commenting configuration for disabling commenting must exist."]); } var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(command.MeetingId)); var meetingGroup = await _meetingGroupRepository.GetByIdAsync(meeting.GetMeetingGroupId()); meetingCommentingConfiguration.DisableCommenting(_memberContext.MemberId, meetingGroup); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingCommentingConfigurations/EnableMeetingCommentingConfiguration/EnableMeetingCommentingConfigurationCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.EnableMeetingCommentingConfiguration { public class EnableMeetingCommentingConfigurationCommand : CommandBase { public Guid MeetingId { get; } public EnableMeetingCommentingConfigurationCommand(Guid meetingId) { MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingCommentingConfigurations/EnableMeetingCommentingConfiguration/EnableMeetingCommentingConfigurationCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.EnableMeetingCommentingConfiguration { internal class EnableMeetingCommentingConfigurationCommandHandler : ICommandHandler { private readonly IMeetingCommentingConfigurationRepository _meetingCommentingConfigurationRepository; private readonly IMeetingRepository _meetingRepository; private readonly IMeetingGroupRepository _meetingGroupRepository; private readonly IMemberContext _memberContext; public EnableMeetingCommentingConfigurationCommandHandler( IMeetingCommentingConfigurationRepository meetingCommentingConfigurationRepository, IMeetingRepository meetingRepository, IMeetingGroupRepository meetingGroupRepository, IMemberContext memberContext) { _meetingCommentingConfigurationRepository = meetingCommentingConfigurationRepository; _meetingRepository = meetingRepository; _meetingGroupRepository = meetingGroupRepository; _memberContext = memberContext; } public async Task Handle(EnableMeetingCommentingConfigurationCommand command, CancellationToken cancellationToken) { var meetingCommentingConfiguration = await _meetingCommentingConfigurationRepository.GetByMeetingIdAsync(new MeetingId(command.MeetingId)); if (meetingCommentingConfiguration == null) { throw new InvalidCommandException(["Meeting commenting configuration for enabling commenting must exist."]); } var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(command.MeetingId)); var meetingGroup = await _meetingGroupRepository.GetByIdAsync(meeting.GetMeetingGroupId()); meetingCommentingConfiguration.EnableCommenting(_memberContext.MemberId, meetingGroup); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingCommentingConfigurations/GetMeetingCommentingConfiguration/GetMeetingCommentingConfigurationQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.GetMeetingCommentingConfiguration { public class GetMeetingCommentingConfigurationQuery : QueryBase { public Guid MeetingId { get; } public GetMeetingCommentingConfigurationQuery(Guid meetingId) { MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingCommentingConfigurations/GetMeetingCommentingConfiguration/GetMeetingCommentingConfigurationQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.GetMeetingCommentingConfiguration { internal class GetMeetingCommentingConfigurationQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetMeetingCommentingConfigurationQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(GetMeetingCommentingConfigurationQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MeetingCommentingConfiguration].[MeetingId] AS [{nameof(MeetingCommentingConfigurationDto.MeetingId)}], [MeetingCommentingConfiguration].[IsCommentingEnabled] AS [{nameof(MeetingCommentingConfigurationDto.IsCommentingEnabled)}] FROM [meetings].[MeetingCommentingConfigurations] AS [MeetingCommentingConfiguration] WHERE [MeetingCommentingConfiguration].[MeetingId] = @MeetingId """; return await connection.QuerySingleOrDefaultAsync(sql, new { query.MeetingId }); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingCommentingConfigurations/GetMeetingCommentingConfiguration/MeetingCommentingConfigurationDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.GetMeetingCommentingConfiguration { public class MeetingCommentingConfigurationDto { public Guid MeetingId { get; } public bool IsCommentingEnabled { get; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingCommentingConfigurations/MeetingCreatedEventHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations { internal class MeetingCreatedEventHandler : INotificationHandler { private readonly IMeetingRepository _meetingRepository; private readonly IMeetingCommentingConfigurationRepository _meetingCommentingConfigurationRepository; public MeetingCreatedEventHandler( IMeetingRepository meetingRepository, IMeetingCommentingConfigurationRepository meetingCommentingConfigurationRepository) { _meetingRepository = meetingRepository; _meetingCommentingConfigurationRepository = meetingCommentingConfigurationRepository; } public async Task Handle(MeetingCreatedDomainEvent @event, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(@event.MeetingId); var meetingCommentingConfiguration = meeting.CreateCommentingConfiguration(); await _meetingCommentingConfigurationRepository.AddAsync(meetingCommentingConfiguration); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/AddMeetingComment/AddMeetingCommentCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingComment { public class AddMeetingCommentCommand : CommandBase { public Guid MeetingId { get; } public string Comment { get; } public AddMeetingCommentCommand(Guid meetingId, string comment) { MeetingId = meetingId; Comment = comment; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/AddMeetingComment/AddMeetingCommentCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingComment { internal class AddMeetingCommentCommandHandler : ICommandHandler { private readonly IMeetingRepository _meetingRepository; private readonly IMeetingCommentRepository _meetingCommentRepository; private readonly IMeetingGroupRepository _meetingGroupRepository; private readonly IMeetingCommentingConfigurationRepository _meetingCommentingConfigurationRepository; private readonly IMemberContext _memberContext; public AddMeetingCommentCommandHandler( IMeetingRepository meetingRepository, IMeetingCommentRepository meetingCommentRepository, IMeetingGroupRepository meetingGroupRepository, IMeetingCommentingConfigurationRepository meetingCommentingConfigurationRepository, IMemberContext memberContext) { _meetingGroupRepository = meetingGroupRepository; _meetingRepository = meetingRepository; _meetingCommentingConfigurationRepository = meetingCommentingConfigurationRepository; _meetingCommentRepository = meetingCommentRepository; _memberContext = memberContext; } public async Task Handle(AddMeetingCommentCommand command, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(command.MeetingId)); if (meeting == null) { throw new InvalidCommandException(["Meeting for adding comment must exist."]); } var meetingGroup = await _meetingGroupRepository.GetByIdAsync(meeting.GetMeetingGroupId()); var meetingCommentingConfiguration = await _meetingCommentingConfigurationRepository.GetByMeetingIdAsync(new MeetingId(command.MeetingId)); var meetingComment = meeting.AddComment(_memberContext.MemberId, command.Comment, meetingGroup, meetingCommentingConfiguration); await _meetingCommentRepository.AddAsync(meetingComment); return meetingComment.Id.Value; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/AddMeetingComment/AddMeetingCommentCommandValidator.cs ================================================ using FluentValidation; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingComment { internal class AddMeetingCommentCommandValidator : AbstractValidator { public AddMeetingCommentCommandValidator() { this.RuleFor(c => c.MeetingId).NotEmpty() .WithMessage("Id of meeting member cannot be empty."); this.RuleFor(c => c.Comment).NotNull().NotEmpty() .WithMessage("Comment cannot be null or empty."); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/AddMeetingCommentLike/AddMeetingCommentLikeCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingCommentLike { public class AddMeetingCommentLikeCommand : CommandBase { public Guid MeetingCommentId { get; } public AddMeetingCommentLikeCommand(Guid meetingCommentId) { MeetingCommentId = meetingCommentId; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/AddMeetingCommentLike/AddMeetingCommentLikeCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingCommentLike { internal class AddMeetingCommentLikeCommandHandler : ICommandHandler { private readonly IMeetingCommentRepository _meetingCommentRepository; private readonly IMeetingMemberCommentLikesRepository _meetingMemberCommentLikesRepository; private readonly IMemberContext _memberContext; private readonly ISqlConnectionFactory _sqlConnectionFactory; public AddMeetingCommentLikeCommandHandler( IMeetingCommentRepository meetingCommentRepository, IMeetingMemberCommentLikesRepository meetingMemberCommentLikesRepository, IMemberContext memberContext, ISqlConnectionFactory sqlConnectionFactory) { _meetingCommentRepository = meetingCommentRepository; _memberContext = memberContext; _sqlConnectionFactory = sqlConnectionFactory; _meetingMemberCommentLikesRepository = meetingMemberCommentLikesRepository; } public async Task Handle(AddMeetingCommentLikeCommand request, CancellationToken cancellationToken) { var meetingComment = await _meetingCommentRepository.GetByIdAsync(new MeetingCommentId(request.MeetingCommentId)); if (meetingComment == null) { throw new InvalidCommandException(["To add like the comment must exist."]); } var connection = _sqlConnectionFactory.GetOpenConnection(); var likerMeetingGroupMemberData = await MembersQueryHelper.GetMeetingGroupMember(_memberContext.MemberId, meetingComment.GetMeetingId(), connection); var meetingMemeberCommentLikesCount = await _meetingMemberCommentLikesRepository.CountMemberCommentLikesAsync( _memberContext.MemberId, new MeetingCommentId(request.MeetingCommentId)); var like = meetingComment.Like(_memberContext.MemberId, likerMeetingGroupMemberData, meetingMemeberCommentLikesCount); await _meetingMemberCommentLikesRepository.AddAsync(like); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/AddMeetingCommentReply/AddReplyToMeetingCommentCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingCommentReply { public class AddReplyToMeetingCommentCommand : CommandBase { public Guid InReplyToCommentId { get; } public string Reply { get; } public AddReplyToMeetingCommentCommand(Guid inReplyToCommentId, string reply) { InReplyToCommentId = inReplyToCommentId; Reply = reply; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/AddMeetingCommentReply/AddReplyToMeetingCommentCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingCommentReply { internal class AddReplyToMeetingCommentCommandHandler : ICommandHandler { private readonly IMeetingCommentRepository _meetingCommentRepository; private readonly IMeetingRepository _meetingRepository; private readonly IMeetingGroupRepository _meetingGroupRepository; private readonly IMeetingCommentingConfigurationRepository _meetingCommentingConfigurationRepository; private readonly IMemberContext _memberContext; internal AddReplyToMeetingCommentCommandHandler(IMeetingCommentRepository meetingCommentRepository, IMeetingRepository meetingRepository, IMeetingGroupRepository meetingGroupRepository, IMeetingCommentingConfigurationRepository meetingCommentingConfigurationRepository, IMemberContext memberContext) { _meetingCommentRepository = meetingCommentRepository; _meetingRepository = meetingRepository; _meetingGroupRepository = meetingGroupRepository; _meetingCommentingConfigurationRepository = meetingCommentingConfigurationRepository; _memberContext = memberContext; } public async Task Handle(AddReplyToMeetingCommentCommand command, CancellationToken cancellationToken) { var meetingComment = await _meetingCommentRepository.GetByIdAsync(new MeetingCommentId(command.InReplyToCommentId)); if (meetingComment == null) { throw new InvalidCommandException(["To create reply the comment must exist."]); } var meeting = await _meetingRepository.GetByIdAsync(meetingComment.GetMeetingId()); var meetingGroup = await _meetingGroupRepository.GetByIdAsync(meeting.GetMeetingGroupId()); var meetingCommentingConfiguration = await _meetingCommentingConfigurationRepository.GetByMeetingIdAsync(meetingComment.GetMeetingId()); var replyToComment = meetingComment.Reply(_memberContext.MemberId, command.Reply, meetingGroup, meetingCommentingConfiguration); await _meetingCommentRepository.AddAsync(replyToComment); return replyToComment.Id.Value; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/EditMeetingComment/EditMeetingCommentCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.EditMeetingComment { public class EditMeetingCommentCommand : CommandBase { public Guid MeetingCommentId { get; } public string EditedComment { get; } public EditMeetingCommentCommand(Guid meetingCommentId, string editedComment) { EditedComment = editedComment; MeetingCommentId = meetingCommentId; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/EditMeetingComment/EditMeetingCommentCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.EditMeetingComment { internal class EditMeetingCommentCommandHandler : ICommandHandler { private readonly IMeetingCommentRepository _meetingCommentRepository; private readonly IMeetingCommentingConfigurationRepository _meetingCommentingConfigurationRepository; private readonly IMemberContext _memberContext; internal EditMeetingCommentCommandHandler( IMeetingCommentRepository meetingCommentRepository, IMeetingCommentingConfigurationRepository meetingCommentingConfigurationRepository, IMemberContext memberContext) { _meetingCommentRepository = meetingCommentRepository; _meetingCommentingConfigurationRepository = meetingCommentingConfigurationRepository; _memberContext = memberContext; } public async Task Handle(EditMeetingCommentCommand command, CancellationToken cancellationToken) { var meetingComment = await _meetingCommentRepository.GetByIdAsync(new MeetingCommentId(command.MeetingCommentId)); if (meetingComment == null) { throw new InvalidCommandException(["Meeting comment for editing must exist."]); } var meetingCommentingConfiguration = await _meetingCommentingConfigurationRepository.GetByMeetingIdAsync(meetingComment.GetMeetingId()); meetingComment.Edit(_memberContext.MemberId, command.EditedComment, meetingCommentingConfiguration); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/EditMeetingComment/EditMeetingCommentCommandValidator.cs ================================================ using FluentValidation; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.EditMeetingComment { internal class EditMeetingCommentCommandValidator : AbstractValidator { public EditMeetingCommentCommandValidator() { RuleFor(c => c.EditedComment).NotNull().NotEmpty() .WithMessage("Comment cannot be null or empty."); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/GetMeetingCommentLikers/GetMeetingCommentLikersQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingCommentLikers { public class GetMeetingCommentLikersQuery : IQuery> { public Guid MeetingCommentId { get; } public GetMeetingCommentLikersQuery(Guid meetingCommentId) { MeetingCommentId = meetingCommentId; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/GetMeetingCommentLikers/GetMeetingCommentLikersQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingCommentLikers { internal class GetMeetingCommentLikersQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetMeetingCommentLikersQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle(GetMeetingCommentLikersQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [Liker].[Id] as [{nameof(MeetingCommentLikerDto.Id)}], [Liker].[Name] as [{nameof(MeetingCommentLikerDto.Name)}] FROM [meetings].[Members] AS [Liker] INNER JOIN [meetings].[MeetingMemberCommentLikes] AS [Like] ON [Liker].[Id] = [Like].[MemberId] WHERE [Like].[MeetingCommentId] = @MeetingCommentId """; var meetingCommentLikers = await connection.QueryAsync(sql, new { query.MeetingCommentId }); return meetingCommentLikers.AsList(); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/GetMeetingCommentLikers/MeetingCommentLikerDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingCommentLikers { public class MeetingCommentLikerDto { public Guid Id { get; set; } public string Name { get; set; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/GetMeetingComments/GetMeetingCommentsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingComments { public class GetMeetingCommentsQuery : QueryBase> { public Guid MeetingId { get; } public GetMeetingCommentsQuery(Guid meetingId) { MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/GetMeetingComments/GetMeetingCommentsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingComments { internal class GetMeetingCommentsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetMeetingCommentsQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle(GetMeetingCommentsQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MeetingComment].[Id] AS [{nameof(MeetingCommentDto.Id)}], [MeetingComment].[InReplyToCommentId] AS [{nameof(MeetingCommentDto.InReplyToCommentId)}], [MeetingComment].[AuthorId] AS [{nameof(MeetingCommentDto.AuthorId)}], [MeetingComment].[Comment] AS [{nameof(MeetingCommentDto.Comment)}], [MeetingComment].[CreateDate] AS [{nameof(MeetingCommentDto.CreateDate)}], [MeetingComment].[EditDate] AS [{nameof(MeetingCommentDto.EditDate)}], [MeetingComment].[LikesCount] AS [{nameof(MeetingCommentDto.LikesCount)}] FROM [meetings].[v_MeetingComments] AS [MeetingComment] WHERE [MeetingComment].[MeetingId] = @MeetingId """; var meetingComments = await connection.QueryAsync(sql, new { query.MeetingId }); return meetingComments.AsList(); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/GetMeetingComments/MeetingCommentDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingComments { public class MeetingCommentDto { public Guid Id { get; } public Guid? InReplyToCommentId { get; } public Guid AuthorId { get; } public string Comment { get; } public DateTime CreateDate { get; } public DateTime? EditDate { get; } public int LikesCount { get; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/MeetingCommentLikedNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes.Events; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments { public class MeetingCommentLikedNotification : DomainNotificationBase { public MeetingCommentLikedNotification(MeetingCommentLikedDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/MeetingCommentLikedNotificationHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using Dapper; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments { internal class MeetingCommentLikedNotificationHandler : INotificationHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public MeetingCommentLikedNotificationHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(MeetingCommentLikedNotification notification, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = """ UPDATE [meetings].[MeetingComments] SET [LikesCount] = (SELECT count(*) FROM [meetings].[MeetingMemberCommentLikes] WHERE [MeetingCommentId] = @MeetingCommentId) WHERE [Id] = @MeetingCommentId; """; await connection.ExecuteAsync( sql, new { MeetingCommentId = notification.DomainEvent.MeetingCommentId.Value }); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/MeetingCommentUnlikeNotificationHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using Dapper; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments { public class MeetingCommentUnlikeNotificationHandler : INotificationHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public MeetingCommentUnlikeNotificationHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(MeetingCommentUnlikedNotification notification, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = "UPDATE [meetings].[MeetingComments] " + "SET [LikesCount] = " + "(SELECT count(*) FROM [meetings].[MeetingMemberCommentLikes] WHERE [MeetingCommentId] = @MeetingCommentId) " + "WHERE [Id] = @MeetingCommentId;"; await connection.ExecuteAsync( sql, new { MeetingCommentId = notification.DomainEvent.MeetingCommentId.Value }); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/MeetingCommentUnlikedNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes.Events; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments { public class MeetingCommentUnlikedNotification : DomainNotificationBase { public MeetingCommentUnlikedNotification(MeetingCommentUnlikedDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/RemoveMeetingComment/RemoveMeetingCommentCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.RemoveMeetingComment { public class RemoveMeetingCommentCommand : CommandBase { public Guid MeetingCommentId { get; } public string Reason { get; } public RemoveMeetingCommentCommand(Guid meetingCommentId, string reason) { MeetingCommentId = meetingCommentId; Reason = reason; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/RemoveMeetingComment/RemoveMeetingCommentCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.RemoveMeetingComment { internal class RemoveMeetingCommentCommandHandler : ICommandHandler { private readonly IMeetingCommentRepository _meetingCommentRepository; private readonly IMeetingRepository _meetingRepository; private readonly IMeetingGroupRepository _meetingGroupRepository; private readonly IMemberContext _memberContext; internal RemoveMeetingCommentCommandHandler(IMeetingCommentRepository meetingCommentRepository, IMeetingRepository meetingRepository, IMeetingGroupRepository meetingGroupRepository, IMemberContext memberContext) { _meetingCommentRepository = meetingCommentRepository; _meetingRepository = meetingRepository; _meetingGroupRepository = meetingGroupRepository; _memberContext = memberContext; } public async Task Handle(RemoveMeetingCommentCommand command, CancellationToken cancellationToken) { var meetingComment = await _meetingCommentRepository.GetByIdAsync(new MeetingCommentId(command.MeetingCommentId)); if (meetingComment == null) { throw new InvalidCommandException(["Meeting comment for removing must exist."]); } var meeting = await _meetingRepository.GetByIdAsync(meetingComment.GetMeetingId()); var meetingGroup = await _meetingGroupRepository.GetByIdAsync(meeting.GetMeetingGroupId()); meetingComment.Remove(_memberContext.MemberId, meetingGroup, command.Reason); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/RemoveMeetingCommentLike/RemoveMeetingCommentLikeCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.RemoveMeetingCommentLike { public class RemoveMeetingCommentLikeCommand : CommandBase { public Guid MeetingCommentId { get; } public RemoveMeetingCommentLikeCommand(Guid meetingCommentId) { MeetingCommentId = meetingCommentId; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingComments/RemoveMeetingCommentLike/RemoveMeetingCommentLikeCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.RemoveMeetingCommentLike { internal class RemoveMeetingCommentLikeCommandHandler : ICommandHandler { private readonly IMeetingMemberCommentLikesRepository _meetingMemberCommentLikesRepository; private readonly IMemberContext _memberContext; internal RemoveMeetingCommentLikeCommandHandler(IMeetingMemberCommentLikesRepository meetingMemberCommentLikesRepository, IMemberContext memberContext) { _meetingMemberCommentLikesRepository = meetingMemberCommentLikesRepository; _memberContext = memberContext; } public async Task Handle(RemoveMeetingCommentLikeCommand command, CancellationToken cancellationToken) { var commentLike = await _meetingMemberCommentLikesRepository.GetAsync(_memberContext.MemberId, new MeetingCommentId(command.MeetingCommentId)); if (commentLike == null) { throw new InvalidCommandException(["Meeting comment like for removing must exist."]); } commentLike.Remove(); _meetingMemberCommentLikesRepository.Remove(commentLike); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/AcceptMeetingGroupProposal/AcceptMeetingGroupProposalCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.AcceptMeetingGroupProposal { public class AcceptMeetingGroupProposalCommand : InternalCommandBase { public Guid MeetingGroupProposalId { get; } [JsonConstructor] public AcceptMeetingGroupProposalCommand(Guid id, Guid meetingGroupProposalId) : base(id) { this.MeetingGroupProposalId = meetingGroupProposalId; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/AcceptMeetingGroupProposal/AcceptMeetingGroupProposalCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.AcceptMeetingGroupProposal { internal class AcceptMeetingGroupProposalCommandHandler : ICommandHandler { private readonly IMeetingGroupProposalRepository _meetingGroupProposalRepository; public AcceptMeetingGroupProposalCommandHandler(IMeetingGroupProposalRepository meetingGroupProposalRepository) { _meetingGroupProposalRepository = meetingGroupProposalRepository; } public async Task Handle(AcceptMeetingGroupProposalCommand request, CancellationToken cancellationToken) { var meetingGroupProposal = await _meetingGroupProposalRepository.GetByIdAsync(new MeetingGroupProposalId(request.MeetingGroupProposalId)); meetingGroupProposal.Accept(); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/AcceptMeetingGroupProposal/AcceptMeetingGroupProposalCommandValidator.cs ================================================ using FluentValidation; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.AcceptMeetingGroupProposal { internal class AcceptMeetingGroupProposalCommandValidator : AbstractValidator { public AcceptMeetingGroupProposalCommandValidator() { this.RuleFor(x => x.MeetingGroupProposalId).NotEmpty() .WithMessage("Id of meeting group proposal cannot be empty"); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/AcceptMeetingGroupProposal/MeetingGroupProposalAcceptedNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals.Events; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.AcceptMeetingGroupProposal { public class MeetingGroupProposalAcceptedNotification : DomainNotificationBase { public MeetingGroupProposalAcceptedNotification(MeetingGroupProposalAcceptedDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/AcceptMeetingGroupProposal/MeetingGroupProposalAcceptedNotificationHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.CreateNewMeetingGroup; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.AcceptMeetingGroupProposal { internal class MeetingGroupProposalAcceptedNotificationHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; internal MeetingGroupProposalAcceptedNotificationHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(MeetingGroupProposalAcceptedNotification notification, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync( new CreateNewMeetingGroupCommand( Guid.NewGuid(), notification.DomainEvent.MeetingGroupProposalId)); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/GetAllMeetingGroupProposals/GetAllMeetingGroupProposalsQuery.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Queries; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetMeetingGroupProposal; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetAllMeetingGroupProposals { public class GetAllMeetingGroupProposalsQuery : QueryBase>, IPagedQuery { public GetAllMeetingGroupProposalsQuery(int? page, int? perPage) { Page = page; PerPage = perPage; } public int? Page { get; } public int? PerPage { get; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/GetAllMeetingGroupProposals/GetAllMeetingGroupProposalsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Queries; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetMeetingGroupProposal; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetAllMeetingGroupProposals { internal class GetAllMeetingGroupProposalsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetAllMeetingGroupProposalsQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle(GetAllMeetingGroupProposalsQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); var parameters = new DynamicParameters(); var pageData = PagedQueryHelper.GetPageData(query); parameters.Add(nameof(PagedQueryHelper.Offset), pageData.Offset); parameters.Add(nameof(PagedQueryHelper.Next), pageData.Next); var sql = $""" SELECT [MeetingGroupProposal].[Id] AS [{nameof(MeetingGroupProposalDto.Id)}], [MeetingGroupProposal].[Name] AS [{nameof(MeetingGroupProposalDto.Name)}], [MeetingGroupProposal].[ProposalUserId] AS [{nameof(MeetingGroupProposalDto.ProposalUserId)}], [MeetingGroupProposal].[LocationCity] AS [{nameof(MeetingGroupProposalDto.LocationCity)}], [MeetingGroupProposal].[LocationCountryCode] AS [{nameof(MeetingGroupProposalDto.LocationCountryCode)}], [MeetingGroupProposal].[Description] AS [{nameof(MeetingGroupProposalDto.Description)}], [MeetingGroupProposal].[ProposalDate] AS [{nameof(MeetingGroupProposalDto.ProposalDate)}], [MeetingGroupProposal].[StatusCode] AS [{nameof(MeetingGroupProposalDto.StatusCode)}] FROM [meetings].[v_MeetingGroupProposals] AS [MeetingGroupProposal] ORDER BY [MeetingGroupProposal].[Name] """; sql = PagedQueryHelper.AppendPageStatement(sql); var meetingGroupProposals = await connection.QueryAsync(sql, parameters); return meetingGroupProposals.AsList(); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/GetMeetingGroupProposal/GetMeetingGroupProposalQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetMeetingGroupProposal { public class GetMeetingGroupProposalQuery : QueryBase { public GetMeetingGroupProposalQuery(Guid meetingGroupProposalId) { MeetingGroupProposalId = meetingGroupProposalId; } public Guid MeetingGroupProposalId { get; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/GetMeetingGroupProposal/GetMeetingGroupProposalQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetMeetingGroupProposal { internal class GetMeetingGroupProposalQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetMeetingGroupProposalQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(GetMeetingGroupProposalQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MeetingGroupProposal].[Id] AS [{nameof(MeetingGroupProposalDto.Id)}], [MeetingGroupProposal].[Name] AS [{nameof(MeetingGroupProposalDto.Name)}], [MeetingGroupProposal].[ProposalUserId] AS [{nameof(MeetingGroupProposalDto.ProposalUserId)}], [MeetingGroupProposal].[LocationCity] AS [{nameof(MeetingGroupProposalDto.LocationCity)}], [MeetingGroupProposal].[LocationCountryCode] AS [{nameof(MeetingGroupProposalDto.LocationCountryCode)}], [MeetingGroupProposal].[Description] AS [{nameof(MeetingGroupProposalDto.Description)}], [MeetingGroupProposal].[ProposalDate] AS [{nameof(MeetingGroupProposalDto.ProposalDate)}], [MeetingGroupProposal].[StatusCode] AS [{nameof(MeetingGroupProposalDto.StatusCode)}] FROM [meetings].[v_MeetingGroupProposals] AS [MeetingGroupProposal] WHERE [MeetingGroupProposal].[Id] = @MeetingGroupProposalId """; return await connection.QuerySingleAsync(sql, new { query.MeetingGroupProposalId }); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/GetMeetingGroupProposal/MeetingGroupProposalDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetMeetingGroupProposal { public class MeetingGroupProposalDto { public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } public string LocationCity { get; set; } public string LocationCountryCode { get; set; } public Guid ProposalUserId { get; set; } public DateTime ProposalDate { get; set; } public string StatusCode { get; set; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/GetMemberMeetingGroupProposals/GetMemberMeetingGroupProposalsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetMeetingGroupProposal; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetMemberMeetingGroupProposals { public class GetMemberMeetingGroupProposalsQuery : QueryBase> { public GetMemberMeetingGroupProposalsQuery() { } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/GetMemberMeetingGroupProposals/GetMemberMeetingGroupProposalsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetMemberMeetingGroupProposals { internal class GetMemberMeetingGroupProposalsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly IMemberContext _memberContext; public GetMemberMeetingGroupProposalsQueryHandler( ISqlConnectionFactory sqlConnectionFactory, IMemberContext memberContext) { _sqlConnectionFactory = sqlConnectionFactory; _memberContext = memberContext; } public async Task> Handle(GetMemberMeetingGroupProposalsQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MeetingGroupProposal].[Id] AS [{nameof(MeetingGroupProposalDto.Id)}], [MeetingGroupProposal].[Name] AS [{nameof(MeetingGroupProposalDto.Name)}], [MeetingGroupProposal].[ProposalUserId] AS [{nameof(MeetingGroupProposalDto.ProposalUserId)}], [MeetingGroupProposal].[LocationCity] AS [{nameof(MeetingGroupProposalDto.LocationCity)}], [MeetingGroupProposal].[LocationCountryCode] AS [{nameof(MeetingGroupProposalDto.LocationCountryCode)}], [MeetingGroupProposal].[Description] AS [{nameof(MeetingGroupProposalDto.Description)}], [MeetingGroupProposal].[ProposalDate] AS [{nameof(MeetingGroupProposalDto.ProposalDate)}], [MeetingGroupProposal].[StatusCode] AS [{nameof(MeetingGroupProposalDto.StatusCode)}] FROM [meetings].[v_MeetingGroupProposals] AS [MeetingGroupProposal] WHERE [MeetingGroupProposal].ProposalUserId = @MemberId ORDER BY [MeetingGroupProposal].[Name] """; var meetingGroupProposals = await connection.QueryAsync( sql, new { MemberId = _memberContext.MemberId.Value }); return meetingGroupProposals.AsList(); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/MeetingGroupProposalAcceptedIntegrationEventHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.IntegrationEvents.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.AcceptMeetingGroupProposal; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals { public class MeetingGroupProposalAcceptedIntegrationEventHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; public MeetingGroupProposalAcceptedIntegrationEventHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(MeetingGroupProposalAcceptedIntegrationEvent notification, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync(new AcceptMeetingGroupProposalCommand( Guid.NewGuid(), notification.MeetingGroupProposalId)); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/MeetingGroupProposedNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals.Events; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals { public class MeetingGroupProposedNotification : DomainNotificationBase { [JsonConstructor] public MeetingGroupProposedNotification(MeetingGroupProposedDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/MeetingGroupProposedNotificationHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals { public class MeetingGroupProposedNotificationHandler : INotificationHandler { private readonly IEventsBus _eventsBus; public MeetingGroupProposedNotificationHandler(IEventsBus eventsBus) { _eventsBus = eventsBus; } public async Task Handle(MeetingGroupProposedNotification notification, CancellationToken cancellationToken) { await _eventsBus.Publish(new MeetingGroupProposedIntegrationEvent( notification.Id, notification.DomainEvent.OccurredOn, notification.DomainEvent.MeetingGroupProposalId.Value, notification.DomainEvent.Name, notification.DomainEvent.Description, notification.DomainEvent.LocationCity, notification.DomainEvent.LocationCountryCode, notification.DomainEvent.ProposalUserId.Value, notification.DomainEvent.ProposalDate)); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/ProposeMeetingGroup/ProposeMeetingGroupCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.ProposeMeetingGroup { public class ProposeMeetingGroupCommand : CommandBase { public ProposeMeetingGroupCommand(string name, string description, string locationCity, string locationCountryCode) { Name = name; Description = description; LocationCity = locationCity; LocationCountryCode = locationCountryCode; } public string Name { get; } public string Description { get; } public string LocationCity { get; } public string LocationCountryCode { get; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/ProposeMeetingGroup/ProposeMeetingGroupCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.ProposeMeetingGroup { internal class ProposeMeetingGroupCommandHandler : ICommandHandler { private readonly IMeetingGroupProposalRepository _meetingGroupProposalRepository; private readonly IMemberContext _memberContext; internal ProposeMeetingGroupCommandHandler( IMeetingGroupProposalRepository meetingGroupProposalRepository, IMemberContext memberContext) { _meetingGroupProposalRepository = meetingGroupProposalRepository; _memberContext = memberContext; } public async Task Handle(ProposeMeetingGroupCommand request, CancellationToken cancellationToken) { var meetingGroupProposal = MeetingGroupProposal.ProposeNew( request.Name, request.Description, MeetingGroupLocation.CreateNew(request.LocationCity, request.LocationCountryCode), _memberContext.MemberId); await _meetingGroupProposalRepository.AddAsync(meetingGroupProposal); return meetingGroupProposal.Id.Value; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroupProposals/ProposeMeetingGroup/ProposeMeetingGroupCommandValidator.cs ================================================ using FluentValidation; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.ProposeMeetingGroup { internal class ProposeMeetingGroupCommandValidator : AbstractValidator { public ProposeMeetingGroupCommandValidator() { this.RuleFor(x => x.Name).NotEmpty().WithMessage("Meeting group name cannot be empty"); this.RuleFor(x => x.LocationCity).NotEmpty().WithMessage("Meeting group city cannot be empty"); this.RuleFor(x => x.LocationCountryCode).NotEmpty().WithMessage("Meeting country code cannot be empty"); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/CreateNewMeetingGroup/CreateNewMeetingGroupCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.CreateNewMeetingGroup { public class CreateNewMeetingGroupCommand : InternalCommandBase { [JsonConstructor] public CreateNewMeetingGroupCommand(Guid id, MeetingGroupProposalId meetingGroupProposalId) : base(id) { this.MeetingGroupProposalId = meetingGroupProposalId; } internal MeetingGroupProposalId MeetingGroupProposalId { get; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/CreateNewMeetingGroup/CreateNewMeetingGroupCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.CreateNewMeetingGroup { internal class CreateNewMeetingGroupCommandHandler : ICommandHandler { private readonly IMeetingGroupRepository _meetingGroupRepository; private readonly IMeetingGroupProposalRepository _meetingGroupProposalRepository; internal CreateNewMeetingGroupCommandHandler( IMeetingGroupRepository meetingGroupRepository, IMeetingGroupProposalRepository meetingGroupProposalRepository) { _meetingGroupRepository = meetingGroupRepository; _meetingGroupProposalRepository = meetingGroupProposalRepository; } public async Task Handle(CreateNewMeetingGroupCommand request, CancellationToken cancellationToken) { var meetingGroupProposal = await _meetingGroupProposalRepository.GetByIdAsync(request.MeetingGroupProposalId); var meetingGroup = meetingGroupProposal.CreateMeetingGroup(); await _meetingGroupRepository.AddAsync(meetingGroup); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/EditMeetingGroupGeneralAttributes/EditMeetingGroupGeneralAttributesCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.EditMeetingGroupGeneralAttributes { public class EditMeetingGroupGeneralAttributesCommand : CommandBase { public EditMeetingGroupGeneralAttributesCommand(Guid meetingGroupId, string name, string description, string locationCity, string locationCountry) { MeetingGroupId = meetingGroupId; Name = name; Description = description; LocationCity = locationCity; LocationCountry = locationCountry; } public string LocationCountry { get; } internal Guid MeetingGroupId { get; } internal string Name { get; } internal string Description { get; } internal string LocationCity { get; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/EditMeetingGroupGeneralAttributes/EditMeetingGroupGeneralAttributesCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.EditMeetingGroupGeneralAttributes { internal class EditMeetingGroupGeneralAttributesCommandHandler : ICommandHandler { private readonly IMemberContext _memberContext; private readonly IMeetingGroupRepository _meetingGroupRepository; internal EditMeetingGroupGeneralAttributesCommandHandler(IMemberContext memberContext, IMeetingGroupRepository meetingGroupRepository) { _memberContext = memberContext; _meetingGroupRepository = meetingGroupRepository; } public async Task Handle(EditMeetingGroupGeneralAttributesCommand request, CancellationToken cancellationToken) { MeetingGroup meetingGroup = await _meetingGroupRepository.GetByIdAsync(new MeetingGroupId(request.MeetingGroupId)); meetingGroup.EditGeneralAttributes(request.Name, request.Description, MeetingGroupLocation.CreateNew(request.LocationCity, request.LocationCountry)); await _meetingGroupRepository.Commit(); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/GetAllMeetingGroups/GetAllMeetingGroupsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetAllMeetingGroups { public class GetAllMeetingGroupsQuery : IQuery> { } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/GetAllMeetingGroups/GetAllMeetingGroupsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetAllMeetingGroups { internal class GetAllMeetingGroupsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; internal GetAllMeetingGroupsQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle(GetAllMeetingGroupsQuery request, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MeetingGroup].[Id] as [{nameof(MeetingGroupDto.Id)}] , [MeetingGroup].[Name] as [{nameof(MeetingGroupDto.Name)}], [MeetingGroup].[Description] as [{nameof(MeetingGroupDto.Description)}], [MeetingGroup].[LocationCountryCode] as [{nameof(MeetingGroupDto.LocationCountryCode)}], [MeetingGroup].[LocationCity] as [{nameof(MeetingGroupDto.LocationCity)}] FROM [meetings].[v_MeetingGroups] AS [MeetingGroup] """; var meetingGroups = await connection.QueryAsync(sql); return meetingGroups.AsList(); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/GetAllMeetingGroups/MeetingGroupDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetAllMeetingGroups { public class MeetingGroupDto { public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } public string LocationCountryCode { get; set; } public string LocationCity { get; set; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/GetAuthenticationMemberMeetingGroups/GetAuthenticationMemberMeetingGroupsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetAuthenticationMemberMeetingGroups { public class GetAuthenticationMemberMeetingGroupsQuery : QueryBase> { } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/GetAuthenticationMemberMeetingGroups/GetAuthenticationMemberMeetingGroupsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetAuthenticationMemberMeetingGroups { internal class GetAuthenticationMemberMeetingGroupsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly IExecutionContextAccessor _executionContextAccessor; public GetAuthenticationMemberMeetingGroupsQueryHandler( ISqlConnectionFactory sqlConnectionFactory, IExecutionContextAccessor executionContextAccessor) { _sqlConnectionFactory = sqlConnectionFactory; _executionContextAccessor = executionContextAccessor; } public async Task> Handle( GetAuthenticationMemberMeetingGroupsQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MemberMeetingGroup].[Id] AS [{nameof(MemberMeetingGroupDto.Id)}], [MemberMeetingGroup].[Name] AS [{nameof(MemberMeetingGroupDto.Name)}], [MemberMeetingGroup].[Description] AS [{nameof(MemberMeetingGroupDto.Description)}], [MemberMeetingGroup].[LocationCountryCode] AS [{nameof(MemberMeetingGroupDto.LocationCountryCode)}], [MemberMeetingGroup].[LocationCity] AS [{nameof(MemberMeetingGroupDto.LocationCity)}], [MemberMeetingGroup].[MemberId] AS [{nameof(MemberMeetingGroupDto.MemberId)}], [MemberMeetingGroup].[RoleCode] AS [{nameof(MemberMeetingGroupDto.RoleCode)}] FROM [meetings].[v_MemberMeetingGroups] AS [MemberMeetingGroup] WHERE [MemberMeetingGroup].MemberId = @MemberId AND [MemberMeetingGroup].[IsActive] = 1 """; var meetingGroups = await connection.QueryAsync( sql, new { MemberId = _executionContextAccessor.UserId }); return meetingGroups.AsList(); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/GetAuthenticationMemberMeetingGroups/MemberMeetingGroupDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetAuthenticationMemberMeetingGroups { public class MemberMeetingGroupDto { public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } public string LocationCountryCode { get; set; } public string LocationCity { get; set; } public Guid MemberId { get; set; } public string RoleCode { get; set; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/GetMeetingGroupDetails/GetMeetingGroupDetailsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetMeetingGroupDetails { public class GetMeetingGroupDetailsQuery : QueryBase { public GetMeetingGroupDetailsQuery(Guid meetingGroupId) { MeetingGroupId = meetingGroupId; } public Guid MeetingGroupId { get; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/GetMeetingGroupDetails/GetMeetingGroupDetailsQueryHandler.cs ================================================ using System.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetMeetingGroupDetails { internal class GetMeetingGroupDetailsQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetMeetingGroupDetailsQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(GetMeetingGroupDetailsQuery query, CancellationToken cancellationToken) { using var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MeetingGroup].[Id] AS [{nameof(MeetingGroupDetailsDto.Id)}], [MeetingGroup].[Name] AS [{nameof(MeetingGroupDetailsDto.Name)}], [MeetingGroup].[Description] AS [{nameof(MeetingGroupDetailsDto.Description)}], [MeetingGroup].[LocationCity] AS [{nameof(MeetingGroupDetailsDto.LocationCity)}], [MeetingGroup].[LocationCountryCode] AS [{nameof(MeetingGroupDetailsDto.LocationCountryCode)}] FROM [meetings].[v_MeetingGroups] AS [MeetingGroup] WHERE [MeetingGroup].[Id] = @MeetingGroupId """; var meetingGroup = await connection.QuerySingleAsync( sql, new { query.MeetingGroupId }); meetingGroup.MembersCount = await GetMembersCount(query.MeetingGroupId, connection); return meetingGroup; } private static async Task GetMembersCount(Guid meetingGroupId, IDbConnection connection) { const string sql = """ SELECT COUNT(*) FROM [meetings].[v_MemberMeetingGroups] AS [MemberMeetingGroup] WHERE [MemberMeetingGroup].[Id] = @MeetingGroupId AND [MemberMeetingGroup].[IsActive] = 1 """; return await connection.ExecuteScalarAsync( sql, new { meetingGroupId }); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/GetMeetingGroupDetails/MeetingGroupDetailsDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetMeetingGroupDetails { public class MeetingGroupDetailsDto { public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } public string LocationCountryCode { get; set; } public string LocationCity { get; set; } public int MembersCount { get; set; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/JoinToGroup/JoinToGroupCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.JoinToGroup { public class JoinToGroupCommand : CommandBase { public JoinToGroupCommand(Guid meetingGroupId) { MeetingGroupId = meetingGroupId; } internal Guid MeetingGroupId { get; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/JoinToGroup/JoinToGroupCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.JoinToGroup { internal class JoinToGroupCommandHandler : ICommandHandler { private readonly IMeetingGroupRepository _meetingGroupRepository; private readonly IMemberContext _memberContext; internal JoinToGroupCommandHandler( IMeetingGroupRepository meetingGroupRepository, IMemberContext memberContext) { _meetingGroupRepository = meetingGroupRepository; _memberContext = memberContext; } public async Task Handle(JoinToGroupCommand request, CancellationToken cancellationToken) { var meetingGroup = await _meetingGroupRepository.GetByIdAsync(new MeetingGroupId(request.MeetingGroupId)); meetingGroup.JoinToGroupMember(_memberContext.MemberId); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/LeaveMeetingGroup/LeaveMeetingGroupCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.LeaveMeetingGroup { public class LeaveMeetingGroupCommand : CommandBase { public LeaveMeetingGroupCommand(Guid meetingGroupId) { MeetingGroupId = meetingGroupId; } internal Guid MeetingGroupId { get; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/LeaveMeetingGroup/LeaveMeetingGroupCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.LeaveMeetingGroup { internal class LeaveMeetingGroupCommandHandler : ICommandHandler { private readonly IMeetingGroupRepository _meetingGroupRepository; private readonly IMemberContext _memberContext; internal LeaveMeetingGroupCommandHandler( IMeetingGroupRepository meetingGroupRepository, IMemberContext memberContext) { _meetingGroupRepository = meetingGroupRepository; _memberContext = memberContext; } public async Task Handle(LeaveMeetingGroupCommand request, CancellationToken cancellationToken) { var meetingGroup = await _meetingGroupRepository.GetByIdAsync(new MeetingGroupId(request.MeetingGroupId)); meetingGroup.LeaveGroup(_memberContext.MemberId); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/MeetingGroupCreatedNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups { public class MeetingGroupCreatedNotification : DomainNotificationBase { [JsonConstructor] internal MeetingGroupCreatedNotification(MeetingGroupCreatedDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/MeetingGroupCreatedSendEmailHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.SendMeetingGroupCreatedEmail; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups { internal class MeetingGroupCreatedSendEmailHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; public MeetingGroupCreatedSendEmailHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(MeetingGroupCreatedNotification notification, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync( new SendMeetingGroupCreatedEmailCommand( Guid.NewGuid(), notification.DomainEvent.MeetingGroupId, notification.DomainEvent.CreatorId)); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/SendMeetingGroupCreatedEmail/SendMeetingGroupCreatedEmailCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.SendMeetingGroupCreatedEmail { internal class SendMeetingGroupCreatedEmailCommand : InternalCommandBase { internal MeetingGroupId MeetingGroupId { get; } internal MemberId CreatorId { get; } [JsonConstructor] internal SendMeetingGroupCreatedEmailCommand(Guid id, MeetingGroupId meetingGroupId, MemberId creatorId) : base(id) { MeetingGroupId = meetingGroupId; CreatorId = creatorId; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/SendMeetingGroupCreatedEmail/SendMeetingGroupCreatedEmailCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetAllMeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Application.Members; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.SendMeetingGroupCreatedEmail { internal class SendMeetingGroupCreatedEmailCommandHandler : ICommandHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly IEmailSender _emailSender; public SendMeetingGroupCreatedEmailCommandHandler( ISqlConnectionFactory sqlConnectionFactory, IEmailSender emailSender) { _sqlConnectionFactory = sqlConnectionFactory; _emailSender = emailSender; } public async Task Handle(SendMeetingGroupCreatedEmailCommand request, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MeetingGroup].[Name] as [{nameof(MeetingGroupDto.Name)}], [MeetingGroup].[LocationCountryCode] as [{nameof(MeetingGroupDto.LocationCountryCode)}], [MeetingGroup].[LocationCity] as [{nameof(MeetingGroupDto.LocationCity)}] FROM [meetings].[v_MeetingGroups] AS [MeetingGroup] WHERE [MeetingGroup].[Id] = @Id """; var meetingGroup = await connection.QuerySingleAsync( sql, new { Id = request.MeetingGroupId.Value }); var member = await MembersQueryHelper.GetMember(request.CreatorId, connection); var email = new EmailMessage( member.Email, $"{meetingGroup.Name} created", $"{meetingGroup.Name} created at {meetingGroup.LocationCity}, {meetingGroup.LocationCountryCode}"); await _emailSender.SendEmail(email); } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/SetMeetingGroupExpirationDate/SetMeetingGroupExpirationDateCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.SetMeetingGroupExpirationDate { public class SetMeetingGroupExpirationDateCommand : InternalCommandBase { [JsonConstructor] public SetMeetingGroupExpirationDateCommand(Guid id, Guid meetingGroupId, DateTime dateTo) : base(id) { MeetingGroupId = meetingGroupId; DateTo = dateTo; } internal Guid MeetingGroupId { get; } internal DateTime DateTo { get; } } } ================================================ FILE: src/Modules/Meetings/Application/MeetingGroups/SetMeetingGroupExpirationDate/SetMeetingGroupExpirationDateCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.SetMeetingGroupExpirationDate { internal class SetMeetingGroupExpirationDateCommandHandler : ICommandHandler { private readonly IMeetingGroupRepository _meetingGroupRepository; internal SetMeetingGroupExpirationDateCommandHandler(IMeetingGroupRepository meetingGroupRepository) { _meetingGroupRepository = meetingGroupRepository; } public async Task Handle(SetMeetingGroupExpirationDateCommand request, CancellationToken cancellationToken) { var meetingGroup = await _meetingGroupRepository.GetByIdAsync(new MeetingGroupId(request.MeetingGroupId)); meetingGroup.SetExpirationDate(request.DateTo); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/AddMeetingAttendee/AddMeetingAttendeeCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.AddMeetingAttendee { public class AddMeetingAttendeeCommand : CommandBase { public Guid MeetingId { get; } public int GuestsNumber { get; } public AddMeetingAttendeeCommand(Guid meetingId, int guestsNumber) { MeetingId = meetingId; GuestsNumber = guestsNumber; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/AddMeetingAttendee/AddMeetingAttendeeCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.AddMeetingAttendee { internal class AddMeetingAttendeeCommandHandler : ICommandHandler { private readonly IMemberContext _memberContext; private readonly IMeetingRepository _meetingRepository; private readonly IMeetingGroupRepository _meetingGroupRepository; public AddMeetingAttendeeCommandHandler( IMemberContext memberContext, IMeetingRepository meetingRepository, IMeetingGroupRepository meetingGroupRepository) { _memberContext = memberContext; _meetingRepository = meetingRepository; _meetingGroupRepository = meetingGroupRepository; } public async Task Handle(AddMeetingAttendeeCommand request, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); var meetingGroup = await _meetingGroupRepository.GetByIdAsync(meeting.GetMeetingGroupId()); meeting.AddAttendee(meetingGroup, _memberContext.MemberId, request.GuestsNumber); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/AddMeetingNotAttendee/AddMeetingNotAttendeeCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.AddMeetingNotAttendee { public class AddMeetingNotAttendeeCommand : CommandBase { public Guid MeetingId { get; } public AddMeetingNotAttendeeCommand(Guid meetingId) { MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/AddMeetingNotAttendee/AddMeetingNotAttendeeCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.AddMeetingNotAttendee { internal class AddMeetingNotAttendeeCommandHandler : ICommandHandler { private readonly IMemberContext _memberContext; private readonly IMeetingRepository _meetingRepository; public AddMeetingNotAttendeeCommandHandler(IMemberContext memberContext, IMeetingRepository meetingRepository) { _memberContext = memberContext; _meetingRepository = meetingRepository; } public async Task Handle(AddMeetingNotAttendeeCommand request, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); meeting.AddNotAttendee(_memberContext.MemberId); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/CancelMeeting/CancelMeetingCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.CancelMeeting { public class CancelMeetingCommand : CommandBase { public CancelMeetingCommand(Guid meetingId) { MeetingId = meetingId; } public Guid MeetingId { get; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/CancelMeeting/CancelMeetingCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.CancelMeeting { internal class CancelMeetingCommandHandler : ICommandHandler { private readonly IMeetingRepository _meetingRepository; private readonly IMemberContext _memberContext; internal CancelMeetingCommandHandler(IMeetingRepository meetingRepository, IMemberContext memberContext) { _meetingRepository = meetingRepository; _memberContext = memberContext; } public async Task Handle(CancelMeetingCommand request, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); meeting.Cancel(_memberContext.MemberId); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/ChangeMeetingMainAttributes/ChangeMeetingMainAttributesCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.ChangeMeetingMainAttributes { public class ChangeMeetingMainAttributesCommand : CommandBase { public ChangeMeetingMainAttributesCommand( Guid meetingId, string title, DateTime termStartDate, DateTime termEndDate, string description, string meetingLocationName, string meetingLocationAddress, string meetingLocationPostalCode, string meetingLocationCity, int? attendeesLimit, int guestsLimit, DateTime? rsvpTermStartDate, DateTime? rsvpTermEndDate, decimal? eventFeeValue, string eventFeeCurrency) { MeetingId = meetingId; Title = title; TermStartDate = termStartDate; TermEndDate = termEndDate; Description = description; MeetingLocationName = meetingLocationName; MeetingLocationAddress = meetingLocationAddress; MeetingLocationPostalCode = meetingLocationPostalCode; MeetingLocationCity = meetingLocationCity; AttendeesLimit = attendeesLimit; GuestsLimit = guestsLimit; RSVPTermStartDate = rsvpTermStartDate; RSVPTermEndDate = rsvpTermEndDate; EventFeeValue = eventFeeValue; EventFeeCurrency = eventFeeCurrency; } public Guid MeetingId { get; } public string Title { get; } public DateTime TermStartDate { get; } public DateTime TermEndDate { get; } public string Description { get; } public string MeetingLocationName { get; } public string MeetingLocationAddress { get; } public string MeetingLocationPostalCode { get; } public string MeetingLocationCity { get; } public int? AttendeesLimit { get; } public int GuestsLimit { get; } public DateTime? RSVPTermStartDate { get; } public DateTime? RSVPTermEndDate { get; } public decimal? EventFeeValue { get; } public string EventFeeCurrency { get; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/ChangeMeetingMainAttributes/ChangeMeetingMainAttributesCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.ChangeMeetingMainAttributes { internal class ChangeMeetingMainAttributesCommandHandler : ICommandHandler { private readonly IMemberContext _memberContext; private readonly IMeetingRepository _meetingRepository; public ChangeMeetingMainAttributesCommandHandler(IMemberContext memberContext, IMeetingRepository meetingRepository) { _memberContext = memberContext; _meetingRepository = meetingRepository; } public async Task Handle(ChangeMeetingMainAttributesCommand request, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); meeting.ChangeMainAttributes( request.Title, MeetingTerm.CreateNewBetweenDates(request.TermStartDate, request.TermStartDate), request.Description, MeetingLocation.CreateNew(request.MeetingLocationName, request.MeetingLocationAddress, request.MeetingLocationPostalCode, request.MeetingLocationCity), MeetingLimits.Create(request.AttendeesLimit, request.GuestsLimit), Term.CreateNewBetweenDates(request.RSVPTermStartDate, request.RSVPTermEndDate), request.EventFeeValue.HasValue ? MoneyValue.Of(request.EventFeeValue.Value, request.EventFeeCurrency) : MoneyValue.Undefined, _memberContext.MemberId); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/ChangeNotAttendeeDecision/ChangeNotAttendeeDecisionCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.ChangeNotAttendeeDecision { public class ChangeNotAttendeeDecisionCommand : CommandBase { public Guid MeetingId { get; } public ChangeNotAttendeeDecisionCommand(Guid meetingId) { MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/ChangeNotAttendeeDecision/ChangeNotAttendeeDecisionCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.ChangeNotAttendeeDecision { internal class ChangeNotAttendeeDecisionCommandHandler : ICommandHandler { private readonly IMemberContext _memberContext; private readonly IMeetingRepository _meetingRepository; public ChangeNotAttendeeDecisionCommandHandler(IMemberContext memberContext, IMeetingRepository meetingRepository) { _memberContext = memberContext; _meetingRepository = meetingRepository; } public async Task Handle(ChangeNotAttendeeDecisionCommand request, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); meeting.ChangeNotAttendeeDecision(_memberContext.MemberId); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/CreateMeeting/CreateMeetingCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.CreateMeeting { public class CreateMeetingCommand : CommandBase { public CreateMeetingCommand( Guid meetingGroupId, string title, DateTime termStartDate, DateTime termEndDate, string description, string meetingLocationName, string meetingLocationAddress, string meetingLocationPostalCode, string meetingLocationCity, int? attendeesLimit, int guestsLimit, DateTime? rsvpTermStartDate, DateTime? rsvpTermEndDate, decimal? eventFeeValue, string eventFeeCurrency, List hostMemberIds) { MeetingGroupId = meetingGroupId; Title = title; TermStartDate = termStartDate; TermEndDate = termEndDate; Description = description; MeetingLocationName = meetingLocationName; MeetingLocationAddress = meetingLocationAddress; MeetingLocationPostalCode = meetingLocationPostalCode; MeetingLocationCity = meetingLocationCity; AttendeesLimit = attendeesLimit; GuestsLimit = guestsLimit; RSVPTermStartDate = rsvpTermStartDate; RSVPTermEndDate = rsvpTermEndDate; EventFeeValue = eventFeeValue; EventFeeCurrency = eventFeeCurrency; HostMemberIds = hostMemberIds; } public Guid MeetingGroupId { get; } public string Title { get; } public DateTime TermStartDate { get; } public DateTime TermEndDate { get; } public string Description { get; } public string MeetingLocationName { get; } public string MeetingLocationAddress { get; } public string MeetingLocationPostalCode { get; } public string MeetingLocationCity { get; } public int? AttendeesLimit { get; } public int GuestsLimit { get; } public DateTime? RSVPTermStartDate { get; } public DateTime? RSVPTermEndDate { get; } public decimal? EventFeeValue { get; } public string EventFeeCurrency { get; } public List HostMemberIds { get; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/CreateMeeting/CreateMeetingCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.CreateMeeting { internal class CreateMeetingCommandHandler : ICommandHandler { private readonly IMemberContext _memberContext; private readonly IMeetingRepository _meetingRepository; private readonly IMeetingGroupRepository _meetingGroupRepository; internal CreateMeetingCommandHandler( IMemberContext memberContext, IMeetingRepository meetingRepository, IMeetingGroupRepository meetingGroupRepository) { _memberContext = memberContext; _meetingRepository = meetingRepository; _meetingGroupRepository = meetingGroupRepository; } public async Task Handle(CreateMeetingCommand request, CancellationToken cancellationToken) { var meetingGroup = await _meetingGroupRepository.GetByIdAsync(new MeetingGroupId(request.MeetingGroupId)); var hostsMembersIds = request.HostMemberIds.Select(x => new MemberId(x)).ToList(); var meeting = meetingGroup.CreateMeeting( request.Title, MeetingTerm.CreateNewBetweenDates(request.TermStartDate, request.TermStartDate), request.Description, MeetingLocation.CreateNew(request.MeetingLocationName, request.MeetingLocationAddress, request.MeetingLocationPostalCode, request.MeetingLocationCity), request.AttendeesLimit, request.GuestsLimit, Term.CreateNewBetweenDates(request.RSVPTermStartDate, request.RSVPTermEndDate), request.EventFeeValue.HasValue ? MoneyValue.Of(request.EventFeeValue.Value, request.EventFeeCurrency) : MoneyValue.Undefined, hostsMembersIds, _memberContext.MemberId); await _meetingRepository.AddAsync(meeting); return meeting.Id.Value; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/GetAuthenticatedMemberMeetings/GetAuthenticatedMemberMeetingsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetAuthenticatedMemberMeetings { public class GetAuthenticatedMemberMeetingsQuery : QueryBase> { } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/GetAuthenticatedMemberMeetings/GetAuthenticatedMemberMeetingsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetAuthenticatedMemberMeetings { internal class GetAuthenticatedMemberMeetingsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly IExecutionContextAccessor _executionContextAccessor; public GetAuthenticatedMemberMeetingsQueryHandler( ISqlConnectionFactory sqlConnectionFactory, IExecutionContextAccessor executionContextAccessor) { _sqlConnectionFactory = sqlConnectionFactory; _executionContextAccessor = executionContextAccessor; } public async Task> Handle(GetAuthenticatedMemberMeetingsQuery request, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); return (await connection.QueryAsync( $""" SELECT [Meeting].[Id] AS [{nameof(MemberMeetingDto.MeetingId)}], [Meeting].[RoleCode] AS [{nameof(MemberMeetingDto.RoleCode)}], [Meeting].[LocationCity] AS [{nameof(MemberMeetingDto.LocationCity)}], [Meeting].[LocationAddress] AS [{nameof(MemberMeetingDto.LocationAddress)}], [Meeting].[LocationPostalCode] AS [{nameof(MemberMeetingDto.LocationPostalCode)}], [Meeting].[TermStartDate] AS [{nameof(MemberMeetingDto.TermStartDate)}], [Meeting].[TermEndDate] AS [{nameof(MemberMeetingDto.TermEndDate)}], [Meeting].[Title] AS [{nameof(MemberMeetingDto.Title)}] FROM [meetings].[v_MemberMeetings] AS [Meeting] WHERE [Meeting].[AttendeeId] = @AttendeeId AND [Meeting].[IsRemoved] = 0 """, new { AttendeeId = _executionContextAccessor.UserId })).AsList(); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/GetAuthenticatedMemberMeetings/MemberMeetingDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetAuthenticatedMemberMeetings { public class MemberMeetingDto { public Guid MeetingId { get; set; } public string Title { get; set; } public string LocationAddress { get; set; } public string LocationCity { get; set; } public string LocationPostalCode { get; set; } public DateTime TermStartDate { get; set; } public DateTime TermEndDate { get; set; } public string RoleCode { get; set; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/GetMeetingAttendees/GetMeetingAttendeesQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetMeetingAttendees { public class GetMeetingAttendeesQuery : QueryBase> { public GetMeetingAttendeesQuery(Guid meetingId) { MeetingId = meetingId; } public Guid MeetingId { get; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/GetMeetingAttendees/GetMeetingAttendeesQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetMeetingAttendees { internal class GetMeetingAttendeesQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetMeetingAttendeesQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle(GetMeetingAttendeesQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MeetingAttendee].[FirstName] AS [{nameof(MeetingAttendeeDto.FirstName)}], [MeetingAttendee].[LastName] AS [{nameof(MeetingAttendeeDto.LastName)}], [MeetingAttendee].[RoleCode] AS [{nameof(MeetingAttendeeDto.RoleCode)}], [MeetingAttendee].[DecisionDate] AS [{nameof(MeetingAttendeeDto.DecisionDate)}], [MeetingAttendee].[GuestsNumber] AS [{nameof(MeetingAttendeeDto.GuestsNumber)}], [MeetingAttendee].[AttendeeId] AS [{nameof(MeetingAttendeeDto.AttendeeId)}] FROM [meetings].[v_MeetingAttendees] AS [MeetingAttendee] WHERE [MeetingAttendee].[MeetingId] = @MeetingId """; return (await connection.QueryAsync( sql, new { query.MeetingId })).AsList(); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/GetMeetingAttendees/MeetingAttendeeDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetMeetingAttendees { public class MeetingAttendeeDto { public string FirstName { get; set; } public string LastName { get; set; } public Guid AttendeeId { get; set; } public string RoleCode { get; set; } public int GuestsNumber { get; set; } public DateTime DecisionDate { get; set; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/GetMeetingDetails/GetMeetingDetailsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetMeetingDetails { public class GetMeetingDetailsQuery : QueryBase { public GetMeetingDetailsQuery(Guid meetingId) { MeetingId = meetingId; } public Guid MeetingId { get; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/GetMeetingDetails/GetMeetingDetailsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetMeetingDetails { internal class GetMeetingDetailsQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetMeetingDetailsQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(GetMeetingDetailsQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); return await connection.QuerySingleAsync( $""" SELECT [MeetingDetails].[Id] AS [{nameof(MeetingDetailsDto.Id)}], [MeetingDetails].[MeetingGroupId] AS [{nameof(MeetingDetailsDto.MeetingGroupId)}], [MeetingDetails].[Title] AS [{nameof(MeetingDetailsDto.Title)}], [MeetingDetails].[TermStartDate] AS [{nameof(MeetingDetailsDto.TermStartDate)}], [MeetingDetails].[TermEndDate] AS [{nameof(MeetingDetailsDto.TermEndDate)}], [MeetingDetails].[Description] AS [{nameof(MeetingDetailsDto.Description)}], [MeetingDetails].[LocationName] AS [{nameof(MeetingDetailsDto.LocationName)}], [MeetingDetails].[LocationAddress] AS [{nameof(MeetingDetailsDto.LocationAddress)}], [MeetingDetails].[LocationPostalCode] AS [{nameof(MeetingDetailsDto.LocationPostalCode)}], [MeetingDetails].[LocationCity] AS [{nameof(MeetingDetailsDto.LocationCity)}], [MeetingDetails].[AttendeesLimit] AS [{nameof(MeetingDetailsDto.AttendeesLimit)}], [MeetingDetails].[GuestsLimit] AS [{nameof(MeetingDetailsDto.GuestsLimit)}], [MeetingDetails].[RSVPTermStartDate] AS [{nameof(MeetingDetailsDto.RSVPTermStartDate)}], [MeetingDetails].[RSVPTermEndDate] AS [{nameof(MeetingDetailsDto.RSVPTermEndDate)}], [MeetingDetails].[EventFeeValue] AS [{nameof(MeetingDetailsDto.EventFeeValue)}], [MeetingDetails].[EventFeeCurrency] AS [{nameof(MeetingDetailsDto.EventFeeCurrency)}] FROM [meetings].[v_MeetingDetails] AS [MeetingDetails] WHERE [MeetingDetails].[Id] = @MeetingId """, new { query.MeetingId }); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/GetMeetingDetails/MeetingDetailsDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetMeetingDetails { public class MeetingDetailsDto { public Guid Id { get; set; } public Guid MeetingGroupId { get; set; } public string Title { get; set; } public DateTime TermStartDate { get; set; } public DateTime TermEndDate { get; set; } public string Description { get; set; } public string LocationName { get; set; } public string LocationAddress { get; set; } public string LocationPostalCode { get; set; } public string LocationCity { get; set; } public int? AttendeesLimit { get; set; } public int GuestsLimit { get; set; } public DateTime? RSVPTermStartDate { get; set; } public DateTime? RSVPTermEndDate { get; set; } public decimal? EventFeeValue { get; set; } public string EventFeeCurrency { get; set; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/MarkMeetingAttendeeFeeAsPayedCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings { public class MarkMeetingAttendeeFeeAsPayedCommand : InternalCommandBase { [JsonConstructor] public MarkMeetingAttendeeFeeAsPayedCommand(Guid id, Guid memberId, Guid meetingId) : base(id) { MemberId = memberId; MeetingId = meetingId; } public Guid MemberId { get; } public Guid MeetingId { get; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/MarkMeetingAttendeeFeeAsPayedCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings { internal class MarkMeetingAttendeeFeeAsPayedCommandHandler : ICommandHandler { private readonly IMeetingRepository _meetingRepository; public MarkMeetingAttendeeFeeAsPayedCommandHandler(IMeetingRepository meetingRepository) { _meetingRepository = meetingRepository; } public async Task Handle(MarkMeetingAttendeeFeeAsPayedCommand command, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(command.MeetingId)); meeting.MarkAttendeeFeeAsPayed(new MemberId(command.MemberId)); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/MeetingDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings { public class MeetingDto { public Guid Id { get; set; } public string Title { get; set; } public string Description { get; set; } public string LocationAddress { get; set; } public string LocationCity { get; set; } public string LocationPostalCode { get; set; } public DateTime TermStartDate { get; set; } public DateTime TermEndDate { get; set; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/MeetingFeePaidIntegrationEventHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings { public class MeetingFeePaidIntegrationEventHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; public MeetingFeePaidIntegrationEventHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(MeetingFeePaidIntegrationEvent @event, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync(new MarkMeetingAttendeeFeeAsPayedCommand( Guid.NewGuid(), @event.PayerId, @event.MeetingId)); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/MeetingsQueryHelper.cs ================================================ using System.Data; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings { public class MeetingsQueryHelper { public static async Task GetMeeting(MeetingId meetingId, IDbConnection connection) { const string sql = $""" SELECT [Meeting].Id as [{nameof(MeetingDto.Id)}], [Meeting].Title as [{nameof(MeetingDto.Title)}], [Meeting].Description as [{nameof(MeetingDto.Description)}], [Meeting].LocationAddress as [{nameof(MeetingDto.LocationAddress)}], [Meeting].LocationCity as [{nameof(MeetingDto.LocationCity)}], [Meeting].LocationPostalCode as [{nameof(MeetingDto.LocationPostalCode)}], [Meeting].TermStartDate as [{nameof(MeetingDto.TermStartDate)}], [Meeting].TermEndDate as [{nameof(MeetingDto.TermEndDate)}] FROM [meetings].[v_Meetings] AS [Meeting] WHERE [Meeting].[Id] = @Id """; return await connection.QuerySingleAsync( sql, new { Id = meetingId.Value }); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/RemoveMeetingAttendee/RemoveMeetingAttendeeCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.RemoveMeetingAttendee { public class RemoveMeetingAttendeeCommand : CommandBase { public RemoveMeetingAttendeeCommand(Guid meetingId, Guid attendeeId, string removingReason) { MeetingId = meetingId; AttendeeId = attendeeId; RemovingReason = removingReason; } public Guid MeetingId { get; } public Guid AttendeeId { get; } public string RemovingReason { get; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/RemoveMeetingAttendee/RemoveMeetingAttendeeCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.RemoveMeetingAttendee { internal class RemoveMeetingAttendeeCommandHandler : ICommandHandler { private readonly IMeetingRepository _meetingRepository; private readonly IMemberContext _memberContext; internal RemoveMeetingAttendeeCommandHandler(IMeetingRepository meetingRepository, IMemberContext memberContext) { _meetingRepository = meetingRepository; _memberContext = memberContext; } public async Task Handle(RemoveMeetingAttendeeCommand request, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); meeting.RemoveAttendee(new MemberId(request.AttendeeId), _memberContext.MemberId, request.RemovingReason); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SendMeetingAttendeeAddedEmail/MeetingAttendeeAddedNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SendMeetingAttendeeAddedEmail { public class MeetingAttendeeAddedNotification : DomainNotificationBase { [JsonConstructor] public MeetingAttendeeAddedNotification(MeetingAttendeeAddedDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SendMeetingAttendeeAddedEmail/MeetingAttendeeAddedNotificationHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SendMeetingAttendeeAddedEmail { internal class MeetingAttendeeAddedNotificationHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; internal MeetingAttendeeAddedNotificationHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(MeetingAttendeeAddedNotification notification, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync( new SendMeetingAttendeeAddedEmailCommand( Guid.NewGuid(), notification.DomainEvent.AttendeeId, notification.DomainEvent.MeetingId)); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SendMeetingAttendeeAddedEmail/MeetingAttendeeAddedPublishEventNotificationHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SendMeetingAttendeeAddedEmail { internal class MeetingAttendeeAddedPublishEventNotificationHandler : INotificationHandler { private readonly IEventsBus _eventsBus; internal MeetingAttendeeAddedPublishEventNotificationHandler(IEventsBus eventsBus) { _eventsBus = eventsBus; } public async Task Handle(MeetingAttendeeAddedNotification notification, CancellationToken cancellationToken) { await _eventsBus.Publish(new MeetingAttendeeAddedIntegrationEvent( Guid.NewGuid(), notification.DomainEvent.OccurredOn, notification.DomainEvent.MeetingId.Value, notification.DomainEvent.AttendeeId.Value, notification.DomainEvent.FeeValue, notification.DomainEvent.FeeCurrency)); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SendMeetingAttendeeAddedEmail/SendMeetingAttendeeAddedEmailCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SendMeetingAttendeeAddedEmail { internal class SendMeetingAttendeeAddedEmailCommand : InternalCommandBase { internal MemberId AttendeeId { get; } internal MeetingId MeetingId { get; } [JsonConstructor] internal SendMeetingAttendeeAddedEmailCommand(Guid id, MemberId attendeeId, MeetingId meetingId) : base(id) { AttendeeId = attendeeId; MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SendMeetingAttendeeAddedEmail/SendMeetingAttendeeAddedEmailCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SendMeetingAttendeeAddedEmail { internal class SendMeetingAttendeeAddedEmailCommandHandler : ICommandHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly IEmailSender _emailSender; internal SendMeetingAttendeeAddedEmailCommandHandler( ISqlConnectionFactory sqlConnectionFactory, IEmailSender emailSender) { _sqlConnectionFactory = sqlConnectionFactory; _emailSender = emailSender; } public async Task Handle(SendMeetingAttendeeAddedEmailCommand request, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); var member = await MembersQueryHelper.GetMember(request.AttendeeId, connection); var meeting = await MeetingsQueryHelper.GetMeeting(request.MeetingId, connection); var email = new EmailMessage( member.Email, $"You joined to {meeting.Title} meeting.", $"You joined to {meeting.Title} title at {meeting.TermStartDate.ToShortDateString()} - {meeting.TermEndDate.ToShortDateString()}, location {meeting.LocationAddress}, {meeting.LocationPostalCode}, {meeting.LocationCity}"); await _emailSender.SendEmail(email); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SetMeetingAttendeeRole/SetMeetingAttendeeRoleCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SetMeetingAttendeeRole { public class SetMeetingAttendeeRoleCommand : CommandBase { public Guid MemberId { get; } public Guid MeetingId { get; } public SetMeetingAttendeeRoleCommand(Guid memberId, Guid meetingId) { MemberId = memberId; MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SetMeetingAttendeeRole/SetMeetingAttendeeRoleCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SetMeetingAttendeeRole { internal class SetMeetingAttendeeRoleCommandHandler : ICommandHandler { private readonly IMemberContext _memberContext; private readonly IMeetingRepository _meetingRepository; private readonly IMeetingGroupRepository _meetingGroupRepository; internal SetMeetingAttendeeRoleCommandHandler( IMemberContext memberContext, IMeetingRepository meetingRepository, IMeetingGroupRepository meetingGroupRepository) { _memberContext = memberContext; _meetingRepository = meetingRepository; _meetingGroupRepository = meetingGroupRepository; } public async Task Handle(SetMeetingAttendeeRoleCommand request, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); var meetingGroup = await _meetingGroupRepository.GetByIdAsync(meeting.GetMeetingGroupId()); meeting.SetAttendeeRole(meetingGroup, _memberContext.MemberId, new MemberId(request.MemberId)); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SetMeetingHostRole/SetMeetingHostRoleCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SetMeetingHostRole { public class SetMeetingHostRoleCommand : CommandBase { public Guid MemberId { get; } public Guid MeetingId { get; } public SetMeetingHostRoleCommand(Guid memberId, Guid meetingId) { MemberId = memberId; MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SetMeetingHostRole/SetMeetingHostRoleCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SetMeetingHostRole { internal class SetMeetingHostRoleCommandHandler : ICommandHandler { private readonly IMemberContext _memberContext; private readonly IMeetingRepository _meetingRepository; private readonly IMeetingGroupRepository _meetingGroupRepository; internal SetMeetingHostRoleCommandHandler( IMemberContext memberContext, IMeetingRepository meetingRepository, IMeetingGroupRepository meetingGroupRepository) { _memberContext = memberContext; _meetingRepository = meetingRepository; _meetingGroupRepository = meetingGroupRepository; } public async Task Handle(SetMeetingHostRoleCommand request, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); var meetingGroup = await _meetingGroupRepository.GetByIdAsync(meeting.GetMeetingGroupId()); meeting.SetHostRole(meetingGroup, _memberContext.MemberId, new MemberId(request.MemberId)); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SignOffMemberFromWaitlist/SignOffMemberFromWaitlistCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SignOffMemberFromWaitlist { public class SignOffMemberFromWaitlistCommand : CommandBase { public Guid MeetingId { get; } public SignOffMemberFromWaitlistCommand(Guid meetingId) { MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SignOffMemberFromWaitlist/SignOffMemberFromWaitlistCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SignOffMemberFromWaitlist { internal class SignOffMemberFromWaitlistCommandHandler : ICommandHandler { private readonly IMemberContext _memberContext; private readonly IMeetingRepository _meetingRepository; public SignOffMemberFromWaitlistCommandHandler(IMemberContext memberContext, IMeetingRepository meetingRepository) { _memberContext = memberContext; _meetingRepository = meetingRepository; } public async Task Handle(SignOffMemberFromWaitlistCommand request, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); meeting.SignOffMemberFromWaitlist(_memberContext.MemberId); } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SignUpMemberToWaitlist/SignUpMemberToWaitlistCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SignUpMemberToWaitlist { public class SignUpMemberToWaitlistCommand : CommandBase { public Guid MeetingId { get; } public SignUpMemberToWaitlistCommand(Guid meetingId) { MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Application/Meetings/SignUpMemberToWaitlist/SignUpMemberToWaitlistCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SignUpMemberToWaitlist { internal class SignUpMemberToWaitlistCommandHandler : ICommandHandler { private readonly IMemberContext _memberContext; private readonly IMeetingRepository _meetingRepository; private readonly IMeetingGroupRepository _meetingGroupRepository; public SignUpMemberToWaitlistCommandHandler( IMemberContext memberContext, IMeetingRepository meetingRepository, IMeetingGroupRepository meetingGroupRepository) { _memberContext = memberContext; _meetingRepository = meetingRepository; _meetingGroupRepository = meetingGroupRepository; } public async Task Handle(SignUpMemberToWaitlistCommand request, CancellationToken cancellationToken) { var meeting = await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); var meetingGroup = await _meetingGroupRepository.GetByIdAsync(meeting.GetMeetingGroupId()); meeting.SignUpMemberToWaitlist(meetingGroup, _memberContext.MemberId); } } } ================================================ FILE: src/Modules/Meetings/Application/MemberSubscriptions/ChangeSubscriptionExpirationDateForMember/ChangeSubscriptionExpirationDateForMemberCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MemberSubscriptions.ChangeSubscriptionExpirationDateForMember { public class ChangeSubscriptionExpirationDateForMemberCommand : InternalCommandBase { [JsonConstructor] public ChangeSubscriptionExpirationDateForMemberCommand( Guid id, MemberId memberId, DateTime expirationDate) : base(id) { MemberId = memberId; ExpirationDate = expirationDate; } public MemberId MemberId { get; } public DateTime ExpirationDate { get; } } } ================================================ FILE: src/Modules/Meetings/Application/MemberSubscriptions/ChangeSubscriptionExpirationDateForMember/ChangeSubscriptionExpirationDateForMemberCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members.MemberSubscriptions; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MemberSubscriptions.ChangeSubscriptionExpirationDateForMember { internal class ChangeSubscriptionExpirationDateForMemberCommandHandler : ICommandHandler { private readonly IMemberSubscriptionRepository _memberSubscriptionRepository; public ChangeSubscriptionExpirationDateForMemberCommandHandler(IMemberSubscriptionRepository memberSubscriptionRepository) { _memberSubscriptionRepository = memberSubscriptionRepository; } public async Task Handle(ChangeSubscriptionExpirationDateForMemberCommand command, CancellationToken cancellationToken) { MemberSubscription memberSubscription = await _memberSubscriptionRepository.GetByIdOptionalAsync(new MemberSubscriptionId(command.MemberId.Value)); if (memberSubscription == null) { memberSubscription = MemberSubscription.CreateForMember(command.MemberId, command.ExpirationDate); await _memberSubscriptionRepository.AddAsync(memberSubscription); } else { memberSubscription.ChangeExpirationDate(command.ExpirationDate); } } } } ================================================ FILE: src/Modules/Meetings/Application/MemberSubscriptions/MemberSubscriptionExpirationDateChangedNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members.MemberSubscriptions.Events; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MemberSubscriptions { public class MemberSubscriptionExpirationDateChangedNotification : DomainNotificationBase { [JsonConstructor] public MemberSubscriptionExpirationDateChangedNotification(MemberSubscriptionExpirationDateChangedDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Meetings/Application/MemberSubscriptions/MemberSubscriptionExpirationDateChangedNotificationHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.SetMeetingGroupExpirationDate; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Policies; using Dapper; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MemberSubscriptions { public class MemberSubscriptionExpirationDateChangedNotificationHandler : INotificationHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly ICommandsScheduler _commandsScheduler; public MemberSubscriptionExpirationDateChangedNotificationHandler(ISqlConnectionFactory sqlConnectionFactory, ICommandsScheduler commandsScheduler) { _sqlConnectionFactory = sqlConnectionFactory; _commandsScheduler = commandsScheduler; } public async Task Handle(MemberSubscriptionExpirationDateChangedNotification notification, CancellationToken cancellationToken) { const string sql = $""" SELECT [MeetingGroupMember].MeetingGroupId AS [{nameof(MeetingGroupMemberResponse.MeetingGroupId)}], [MeetingGroupMember].RoleCode AS [{nameof(MeetingGroupMemberResponse.RoleCode)}] FROM [meetings].[v_MeetingGroupMembers] AS [MeetingGroupMember] WHERE [MeetingGroupMember].MemberId = @MemberId """; var connection = _sqlConnectionFactory.GetOpenConnection(); var meetingGroupMembers = await connection.QueryAsync( sql, new { MemberId = notification.DomainEvent.MemberId.Value }); var meetingGroupList = meetingGroupMembers.AsList(); List meetingGroups = meetingGroupList .Select(x => new MeetingGroupMemberData( new MeetingGroupId(x.MeetingGroupId), MeetingGroupMemberRole.Of(x.RoleCode))) .ToList(); var meetingGroupsCoveredByMemberSubscription = MeetingGroupExpirationDatePolicy.GetMeetingGroupsCoveredByMemberSubscription(meetingGroups); foreach (var meetingGroup in meetingGroupsCoveredByMemberSubscription) { await _commandsScheduler.EnqueueAsync(new SetMeetingGroupExpirationDateCommand( Guid.NewGuid(), meetingGroup.Value, notification.DomainEvent.ExpirationDate)); } } private class MeetingGroupMemberResponse { public Guid MeetingGroupId { get; set; } public string RoleCode { get; set; } } } } ================================================ FILE: src/Modules/Meetings/Application/MemberSubscriptions/SubscriptionExpirationDateChangedIntegrationEventHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.MemberSubscriptions.ChangeSubscriptionExpirationDateForMember; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Payments.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.MemberSubscriptions { public class SubscriptionExpirationDateChangedIntegrationEventHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; public SubscriptionExpirationDateChangedIntegrationEventHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(SubscriptionExpirationDateChangedIntegrationEvent @event, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync(new ChangeSubscriptionExpirationDateForMemberCommand( Guid.NewGuid(), new MemberId(@event.PayerId), @event.ExpirationDate)); } } } ================================================ FILE: src/Modules/Meetings/Application/Members/CreateMember/CreateMemberCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Members.CreateMember { public class CreateMemberCommand : InternalCommandBase { [JsonConstructor] public CreateMemberCommand( Guid id, Guid memberId, string login, string email, string firstName, string lastName, string name) : base(id) { Login = login; MemberId = memberId; Email = email; FirstName = firstName; LastName = lastName; Name = name; } internal Guid MemberId { get; } internal string Login { get; } internal string Email { get; } internal string FirstName { get; } internal string LastName { get; } internal string Name { get; } } } ================================================ FILE: src/Modules/Meetings/Application/Members/CreateMember/CreateMemberCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Members.CreateMember { internal class CreateMemberCommandHandler : ICommandHandler { private readonly IMemberRepository _memberRepository; public CreateMemberCommandHandler(IMemberRepository memberRepository) { _memberRepository = memberRepository; } public async Task Handle(CreateMemberCommand request, CancellationToken cancellationToken) { var member = Member.Create(request.MemberId, request.Login, request.Email, request.FirstName, request.LastName, request.Name); await _memberRepository.AddAsync(member); } } } ================================================ FILE: src/Modules/Meetings/Application/Members/CreateMember/MemberCratedNotificationHandler.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Members.CreateMember { public class MemberCratedNotificationHandler : INotificationHandler { public Task Handle(MemberCreatedNotification notification, CancellationToken cancellationToken) { return Task.CompletedTask; } } } ================================================ FILE: src/Modules/Meetings/Application/Members/CreateMember/MemberCreatedNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Members.CreateMember { public class MemberCreatedNotification : DomainNotificationBase { [JsonConstructor] public MemberCreatedNotification(MeetingCreatedDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Meetings/Application/Members/CreateMember/NewUserRegisteredIntegrationEventHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Members.CreateMember { public class NewUserRegisteredIntegrationEventHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; public NewUserRegisteredIntegrationEventHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(NewUserRegisteredIntegrationEvent notification, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync(new CreateMemberCommand( Guid.NewGuid(), notification.UserId, notification.Login, notification.Email, notification.FirstName, notification.LastName, notification.Name)); } } } ================================================ FILE: src/Modules/Meetings/Application/Members/MemberContext.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Members { public class MemberContext : IMemberContext { private readonly IExecutionContextAccessor _executionContextAccessor; public MemberContext(IExecutionContextAccessor executionContextAccessor) { this._executionContextAccessor = executionContextAccessor; } public MemberId MemberId => new MemberId(_executionContextAccessor.UserId); } } ================================================ FILE: src/Modules/Meetings/Application/Members/MemberDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Application.Members { public class MemberDto { public Guid Id { get; set; } public string Name { get; set; } public string Email { get; set; } public string Login { get; set; } } } ================================================ FILE: src/Modules/Meetings/Application/Members/MembersQueryHelper.cs ================================================ using System.Data; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Dapper; namespace CompanyName.MyMeetings.Modules.Meetings.Application.Members { public class MembersQueryHelper { public static async Task GetMember(MemberId memberId, IDbConnection connection) { const string sql = $""" SELECT [Member].Id as [{nameof(MemberDto.Id)}], [Member].[Name] as [{nameof(MemberDto.Name)}], [Member].[Login] as [{nameof(MemberDto.Login)}], [Member].[Email] as [{nameof(MemberDto.Email)}] FROM [meetings].[v_Members] AS [Member] WHERE [Member].[Id] = @Id """; return await connection.QuerySingleAsync( sql, new { Id = memberId.Value }); } public static async Task GetMeetingGroupMember(MemberId memberId, MeetingId meetingOfGroupId, IDbConnection connection) { const string sql = $""" SELECT [MeetingGroupMember].{nameof(MeetingGroupMemberResponse.MeetingGroupId)}, [MeetingGroupMember].{nameof(MeetingGroupMemberResponse.MemberId)} FROM [meetings].[v_MeetingGroupMembers] AS [MeetingGroupMember] INNER JOIN [meetings].[Meetings] AS [Meeting] ON [Meeting].[MeetingGroupId] = [MeetingGroupMember].[MeetingGroupId] WHERE [MeetingGroupMember].[MemberId] = @MemberId AND [Meeting].[Id] = @MeetingId """; var result = await connection.QuerySingleAsync( sql, new { MemberId = memberId.Value, MeetingId = meetingOfGroupId.Value }); return new MeetingGroupMemberData( new MeetingGroupId(result.MeetingGroupId), new MemberId(result.MemberId)); } private class MeetingGroupMemberResponse { public Guid MeetingGroupId { get; set; } public Guid MemberId { get; set; } } } } ================================================ FILE: src/Modules/Meetings/Domain/CompanyName.MyMeetings.Modules.Meetings.Domain.csproj ================================================ ================================================ FILE: src/Modules/Meetings/Domain/MeetingCommentingConfigurations/Events/MeetingCommentingConfigurationCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations.Events { public class MeetingCommentingConfigurationCreatedDomainEvent : DomainEventBase { public MeetingId MeetingId { get; } public bool IsEnabled { get; } public MeetingCommentingConfigurationCreatedDomainEvent(MeetingId meetingId, bool isEnabled) { MeetingId = meetingId; IsEnabled = isEnabled; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingCommentingConfigurations/Events/MeetingCommentingDisabledDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations.Events { public class MeetingCommentingDisabledDomainEvent : DomainEventBase { public MeetingId MeetingId { get; } public MeetingCommentingDisabledDomainEvent(MeetingId meetingId) { MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingCommentingConfigurations/Events/MeetingCommentingEnabledDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations.Events { public class MeetingCommentingEnabledDomainEvent : DomainEventBase { public MeetingId MeetingId { get; } public MeetingCommentingEnabledDomainEvent(MeetingId meetingId) { MeetingId = meetingId; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingCommentingConfigurations/IMeetingCommentingConfigurationRepository.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations { public interface IMeetingCommentingConfigurationRepository { Task AddAsync(MeetingCommentingConfiguration meetingCommentingConfiguration); Task GetByMeetingIdAsync(MeetingId meetingId); } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingCommentingConfigurations/MeetingCommentingConfiguration.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations { public class MeetingCommentingConfiguration : Entity, IAggregateRoot { public MeetingCommentingConfigurationId Id { get; } private MeetingId _meetingId; private bool _isCommentingEnabled; private MeetingCommentingConfiguration(MeetingId meetingId) { this.Id = new MeetingCommentingConfigurationId(Guid.NewGuid()); this._meetingId = meetingId; this._isCommentingEnabled = true; this.AddDomainEvent(new MeetingCommentingConfigurationCreatedDomainEvent(this._meetingId, this._isCommentingEnabled)); } private MeetingCommentingConfiguration() { // Only for EF. } public void EnableCommenting(MemberId enablingMemberId, MeetingGroup meetingGroup) { CheckRule(new MeetingCommentingCanBeEnabledOnlyByGroupOrganizerRule(enablingMemberId, meetingGroup)); if (!this._isCommentingEnabled) { this._isCommentingEnabled = true; AddDomainEvent(new MeetingCommentingEnabledDomainEvent(this._meetingId)); } } public void DisableCommenting(MemberId disablingMemberId, MeetingGroup meetingGroup) { CheckRule(new MeetingCommentingCanBeDisabledOnlyByGroupOrganizerRule(disablingMemberId, meetingGroup)); if (this._isCommentingEnabled) { this._isCommentingEnabled = false; AddDomainEvent(new MeetingCommentingDisabledDomainEvent(this._meetingId)); } } public bool GetIsCommentingEnabled() => _isCommentingEnabled; internal static MeetingCommentingConfiguration Create(MeetingId meetingId) => new MeetingCommentingConfiguration(meetingId); } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingCommentingConfigurations/MeetingCommentingConfigurationId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations { public class MeetingCommentingConfigurationId : TypedIdValueBase { public MeetingCommentingConfigurationId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingCommentingConfigurations/Rules/MeetingCommentingCanBeDisabledOnlyByGroupOrganizerRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations.Rules { public class MeetingCommentingCanBeDisabledOnlyByGroupOrganizerRule : IBusinessRule { private readonly MeetingGroup _meetingGroup; private readonly MemberId _disablingMemberId; public MeetingCommentingCanBeDisabledOnlyByGroupOrganizerRule(MemberId disablingMemberId, MeetingGroup meetingGroup) { _meetingGroup = meetingGroup; _disablingMemberId = disablingMemberId; } public bool IsBroken() => !_meetingGroup.IsOrganizer(_disablingMemberId); public string Message => "Commenting for meeting can be disabled only by group organizer"; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingCommentingConfigurations/Rules/MeetingCommentingCanBeEnabledOnlyByGroupOrganizerRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations.Rules { public class MeetingCommentingCanBeEnabledOnlyByGroupOrganizerRule : IBusinessRule { private readonly MeetingGroup _meetingGroup; private readonly MemberId _enablingMemberId; public MeetingCommentingCanBeEnabledOnlyByGroupOrganizerRule(MemberId enablingMemberId, MeetingGroup meetingGroup) { _meetingGroup = meetingGroup; _enablingMemberId = enablingMemberId; } public bool IsBroken() => !_meetingGroup.IsOrganizer(_enablingMemberId); public string Message => "Commenting for meeting can be enabled only by group organizer"; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Events/MeetingCommentAddedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Events { public class MeetingCommentAddedDomainEvent : DomainEventBase { public MeetingCommentId MeetingCommentId { get; } public MeetingId MeetingId { get; } public string Comment { get; } public MeetingCommentAddedDomainEvent(MeetingCommentId meetingCommentId, MeetingId meetingId, string comment) { MeetingCommentId = meetingCommentId; MeetingId = meetingId; Comment = comment; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Events/MeetingCommentEditedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Events { public class MeetingCommentEditedDomainEvent : DomainEventBase { public MeetingCommentId MeetingCommentId { get; } public string EditedComment { get; } public MeetingCommentEditedDomainEvent(MeetingCommentId meetingCommentId, string editedComment) { MeetingCommentId = meetingCommentId; EditedComment = editedComment; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Events/MeetingCommentRemovedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Events { public class MeetingCommentRemovedDomainEvent : DomainEventBase { public MeetingCommentId MeetingCommentId { get; } public MeetingCommentRemovedDomainEvent(MeetingCommentId meetingCommentId) { MeetingCommentId = meetingCommentId; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Events/ReplyToMeetingCommentAddedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Events { public class ReplyToMeetingCommentAddedDomainEvent : DomainEventBase { public MeetingCommentId MeetingCommentId { get; } public MeetingCommentId InReplyToCommentId { get; } public string Reply { get; } public ReplyToMeetingCommentAddedDomainEvent(MeetingCommentId meetingCommentId, MeetingCommentId inReplyToCommentId, string reply) { MeetingCommentId = meetingCommentId; InReplyToCommentId = inReplyToCommentId; Reply = reply; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/IMeetingCommentRepository.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments { public interface IMeetingCommentRepository { Task AddAsync(MeetingComment meetingComment); Task GetByIdAsync(MeetingCommentId meetingCommentId); } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/MeetingComment.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; #nullable enable namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments { public class MeetingComment : Entity, IAggregateRoot { public MeetingCommentId Id { get; } private MeetingId _meetingId; private MemberId _authorId; private MeetingCommentId? _inReplyToCommentId; private string _comment; private DateTime _createDate; private DateTime? _editDate; #pragma warning disable CS0414 // Field is assigned but its value is never used private bool _isRemoved; #pragma warning restore CS0414 // Field is assigned but its value is never used private string _removedByReason; private MeetingComment( MeetingId meetingId, MemberId authorId, string comment, MeetingCommentId? inReplyToCommentId, MeetingCommentingConfiguration meetingCommentingConfiguration, MeetingGroup meetingGroup) { this.CheckRule(new CommentTextMustBeProvidedRule(comment)); this.CheckRule(new CommentCanBeCreatedOnlyIfCommentingForMeetingEnabledRule(meetingCommentingConfiguration)); this.CheckRule(new CommentCanBeAddedOnlyByMeetingGroupMemberRule(authorId, meetingGroup)); this.Id = new MeetingCommentId(Guid.NewGuid()); _meetingId = meetingId; _authorId = authorId; _comment = comment; _inReplyToCommentId = inReplyToCommentId; _createDate = SystemClock.Now; _editDate = null; _isRemoved = false; _removedByReason = string.Empty; if (inReplyToCommentId == null) { this.AddDomainEvent(new MeetingCommentAddedDomainEvent(this.Id, _meetingId, comment)); } else { this.AddDomainEvent(new ReplyToMeetingCommentAddedDomainEvent(this.Id, inReplyToCommentId, comment)); } } #pragma warning disable CS8618 private MeetingComment() #pragma warning restore CS8618 { // Only for EF. } public void Edit(MemberId editorId, string editedComment, MeetingCommentingConfiguration meetingCommentingConfiguration) { this.CheckRule(new CommentTextMustBeProvidedRule(editedComment)); this.CheckRule(new MeetingCommentCanBeEditedOnlyByAuthorRule(this._authorId, editorId)); this.CheckRule(new CommentCanBeEditedOnlyIfCommentingForMeetingEnabledRule(meetingCommentingConfiguration)); _comment = editedComment; _editDate = SystemClock.Now; this.AddDomainEvent(new MeetingCommentEditedDomainEvent(this.Id, editedComment)); } public void Remove(MemberId removingMemberId, MeetingGroup meetingGroup, string reason = "") { this.CheckRule(new MeetingCommentCanBeRemovedOnlyByAuthorOrGroupOrganizerRule(meetingGroup, this._authorId, removingMemberId)); this.CheckRule(new RemovingReasonCanBeProvidedOnlyByGroupOrganizerRule(meetingGroup, removingMemberId, reason)); _isRemoved = true; _removedByReason = reason ?? string.Empty; this.AddDomainEvent(new MeetingCommentRemovedDomainEvent(this.Id)); } public MeetingComment Reply(MemberId replierId, string reply, MeetingGroup meetingGroup, MeetingCommentingConfiguration meetingCommentingConfiguration) => new MeetingComment( _meetingId, replierId, reply, this.Id, meetingCommentingConfiguration, meetingGroup); public MeetingMemberCommentLike Like( MemberId likerId, MeetingGroupMemberData likerMeetingGroupMember, int meetingMemberCommentLikesCount) { this.CheckRule(new CommentCanBeLikedOnlyByMeetingGroupMemberRule(likerMeetingGroupMember)); this.CheckRule(new CommentCannotBeLikedByTheSameMemberMoreThanOnceRule(meetingMemberCommentLikesCount)); return MeetingMemberCommentLike.Create(this.Id, likerId); } public MeetingId GetMeetingId() => this._meetingId; internal static MeetingComment Create( MeetingId meetingId, MemberId authorId, string comment, MeetingGroup meetingGroup, MeetingCommentingConfiguration meetingCommentingConfiguration) => new MeetingComment( meetingId, authorId, comment, inReplyToCommentId: null, meetingCommentingConfiguration, meetingGroup); } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/MeetingCommentId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments { public class MeetingCommentId : TypedIdValueBase { public MeetingCommentId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Rules/CommentCanBeAddedOnlyByMeetingGroupMemberRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Rules { public class CommentCanBeAddedOnlyByMeetingGroupMemberRule : IBusinessRule { private readonly MemberId _authorId; private readonly MeetingGroup _meetingGroup; public CommentCanBeAddedOnlyByMeetingGroupMemberRule(MemberId authorId, MeetingGroup meetingGroup) { _authorId = authorId; _meetingGroup = meetingGroup; } public bool IsBroken() => !_meetingGroup.IsMemberOfGroup(_authorId); public string Message => "Only meeting attendee can add comments"; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Rules/CommentCanBeCreatedOnlyIfCommentingForMeetingEnabledRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Rules { public class CommentCanBeCreatedOnlyIfCommentingForMeetingEnabledRule : IBusinessRule { private readonly MeetingCommentingConfiguration _meetingCommentingConfiguration; public CommentCanBeCreatedOnlyIfCommentingForMeetingEnabledRule(MeetingCommentingConfiguration meetingCommentingConfiguration) { _meetingCommentingConfiguration = meetingCommentingConfiguration; } public bool IsBroken() => !_meetingCommentingConfiguration.GetIsCommentingEnabled(); public string Message => "Commenting for meeting is disabled."; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Rules/CommentCanBeEditedOnlyIfCommentingForMeetingEnabledRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Rules { public class CommentCanBeEditedOnlyIfCommentingForMeetingEnabledRule : IBusinessRule { private readonly MeetingCommentingConfiguration _meetingCommentingConfiguration; public CommentCanBeEditedOnlyIfCommentingForMeetingEnabledRule(MeetingCommentingConfiguration meetingCommentingConfiguration) { _meetingCommentingConfiguration = meetingCommentingConfiguration; } public bool IsBroken() => !_meetingCommentingConfiguration.GetIsCommentingEnabled(); public string Message => "Commenting for meeting is disabled."; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Rules/CommentCanBeLikedOnlyByMeetingGroupMemberRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Rules { public class CommentCanBeLikedOnlyByMeetingGroupMemberRule : IBusinessRule { private readonly MeetingGroupMemberData _likerMeetingGroupMember; #nullable enable public CommentCanBeLikedOnlyByMeetingGroupMemberRule(MeetingGroupMemberData? likerMeetingGroupMember) { _likerMeetingGroupMember = likerMeetingGroupMember; } public bool IsBroken() => _likerMeetingGroupMember == null; public string Message => "Comment can be liked only by meeting group member."; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Rules/CommentCannotBeLikedByTheSameMemberMoreThanOnceRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Rules { public class CommentCannotBeLikedByTheSameMemberMoreThanOnceRule : IBusinessRule { private readonly int _memberCommentLikesCount; public CommentCannotBeLikedByTheSameMemberMoreThanOnceRule(int memberCommentLikesCount) { _memberCommentLikesCount = memberCommentLikesCount; } public bool IsBroken() => _memberCommentLikesCount > 0; public string Message => "Member cannot like one comment more than once."; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Rules/CommentTextMustBeProvidedRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Rules { public class CommentTextMustBeProvidedRule : IBusinessRule { private readonly string _comment; public CommentTextMustBeProvidedRule(string comment) { _comment = comment; } public bool IsBroken() => string.IsNullOrEmpty(_comment); public string Message => "Comment text must be provided."; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Rules/MeetingCommentCanBeEditedOnlyByAuthorRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Rules { public class MeetingCommentCanBeEditedOnlyByAuthorRule : IBusinessRule { private readonly MemberId _authorId; private readonly MemberId _editorId; public MeetingCommentCanBeEditedOnlyByAuthorRule(MemberId authorId, MemberId editorId) { _authorId = authorId; _editorId = editorId; } public bool IsBroken() => _editorId != _authorId; public string Message => "Only the author of a comment can edit it."; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Rules/MeetingCommentCanBeRemovedOnlyByAuthorOrGroupOrganizerRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Rules { public class MeetingCommentCanBeRemovedOnlyByAuthorOrGroupOrganizerRule : IBusinessRule { private readonly MeetingGroup _meetingGroup; private readonly MemberId _authorId; private readonly MemberId _removingMemberId; public MeetingCommentCanBeRemovedOnlyByAuthorOrGroupOrganizerRule(MeetingGroup meetingGroup, MemberId authorId, MemberId removingMemberId) { _meetingGroup = meetingGroup; _authorId = authorId; _removingMemberId = removingMemberId; } public bool IsBroken() => _removingMemberId != _authorId && !_meetingGroup.IsOrganizer(_removingMemberId); public string Message => "Only author of comment or group organizer can remove meeting comment."; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingComments/Rules/RemovingReasonCanBeProvidedOnlyByGroupOrganizerRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Rules { public class RemovingReasonCanBeProvidedOnlyByGroupOrganizerRule : IBusinessRule { private readonly MeetingGroup _meetingGroup; private readonly MemberId _removingMemberId; private readonly string _removingReason; public RemovingReasonCanBeProvidedOnlyByGroupOrganizerRule(MeetingGroup meetingGroup, MemberId removingMemberId, string removingReason) { _meetingGroup = meetingGroup; _removingMemberId = removingMemberId; _removingReason = removingReason; } public bool IsBroken() => !string.IsNullOrEmpty(_removingReason) && !_meetingGroup.IsOrganizer(_removingMemberId); public string Message => "Only group organizer can provide comment's removing reason."; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroupProposals/Events/MeetingGroupProposalAcceptedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals.Events { public class MeetingGroupProposalAcceptedDomainEvent : DomainEventBase { public MeetingGroupProposalId MeetingGroupProposalId { get; } public MeetingGroupProposalAcceptedDomainEvent(MeetingGroupProposalId meetingGroupProposalId) { MeetingGroupProposalId = meetingGroupProposalId; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroupProposals/Events/MeetingGroupProposedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals.Events { public class MeetingGroupProposedDomainEvent : DomainEventBase { public MeetingGroupProposedDomainEvent( MeetingGroupProposalId meetingGroupProposalId, string name, string description, MemberId proposalUserId, DateTime proposalDate, string locationCity, string locationCountryCode) { this.MeetingGroupProposalId = meetingGroupProposalId; this.Name = name; this.Description = description; this.LocationCity = locationCity; this.LocationCountryCode = locationCountryCode; this.ProposalDate = proposalDate; this.ProposalUserId = proposalUserId; } public MeetingGroupProposalId MeetingGroupProposalId { get; } public string Name { get; } public string Description { get; } public string LocationCity { get; } public string LocationCountryCode { get; } public MemberId ProposalUserId { get; } public DateTime ProposalDate { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroupProposals/IMeetingGroupProposalRepository.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals { public interface IMeetingGroupProposalRepository { Task AddAsync(MeetingGroupProposal meetingGroupProposal); Task GetByIdAsync(MeetingGroupProposalId meetingGroupProposalId); } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroupProposals/MeetingGroupProposal.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals { public class MeetingGroupProposal : Entity, IAggregateRoot { public MeetingGroupProposalId Id { get; private set; } private string _name; private string _description; private MeetingGroupLocation _location; private DateTime _proposalDate; private MemberId _proposalUserId; private MeetingGroupProposalStatus _status; public MeetingGroup CreateMeetingGroup() { return MeetingGroup.CreateBasedOnProposal(this.Id, _name, _description, _location, _proposalUserId); } private MeetingGroupProposal() { // Only for EF. } private MeetingGroupProposal( string name, string description, MeetingGroupLocation location, MemberId proposalUserId) { Id = new MeetingGroupProposalId(Guid.NewGuid()); _name = name; _description = description; _location = location; _proposalUserId = proposalUserId; _proposalDate = SystemClock.Now; _status = MeetingGroupProposalStatus.InVerification; this.AddDomainEvent(new MeetingGroupProposedDomainEvent(this.Id, _name, _description, proposalUserId, _proposalDate, _location.City, _location.CountryCode)); } public static MeetingGroupProposal ProposeNew( string name, string description, MeetingGroupLocation location, MemberId proposalMemberId) { return new MeetingGroupProposal(name, description, location, proposalMemberId); } public void Accept() { this.CheckRule(new MeetingGroupProposalCannotBeAcceptedMoreThanOnceRule(_status)); _status = MeetingGroupProposalStatus.Accepted; this.AddDomainEvent(new MeetingGroupProposalAcceptedDomainEvent(this.Id)); } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroupProposals/MeetingGroupProposalId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals { public class MeetingGroupProposalId : TypedIdValueBase { public MeetingGroupProposalId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroupProposals/MeetingGroupProposalStatus.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals { public class MeetingGroupProposalStatus : ValueObject { public string Value { get; } internal static MeetingGroupProposalStatus InVerification => new MeetingGroupProposalStatus("InVerification"); internal static MeetingGroupProposalStatus Accepted => new MeetingGroupProposalStatus("Accepted"); internal bool IsAccepted => Value == "Accepted"; private MeetingGroupProposalStatus(string value) { Value = value; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroupProposals/Rules/MeetingGroupProposalCannotBeAcceptedMoreThanOnceRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals.Rules { public class MeetingGroupProposalCannotBeAcceptedMoreThanOnceRule : IBusinessRule { private readonly MeetingGroupProposalStatus _actualStatus; internal MeetingGroupProposalCannotBeAcceptedMoreThanOnceRule(MeetingGroupProposalStatus actualStatus) { _actualStatus = actualStatus; } public bool IsBroken() => _actualStatus.IsAccepted; public string Message => "Meeting group proposal cannot be accepted more than once rule"; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Events/MeetingAttendeeChangedDecisionDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events { public class MeetingAttendeeChangedDecisionDomainEvent : DomainEventBase { public MeetingAttendeeChangedDecisionDomainEvent(MemberId memberId, MeetingId meetingId) { MemberId = memberId; MeetingId = meetingId; } public MemberId MemberId { get; } public MeetingId MeetingId { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Events/MeetingGroupCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events { public class MeetingGroupCreatedDomainEvent : DomainEventBase { public MeetingGroupId MeetingGroupId { get; } public MemberId CreatorId { get; } public MeetingGroupCreatedDomainEvent(MeetingGroupId meetingGroupId, MemberId creatorId) { this.MeetingGroupId = meetingGroupId; this.CreatorId = creatorId; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Events/MeetingGroupGeneralAttributesEditedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events { public class MeetingGroupGeneralAttributesEditedDomainEvent : DomainEventBase { public string NewName { get; } public string NewDescription { get; } public MeetingGroupLocation NewLocation { get; } public MeetingGroupGeneralAttributesEditedDomainEvent(string newName, string newDescription, MeetingGroupLocation newLocation) { this.NewName = newName; this.NewDescription = newDescription; this.NewLocation = newLocation; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Events/MeetingGroupMemberLeftGroupDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events { public class MeetingGroupMemberLeftGroupDomainEvent : DomainEventBase { public MeetingGroupMemberLeftGroupDomainEvent(MeetingGroupId meetingGroupId, MemberId memberId) { MeetingGroupId = meetingGroupId; MemberId = memberId; } public MeetingGroupId MeetingGroupId { get; } public MemberId MemberId { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Events/MeetingGroupPaymentInfoUpdatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events { public class MeetingGroupPaymentInfoUpdatedDomainEvent : DomainEventBase { public MeetingGroupPaymentInfoUpdatedDomainEvent(MeetingGroupId meetingGroupId, DateTime paymentDateTo) { MeetingGroupId = meetingGroupId; PaymentDateTo = paymentDateTo; } public MeetingGroupId MeetingGroupId { get; } public DateTime PaymentDateTo { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Events/MeetingNotAttendeeChangedDecisionDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events { public class MeetingNotAttendeeChangedDecisionDomainEvent : DomainEventBase { public MeetingNotAttendeeChangedDecisionDomainEvent(MemberId memberId, MeetingId meetingId) { MemberId = memberId; MeetingId = meetingId; } public MemberId MemberId { get; } public MeetingId MeetingId { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Events/NewMeetingGroupMemberJoinedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events { public class NewMeetingGroupMemberJoinedDomainEvent : DomainEventBase { public MeetingGroupId MeetingGroupId { get; } public MemberId MemberId { get; } public MeetingGroupMemberRole Role { get; } public NewMeetingGroupMemberJoinedDomainEvent(MeetingGroupId meetingGroupId, MemberId memberId, MeetingGroupMemberRole role) { this.MeetingGroupId = meetingGroupId; this.MemberId = memberId; this.Role = role; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/IMeetingGroupRepository.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups { public interface IMeetingGroupRepository { Task AddAsync(MeetingGroup meetingGroup); Task Commit(); Task GetByIdAsync(MeetingGroupId id); } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/MeetingGroup.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups { public class MeetingGroup : Entity, IAggregateRoot { public MeetingGroupId Id { get; private set; } private string _name; private string _description; private MeetingGroupLocation _location; private MemberId _creatorId; private List _members; private DateTime _createDate; private DateTime? _paymentDateTo; internal static MeetingGroup CreateBasedOnProposal( MeetingGroupProposalId meetingGroupProposalId, string name, string description, MeetingGroupLocation location, MemberId creatorId) { return new MeetingGroup(meetingGroupProposalId, name, description, location, creatorId); } private MeetingGroup() { // Only for EF. } private MeetingGroup(MeetingGroupProposalId meetingGroupProposalId, string name, string description, MeetingGroupLocation location, MemberId creatorId) { this.Id = new MeetingGroupId(meetingGroupProposalId.Value); this._name = name; this._description = description; this._creatorId = creatorId; this._location = location; this._createDate = SystemClock.Now; this.AddDomainEvent(new MeetingGroupCreatedDomainEvent(this.Id, creatorId)); this._members = [MeetingGroupMember.CreateNew(this.Id, this._creatorId, MeetingGroupMemberRole.Organizer)]; } public void EditGeneralAttributes(string name, string description, MeetingGroupLocation location) { this._name = name; this._description = description; this._location = location; this.AddDomainEvent(new MeetingGroupGeneralAttributesEditedDomainEvent(this._name, this._description, this._location)); } public void JoinToGroupMember(MemberId memberId) { this.CheckRule(new MeetingGroupMemberCannotBeAddedTwiceRule(_members, memberId)); this._members.Add(MeetingGroupMember.CreateNew(this.Id, memberId, MeetingGroupMemberRole.Member)); } public void LeaveGroup(MemberId memberId) { this.CheckRule(new NotActualGroupMemberCannotLeaveGroupRule(_members, memberId)); var member = this._members.Single(x => x.IsMember(memberId)); member.Leave(); } public void SetExpirationDate(DateTime dateTo) { _paymentDateTo = dateTo; this.AddDomainEvent(new MeetingGroupPaymentInfoUpdatedDomainEvent(this.Id, _paymentDateTo.Value)); } public Meeting CreateMeeting( string title, MeetingTerm term, string description, MeetingLocation location, int? attendeesLimit, int guestsLimit, Term rsvpTerm, MoneyValue eventFee, List hostsMembersIds, MemberId creatorId) { this.CheckRule(new MeetingCanBeOrganizedOnlyByPayedGroupRule(_paymentDateTo)); this.CheckRule(new MeetingHostMustBeAMeetingGroupMemberRule(creatorId, hostsMembersIds, _members)); return Meeting.CreateNew( this.Id, title, term, description, location, MeetingLimits.Create(attendeesLimit, guestsLimit), rsvpTerm, eventFee, hostsMembersIds, creatorId); } internal bool IsMemberOfGroup(MemberId attendeeId) { return _members.Any(x => x.IsMember(attendeeId)); } internal bool IsOrganizer(MemberId memberId) { return _members.Any(x => x.IsOrganizer(memberId)); } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/MeetingGroupId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups { public class MeetingGroupId : TypedIdValueBase { public MeetingGroupId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/MeetingGroupLocation.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups { public class MeetingGroupLocation : ValueObject { public static MeetingGroupLocation CreateNew(string city, string countryCode) { return new MeetingGroupLocation(city, countryCode); } public string City { get; } public string CountryCode { get; } private MeetingGroupLocation(string city, string countryCode) { City = city; CountryCode = countryCode; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/MeetingGroupMember.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups { public class MeetingGroupMember : Entity { internal MeetingGroupId MeetingGroupId { get; private set; } internal MemberId MemberId { get; private set; } private MeetingGroupMemberRole _role; internal DateTime JoinedDate { get; private set; } private bool _isActive; private DateTime? _leaveDate; private MeetingGroupMember() { // Only for EF. } private MeetingGroupMember( MeetingGroupId meetingGroupId, MemberId memberId, MeetingGroupMemberRole role) { this.MeetingGroupId = meetingGroupId; this.MemberId = memberId; this._role = role; this.JoinedDate = SystemClock.Now; this._isActive = true; this.AddDomainEvent(new NewMeetingGroupMemberJoinedDomainEvent(this.MeetingGroupId, this.MemberId, this._role)); } internal static MeetingGroupMember CreateNew( MeetingGroupId meetingGroupId, MemberId memberId, MeetingGroupMemberRole role) { return new MeetingGroupMember(meetingGroupId, memberId, role); } internal void Leave() { _isActive = false; _leaveDate = SystemClock.Now; this.AddDomainEvent(new MeetingGroupMemberLeftGroupDomainEvent(this.MeetingGroupId, this.MemberId)); } internal bool IsMember(MemberId memberId) { return this._isActive && this.MemberId == memberId; } internal bool IsOrganizer(MemberId memberId) { return this.IsMember(memberId) && _role == MeetingGroupMemberRole.Organizer; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/MeetingGroupMemberRole.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups { public class MeetingGroupMemberRole : ValueObject { public static MeetingGroupMemberRole Organizer => new MeetingGroupMemberRole("Organizer"); public static MeetingGroupMemberRole Member => new MeetingGroupMemberRole("Member"); public string Value { get; } private MeetingGroupMemberRole(string value) { this.Value = value; } public static MeetingGroupMemberRole Of(string roleCode) { return new MeetingGroupMemberRole(roleCode); } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Policies/MeetingGroupExpirationDatePolicy.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Policies { public static class MeetingGroupExpirationDatePolicy { public static List GetMeetingGroupsCoveredByMemberSubscription( List meetingGroups) { return meetingGroups .Where(x => x.Role == MeetingGroupMemberRole.Organizer) .Select(x => x.MeetingGroupId) .ToList(); } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Policies/MeetingGroupMemberData.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Policies { public class MeetingGroupMemberData { public MeetingGroupMemberData(MeetingGroupId meetingGroupId, MeetingGroupMemberRole role) { MeetingGroupId = meetingGroupId; Role = role; } public MeetingGroupId MeetingGroupId { get; } public MeetingGroupMemberRole Role { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Rules/MeetingCanBeOrganizedOnlyByPayedGroupRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Rules { public class MeetingCanBeOrganizedOnlyByPayedGroupRule : IBusinessRule { private readonly DateTime? _paymentDateTo; internal MeetingCanBeOrganizedOnlyByPayedGroupRule(DateTime? paymentDateTo) { _paymentDateTo = paymentDateTo; } public bool IsBroken() => !_paymentDateTo.HasValue || _paymentDateTo < SystemClock.Now; public string Message => "Meeting can be organized only by payed group"; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Rules/MeetingGroupMemberCannotBeAddedTwiceRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Rules { public class MeetingGroupMemberCannotBeAddedTwiceRule : IBusinessRule { private readonly List _members; private readonly MemberId _memberId; public MeetingGroupMemberCannotBeAddedTwiceRule(List members, MemberId memberId) : base() { _members = members; _memberId = memberId; } public bool IsBroken() => this._members.SingleOrDefault(x => x.IsMember(_memberId)) != null; public string Message => "Member cannot be added twice to the same group"; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Rules/MeetingHostMustBeAMeetingGroupMemberRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Rules { public class MeetingHostMustBeAMeetingGroupMemberRule : IBusinessRule { private readonly MemberId _creatorId; private readonly List _hostsMembersIds; private readonly List _members; public MeetingHostMustBeAMeetingGroupMemberRule( MemberId creatorId, List hostsMembersIds, List members) { _creatorId = creatorId; _hostsMembersIds = hostsMembersIds; _members = members; } public bool IsBroken() { var memberIds = _members.Select(x => x.MemberId).ToList(); if (!_hostsMembersIds.Any() && !memberIds.Contains(_creatorId)) { return true; } return _hostsMembersIds.Any() && _hostsMembersIds.Except(memberIds).Any(); } public string Message => "Meeting host must be a meeting group member"; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingGroups/Rules/NotActualGroupMemberCannotLeaveGroupRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Rules { public class NotActualGroupMemberCannotLeaveGroupRule : IBusinessRule { private readonly List _members; private readonly MemberId memberId; public NotActualGroupMemberCannotLeaveGroupRule(List members, MemberId memberId) : base() { _members = members; this.memberId = memberId; } public bool IsBroken() => this._members.SingleOrDefault(x => x.IsMember(memberId)) == null; public string Message => "Member is not member of this group so he cannot leave it"; } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingMemberCommentLikes/Events/MeetingCommentLikedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes.Events { public class MeetingCommentLikedDomainEvent : DomainEventBase { public MeetingCommentId MeetingCommentId { get; } public MemberId LikerId { get; } public MeetingCommentLikedDomainEvent(MeetingCommentId meetingCommentId, MemberId likerId) { MeetingCommentId = meetingCommentId; LikerId = likerId; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingMemberCommentLikes/Events/MeetingCommentUnlikedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes.Events { public class MeetingCommentUnlikedDomainEvent : DomainEventBase { public MeetingCommentId MeetingCommentId { get; } public MemberId LikerId { get; } public MeetingCommentUnlikedDomainEvent(MeetingCommentId meetingCommentId, MemberId likerId) { MeetingCommentId = meetingCommentId; LikerId = likerId; } } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingMemberCommentLikes/IMeetingMemberCommentLikesRepository.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes { public interface IMeetingMemberCommentLikesRepository { Task AddAsync(MeetingMemberCommentLike meetingMemberCommentLike); Task GetAsync(MemberId memberId, MeetingCommentId meetingCommentId); Task CountMemberCommentLikesAsync(MemberId memberId, MeetingCommentId meetingCommentId); void Remove(MeetingMemberCommentLike meetingMemberCommentLike); } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingMemberCommentLikes/MeetingMemberCommentLike.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes { public class MeetingMemberCommentLike : Entity, IAggregateRoot { public MeetingMemberCommentLikeId Id { get; } private MeetingCommentId _meetingCommentId; private MemberId _memberId; private MeetingMemberCommentLike() { // Only for EF. } private MeetingMemberCommentLike(MeetingCommentId meetingCommentId, MemberId memberId) { Id = new MeetingMemberCommentLikeId(Guid.NewGuid()); _meetingCommentId = meetingCommentId; _memberId = memberId; this.AddDomainEvent(new MeetingCommentLikedDomainEvent(meetingCommentId, memberId)); } public void Remove() { this.AddDomainEvent(new MeetingCommentUnlikedDomainEvent(_meetingCommentId, _memberId)); } public static MeetingMemberCommentLike Create(MeetingCommentId meetingCommentId, MemberId memberId) => new MeetingMemberCommentLike(meetingCommentId, memberId); } } ================================================ FILE: src/Modules/Meetings/Domain/MeetingMemberCommentLikes/MeetingMemberCommentLikeId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes { public class MeetingMemberCommentLikeId : TypedIdValueBase { public MeetingMemberCommentLikeId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Events/MeetingAttendeeAddedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events { public class MeetingAttendeeAddedDomainEvent : DomainEventBase { public MeetingAttendeeAddedDomainEvent( MeetingId meetingId, MemberId attendeeId, DateTime rsvpDate, string role, int guestsNumber, decimal? feeValue, string feeCurrency) { MeetingId = meetingId; AttendeeId = attendeeId; RSVPDate = rsvpDate; Role = role; GuestsNumber = guestsNumber; FeeValue = feeValue; FeeCurrency = feeCurrency; } public MeetingId MeetingId { get; } public MemberId AttendeeId { get; } public DateTime RSVPDate { get; } public string Role { get; } public int GuestsNumber { get; } public decimal? FeeValue { get; } public string FeeCurrency { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Events/MeetingAttendeeFeePaidDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events { public class MeetingAttendeeFeePaidDomainEvent : DomainEventBase { public MeetingAttendeeFeePaidDomainEvent(MeetingId meetingId, MemberId attendeeId) { MeetingId = meetingId; AttendeeId = attendeeId; } public MeetingId MeetingId { get; } public MemberId AttendeeId { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Events/MeetingAttendeeRemovedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events { public class MeetingAttendeeRemovedDomainEvent : DomainEventBase { public MeetingAttendeeRemovedDomainEvent(MemberId memberId, MeetingId meetingId, string reason) { MemberId = memberId; MeetingId = meetingId; Reason = reason; } public MemberId MemberId { get; } public MeetingId MeetingId { get; } public string Reason { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Events/MeetingCanceledDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events { public class MeetingCanceledDomainEvent : DomainEventBase { public MeetingCanceledDomainEvent(MeetingId meetingId, MemberId cancelMemberId, DateTime cancelDate) { MeetingId = meetingId; CancelMemberId = cancelMemberId; CancelDate = cancelDate; } public MeetingId MeetingId { get; } public MemberId CancelMemberId { get; } public DateTime CancelDate { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Events/MeetingCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events { public class MeetingCreatedDomainEvent : DomainEventBase { public MeetingCreatedDomainEvent(MeetingId meetingId) { MeetingId = meetingId; } public MeetingId MeetingId { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Events/MeetingEditedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events { public class MeetingEditedDomainEvent : DomainEventBase { public MeetingEditedDomainEvent(Guid meetingId) { MeetingId = meetingId; } public Guid MeetingId { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Events/MeetingMainAttributesChangedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events { public class MeetingMainAttributesChangedDomainEvent : DomainEventBase { public MeetingMainAttributesChangedDomainEvent(MeetingId meetingId) { MeetingId = meetingId; } public MeetingId MeetingId { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Events/MeetingNotAttendeeAddedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events { public class MeetingNotAttendeeAddedDomainEvent : DomainEventBase { public MeetingId MeetingId { get; } public MemberId MemberId { get; } public MeetingNotAttendeeAddedDomainEvent(MeetingId meetingId, MemberId memberId) { MeetingId = meetingId; MemberId = memberId; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Events/MeetingWaitlistMemberAddedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events { public class MeetingWaitlistMemberAddedDomainEvent : DomainEventBase { public MeetingWaitlistMemberAddedDomainEvent(MeetingId meetingId, MemberId memberId) { MeetingId = meetingId; MemberId = memberId; } public MeetingId MeetingId { get; } public MemberId MemberId { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Events/MemberSetAsAttendeeDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events { public class MemberSetAsAttendeeDomainEvent : DomainEventBase { public MemberSetAsAttendeeDomainEvent(MeetingId meetingId, MemberId hostId) { MeetingId = meetingId; HostId = hostId; } public MeetingId MeetingId { get; } public MemberId HostId { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Events/MemberSignedOffFromMeetingWaitlistDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events { public class MemberSignedOffFromMeetingWaitlistDomainEvent : DomainEventBase { public MemberSignedOffFromMeetingWaitlistDomainEvent(MeetingId meetingId, MemberId memberId) { MeetingId = meetingId; MemberId = memberId; } public MeetingId MeetingId { get; } public MemberId MemberId { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Events/NewMeetingHostSetDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events { public class NewMeetingHostSetDomainEvent : DomainEventBase { public NewMeetingHostSetDomainEvent(MeetingId meetingId, MemberId hostId) { MeetingId = meetingId; HostId = hostId; } public MeetingId MeetingId { get; } public MemberId HostId { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/IMeetingRepository.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings { public interface IMeetingRepository { Task AddAsync(Meeting meeting); Task GetByIdAsync(MeetingId id); } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Meeting.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings { public class Meeting : Entity, IAggregateRoot { private readonly MeetingGroupId _meetingGroupId; private readonly List _attendees; private readonly List _notAttendees; private readonly List _waitlistMembers; public MeetingId Id { get; private set; } private string _title; private MeetingTerm _term; private string _description; private MeetingLocation _location; private MeetingLimits _meetingLimits; private Term _rsvpTerm; private MoneyValue _eventFee; private MemberId _creatorId; private DateTime _createDate; private MemberId _changeMemberId; private DateTime? _changeDate; private DateTime? _cancelDate; private MemberId _cancelMemberId; private bool _isCanceled; private Meeting() { _attendees = []; _notAttendees = []; _waitlistMembers = []; } internal static Meeting CreateNew( MeetingGroupId meetingGroupId, string title, MeetingTerm term, string description, MeetingLocation location, MeetingLimits meetingLimits, Term rsvpTerm, MoneyValue eventFee, List hostsMembersIds, MemberId creatorId) { return new Meeting( meetingGroupId, title, term, description, location, meetingLimits, rsvpTerm, eventFee, hostsMembersIds, creatorId); } private Meeting( MeetingGroupId meetingGroupId, string title, MeetingTerm term, string description, MeetingLocation location, MeetingLimits meetingLimits, Term rsvpTerm, MoneyValue eventFee, List hostsMembersIds, MemberId creatorId) { Id = new MeetingId(Guid.NewGuid()); _meetingGroupId = meetingGroupId; _title = title; _term = term; _description = description; _location = location; _meetingLimits = meetingLimits; this.SetRsvpTerm(rsvpTerm, _term); _eventFee = eventFee; _creatorId = creatorId; _createDate = SystemClock.Now; _attendees = []; _notAttendees = []; _waitlistMembers = []; this.AddDomainEvent(new MeetingCreatedDomainEvent(this.Id)); var rsvpDate = SystemClock.Now; if (hostsMembersIds.Any()) { foreach (var hostMemberId in hostsMembersIds) { _attendees.Add(MeetingAttendee.CreateNew(this.Id, hostMemberId, rsvpDate, MeetingAttendeeRole.Host, 0, MoneyValue.Undefined)); } } else { _attendees.Add(MeetingAttendee.CreateNew(this.Id, creatorId, rsvpDate, MeetingAttendeeRole.Host, 0, MoneyValue.Undefined)); } } public void ChangeMainAttributes( string title, MeetingTerm term, string description, MeetingLocation location, MeetingLimits meetingLimits, Term rsvpTerm, MoneyValue eventFee, MemberId modifyUserId) { this.CheckRule(new AttendeesLimitCannotBeChangedToSmallerThanActiveAttendeesRule( meetingLimits, this.GetAllActiveAttendeesWithGuestsNumber())); _title = title; _term = term; _description = description; _location = location; _meetingLimits = meetingLimits; this.SetRsvpTerm(rsvpTerm, _term); _eventFee = eventFee; _changeDate = SystemClock.Now; _changeMemberId = modifyUserId; this.AddDomainEvent(new MeetingMainAttributesChangedDomainEvent(this.Id)); } public void AddAttendee(MeetingGroup meetingGroup, MemberId attendeeId, int guestsNumber) { this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term)); this.CheckRule(new AttendeeCanBeAddedOnlyInRsvpTermRule(_rsvpTerm)); this.CheckRule(new MeetingAttendeeMustBeAMemberOfGroupRule(attendeeId, meetingGroup)); this.CheckRule(new MemberCannotBeAnAttendeeOfMeetingMoreThanOnceRule(attendeeId, _attendees)); this.CheckRule(new MeetingGuestsNumberIsAboveLimitRule(_meetingLimits.GuestsLimit, guestsNumber)); this.CheckRule(new MeetingAttendeesNumberIsAboveLimitRule(_meetingLimits.AttendeesLimit, this.GetAllActiveAttendeesWithGuestsNumber(), guestsNumber)); var notAttendee = this.GetActiveNotAttendee(attendeeId); notAttendee?.ChangeDecision(); _attendees.Add(MeetingAttendee.CreateNew( this.Id, attendeeId, SystemClock.Now, MeetingAttendeeRole.Attendee, guestsNumber, _eventFee)); } public void AddNotAttendee(MemberId memberId) { this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term)); this.CheckRule(new MemberCannotBeNotAttendeeTwiceRule(_notAttendees, memberId)); _notAttendees.Add(MeetingNotAttendee.CreateNew(this.Id, memberId)); var attendee = this.GetActiveAttendee(memberId); attendee?.ChangeDecision(); var nextWaitlistMember = _waitlistMembers .Where(x => x.IsActive()) .OrderBy(x => x.SignUpDate) .FirstOrDefault(); if (nextWaitlistMember != null) { _attendees.Add(MeetingAttendee.CreateNew( this.Id, nextWaitlistMember.MemberId, nextWaitlistMember.SignUpDate, MeetingAttendeeRole.Attendee, 0, this._eventFee)); nextWaitlistMember.MarkIsMovedToAttendees(); } } public void ChangeNotAttendeeDecision(MemberId memberId) { this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term)); this.CheckRule(new NotActiveNotAttendeeCannotChangeDecisionRule(_notAttendees, memberId)); var notAttendee = _notAttendees.Single(x => x.IsActiveNotAttendee(memberId)); notAttendee.ChangeDecision(); } public void SignUpMemberToWaitlist(MeetingGroup meetingGroup, MemberId memberId) { this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term)); this.CheckRule(new AttendeeCanBeAddedOnlyInRsvpTermRule(_rsvpTerm)); this.CheckRule(new MemberOnWaitlistMustBeAMemberOfGroupRule(meetingGroup, memberId, _attendees)); this.CheckRule(new MemberCannotBeMoreThanOnceOnMeetingWaitlistRule(_waitlistMembers, memberId)); _waitlistMembers.Add(MeetingWaitlistMember.CreateNew(this.Id, memberId)); } public void SignOffMemberFromWaitlist(MemberId memberId) { this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term)); this.CheckRule(new NotActiveMemberOfWaitlistCannotBeSignedOffRule(_waitlistMembers, memberId)); var memberOnWaitlist = this.GetActiveMemberOnWaitlist(memberId); memberOnWaitlist.SignOff(); } public void SetHostRole(MeetingGroup meetingGroup, MemberId settingMemberId, MemberId newOrganizerId) { this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term)); this.CheckRule(new OnlyMeetingOrGroupOrganizerCanSetMeetingMemberRolesRule(settingMemberId, meetingGroup, _attendees)); this.CheckRule(new OnlyMeetingAttendeeCanHaveChangedRoleRule(_attendees, newOrganizerId)); var attendee = this.GetActiveAttendee(newOrganizerId); attendee.SetAsHost(); } public void SetAttendeeRole(MeetingGroup meetingGroup, MemberId settingMemberId, MemberId newOrganizerId) { this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term)); this.CheckRule(new OnlyMeetingOrGroupOrganizerCanSetMeetingMemberRolesRule(settingMemberId, meetingGroup, _attendees)); this.CheckRule(new OnlyMeetingAttendeeCanHaveChangedRoleRule(_attendees, newOrganizerId)); var attendee = this.GetActiveAttendee(newOrganizerId); attendee.SetAsAttendee(); var meetingHostNumber = _attendees.Count(x => x.IsActiveHost()); this.CheckRule(new MeetingMustHaveAtLeastOneHostRule(meetingHostNumber)); } public MeetingGroupId GetMeetingGroupId() => _meetingGroupId; public void Cancel(MemberId cancelMemberId) { this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term)); if (!_isCanceled) { _isCanceled = true; _cancelDate = SystemClock.Now; _cancelMemberId = cancelMemberId; this.AddDomainEvent(new MeetingCanceledDomainEvent(this.Id, _cancelMemberId, _cancelDate.Value)); } } public void RemoveAttendee(MemberId attendeeId, MemberId removingPersonId, string reason) { this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term)); this.CheckRule(new OnlyActiveAttendeeCanBeRemovedFromMeetingRule(_attendees, attendeeId)); var attendee = this.GetActiveAttendee(attendeeId); attendee.Remove(removingPersonId, reason); } public void MarkAttendeeFeeAsPayed(MemberId memberId) { var attendee = GetActiveAttendee(memberId); attendee.MarkFeeAsPayed(); } public MeetingComment AddComment(MemberId authorId, string comment, MeetingGroup meetingGroup, MeetingCommentingConfiguration meetingCommentingConfiguration) => MeetingComment.Create( this.Id, authorId, comment, meetingGroup, meetingCommentingConfiguration); public MeetingCommentingConfiguration CreateCommentingConfiguration() { return MeetingCommentingConfiguration.Create(this.Id); } private MeetingWaitlistMember GetActiveMemberOnWaitlist(MemberId memberId) { return _waitlistMembers.SingleOrDefault(x => x.IsActiveOnWaitList(memberId)); } private MeetingAttendee GetActiveAttendee(MemberId attendeeId) { return _attendees.SingleOrDefault(x => x.IsActiveAttendee(attendeeId)); } private MeetingNotAttendee GetActiveNotAttendee(MemberId memberId) { return _notAttendees.SingleOrDefault(x => x.IsActiveNotAttendee(memberId)); } private int GetAllActiveAttendeesWithGuestsNumber() { return _attendees.Where(x => x.IsActive()).Sum(x => x.GetAttendeeWithGuestsNumber()); } private void SetRsvpTerm(Term rsvpTerm, MeetingTerm meetingTerm) { if (!rsvpTerm.EndDate.HasValue || rsvpTerm.EndDate > meetingTerm.StartDate) { _rsvpTerm = Term.CreateNewBetweenDates(rsvpTerm.StartDate, meetingTerm.StartDate); } else { _rsvpTerm = rsvpTerm; } } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/MeetingAttendee.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings { public class MeetingAttendee : Entity { internal MemberId AttendeeId { get; private set; } internal MeetingId MeetingId { get; private set; } private DateTime _decisionDate; private MeetingAttendeeRole _role; private int _guestsNumber; private bool _decisionChanged; private DateTime? _decisionChangeDate; private DateTime? _removedDate; private MemberId _removingMemberId; private string _removingReason; private bool _isRemoved; private MoneyValue _fee; #pragma warning disable CS0414 // Field is assigned but its value is never used private bool _isFeePaid; #pragma warning restore CS0414 // Field is assigned but its value is never used private MeetingAttendee() { } internal static MeetingAttendee CreateNew( MeetingId meetingId, MemberId attendeeId, DateTime decisionDate, MeetingAttendeeRole role, int guestsNumber, MoneyValue eventFee) { return new MeetingAttendee(meetingId, attendeeId, decisionDate, role, guestsNumber, eventFee); } private MeetingAttendee( MeetingId meetingId, MemberId attendeeId, DateTime decisionDate, MeetingAttendeeRole role, int guestsNumber, MoneyValue eventFee) { this.AttendeeId = attendeeId; this.MeetingId = meetingId; this._decisionDate = decisionDate; this._role = role; _guestsNumber = guestsNumber; _decisionChanged = false; _isFeePaid = false; if (eventFee != MoneyValue.Undefined) { _fee = (1 + guestsNumber) * eventFee; } else { _fee = MoneyValue.Undefined; } this.AddDomainEvent(new MeetingAttendeeAddedDomainEvent( this.MeetingId, AttendeeId, decisionDate, role.Value, guestsNumber, _fee.Value, _fee.Currency)); } internal void ChangeDecision() { _decisionChanged = true; _decisionChangeDate = SystemClock.Now; this.AddDomainEvent(new MeetingAttendeeChangedDecisionDomainEvent(this.AttendeeId, this.MeetingId)); } internal bool IsActiveAttendee(MemberId attendeeId) { return this.AttendeeId == attendeeId && !_decisionChanged; } internal bool IsActive() { return !_decisionChangeDate.HasValue && !_isRemoved; } internal bool IsActiveHost() { return this.IsActive() && _role == MeetingAttendeeRole.Host; } internal int GetAttendeeWithGuestsNumber() { return 1 + _guestsNumber; } internal void SetAsHost() { _role = MeetingAttendeeRole.Host; this.AddDomainEvent(new NewMeetingHostSetDomainEvent(this.MeetingId, this.AttendeeId)); } internal void SetAsAttendee() { this.CheckRule(new MemberCannotHaveSetAttendeeRoleMoreThanOnceRule(_role)); _role = MeetingAttendeeRole.Attendee; this.AddDomainEvent(new MemberSetAsAttendeeDomainEvent(this.MeetingId, this.AttendeeId)); } internal void Remove(MemberId removingMemberId, string reason) { this.CheckRule(new ReasonOfRemovingAttendeeFromMeetingMustBeProvidedRule(reason)); _isRemoved = true; _removedDate = SystemClock.Now; _removingReason = reason; _removingMemberId = removingMemberId; this.AddDomainEvent(new MeetingAttendeeRemovedDomainEvent(this.AttendeeId, this.MeetingId, reason)); } internal void MarkFeeAsPayed() { _isFeePaid = true; this.AddDomainEvent(new MeetingAttendeeFeePaidDomainEvent(this.MeetingId, this.AttendeeId)); } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/MeetingAttendeeRole.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings { public class MeetingAttendeeRole : ValueObject { public static MeetingAttendeeRole Host => new MeetingAttendeeRole("Host"); public static MeetingAttendeeRole Attendee => new MeetingAttendeeRole("Attendee"); public string Value { get; } private MeetingAttendeeRole(string value) { this.Value = value; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/MeetingId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings { public class MeetingId : TypedIdValueBase { public MeetingId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/MeetingLimits.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings { public class MeetingLimits : ValueObject { public int? AttendeesLimit { get; } public int GuestsLimit { get; } private MeetingLimits(int? attendeesLimit, int guestsLimit) { AttendeesLimit = attendeesLimit; GuestsLimit = guestsLimit; } public static MeetingLimits Create(int? attendeesLimit, int guestsLimit) { CheckRule(new MeetingAttendeesLimitCannotBeNegativeRule(attendeesLimit)); CheckRule(new MeetingGuestsLimitCannotBeNegativeRule(guestsLimit)); CheckRule(new MeetingAttendeesLimitMustBeGreaterThanGuestsLimitRule(attendeesLimit, guestsLimit)); return new MeetingLimits(attendeesLimit, guestsLimit); } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/MeetingLocation.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings { public class MeetingLocation : ValueObject { public static MeetingLocation CreateNew(string name, string address, string postalCode, string city) { return new MeetingLocation(name, address, postalCode, city); } private MeetingLocation(string name, string address, string postalCode, string city) { Name = name; Address = address; PostalCode = postalCode; City = city; } public string Name { get; } public string Address { get; } public string PostalCode { get; } public string City { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/MeetingNotAttendee.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings { public class MeetingNotAttendee : Entity { internal MemberId MemberId { get; private set; } internal MeetingId MeetingId { get; private set; } private DateTime _decisionDate; private bool _decisionChanged; private DateTime? _decisionChangeDate; private MeetingNotAttendee() { } private MeetingNotAttendee(MeetingId meetingId, MemberId memberId) { this.MemberId = memberId; this.MeetingId = meetingId; _decisionDate = DateTime.UtcNow; this.AddDomainEvent(new MeetingNotAttendeeAddedDomainEvent(this.MeetingId, this.MemberId)); } internal static MeetingNotAttendee CreateNew(MeetingId meetingId, MemberId memberId) { return new MeetingNotAttendee(meetingId, memberId); } internal bool IsActiveNotAttendee(MemberId memberId) { return !this._decisionChanged && this.MemberId == memberId; } internal void ChangeDecision() { _decisionChanged = true; _decisionChangeDate = SystemClock.Now; this.AddDomainEvent(new MeetingNotAttendeeChangedDecisionDomainEvent(this.MemberId, this.MeetingId)); } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/MeetingTerm.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings { public class MeetingTerm : ValueObject { public DateTime StartDate { get; } public DateTime EndDate { get; } public static MeetingTerm CreateNewBetweenDates(DateTime startDate, DateTime endDate) { return new MeetingTerm(startDate, endDate); } private MeetingTerm(DateTime startDate, DateTime endDate) { this.StartDate = startDate; this.EndDate = endDate; } internal bool IsAfterStart() { return SystemClock.Now > this.StartDate; } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/MeetingWaitlistMember.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings { public class MeetingWaitlistMember : Entity { internal MemberId MemberId { get; private set; } internal MeetingId MeetingId { get; private set; } internal DateTime SignUpDate { get; private set; } private bool _isSignedOff; private DateTime? _signOffDate; private bool _isMovedToAttendees; private DateTime? _movedToAttendeesDate; private MeetingWaitlistMember() { } private MeetingWaitlistMember(MeetingId meetingId, MemberId memberId) { this.MemberId = memberId; this.MeetingId = meetingId; this.SignUpDate = SystemClock.Now; _isMovedToAttendees = false; this.AddDomainEvent(new MeetingWaitlistMemberAddedDomainEvent(this.MeetingId, this.MemberId)); } internal static MeetingWaitlistMember CreateNew(MeetingId meetingId, MemberId memberId) { return new MeetingWaitlistMember(meetingId, memberId); } internal void MarkIsMovedToAttendees() { _isMovedToAttendees = true; _movedToAttendeesDate = SystemClock.Now; } internal bool IsActiveOnWaitList(MemberId memberId) { return this.MemberId == memberId && this.IsActive(); } internal bool IsActive() { return !_isSignedOff && !_isMovedToAttendees; } internal void SignOff() { _isSignedOff = true; _signOffDate = SystemClock.Now; this.AddDomainEvent(new MemberSignedOffFromMeetingWaitlistDomainEvent(this.MeetingId, this.MemberId)); } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/MoneyValue.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings { public class MoneyValue : ValueObject { public static MoneyValue Undefined => new MoneyValue(null, null); public decimal? Value { get; } public string Currency { get; } public static MoneyValue Of(decimal value, string currency) { return new MoneyValue(value, currency); } private MoneyValue(decimal? value, string currency) { this.Value = value; this.Currency = currency; } public static MoneyValue operator *(int left, MoneyValue right) { return new MoneyValue(right.Value * left, right.Currency); } } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/AttendeeCanBeAddedOnlyInRsvpTermRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class AttendeeCanBeAddedOnlyInRsvpTermRule : IBusinessRule { private readonly Term _rsvpTerm; internal AttendeeCanBeAddedOnlyInRsvpTermRule(Term rsvpTerm) { _rsvpTerm = rsvpTerm; } public bool IsBroken() => !_rsvpTerm.IsInTerm(SystemClock.Now); public string Message => "Attendee can be added only in RSVP term"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/AttendeesLimitCannotBeChangedToSmallerThanActiveAttendeesRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { internal class AttendeesLimitCannotBeChangedToSmallerThanActiveAttendeesRule : IBusinessRule { private readonly int? _attendeesLimit; private readonly int _allActiveAttendeesWithGuestsNumber; internal AttendeesLimitCannotBeChangedToSmallerThanActiveAttendeesRule( MeetingLimits meetingLimits, int allActiveAttendeesWithGuestsNumber) { this._attendeesLimit = meetingLimits.AttendeesLimit; this._allActiveAttendeesWithGuestsNumber = allActiveAttendeesWithGuestsNumber; } public bool IsBroken() => _attendeesLimit.HasValue && _attendeesLimit.Value < _allActiveAttendeesWithGuestsNumber; public string Message => "Attendees limit cannot be change to smaller than active attendees number"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MeetingAttendeeMustBeAMemberOfGroupRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MeetingAttendeeMustBeAMemberOfGroupRule : IBusinessRule { private readonly MeetingGroup _meetingGroup; private readonly MemberId _attendeeId; internal MeetingAttendeeMustBeAMemberOfGroupRule(MemberId attendeeId, MeetingGroup meetingGroup) { _attendeeId = attendeeId; _meetingGroup = meetingGroup; } public bool IsBroken() { return !_meetingGroup.IsMemberOfGroup(_attendeeId); } public string Message => "Meeting attendee must be a member of group"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MeetingAttendeesLimitCannotBeNegativeRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MeetingAttendeesLimitCannotBeNegativeRule : IBusinessRule { private readonly int? _attendeesLimit; public MeetingAttendeesLimitCannotBeNegativeRule(int? attendeesLimit) { _attendeesLimit = attendeesLimit; } public bool IsBroken() => _attendeesLimit.HasValue && _attendeesLimit.Value < 0; public string Message => "Attendees limit cannot be negative"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MeetingAttendeesLimitMustBeGreaterThanGuestsLimitRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MeetingAttendeesLimitMustBeGreaterThanGuestsLimitRule : IBusinessRule { private readonly int? _attendeesLimit; private readonly int _guestsLimit; public MeetingAttendeesLimitMustBeGreaterThanGuestsLimitRule(int? attendeesLimit, int guestsLimit) { _attendeesLimit = attendeesLimit; _guestsLimit = guestsLimit; } public bool IsBroken() => _attendeesLimit.HasValue && _attendeesLimit.Value <= _guestsLimit; public string Message => "Attendees limit must be greater than guests limit"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MeetingAttendeesNumberIsAboveLimitRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MeetingAttendeesNumberIsAboveLimitRule : IBusinessRule { private readonly int? _attendeesLimit; private readonly int _allActiveAttendeesWithGuestsNumber; private readonly int _guestsNumber; internal MeetingAttendeesNumberIsAboveLimitRule( int? attendeesLimit, int allActiveAttendeesWithGuestsNumber, int guestsNumber) { _attendeesLimit = attendeesLimit; _allActiveAttendeesWithGuestsNumber = allActiveAttendeesWithGuestsNumber; _guestsNumber = guestsNumber; } public bool IsBroken() => this._attendeesLimit.HasValue && this._attendeesLimit.Value < _allActiveAttendeesWithGuestsNumber + 1 + _guestsNumber; public string Message => "Meeting attendees number is above limit"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MeetingCannotBeChangedAfterStartRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MeetingCannotBeChangedAfterStartRule : IBusinessRule { private readonly MeetingTerm _meetingTerm; public MeetingCannotBeChangedAfterStartRule(MeetingTerm meetingTerm) { _meetingTerm = meetingTerm; } public bool IsBroken() => _meetingTerm.IsAfterStart(); public string Message => "Meeting cannot be changed after start"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MeetingGuestsLimitCannotBeNegativeRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MeetingGuestsLimitCannotBeNegativeRule : IBusinessRule { private readonly int _guestsLimit; public MeetingGuestsLimitCannotBeNegativeRule(int guestsLimit) { _guestsLimit = guestsLimit; } public bool IsBroken() => _guestsLimit < 0; public string Message => "Guests limit cannot be negative"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MeetingGuestsNumberIsAboveLimitRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MeetingGuestsNumberIsAboveLimitRule : IBusinessRule { private readonly int _guestsNumber; private readonly int _guestsLimit; public MeetingGuestsNumberIsAboveLimitRule(int guestsLimit, int guestsNumber) { _guestsNumber = guestsNumber; _guestsLimit = guestsLimit; } public bool IsBroken() => this._guestsLimit > 0 && this._guestsLimit < _guestsNumber; public string Message => "Meeting guests number is above limit"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MeetingMustHaveAtLeastOneHostRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MeetingMustHaveAtLeastOneHostRule : IBusinessRule { private readonly int _meetingHostNumber; public MeetingMustHaveAtLeastOneHostRule(int meetingHostNumber) { _meetingHostNumber = meetingHostNumber; } public bool IsBroken() => _meetingHostNumber == 0; public string Message => "Meeting must have at least one host"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MemberCannotBeAnAttendeeOfMeetingMoreThanOnceRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MemberCannotBeAnAttendeeOfMeetingMoreThanOnceRule : IBusinessRule { private readonly MemberId _attendeeId; private readonly List _attendees; public MemberCannotBeAnAttendeeOfMeetingMoreThanOnceRule(MemberId attendeeId, List attendees) { this._attendeeId = attendeeId; _attendees = attendees; } public bool IsBroken() => _attendees.SingleOrDefault(x => x.IsActiveAttendee(_attendeeId)) != null; public string Message => "Member is already an attendee of this meeting"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MemberCannotBeMoreThanOnceOnMeetingWaitlistRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MemberCannotBeMoreThanOnceOnMeetingWaitlistRule : IBusinessRule { private readonly List _waitListMembers; private readonly MemberId _memberId; internal MemberCannotBeMoreThanOnceOnMeetingWaitlistRule(List waitListMembers, MemberId memberId) { _waitListMembers = waitListMembers; _memberId = memberId; } public bool IsBroken() => _waitListMembers.SingleOrDefault(x => x.IsActiveOnWaitList(_memberId)) != null; public string Message => "Member cannot be more than once on the meeting waitlist"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MemberCannotBeNotAttendeeTwiceRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MemberCannotBeNotAttendeeTwiceRule : IBusinessRule { private readonly List _notAttendees; private readonly MemberId _memberId; public MemberCannotBeNotAttendeeTwiceRule(List notAttendees, MemberId memberId) { _notAttendees = notAttendees; _memberId = memberId; } public bool IsBroken() => _notAttendees.SingleOrDefault(x => x.IsActiveNotAttendee(_memberId)) != null; public string Message => "Member cannot be active not attendee twice"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MemberCannotHaveSetAttendeeRoleMoreThanOnceRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MemberCannotHaveSetAttendeeRoleMoreThanOnceRule : IBusinessRule { private readonly MeetingAttendeeRole _meetingAttendeeRole; internal MemberCannotHaveSetAttendeeRoleMoreThanOnceRule(MeetingAttendeeRole meetingAttendeeRole) { _meetingAttendeeRole = meetingAttendeeRole; } public bool IsBroken() => _meetingAttendeeRole == MeetingAttendeeRole.Attendee; public string Message => "Member cannot be attendee of meeting more than once"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/MemberOnWaitlistMustBeAMemberOfGroupRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class MemberOnWaitlistMustBeAMemberOfGroupRule : IBusinessRule { private readonly MeetingGroup _meetingGroup; private readonly MemberId _memberId; private readonly List _attendees; internal MemberOnWaitlistMustBeAMemberOfGroupRule(MeetingGroup meetingGroup, MemberId memberId, List attendees) : base() { _meetingGroup = meetingGroup; _memberId = memberId; _attendees = attendees; } public bool IsBroken() => !_meetingGroup.IsMemberOfGroup(_memberId); public string Message => "Member on waitlist must be a member of group"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/NotActiveMemberOfWaitlistCannotBeSignedOffRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class NotActiveMemberOfWaitlistCannotBeSignedOffRule : IBusinessRule { private readonly List _waitlistMembers; private readonly MemberId _memberId; public NotActiveMemberOfWaitlistCannotBeSignedOffRule(List waitlistMembers, MemberId memberId) { _waitlistMembers = waitlistMembers; _memberId = memberId; } public bool IsBroken() => _waitlistMembers.SingleOrDefault(x => x.IsActiveOnWaitList(_memberId)) == null; public string Message => "Not active member of waitlist cannot be signed off"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/NotActiveNotAttendeeCannotChangeDecisionRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class NotActiveNotAttendeeCannotChangeDecisionRule : IBusinessRule { private readonly List _notAttendees; private readonly MemberId _memberId; internal NotActiveNotAttendeeCannotChangeDecisionRule(List notAttendees, MemberId memberId) { _notAttendees = notAttendees; _memberId = memberId; } public bool IsBroken() => _notAttendees.SingleOrDefault(x => x.IsActiveNotAttendee(_memberId)) == null; public string Message => "Member is not active not attendee"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/OnlyActiveAttendeeCanBeRemovedFromMeetingRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class OnlyActiveAttendeeCanBeRemovedFromMeetingRule : IBusinessRule { private readonly List _attendees; private readonly MemberId _attendeeId; internal OnlyActiveAttendeeCanBeRemovedFromMeetingRule( List attendees, MemberId attendeeId) { _attendees = attendees; _attendeeId = attendeeId; } public bool IsBroken() => _attendees.SingleOrDefault(x => x.IsActiveAttendee(_attendeeId)) == null; public string Message => "Only active attendee can be removed from meeting"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/OnlyMeetingAttendeeCanHaveChangedRoleRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { internal class OnlyMeetingAttendeeCanHaveChangedRoleRule : IBusinessRule { private readonly List _attendees; private readonly MemberId _newOrganizerId; internal OnlyMeetingAttendeeCanHaveChangedRoleRule(List attendees, MemberId newOrganizerId) { _attendees = attendees; _newOrganizerId = newOrganizerId; } public bool IsBroken() => _attendees.SingleOrDefault(x => x.IsActiveAttendee(_newOrganizerId)) == null; public string Message => "Only meeting attendee can be se as organizer of meeting"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/OnlyMeetingOrGroupOrganizerCanSetMeetingMemberRolesRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class OnlyMeetingOrGroupOrganizerCanSetMeetingMemberRolesRule : IBusinessRule { private readonly MemberId _settingMemberId; private readonly MeetingGroup _meetingGroup; private readonly List _attendees; public OnlyMeetingOrGroupOrganizerCanSetMeetingMemberRolesRule(MemberId settingMemberId, MeetingGroup meetingGroup, List attendees) { _settingMemberId = settingMemberId; _meetingGroup = meetingGroup; _attendees = attendees; } public bool IsBroken() { var settingMember = _attendees.SingleOrDefault(x => x.IsActiveAttendee(_settingMemberId)); var isHost = settingMember != null && settingMember.IsActiveHost(); var isOrganizer = _meetingGroup.IsOrganizer(_settingMemberId); return !isHost && !isOrganizer; } public string Message => "Only meeting host or group organizer can set meeting member roles"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Rules/ReasonOfRemovingAttendeeFromMeetingMustBeProvidedRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules { public class ReasonOfRemovingAttendeeFromMeetingMustBeProvidedRule : IBusinessRule { private readonly string _reason; internal ReasonOfRemovingAttendeeFromMeetingMustBeProvidedRule(string reason) { _reason = reason; } public bool IsBroken() => string.IsNullOrEmpty(_reason); public string Message => "Reason of removing attendee from meeting must be provided"; } } ================================================ FILE: src/Modules/Meetings/Domain/Meetings/Term.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings { public class Term : ValueObject { public static Term NoTerm => new Term(null, null); public DateTime? StartDate { get; } public DateTime? EndDate { get; } public static Term CreateNewBetweenDates(DateTime? startDate, DateTime? endDate) { return new Term(startDate, endDate); } private Term(DateTime? startDate, DateTime? endDate) { this.StartDate = startDate; this.EndDate = endDate; } internal bool IsInTerm(DateTime date) { var left = !this.StartDate.HasValue || this.StartDate.Value <= date; var right = !this.EndDate.HasValue || this.EndDate.Value >= date; return left && right; } } } ================================================ FILE: src/Modules/Meetings/Domain/Members/Events/MemberCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Members.Events { public class MemberCreatedDomainEvent : DomainEventBase { public MemberId MemberId { get; } public MemberCreatedDomainEvent(MemberId memberId) { MemberId = memberId; } } } ================================================ FILE: src/Modules/Meetings/Domain/Members/IMemberContext.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Members { public interface IMemberContext { MemberId MemberId { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Members/IMemberRepository.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Members { public interface IMemberRepository { Task AddAsync(Member member); Task GetByIdAsync(MemberId memberId); } } ================================================ FILE: src/Modules/Meetings/Domain/Members/MeetingGroupMemberData.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Members { public class MeetingGroupMemberData { public MeetingGroupId MeetingGroupId { get; } public MemberId MemberId { get; } public MeetingGroupMemberData(MeetingGroupId meetingGroupId, MemberId memberId) { MemberId = memberId; MeetingGroupId = meetingGroupId; } } } ================================================ FILE: src/Modules/Meetings/Domain/Members/Member.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Members { public class Member : Entity, IAggregateRoot { public MemberId Id { get; private set; } private string _login; private string _email; private string _firstName; private string _lastName; private string _name; private DateTime _createDate; private Member() { // Only for EF. } public static Member Create(Guid id, string login, string email, string firstName, string lastName, string name) { return new Member(id, login, email, firstName, lastName, name); } private Member(Guid id, string login, string email, string firstName, string lastName, string name) { this.Id = new MemberId(id); _login = login; _email = email; _firstName = firstName; _lastName = lastName; _name = name; _createDate = SystemClock.Now; this.AddDomainEvent(new MemberCreatedDomainEvent(this.Id)); } } } ================================================ FILE: src/Modules/Meetings/Domain/Members/MemberId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Members { public class MemberId : TypedIdValueBase { public MemberId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Meetings/Domain/Members/MemberSubscriptions/Events/MemberSubscriptionExpirationDateChangedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Members.MemberSubscriptions.Events { public class MemberSubscriptionExpirationDateChangedDomainEvent : DomainEventBase { public MemberSubscriptionExpirationDateChangedDomainEvent(MemberId memberId, DateTime expirationDate) { MemberId = memberId; ExpirationDate = expirationDate; } public MemberId MemberId { get; } public DateTime ExpirationDate { get; } } } ================================================ FILE: src/Modules/Meetings/Domain/Members/MemberSubscriptions/IMemberSubscriptionRepository.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Members.MemberSubscriptions { public interface IMemberSubscriptionRepository { Task GetByIdOptionalAsync(MemberSubscriptionId memberSubscriptionId); Task AddAsync(MemberSubscription memberSubscription); } } ================================================ FILE: src/Modules/Meetings/Domain/Members/MemberSubscriptions/MemberSubscription.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members.MemberSubscriptions.Events; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Members.MemberSubscriptions { public class MemberSubscription : Entity, IAggregateRoot { public MemberSubscriptionId Id { get; private set; } private DateTime _expirationDate; private MemberSubscription() { // Only for EF. } private MemberSubscription(MemberId memberId, DateTime expirationDate) { this.Id = new MemberSubscriptionId(memberId.Value); _expirationDate = expirationDate; this.AddDomainEvent(new MemberSubscriptionExpirationDateChangedDomainEvent(memberId, _expirationDate)); } public static MemberSubscription CreateForMember(MemberId memberId, DateTime expirationDate) { return new MemberSubscription(memberId, expirationDate); } public void ChangeExpirationDate(DateTime expirationDate) { _expirationDate = expirationDate; this.AddDomainEvent(new MemberSubscriptionExpirationDateChangedDomainEvent( new MemberId(this.Id.Value), _expirationDate)); } } } ================================================ FILE: src/Modules/Meetings/Domain/Members/MemberSubscriptions/MemberSubscriptionId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.Members.MemberSubscriptions { public class MemberSubscriptionId : TypedIdValueBase { public MemberSubscriptionId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Meetings/Domain/SharedKernel/SystemClock.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel { public static class SystemClock { private static DateTime? _customDate; public static DateTime Now { get { if (_customDate.HasValue) { return _customDate.Value; } return DateTime.UtcNow; } } public static void Set(DateTime customDate) => _customDate = customDate; public static void Reset() => _customDate = null; } } ================================================ FILE: src/Modules/Meetings/Infrastructure/CompanyName.MyMeetings.Modules.Meetings.Infrastructure.csproj ================================================  ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/AllConstructorFinder.cs ================================================ using System.Collections.Concurrent; using System.Reflection; using Autofac.Core.Activators.Reflection; namespace CompanyName.MyMeetings.Modules.Meetings.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); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Assemblies.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration { internal static class Assemblies { public static readonly Assembly Application = typeof(IMeetingsModule).Assembly; } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Authentication/AuthenticationModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.Meetings.Application.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Authentication { internal class AuthenticationModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType() .As() .InstancePerLifetimeScope(); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/DataAccess/DataAccessModule.cs ================================================ 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.Meetings.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 MeetingsContext(dbContextOptionsBuilder.Options, _loggerFactory); }) .AsSelf() .As() .InstancePerLifetimeScope(); var infrastructureAssembly = typeof(MeetingsContext).Assembly; builder.RegisterAssemblyTypes(infrastructureAssembly) .Where(type => type.Name.EndsWith("Repository")) .AsImplementedInterfaces() .InstancePerLifetimeScope() .FindConstructorsWith(new AllConstructorFinder()); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Email/EmailModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Email { internal class EmailModule : Module { private readonly EmailsConfiguration _configuration; public EmailModule(EmailsConfiguration configuration) { _configuration = configuration; } protected override void Load(ContainerBuilder builder) { builder.RegisterType() .As() .WithParameter("configuration", _configuration) .InstancePerLifetimeScope(); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/EventsBus/EventsBusModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.Meetings.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(); } } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Administration.IntegrationEvents.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Payments.IntegrationEvents; using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using Serilog; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.EventsBus { public static class EventsBusStartup { public static void Initialize( ILogger logger) { SubscribeToIntegrationEvents(logger); } private static void SubscribeToIntegrationEvents(ILogger logger) { var eventBus = MeetingsCompositionRoot.BeginLifetimeScope().Resolve(); SubscribeToIntegrationEvent(eventBus, logger); SubscribeToIntegrationEvent(eventBus, logger); SubscribeToIntegrationEvent(eventBus, logger); 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()); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs ================================================ 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.Meetings.Infrastructure.Configuration.EventsBus { internal class IntegrationEventGenericHandler : IIntegrationEventHandler where T : IntegrationEvent { public async Task Handle(T @event) { using (var scope = MeetingsCompositionRoot.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 [meetings].[InboxMessages] (Id, OccurredOn, Type, Data) " + "VALUES (@Id, @OccurredOn, @Type, @Data)"; await connection.ExecuteScalarAsync(sql, new { @event.Id, @event.OccurredOn, type, data }); } } } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Logging/LoggingModule.cs ================================================ using Autofac; using Serilog; namespace CompanyName.MyMeetings.Modules.Meetings.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(); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Mediation/MediatorModule.cs ================================================ using System.Reflection; using Autofac; using Autofac.Core; using Autofac.Features.Variance; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using FluentValidation; using MediatR; using MediatR.Pipeline; namespace CompanyName.MyMeetings.Modules.Meetings.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; } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/MeetingsCompositionRoot.cs ================================================ using Autofac; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration { internal static class MeetingsCompositionRoot { private static IContainer _container; internal static void SetContainer(IContainer container) { _container = container; } internal static ILifetimeScope BeginLifetimeScope() { return _container.BeginLifetimeScope(); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/MeetingsStartup.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.AcceptMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.SendMeetingAttendeeAddedEmail; using CompanyName.MyMeetings.Modules.Meetings.Application.Members.CreateMember; using CompanyName.MyMeetings.Modules.Meetings.Application.MemberSubscriptions; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Authentication; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.DataAccess; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Email; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.EventsBus; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Logging; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Mediation; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.Outbox; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Quartz; using Serilog.Extensions.Logging; using ILogger = Serilog.ILogger; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration { public class MeetingsStartup { private static IContainer _container; public static void Initialize( string connectionString, IExecutionContextAccessor executionContextAccessor, ILogger logger, EmailsConfiguration emailsConfiguration, IEventsBus eventsBus, long? internalProcessingPoolingInterval = null) { var moduleLogger = logger.ForContext("Module", "Meetings"); ConfigureCompositionRoot( connectionString, executionContextAccessor, moduleLogger, emailsConfiguration, eventsBus); QuartzStartup.Initialize(moduleLogger, internalProcessingPoolingInterval); EventsBusStartup.Initialize(moduleLogger); } public static void Stop() { QuartzStartup.StopQuartz(); } private static void ConfigureCompositionRoot( string connectionString, IExecutionContextAccessor executionContextAccessor, ILogger logger, EmailsConfiguration emailsConfiguration, IEventsBus eventsBus) { var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterModule(new LoggingModule(logger.ForContext("Module", "Meetings"))); var loggerFactory = new SerilogLoggerFactory(logger); containerBuilder.RegisterModule(new DataAccessModule(connectionString, loggerFactory)); containerBuilder.RegisterModule(new ProcessingModule()); containerBuilder.RegisterModule(new EventsBusModule(eventsBus)); containerBuilder.RegisterModule(new MediatorModule()); containerBuilder.RegisterModule(new AuthenticationModule()); var domainNotificationsMap = new BiDictionary(); domainNotificationsMap.Add("MeetingGroupProposalAcceptedNotification", typeof(MeetingGroupProposalAcceptedNotification)); domainNotificationsMap.Add("MeetingGroupProposedNotification", typeof(MeetingGroupProposedNotification)); domainNotificationsMap.Add("MeetingGroupCreatedNotification", typeof(MeetingGroupCreatedNotification)); domainNotificationsMap.Add("MeetingAttendeeAddedNotification", typeof(MeetingAttendeeAddedNotification)); domainNotificationsMap.Add("MemberCreatedNotification", typeof(MemberCreatedNotification)); domainNotificationsMap.Add("MemberSubscriptionExpirationDateChangedNotification", typeof(MemberSubscriptionExpirationDateChangedNotification)); domainNotificationsMap.Add("MeetingCommentLikedNotification", typeof(MeetingCommentLikedNotification)); domainNotificationsMap.Add("MeetingCommentUnlikedNotification", typeof(MeetingCommentUnlikedNotification)); containerBuilder.RegisterModule(new OutboxModule(domainNotificationsMap)); containerBuilder.RegisterModule(new EmailModule(emailsConfiguration)); containerBuilder.RegisterModule(new QuartzModule()); containerBuilder.RegisterInstance(executionContextAccessor); _container = containerBuilder.Build(); MeetingsCompositionRoot.SetContainer(_container); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/CommandsExecutor.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing { internal static class CommandsExecutor { internal static async Task Execute(ICommand command) { using (var scope = MeetingsCompositionRoot.BeginLifetimeScope()) { var mediator = scope.Resolve(); await mediator.Send(command); } } internal static async Task Execute(ICommand command) { using (var scope = MeetingsCompositionRoot.BeginLifetimeScope()) { var mediator = scope.Resolve(); return await mediator.Send(command); } } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/IRecurringCommand.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing { public interface IRecurringCommand { } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/Inbox/InboxMessageDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.Inbox { public class InboxMessageDto { public Guid Id { get; set; } public string Type { get; set; } public string Data { get; set; } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.Inbox { public class ProcessInboxCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using Dapper; using MediatR; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.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 [meetings].[InboxMessages] AS [InboxMessage] WHERE [InboxMessage].[ProcessedDate] IS NULL ORDER BY [InboxMessage].[OccurredOn] """; var messages = await connection.QueryAsync(sql); const string sqlUpdateProcessedDate = """ UPDATE [meetings].[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.ExecuteScalarAsync(sqlUpdateProcessedDate, new { Date = DateTime.UtcNow, message.Id }); } } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/Inbox/ProcessInboxJob.cs ================================================ using Quartz; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.Inbox { [DisallowConcurrentExecution] public class ProcessInboxJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ProcessInboxCommand()); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Serialization; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using Dapper; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.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 [meetings].[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 Task EnqueueAsync(ICommand command) { throw new NotImplementedException(); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.InternalCommands { internal class ProcessInternalCommandsCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using Dapper; using Newtonsoft.Json; using Polly; namespace CompanyName.MyMeetings.Modules.Meetings.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 [meetings].[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 [meetings].[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; } } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsJob.cs ================================================ using Quartz; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.InternalCommands { [DisallowConcurrentExecution] public class ProcessInternalCommandsJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ProcessInternalCommandsCommand()); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using Serilog; using Serilog.Context; using Serilog.Core; using Serilog.Events; namespace CompanyName.MyMeetings.Modules.Meetings.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); return; } 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))); } } } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using Serilog; using Serilog.Context; using Serilog.Core; using Serilog.Events; namespace CompanyName.MyMeetings.Modules.Meetings.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) { if (command is IRecurringCommand) { return await _decorated.Handle(command, 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))); } } } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/Outbox/OutboxMessageDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.Outbox { public class OutboxMessageDto { public Guid Id { get; set; } public string Type { get; set; } public string Data { get; set; } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/Outbox/OutboxModule.cs ================================================ 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.Meetings.Infrastructure.Outbox; using Module = Autofac.Module; namespace CompanyName.MyMeetings.Modules.Meetings.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(); this.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"); } } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.Outbox { public class ProcessOutboxCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using Dapper; using MediatR; using Newtonsoft.Json; using Serilog.Context; using Serilog.Core; using Serilog.Events; namespace CompanyName.MyMeetings.Modules.Meetings.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 [meetings].[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 [meetings].[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()}"))); } } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxJob.cs ================================================ using Quartz; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.Outbox { [DisallowConcurrentExecution] public class ProcessOutboxJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ProcessOutboxCommand()); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/ProcessingModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.InternalCommands; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.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.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()); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing { internal class UnitOfWorkCommandHandlerDecorator : ICommandHandler where T : ICommand { private readonly ICommandHandler _decorated; private readonly IUnitOfWork _unitOfWork; private readonly MeetingsContext _meetingContext; public UnitOfWorkCommandHandlerDecorator( ICommandHandler decorated, IUnitOfWork unitOfWork, MeetingsContext meetingContext) { _decorated = decorated; _unitOfWork = unitOfWork; _meetingContext = meetingContext; } public async Task Handle(T command, CancellationToken cancellationToken) { await this._decorated.Handle(command, cancellationToken); if (command is InternalCommandBase) { var internalCommand = await _meetingContext.InternalCommands.FirstOrDefaultAsync( x => x.Id == command.Id, cancellationToken: cancellationToken); if (internalCommand != null) { internalCommand.ProcessedDate = DateTime.UtcNow; } } await this._unitOfWork.CommitAsync(cancellationToken); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerWithResultDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing { internal class UnitOfWorkCommandHandlerWithResultDecorator : ICommandHandler where T : ICommand { private readonly ICommandHandler _decorated; private readonly IUnitOfWork _unitOfWork; private readonly MeetingsContext _meetingsContext; public UnitOfWorkCommandHandlerWithResultDecorator( ICommandHandler decorated, IUnitOfWork unitOfWork, MeetingsContext meetingsContext) { _decorated = decorated; _unitOfWork = unitOfWork; _meetingsContext = meetingsContext; } public async Task Handle(T command, CancellationToken cancellationToken) { var result = await this._decorated.Handle(command, cancellationToken); if (command is InternalCommandBase) { var internalCommand = await _meetingsContext.InternalCommands.FirstOrDefaultAsync(x => x.Id == command.Id, cancellationToken: cancellationToken); if (internalCommand != null) { internalCommand.ProcessedDate = DateTime.UtcNow; } } await this._unitOfWork.CommitAsync(cancellationToken); return result; } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/ValidationCommandHandlerDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using FluentValidation; namespace CompanyName.MyMeetings.Modules.Meetings.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); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Processing/ValidationCommandHandlerWithResultDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using FluentValidation; namespace CompanyName.MyMeetings.Modules.Meetings.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); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Quartz/QuartzModule.cs ================================================ using Autofac; using Quartz; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Quartz { public class QuartzModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { builder.RegisterAssemblyTypes(ThisAssembly) .Where(x => typeof(IJob).IsAssignableFrom(x)).InstancePerDependency(); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Quartz/QuartzStartup.cs ================================================ using System.Collections.Specialized; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.Inbox; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.InternalCommands; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.Outbox; using Quartz; using Quartz.Impl; using Quartz.Logging; using Serilog; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Quartz { internal static class QuartzStartup { private static IScheduler _scheduler; internal static void Initialize(ILogger logger, long? internalProcessingPoolingInterval) { logger.Information("Quartz starting..."); var schedulerConfiguration = new NameValueCollection(); schedulerConfiguration.Add("quartz.scheduler.instanceName", "Meetings"); ISchedulerFactory schedulerFactory = new StdSchedulerFactory(schedulerConfiguration); _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 triggerCommandsProcessing; if (internalProcessingPoolingInterval.HasValue) { triggerCommandsProcessing = TriggerBuilder .Create() .StartNow() .WithSimpleSchedule(x => x.WithInterval(TimeSpan.FromMilliseconds(internalProcessingPoolingInterval.Value)) .RepeatForever()) .Build(); } else { triggerCommandsProcessing = TriggerBuilder .Create() .StartNow() .WithCronSchedule("0/2 * * ? * *") .Build(); } _scheduler.ScheduleJob(processInternalCommandsJob, triggerCommandsProcessing).GetAwaiter().GetResult(); logger.Information("Quartz started."); } internal static void StopQuartz() { _scheduler?.Shutdown(); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Configuration/Quartz/SerilogLogProvider.cs ================================================ using Quartz.Logging; using Serilog; namespace CompanyName.MyMeetings.Modules.Meetings.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(); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/MeetingCommentingConfigurations/MeetingCommentingConfigurationEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.MeetingCommentingConfigurations { public class MeetingCommentingConfigurationEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("MeetingCommentingConfigurations", "meetings"); builder.HasKey(c => c.Id); builder.HasOne() .WithOne() .HasForeignKey(nameof(MeetingCommentingConfiguration), "_meetingId") .OnDelete(DeleteBehavior.Restrict); builder.Property("_meetingId").HasColumnName("MeetingId"); builder.Property("_isCommentingEnabled").HasColumnName("IsCommentingEnabled"); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/MeetingCommentingConfigurations/MeetingCommentingConfigurationRepository.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.MeetingCommentingConfigurations { public class MeetingCommentingConfigurationRepository : IMeetingCommentingConfigurationRepository { private readonly MeetingsContext _meetingsContext; public MeetingCommentingConfigurationRepository(MeetingsContext meetingsContext) { _meetingsContext = meetingsContext; } public async Task AddAsync(MeetingCommentingConfiguration meetingCommentingConfiguration) { await _meetingsContext.MeetingCommentingConfigurations.AddAsync(meetingCommentingConfiguration); } public async Task GetByMeetingIdAsync(MeetingId meetingId) { return await _meetingsContext.MeetingCommentingConfigurations.SingleOrDefaultAsync(c => EF.Property(c, "_meetingId") == meetingId); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/MeetingComments/MeetingCommentEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.MeetingComments { public class MeetingCommentEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("MeetingComments", "meetings"); builder.HasKey(c => c.Id); builder.Property("_comment").HasColumnName("Comment"); builder.Property("_meetingId").HasColumnName("MeetingId"); builder.Property("_authorId").HasColumnName("AuthorId"); builder.Property("_inReplyToCommentId").HasColumnName("InReplyToCommentId"); builder.Property("_isRemoved").HasColumnName("IsRemoved"); builder.Property("_removedByReason").HasColumnName("RemovedByReason"); builder.Property("_createDate").HasColumnName("CreateDate"); builder.Property("_editDate").HasColumnName("EditDate"); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/MeetingComments/MeetingCommentRepository.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.MeetingComments { public class MeetingCommentRepository : IMeetingCommentRepository { private readonly MeetingsContext _meetingsContext; public MeetingCommentRepository(MeetingsContext meetingsContext) { _meetingsContext = meetingsContext; } public async Task AddAsync(MeetingComment meetingComment) { await _meetingsContext.MeetingComments.AddAsync(meetingComment); } public async Task GetByIdAsync(MeetingCommentId meetingCommentId) { return await _meetingsContext.MeetingComments.FindAsync(meetingCommentId); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/MeetingGroupProposals/MeetingGroupProposalEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.MeetingGroupProposals { internal class MeetingGroupProposalEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("MeetingGroupProposals", "meetings"); builder.HasKey(x => x.Id); builder.Property(x => x.Id); builder.Property("_name").HasColumnName("Name"); builder.Property("_description").HasColumnName("Description"); builder.Property("_proposalUserId").HasColumnName("ProposalUserId"); builder.Property("_proposalDate").HasColumnName("ProposalDate"); builder.OwnsOne("_location", b => { b.Property(p => p.City).HasColumnName("LocationCity"); b.Property(p => p.CountryCode).HasColumnName("LocationCountryCode"); }); builder.OwnsOne("_status", b => { b.Property(p => p.Value).HasColumnName("StatusCode"); }); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/MeetingGroupProposals/MeetingGroupProposalRepository.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.MeetingGroupProposals { internal class MeetingGroupProposalRepository : IMeetingGroupProposalRepository { private readonly MeetingsContext _context; internal MeetingGroupProposalRepository(MeetingsContext context) { _context = context; } public async Task AddAsync(MeetingGroupProposal meetingGroupProposal) { await _context.MeetingGroupProposals.AddAsync(meetingGroupProposal); } public async Task GetByIdAsync(MeetingGroupProposalId meetingGroupProposalId) { return await _context.MeetingGroupProposals.FirstOrDefaultAsync(x => x.Id == meetingGroupProposalId); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/MeetingGroups/MeetingGroupRepository.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.MeetingGroups { internal class MeetingGroupRepository : IMeetingGroupRepository { private readonly MeetingsContext _meetingsContext; internal MeetingGroupRepository(MeetingsContext meetingsContext) { _meetingsContext = meetingsContext; } public async Task AddAsync(MeetingGroup meetingGroup) { await _meetingsContext.MeetingGroups.AddAsync(meetingGroup); } public async Task Commit() { return await _meetingsContext.SaveChangesAsync(); } public async Task GetByIdAsync(MeetingGroupId id) { return await _meetingsContext.MeetingGroups.FirstOrDefaultAsync(x => x.Id == id); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/MeetingGroups/MeetingGroupsEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.MeetingGroups { internal class MeetingGroupsEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("MeetingGroups", "meetings"); builder.HasKey(x => x.Id); builder.Property("_name").HasColumnName("Name"); builder.Property("_description").HasColumnName("Description"); builder.Property("_creatorId").HasColumnName("CreatorId"); builder.Property("_createDate").HasColumnName("CreateDate"); builder.Property("_paymentDateTo").HasColumnName("PaymentDateTo"); builder.OwnsOne("_location", b => { b.Property(p => p.City).HasColumnName("LocationCity"); b.Property(p => p.CountryCode).HasColumnName("LocationCountryCode"); }); builder.OwnsMany("_members", y => { y.WithOwner().HasForeignKey("MeetingGroupId"); y.ToTable("MeetingGroupMembers", "meetings"); y.Property("MemberId"); y.Property("MeetingGroupId"); y.Property("JoinedDate").HasColumnName("JoinedDate"); y.HasKey("MemberId", "MeetingGroupId", "JoinedDate"); y.Property("_leaveDate").HasColumnName("LeaveDate"); y.Property("_isActive").HasColumnName("IsActive"); y.OwnsOne("_role", b => { b.Property(x => x.Value).HasColumnName("RoleCode"); }); }); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/MeetingMemberCommentLikes/MeetingMemberCommentLikeEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.MeetingMemberCommentLikes { public class MeetingMemberCommentLikeEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("MeetingMemberCommentLikes", "meetings"); builder.HasKey(c => c.Id); builder.Property("_meetingCommentId").HasColumnName("MeetingCommentId"); builder.Property("_memberId").HasColumnName("MemberId"); builder.HasOne() .WithMany() .HasForeignKey("_memberId") .OnDelete(DeleteBehavior.Restrict); builder.HasOne() .WithMany() .HasForeignKey("_meetingCommentId") .OnDelete(DeleteBehavior.Restrict); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/MeetingMemberCommentLikes/MeetingMemberCommentLikeRepository.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Dapper; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.MeetingMemberCommentLikes { public class MeetingMemberCommentLikeRepository : IMeetingMemberCommentLikesRepository { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly MeetingsContext _meetingsContext; public MeetingMemberCommentLikeRepository(ISqlConnectionFactory sqlConnectionFactory, MeetingsContext meetingsContext) { _sqlConnectionFactory = sqlConnectionFactory; _meetingsContext = meetingsContext; } public async Task AddAsync(MeetingMemberCommentLike meetingMemberCommentLike) { await _meetingsContext.MeetingMemberCommentLikes.AddAsync(meetingMemberCommentLike); } public async Task GetAsync(MemberId memberId, MeetingCommentId meetingCommentId) { return await _meetingsContext.MeetingMemberCommentLikes.SingleOrDefaultAsync(l => EF.Property(l, "_memberId") == memberId && EF.Property(l, "_meetingCommentId") == meetingCommentId); } public Task CountMemberCommentLikesAsync(MemberId memberId, MeetingCommentId meetingCommentId) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = """ SELECT COUNT(*) FROM [meetings].[MeetingMemberCommentLikes] AS [Likes] WHERE [Likes].[MemberId] = @MemberId AND [Likes].[MeetingCommentId] = @MeetingCommentId """; return connection.QuerySingleAsync( sql, new { memberId = memberId.Value, meetingCommentId = meetingCommentId.Value }); } public void Remove(MeetingMemberCommentLike meetingMemberCommentLike) { _meetingsContext.MeetingMemberCommentLikes.Remove(meetingMemberCommentLike); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/Meetings/MeetingEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.Meetings { internal class MeetingEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("Meetings", "meetings"); builder.HasKey(x => x.Id); builder.Property("_meetingGroupId").HasColumnName("MeetingGroupId"); builder.Property("_title").HasColumnName("Title"); builder.Property("_description").HasColumnName("Description"); builder.Property("_creatorId").HasColumnName("CreatorId"); builder.Property("_changeMemberId").HasColumnName("ChangeMemberId"); builder.Property("_createDate").HasColumnName("CreateDate"); builder.Property("_changeDate").HasColumnName("ChangeDate"); builder.Property("_cancelDate").HasColumnName("CancelDate"); builder.Property("_isCanceled").HasColumnName("IsCanceled"); builder.Property("_cancelMemberId").HasColumnName("CancelMemberId"); builder.OwnsOne("_term", b => { b.Property(p => p.StartDate).HasColumnName("TermStartDate"); b.Property(p => p.EndDate).HasColumnName("TermEndDate"); }); builder.OwnsOne("_rsvpTerm", b => { b.Property(p => p.StartDate).HasColumnName("RSVPTermStartDate"); b.Property(p => p.EndDate).HasColumnName("RSVPTermEndDate"); }); builder.OwnsOne("_eventFee", b => { b.Property(p => p.Value).HasColumnName("EventFeeValue"); b.Property(p => p.Currency).HasColumnName("EventFeeCurrency"); }); builder.OwnsOne("_location", b => { b.Property(p => p.Name).HasColumnName("LocationName"); b.Property(p => p.Address).HasColumnName("LocationAddress"); b.Property(p => p.PostalCode).HasColumnName("LocationPostalCode"); b.Property(p => p.City).HasColumnName("LocationCity"); }); builder.OwnsMany("_attendees", y => { y.WithOwner().HasForeignKey("MeetingId"); y.ToTable("MeetingAttendees", "meetings"); y.Property("AttendeeId"); y.Property("MeetingId"); y.Property("_decisionDate").HasColumnName("DecisionDate"); y.HasKey("AttendeeId", "MeetingId", "_decisionDate"); y.Property("_decisionChanged").HasColumnName("DecisionChanged"); y.Property("_guestsNumber").HasColumnName("GuestsNumber"); y.Property("_decisionChangeDate").HasColumnName("DecisionChangeDate"); y.Property("_isRemoved").HasColumnName("IsRemoved"); y.Property("_removingReason").HasColumnName("RemovingReason"); y.Property("_removingMemberId").HasColumnName("RemovingMemberId"); y.Property("_removedDate").HasColumnName("RemovedDate"); y.Property("_isFeePaid").HasColumnName("IsFeePaid"); y.OwnsOne("_role", b => { b.Property(x => x.Value).HasColumnName("RoleCode"); }); y.OwnsOne("_fee", b => { b.Property(p => p.Value).HasColumnName("FeeValue"); b.Property(p => p.Currency).HasColumnName("FeeCurrency"); }); }); builder.OwnsMany("_notAttendees", y => { y.WithOwner().HasForeignKey("MeetingId"); y.ToTable("MeetingNotAttendees", "meetings"); y.Property("MemberId"); y.Property("MeetingId"); y.Property("_decisionDate").HasColumnName("DecisionDate"); y.HasKey("MemberId", "MeetingId", "_decisionDate"); y.Property("_decisionChanged").HasColumnName("DecisionChanged"); y.Property("_decisionChangeDate").HasColumnName("DecisionChangeDate"); }); builder.OwnsMany("_waitlistMembers", y => { y.WithOwner().HasForeignKey("MeetingId"); y.ToTable("MeetingWaitlistMembers", "meetings"); y.Property("MemberId"); y.Property("MeetingId"); y.Property("SignUpDate").HasColumnName("SignUpDate"); y.HasKey("MemberId", "MeetingId", "SignUpDate"); y.Property("_isSignedOff").HasColumnName("IsSignedOff"); y.Property("_signOffDate").HasColumnName("SignOffDate"); y.Property("_isMovedToAttendees").HasColumnName("IsMovedToAttendees"); y.Property("_movedToAttendeesDate").HasColumnName("MovedToAttendeesDate"); }); builder.OwnsOne("_meetingLimits", meetingLimits => { meetingLimits.Property(x => x.AttendeesLimit).HasColumnName("AttendeesLimit"); meetingLimits.Property(x => x.GuestsLimit).HasColumnName("GuestsLimit"); }); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/Meetings/MeetingRepository.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.Meetings { internal class MeetingRepository : IMeetingRepository { private readonly MeetingsContext _meetingsContext; internal MeetingRepository(MeetingsContext meetingsContext) { _meetingsContext = meetingsContext; } public async Task AddAsync(Meeting meeting) { await _meetingsContext.Meetings.AddAsync(meeting); } public async Task GetByIdAsync(MeetingId id) { return await _meetingsContext.Meetings.FindAsync(id); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/Members/MemberEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.Members { internal class MemberEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("Members", "meetings"); builder.HasKey(x => x.Id); builder.Property("_login").HasColumnName("Login"); builder.Property("_email").HasColumnName("Email"); builder.Property("_firstName").HasColumnName("FirstName"); builder.Property("_lastName").HasColumnName("LastName"); builder.Property("_name").HasColumnName("Name"); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/Members/MemberRepository.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.Members { internal class MemberRepository : IMemberRepository { private readonly MeetingsContext _meetingsContext; internal MemberRepository(MeetingsContext meetingsContext) { _meetingsContext = meetingsContext; } public async Task AddAsync(Member member) { await _meetingsContext.Members.AddAsync(member); } public async Task GetByIdAsync(MemberId memberId) { return await _meetingsContext.Members.FirstOrDefaultAsync(x => x.Id == memberId); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/Members/MemberSubscriptions/MemberSubscriptionEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.Members.MemberSubscriptions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.Members.MemberSubscriptions { internal class MemberSubscriptionEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("MemberSubscriptions", "meetings"); builder.HasKey(x => x.Id); builder.Property("_expirationDate").HasColumnName("ExpirationDate"); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Domain/Members/MemberSubscriptions/MemberSubscriptionRepository.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.Members.MemberSubscriptions; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Domain.Members.MemberSubscriptions { internal class MemberSubscriptionRepository : IMemberSubscriptionRepository { private readonly MeetingsContext _meetingsContext; internal MemberSubscriptionRepository(MeetingsContext meetingsContext) { _meetingsContext = meetingsContext; } public async Task AddAsync(MemberSubscription member) { await _meetingsContext.MemberSubscriptions.AddAsync(member); } public async Task GetByIdOptionalAsync(MemberSubscriptionId memberSubscriptionId) { return await _meetingsContext.MemberSubscriptions.FirstOrDefaultAsync(x => x.Id == memberSubscriptionId); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/InternalCommands/InternalCommandEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.InternalCommands { internal class InternalCommandEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("InternalCommands", "meetings"); builder.HasKey(b => b.Id); builder.Property(b => b.Id).ValueGeneratedNever(); } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/MeetingsContext.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members.MemberSubscriptions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure { public class MeetingsContext : DbContext { public DbSet MeetingGroups { get; set; } public DbSet Meetings { get; set; } public DbSet MeetingGroupProposals { get; set; } public DbSet OutboxMessages { get; set; } public DbSet InternalCommands { get; set; } public DbSet Members { get; set; } public DbSet MemberSubscriptions { get; set; } public DbSet MeetingComments { get; set; } public DbSet MeetingCommentingConfigurations { get; set; } public DbSet MeetingMemberCommentLikes { get; set; } private readonly ILoggerFactory _loggerFactory; public MeetingsContext(DbContextOptions options, ILoggerFactory loggerFactory) : base(options) { _loggerFactory = loggerFactory; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // optionsBuilder.UseLoggerFactory(_loggerFactory).EnableSensitiveDataLogging(); } protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); } } ================================================ FILE: src/Modules/Meetings/Infrastructure/MeetingsModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing; using MediatR; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure { public class MeetingsModule : IMeetingsModule { 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 = MeetingsCompositionRoot.BeginLifetimeScope()) { var mediator = scope.Resolve(); return await mediator.Send(query); } } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Outbox/OutboxAccessor.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Outbox { public class OutboxAccessor : IOutbox { private readonly MeetingsContext _meetingsContext; internal OutboxAccessor(MeetingsContext meetingsContext) { _meetingsContext = meetingsContext; } public void Add(OutboxMessage message) { _meetingsContext.OutboxMessages.Add(message); } public Task Save() { return Task.CompletedTask; // Save is done automatically using EF Core Change Tracking mechanism during SaveChanges. } } } ================================================ FILE: src/Modules/Meetings/Infrastructure/Outbox/OutboxMessageEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Outbox { internal class OutboxMessageEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("OutboxMessages", "meetings"); builder.HasKey(b => b.Id); builder.Property(b => b.Id).ValueGeneratedNever(); } } } ================================================ FILE: src/Modules/Meetings/IntegrationEvents/CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents.csproj ================================================  ================================================ FILE: src/Modules/Meetings/IntegrationEvents/MeetingAttendeeAddedIntegrationEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents { public class MeetingAttendeeAddedIntegrationEvent : IntegrationEvent { public Guid MeetingId { get; } public Guid AttendeeId { get; } public decimal? FeeValue { get; } public string FeeCurrency { get; } public MeetingAttendeeAddedIntegrationEvent( Guid id, DateTime occurredOn, Guid meetingId, Guid attendeeId, decimal? feeValue, string feeCurrency) : base(id, occurredOn) { MeetingId = meetingId; AttendeeId = attendeeId; FeeValue = feeValue; FeeCurrency = feeCurrency; } } } ================================================ FILE: src/Modules/Meetings/IntegrationEvents/MeetingGroupProposedIntegrationEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents { public class MeetingGroupProposedIntegrationEvent : IntegrationEvent { public Guid MeetingGroupProposalId { get; } public string Name { get; } public string Description { get; } public string LocationCity { get; } public string LocationCountryCode { get; } public Guid ProposalUserId { get; } public DateTime ProposalDate { get; } public MeetingGroupProposedIntegrationEvent( Guid id, DateTime occurredOn, Guid meetingGroupProposalId, string name, string description, string locationCity, string locationCountryCode, Guid proposalUserId, DateTime proposalDate) : base(id, occurredOn) { this.MeetingGroupProposalId = meetingGroupProposalId; this.Name = name; this.Description = description; this.LocationCity = locationCity; this.LocationCountryCode = locationCountryCode; this.ProposalUserId = proposalUserId; this.ProposalDate = proposalDate; } } } ================================================ FILE: src/Modules/Meetings/IntegrationEvents/MemberCreatedIntegrationEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents { public class MemberCreatedIntegrationEvent : IntegrationEvent { public Guid MemberId { get; } public MemberCreatedIntegrationEvent(Guid id, DateTime occurredOn, Guid memberId) : base(id, occurredOn) { MemberId = memberId; } } } ================================================ FILE: src/Modules/Meetings/Tests/ArchTests/Application/ApplicationTests.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Meetings.Application.Configuration.Queries; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.ArchTests.SeedWork; using FluentValidation; using MediatR; using NetArchTest.Rules; using Newtonsoft.Json; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.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_Constructor_With_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.NonPublic | 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); } } } ================================================ FILE: src/Modules/Meetings/Tests/ArchTests/CompanyName.MyMeetings.Modules.Meetings.ArchTests.csproj ================================================  ================================================ FILE: src/Modules/Meetings/Tests/ArchTests/Domain/DomainTests.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.ArchTests.SeedWork; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.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); } } } ================================================ FILE: src/Modules/Meetings/Tests/ArchTests/Module/LayersTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.ArchTests.SeedWork; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.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); } } } ================================================ FILE: src/Modules/Meetings/Tests/ArchTests/SeedWork/TestBase.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.ArchTests.SeedWork { public abstract class TestBase { protected static Assembly ApplicationAssembly => typeof(CommandBase).Assembly; protected static Assembly DomainAssembly => typeof(MeetingGroupProposal).Assembly; protected static Assembly InfrastructureAssembly => typeof(MeetingsContext).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); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/AssemblyInfo.cs ================================================ using NUnit.Framework; [assembly: NonParallelizable] [assembly: LevelOfParallelism(1)] namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests { public class AssemblyInfo { } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.csproj ================================================  ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/Countries/0001_SeedCountries.sql ================================================ INSERT INTO [meetings].[Countries] VALUES ('AD', 'Andorra') INSERT INTO [meetings].[Countries] VALUES ('AE', 'United Arab Emirates') INSERT INTO [meetings].[Countries] VALUES ('AF', 'Afghanistan') INSERT INTO [meetings].[Countries] VALUES ('AG', 'Antigua and Barbuda') INSERT INTO [meetings].[Countries] VALUES ('AI', 'Anguilla') INSERT INTO [meetings].[Countries] VALUES ('AL', 'Albania') INSERT INTO [meetings].[Countries] VALUES ('AM', 'Armenia') INSERT INTO [meetings].[Countries] VALUES ('AN', 'Netherlands Antilles') INSERT INTO [meetings].[Countries] VALUES ('AO', 'Angola') INSERT INTO [meetings].[Countries] VALUES ('AQ', 'Antarctica') INSERT INTO [meetings].[Countries] VALUES ('AR', 'Argentina') INSERT INTO [meetings].[Countries] VALUES ('AS', 'American Samoa') INSERT INTO [meetings].[Countries] VALUES ('AT', 'Austria') INSERT INTO [meetings].[Countries] VALUES ('AU', 'Australia') INSERT INTO [meetings].[Countries] VALUES ('AW', 'Aruba') INSERT INTO [meetings].[Countries] VALUES ('AX', 'Aland Islands') INSERT INTO [meetings].[Countries] VALUES ('AZ', 'Azerbaijan') INSERT INTO [meetings].[Countries] VALUES ('BA', 'Bosnia and Herzegovina') INSERT INTO [meetings].[Countries] VALUES ('BB', 'Barbados') INSERT INTO [meetings].[Countries] VALUES ('BD', 'Bangladesh') INSERT INTO [meetings].[Countries] VALUES ('BE', 'Belgium') INSERT INTO [meetings].[Countries] VALUES ('BF', 'Burkina Faso') INSERT INTO [meetings].[Countries] VALUES ('BG', 'Bulgaria') INSERT INTO [meetings].[Countries] VALUES ('BH', 'Bahrain') INSERT INTO [meetings].[Countries] VALUES ('BI', 'Burundi') INSERT INTO [meetings].[Countries] VALUES ('BJ', 'Benin') INSERT INTO [meetings].[Countries] VALUES ('BM', 'Bermuda') INSERT INTO [meetings].[Countries] VALUES ('BN', 'Brunei Darussalam') INSERT INTO [meetings].[Countries] VALUES ('BO', 'Bolivia') INSERT INTO [meetings].[Countries] VALUES ('BR', 'Brazil') INSERT INTO [meetings].[Countries] VALUES ('BS', 'Bahamas') INSERT INTO [meetings].[Countries] VALUES ('BT', 'Bhutan') INSERT INTO [meetings].[Countries] VALUES ('BV', 'Bouvet Island') INSERT INTO [meetings].[Countries] VALUES ('BW', 'Botswana') INSERT INTO [meetings].[Countries] VALUES ('BY', 'Belarus') INSERT INTO [meetings].[Countries] VALUES ('BZ', 'Belize') INSERT INTO [meetings].[Countries] VALUES ('CA', 'Canada') INSERT INTO [meetings].[Countries] VALUES ('CC', 'Cocos (Keeling) Islands') INSERT INTO [meetings].[Countries] VALUES ('CD', 'Congo, The Democratic Republic of the') INSERT INTO [meetings].[Countries] VALUES ('CF', 'Central African Republic') INSERT INTO [meetings].[Countries] VALUES ('CG', 'Congo') INSERT INTO [meetings].[Countries] VALUES ('CH', 'Switzerland') INSERT INTO [meetings].[Countries] VALUES ('CI', 'Cote DIvoire') INSERT INTO [meetings].[Countries] VALUES ('CK', 'Cook Islands') INSERT INTO [meetings].[Countries] VALUES ('CL', 'Chile') INSERT INTO [meetings].[Countries] VALUES ('CM', 'Cameroon') INSERT INTO [meetings].[Countries] VALUES ('CN', 'China') INSERT INTO [meetings].[Countries] VALUES ('CO', 'Colombia') INSERT INTO [meetings].[Countries] VALUES ('CR', 'Costa Rica') INSERT INTO [meetings].[Countries] VALUES ('CU', 'Cuba') INSERT INTO [meetings].[Countries] VALUES ('CV', 'Cape Verde') INSERT INTO [meetings].[Countries] VALUES ('CX', 'Christmas Island') INSERT INTO [meetings].[Countries] VALUES ('CY', 'Cyprus') INSERT INTO [meetings].[Countries] VALUES ('CZ', 'Czech Republic') INSERT INTO [meetings].[Countries] VALUES ('DE', 'Germany') INSERT INTO [meetings].[Countries] VALUES ('DJ', 'Djibouti') INSERT INTO [meetings].[Countries] VALUES ('DK', 'Denmark') INSERT INTO [meetings].[Countries] VALUES ('DM', 'Dominica') INSERT INTO [meetings].[Countries] VALUES ('DO', 'Dominican Republic') INSERT INTO [meetings].[Countries] VALUES ('DZ', 'Algeria') INSERT INTO [meetings].[Countries] VALUES ('EC', 'Ecuador') INSERT INTO [meetings].[Countries] VALUES ('EE', 'Estonia') INSERT INTO [meetings].[Countries] VALUES ('EG', 'Egypt') INSERT INTO [meetings].[Countries] VALUES ('EH', 'Western Sahara') INSERT INTO [meetings].[Countries] VALUES ('ER', 'Eritrea') INSERT INTO [meetings].[Countries] VALUES ('ES', 'Spain') INSERT INTO [meetings].[Countries] VALUES ('ET', 'Ethiopia') INSERT INTO [meetings].[Countries] VALUES ('FI', 'Finland') INSERT INTO [meetings].[Countries] VALUES ('FJ', 'Fiji') INSERT INTO [meetings].[Countries] VALUES ('FK', 'Falkland Islands (Malvinas)') INSERT INTO [meetings].[Countries] VALUES ('FM', 'Micronesia, Federated States of') INSERT INTO [meetings].[Countries] VALUES ('FO', 'Faroe Islands') INSERT INTO [meetings].[Countries] VALUES ('FR', 'France') INSERT INTO [meetings].[Countries] VALUES ('GA', 'Gabon') INSERT INTO [meetings].[Countries] VALUES ('GB', 'United Kingdom') INSERT INTO [meetings].[Countries] VALUES ('GD', 'Grenada') INSERT INTO [meetings].[Countries] VALUES ('GE', 'Georgia') INSERT INTO [meetings].[Countries] VALUES ('GF', 'French Guiana') INSERT INTO [meetings].[Countries] VALUES ('GG', 'Guernsey') INSERT INTO [meetings].[Countries] VALUES ('GH', 'Ghana') INSERT INTO [meetings].[Countries] VALUES ('GI', 'Gibraltar') INSERT INTO [meetings].[Countries] VALUES ('GL', 'Greenland') INSERT INTO [meetings].[Countries] VALUES ('GM', 'Gambia') INSERT INTO [meetings].[Countries] VALUES ('GN', 'Guinea') INSERT INTO [meetings].[Countries] VALUES ('GP', 'Guadeloupe') INSERT INTO [meetings].[Countries] VALUES ('GQ', 'Equatorial Guinea') INSERT INTO [meetings].[Countries] VALUES ('GR', 'Greece') INSERT INTO [meetings].[Countries] VALUES ('GS', 'South Georgia and the South Sandwich Islands') INSERT INTO [meetings].[Countries] VALUES ('GT', 'Guatemala') INSERT INTO [meetings].[Countries] VALUES ('GU', 'Guam') INSERT INTO [meetings].[Countries] VALUES ('GW', 'Guinea-Bissau') INSERT INTO [meetings].[Countries] VALUES ('GY', 'Guyana') INSERT INTO [meetings].[Countries] VALUES ('HK', 'Hong Kong') INSERT INTO [meetings].[Countries] VALUES ('HM', 'Heard Island and Mcdonald Islands') INSERT INTO [meetings].[Countries] VALUES ('HN', 'Honduras') INSERT INTO [meetings].[Countries] VALUES ('HR', 'Croatia') INSERT INTO [meetings].[Countries] VALUES ('HT', 'Haiti') INSERT INTO [meetings].[Countries] VALUES ('HU', 'Hungary') INSERT INTO [meetings].[Countries] VALUES ('ID', 'Indonesia') INSERT INTO [meetings].[Countries] VALUES ('IE', 'Ireland') INSERT INTO [meetings].[Countries] VALUES ('IL', 'Israel') INSERT INTO [meetings].[Countries] VALUES ('IM', 'Isle of Man') INSERT INTO [meetings].[Countries] VALUES ('IN', 'India') INSERT INTO [meetings].[Countries] VALUES ('IO', 'British Indian Ocean Territory') INSERT INTO [meetings].[Countries] VALUES ('IQ', 'Iraq') INSERT INTO [meetings].[Countries] VALUES ('IR', 'Iran, Islamic Republic Of') INSERT INTO [meetings].[Countries] VALUES ('IS', 'Iceland') INSERT INTO [meetings].[Countries] VALUES ('IT', 'Italy') INSERT INTO [meetings].[Countries] VALUES ('JE', 'Jersey') INSERT INTO [meetings].[Countries] VALUES ('JM', 'Jamaica') INSERT INTO [meetings].[Countries] VALUES ('JO', 'Jordan') INSERT INTO [meetings].[Countries] VALUES ('JP', 'Japan') INSERT INTO [meetings].[Countries] VALUES ('KE', 'Kenya') INSERT INTO [meetings].[Countries] VALUES ('KG', 'Kyrgyzstan') INSERT INTO [meetings].[Countries] VALUES ('KH', 'Cambodia') INSERT INTO [meetings].[Countries] VALUES ('KI', 'Kiribati') INSERT INTO [meetings].[Countries] VALUES ('KM', 'Comoros') INSERT INTO [meetings].[Countries] VALUES ('KN', 'Saint Kitts and Nevis') INSERT INTO [meetings].[Countries] VALUES ('KP', 'Korea, Democratic PeopleS Republic of') INSERT INTO [meetings].[Countries] VALUES ('KR', 'Korea, Republic of') INSERT INTO [meetings].[Countries] VALUES ('KW', 'Kuwait') INSERT INTO [meetings].[Countries] VALUES ('KY', 'Cayman Islands') INSERT INTO [meetings].[Countries] VALUES ('KZ', 'Kazakhstan') INSERT INTO [meetings].[Countries] VALUES ('LA', 'Lao PeopleS Democratic Republic') INSERT INTO [meetings].[Countries] VALUES ('LB', 'Lebanon') INSERT INTO [meetings].[Countries] VALUES ('LC', 'Saint Lucia') INSERT INTO [meetings].[Countries] VALUES ('LI', 'Liechtenstein') INSERT INTO [meetings].[Countries] VALUES ('LK', 'Sri Lanka') INSERT INTO [meetings].[Countries] VALUES ('LR', 'Liberia') INSERT INTO [meetings].[Countries] VALUES ('LS', 'Lesotho') INSERT INTO [meetings].[Countries] VALUES ('LT', 'Lithuania') INSERT INTO [meetings].[Countries] VALUES ('LU', 'Luxembourg') INSERT INTO [meetings].[Countries] VALUES ('LV', 'Latvia') INSERT INTO [meetings].[Countries] VALUES ('LY', 'Libyan Arab Jamahiriya') INSERT INTO [meetings].[Countries] VALUES ('MA', 'Morocco') INSERT INTO [meetings].[Countries] VALUES ('MC', 'Monaco') INSERT INTO [meetings].[Countries] VALUES ('MD', 'Moldova, Republic of') INSERT INTO [meetings].[Countries] VALUES ('ME', 'Montenegro') INSERT INTO [meetings].[Countries] VALUES ('MG', 'Madagascar') INSERT INTO [meetings].[Countries] VALUES ('MH', 'Marshall Islands') INSERT INTO [meetings].[Countries] VALUES ('MK', 'Macedonia, The Former Yugoslav Republic of') INSERT INTO [meetings].[Countries] VALUES ('ML', 'Mali') INSERT INTO [meetings].[Countries] VALUES ('MM', 'Myanmar') INSERT INTO [meetings].[Countries] VALUES ('MN', 'Mongolia') INSERT INTO [meetings].[Countries] VALUES ('MO', 'Macao') INSERT INTO [meetings].[Countries] VALUES ('MP', 'Northern Mariana Islands') INSERT INTO [meetings].[Countries] VALUES ('MQ', 'Martinique') INSERT INTO [meetings].[Countries] VALUES ('MR', 'Mauritania') INSERT INTO [meetings].[Countries] VALUES ('MS', 'Montserrat') INSERT INTO [meetings].[Countries] VALUES ('MT', 'Malta') INSERT INTO [meetings].[Countries] VALUES ('MU', 'Mauritius') INSERT INTO [meetings].[Countries] VALUES ('MV', 'Maldives') INSERT INTO [meetings].[Countries] VALUES ('MW', 'Malawi') INSERT INTO [meetings].[Countries] VALUES ('MX', 'Mexico') INSERT INTO [meetings].[Countries] VALUES ('MY', 'Malaysia') INSERT INTO [meetings].[Countries] VALUES ('MZ', 'Mozambique') INSERT INTO [meetings].[Countries] VALUES ('NA', 'Namibia') INSERT INTO [meetings].[Countries] VALUES ('NC', 'New Caledonia') INSERT INTO [meetings].[Countries] VALUES ('NE', 'Niger') INSERT INTO [meetings].[Countries] VALUES ('NF', 'Norfolk Island') INSERT INTO [meetings].[Countries] VALUES ('NG', 'Nigeria') INSERT INTO [meetings].[Countries] VALUES ('NI', 'Nicaragua') INSERT INTO [meetings].[Countries] VALUES ('NL', 'Netherlands') INSERT INTO [meetings].[Countries] VALUES ('NO', 'Norway') INSERT INTO [meetings].[Countries] VALUES ('NP', 'Nepal') INSERT INTO [meetings].[Countries] VALUES ('NR', 'Nauru') INSERT INTO [meetings].[Countries] VALUES ('NU', 'Niue') INSERT INTO [meetings].[Countries] VALUES ('NZ', 'New Zealand') INSERT INTO [meetings].[Countries] VALUES ('OM', 'Oman') INSERT INTO [meetings].[Countries] VALUES ('PA', 'Panama') INSERT INTO [meetings].[Countries] VALUES ('PE', 'Peru') INSERT INTO [meetings].[Countries] VALUES ('PF', 'French Polynesia') INSERT INTO [meetings].[Countries] VALUES ('PG', 'Papua New Guinea') INSERT INTO [meetings].[Countries] VALUES ('PH', 'Philippines') INSERT INTO [meetings].[Countries] VALUES ('PK', 'Pakistan') INSERT INTO [meetings].[Countries] VALUES ('PL', 'Poland') INSERT INTO [meetings].[Countries] VALUES ('PM', 'Saint Pierre and Miquelon') INSERT INTO [meetings].[Countries] VALUES ('PN', 'Pitcairn') INSERT INTO [meetings].[Countries] VALUES ('PR', 'Puerto Rico') INSERT INTO [meetings].[Countries] VALUES ('PS', 'Palestinian Territory, Occupied') INSERT INTO [meetings].[Countries] VALUES ('PT', 'Portugal') INSERT INTO [meetings].[Countries] VALUES ('PW', 'Palau') INSERT INTO [meetings].[Countries] VALUES ('PY', 'Paraguay') INSERT INTO [meetings].[Countries] VALUES ('QA', 'Qatar') INSERT INTO [meetings].[Countries] VALUES ('RE', 'Reunion') INSERT INTO [meetings].[Countries] VALUES ('RO', 'Romania') INSERT INTO [meetings].[Countries] VALUES ('RS', 'Serbia') INSERT INTO [meetings].[Countries] VALUES ('RU', 'Russian Federation') INSERT INTO [meetings].[Countries] VALUES ('RW', 'RWANDA') INSERT INTO [meetings].[Countries] VALUES ('SA', 'Saudi Arabia') INSERT INTO [meetings].[Countries] VALUES ('SB', 'Solomon Islands') INSERT INTO [meetings].[Countries] VALUES ('SC', 'Seychelles') INSERT INTO [meetings].[Countries] VALUES ('SD', 'Sudan') INSERT INTO [meetings].[Countries] VALUES ('SE', 'Sweden') INSERT INTO [meetings].[Countries] VALUES ('SG', 'Singapore') INSERT INTO [meetings].[Countries] VALUES ('SH', 'Saint Helena') INSERT INTO [meetings].[Countries] VALUES ('SI', 'Slovenia') INSERT INTO [meetings].[Countries] VALUES ('SJ', 'Svalbard and Jan Mayen') INSERT INTO [meetings].[Countries] VALUES ('SK', 'Slovakia') INSERT INTO [meetings].[Countries] VALUES ('SL', 'Sierra Leone') INSERT INTO [meetings].[Countries] VALUES ('SM', 'San Marino') INSERT INTO [meetings].[Countries] VALUES ('SN', 'Senegal') INSERT INTO [meetings].[Countries] VALUES ('SO', 'Somalia') INSERT INTO [meetings].[Countries] VALUES ('SR', 'Suriname') INSERT INTO [meetings].[Countries] VALUES ('ST', 'Sao Tome and Principe') INSERT INTO [meetings].[Countries] VALUES ('SV', 'El Salvador') INSERT INTO [meetings].[Countries] VALUES ('SY', 'Syrian Arab Republic') INSERT INTO [meetings].[Countries] VALUES ('SZ', 'Swaziland') INSERT INTO [meetings].[Countries] VALUES ('TC', 'Turks and Caicos Islands') INSERT INTO [meetings].[Countries] VALUES ('TD', 'Chad') INSERT INTO [meetings].[Countries] VALUES ('TF', 'French Southern Territories') INSERT INTO [meetings].[Countries] VALUES ('TG', 'Togo') INSERT INTO [meetings].[Countries] VALUES ('TH', 'Thailand') INSERT INTO [meetings].[Countries] VALUES ('TJ', 'Tajikistan') INSERT INTO [meetings].[Countries] VALUES ('TK', 'Tokelau') INSERT INTO [meetings].[Countries] VALUES ('TL', 'Timor-Leste') INSERT INTO [meetings].[Countries] VALUES ('TM', 'Turkmenistan') INSERT INTO [meetings].[Countries] VALUES ('TN', 'Tunisia') INSERT INTO [meetings].[Countries] VALUES ('TO', 'Tonga') INSERT INTO [meetings].[Countries] VALUES ('TR', 'Turkey') INSERT INTO [meetings].[Countries] VALUES ('TT', 'Trinidad and Tobago') INSERT INTO [meetings].[Countries] VALUES ('TV', 'Tuvalu') INSERT INTO [meetings].[Countries] VALUES ('TW', 'Taiwan, Province of China') INSERT INTO [meetings].[Countries] VALUES ('TZ', 'Tanzania, United Republic of') INSERT INTO [meetings].[Countries] VALUES ('UA', 'Ukraine') INSERT INTO [meetings].[Countries] VALUES ('UG', 'Uganda') INSERT INTO [meetings].[Countries] VALUES ('UM', 'United States Minor Outlying Islands') INSERT INTO [meetings].[Countries] VALUES ('US', 'United States') INSERT INTO [meetings].[Countries] VALUES ('UY', 'Uruguay') INSERT INTO [meetings].[Countries] VALUES ('UZ', 'Uzbekistan') INSERT INTO [meetings].[Countries] VALUES ('VA', 'Holy See (Vatican City State)') INSERT INTO [meetings].[Countries] VALUES ('VC', 'Saint Vincent and the Grenadines') INSERT INTO [meetings].[Countries] VALUES ('VE', 'Venezuela') INSERT INTO [meetings].[Countries] VALUES ('VG', 'Virgin Islands, British') INSERT INTO [meetings].[Countries] VALUES ('VI', 'Virgin Islands, U.S.') INSERT INTO [meetings].[Countries] VALUES ('VN', 'Viet Nam') INSERT INTO [meetings].[Countries] VALUES ('VU', 'Vanuatu') INSERT INTO [meetings].[Countries] VALUES ('WF', 'Wallis and Futuna') INSERT INTO [meetings].[Countries] VALUES ('WS', 'Samoa') INSERT INTO [meetings].[Countries] VALUES ('YE', 'Yemen') INSERT INTO [meetings].[Countries] VALUES ('YT', 'Mayotte') INSERT INTO [meetings].[Countries] VALUES ('ZA', 'South Africa') INSERT INTO [meetings].[Countries] VALUES ('ZM', 'Zambia') INSERT INTO [meetings].[Countries] VALUES ('ZW', 'Zimbabwe') ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/Countries/GetCountriesTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Countries; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Countries { [TestFixture] public class GetCountriesTests : TestBase { [Test] public async Task GetCountriesTest() { // Arrange await ExecuteScript("Countries/0001_SeedCountries.sql"); // Act var countries = await MeetingsModule.ExecuteQueryAsync(new GetAllCountriesQuery()); // Assert Assert.That(countries, Is.Not.Empty); Assert.That(countries.Any(x => x.Code == "PL")); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingCommentLikes/AddMeetingCommentLikeTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingComment; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingCommentLike; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingCommentLikers; using CompanyName.MyMeetings.Modules.Meetings.Application.Members.CreateMember; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Meetings; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingCommentLikes { [TestFixture] public class AddMeetingCommentLikeTests : TestBase { [Test] public async Task AddMeetingCommentLike_WhenDataIsValid_IsSuccess() { // Arrange await MeetingsModule.ExecuteCommandAsync( new CreateMemberCommand( Guid.NewGuid(), ExecutionContext.UserId, "ivan_petrov", "ivan@mail.com", "Ivan", "Petrov", "Ivan Petrov")); var meetingId = await MeetingHelper.CreateMeetingAsync(MeetingsModule, ExecutionContext); var meetingCommentId = await MeetingsModule.ExecuteCommandAsync(new AddMeetingCommentCommand(meetingId, "The meeting was awesome.")); // Act await MeetingsModule.ExecuteCommandAsync(new AddMeetingCommentLikeCommand(meetingCommentId)); // Assert var meetingCommentLikers = await MeetingsModule.ExecuteQueryAsync(new GetMeetingCommentLikersQuery(meetingCommentId)); Assert.That(meetingCommentLikers.Count, Is.EqualTo(1)); Assert.That(meetingCommentLikers.Single().Id, Is.EqualTo(ExecutionContext.UserId)); await AssertEventually( new GetMeetingCommentsProbe(MeetingsModule, meetingId, meetingCommentId, expectedCommentLikesCount: 1), 10000); } [Test] public void AddMeetingCommentLike_WhenCommentNotExists_ThrowsInvalidCommandException() { // Assert Assert.CatchAsync(async () => { // Act await MeetingsModule.ExecuteCommandAsync(new AddMeetingCommentLikeCommand(meetingCommentId: Guid.NewGuid())); }); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingCommentLikes/GetLikedMeetingCommentProbe.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingComments; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingCommentLikes { public class GetLikedMeetingCommentProbe : IProbe { private readonly IMeetingsModule _meetingsModule; private readonly Guid _meetingId; private readonly Guid _likedMeetingCommentId; private List _meetingComments; public GetLikedMeetingCommentProbe(IMeetingsModule meetingsModule, Guid meetingId, Guid likedMeetingCommentId) { _meetingsModule = meetingsModule; _meetingId = meetingId; _likedMeetingCommentId = likedMeetingCommentId; } public bool IsSatisfied() => _meetingComments != null && _meetingComments.Any(c => c.Id == _likedMeetingCommentId && c.LikesCount == 1); public async Task SampleAsync() { _meetingComments = await _meetingsModule.ExecuteQueryAsync(new GetMeetingCommentsQuery(_meetingId)); } public string DescribeFailureTo() => "MeetingComment read model is not in expected state"; } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingCommentLikes/GetMeetingCommentsProbe.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingComments; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingCommentLikes { public class GetMeetingCommentsProbe : IProbe { private readonly IMeetingsModule _meetingsModule; private readonly Guid _meetingId; private readonly Guid _likedMeetingCommentId; private readonly int _expectedCommentLikesCount; private List _meetingComments; public GetMeetingCommentsProbe( IMeetingsModule meetingsModule, Guid meetingId, Guid likedMeetingCommentId, int expectedCommentLikesCount) { _meetingsModule = meetingsModule; _meetingId = meetingId; _likedMeetingCommentId = likedMeetingCommentId; _expectedCommentLikesCount = expectedCommentLikesCount; } public bool IsSatisfied() => _meetingComments != null && _meetingComments.Any(c => c.Id == _likedMeetingCommentId && c.LikesCount == _expectedCommentLikesCount); public async Task SampleAsync() { _meetingComments = await _meetingsModule.ExecuteQueryAsync(new GetMeetingCommentsQuery(_meetingId)); } public string DescribeFailureTo() => "MeetingComment read model is not in expected state"; } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingCommentLikes/RemoveMeetingCommentLikeTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingComment; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingCommentLike; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingCommentLikers; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.RemoveMeetingCommentLike; using CompanyName.MyMeetings.Modules.Meetings.Application.Members.CreateMember; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Meetings; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingCommentLikes { [TestFixture] public class RemoveMeetingCommentLikeTests : TestBase { [Test] public async Task UnlikeMeetingComment_WhenDataIsValid_IsSuccessful() { // Arrange await MeetingsModule.ExecuteCommandAsync( new CreateMemberCommand( Guid.NewGuid(), ExecutionContext.UserId, "ivan_petrov", "ivan@mail.com", "Ivan", "Petrov", "Ivan Petrov")); var meetingId = await MeetingHelper.CreateMeetingAsync(MeetingsModule, ExecutionContext); var meetingCommentId = await MeetingsModule.ExecuteCommandAsync(new AddMeetingCommentCommand(meetingId, "The meeting was awesome.")); await MeetingsModule.ExecuteCommandAsync(new AddMeetingCommentLikeCommand(meetingCommentId)); await AssertEventually( new GetMeetingCommentsProbe(MeetingsModule, meetingId, meetingCommentId, expectedCommentLikesCount: 1), 10000); // Act await MeetingsModule.ExecuteCommandAsync(new RemoveMeetingCommentLikeCommand(meetingCommentId)); // Assert var meetingCommentLikers = await MeetingsModule.ExecuteQueryAsync(new GetMeetingCommentLikersQuery(meetingCommentId)); Assert.That(meetingCommentLikers.Count, Is.EqualTo(0)); await AssertEventually( new GetMeetingCommentsProbe(MeetingsModule, meetingId, meetingCommentId, expectedCommentLikesCount: 0), 10000); } [Test] public void UnlikeMeetingComment_WhenCommentNotExists_ThrowsInvalidCommandException() { // Assert Assert.CatchAsync(async () => { // Act await MeetingsModule.ExecuteCommandAsync(new RemoveMeetingCommentLikeCommand(meetingCommentId: Guid.NewGuid())); }); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingCommentingConfigurations/CreateMeetingCommentingConfigurationTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.GetMeetingCommentingConfiguration; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Meetings; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingCommentingConfigurations { [TestFixture] public class CreateMeetingCommentingConfigurationTests : TestBase { [Test] public async Task CreateMeetingCommentingCofiguration_WhenDataIsValid_IsSuccessful() { // Act var meetingId = await MeetingHelper.CreateMeetingAsync(MeetingsModule, ExecutionContext); // Assert var meetingCommentingConfiguration = await MeetingsModule.ExecuteQueryAsync(new GetMeetingCommentingConfigurationQuery(meetingId)); Assert.That(meetingCommentingConfiguration, Is.Not.Null); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingCommentingConfigurations/DisableMeetingCommentingConfigurationTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.DisableMeetingCommentingConfiguration; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.GetMeetingCommentingConfiguration; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Meetings; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingCommentingConfigurations { [TestFixture] public class DisableMeetingCommentingConfigurationTests : TestBase { [Test] public async Task DisableMeetingCommenting_WhenDataIsValid_IsSuccess() { // Arrange var meetingId = await MeetingHelper.CreateMeetingAsync(MeetingsModule, ExecutionContext); // Act await MeetingsModule.ExecuteCommandAsync(new DisableMeetingCommentingConfigurationCommand(meetingId)); // Assert var meetingConfiguration = await MeetingsModule.ExecuteQueryAsync(new GetMeetingCommentingConfigurationQuery(meetingId)); Assert.That(meetingConfiguration, Is.Not.Null); Assert.That(meetingConfiguration.IsCommentingEnabled, Is.False); } [Test] public void DisableMeetingCommenting_WhenConfigurationNotExist_ThrowsInvalidCommandException() { // Assert Assert.CatchAsync(async () => { // Act await MeetingsModule.ExecuteCommandAsync(new DisableMeetingCommentingConfigurationCommand(Guid.NewGuid())); }); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingCommentingConfigurations/EnableMeetingCommentingConfigurationTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.DisableMeetingCommentingConfiguration; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.EnableMeetingCommentingConfiguration; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingCommentingConfigurations.GetMeetingCommentingConfiguration; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Meetings; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingCommentingConfigurations { [TestFixture] public class EnableMeetingCommentingConfigurationTests : TestBase { [Test] public async Task EnableMeetingCommenting_WhenDataIsValid_IsSuccess() { // Arrange var meetingId = await MeetingHelper.CreateMeetingAsync(MeetingsModule, ExecutionContext); await MeetingsModule.ExecuteCommandAsync(new DisableMeetingCommentingConfigurationCommand(meetingId)); // Act await MeetingsModule.ExecuteCommandAsync(new EnableMeetingCommentingConfigurationCommand(meetingId)); // Assert var meetingConfiguration = await MeetingsModule.ExecuteQueryAsync(new GetMeetingCommentingConfigurationQuery(meetingId)); Assert.That(meetingConfiguration, Is.Not.Null); Assert.That(meetingConfiguration.IsCommentingEnabled, Is.True); } [Test] public void EnableMeetingCommenting_WhenConfigurationNotExist_ThrowsInvalidCommandException() { // Assert Assert.CatchAsync(async () => { // Act await MeetingsModule.ExecuteCommandAsync(new EnableMeetingCommentingConfigurationCommand(Guid.NewGuid())); }); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingComments/AddMeetingCommentTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingComment; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Meetings; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingComments { [TestFixture] public class AddMeetingCommentTests : TestBase { [Test] public async Task AddMeetingComment_WhenDataIsValid_IsSuccessful() { // Arrange var meetingId = await MeetingHelper.CreateMeetingAsync(MeetingsModule, ExecutionContext); var date = new DateTime(2020, 1, 1, 01, 00, 00); SystemClock.Set(date); var comment = "The meeting was great."; // Act var meetingCommentId = await MeetingsModule.ExecuteCommandAsync(new AddMeetingCommentCommand(meetingId, comment)); // Assert var meetingComments = await MeetingsModule.ExecuteQueryAsync(new GetMeetingCommentsQuery(meetingId)); Assert.That(meetingComments.Count, Is.EqualTo(1)); var meetingComment = meetingComments.Single(); Assert.That(meetingComment.Id, Is.EqualTo(meetingCommentId)); Assert.That(meetingComment.Comment, Is.EqualTo(comment)); Assert.That(meetingComment.AuthorId, Is.EqualTo(ExecutionContext.UserId)); Assert.That(meetingComment.CreateDate, Is.EqualTo(date)); Assert.That(meetingComment.EditDate, Is.Null); } [Test] public void AddMeetingComment_WhenMeetingIsNonexistent_ThrowsInvalidCommandException() { // Assert Assert.CatchAsync(async () => { // Act await MeetingsModule.ExecuteCommandAsync(new AddMeetingCommentCommand( meetingId: Guid.NewGuid(), "Comment for a nonexistent meeting.")); }); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingComments/AddReplyToMeetingCommentTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingComment; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingCommentReply; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Meetings; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingComments { [TestFixture] public class AddReplyToMeetingCommentTests : TestBase { [Test] public async Task AddReply_WhenDataIsValid_IsSuccessful() { // Arrange var meetingId = await MeetingHelper.CreateMeetingAsync(MeetingsModule, ExecutionContext); var meetingCommentId = await MeetingsModule.ExecuteCommandAsync(new AddMeetingCommentCommand(meetingId, "The meeting was great.")); var date = new DateTime(2020, 1, 1, 01, 00, 00); SystemClock.Set(date); var reply = "Absolutely!"; // Act var replyId = await MeetingsModule.ExecuteCommandAsync(new AddReplyToMeetingCommentCommand(meetingCommentId, reply)); // Assert var meetingComments = await MeetingsModule.ExecuteQueryAsync(new GetMeetingCommentsQuery(meetingId)); Assert.That(meetingComments.Count, Is.EqualTo(2)); var commentReply = meetingComments.Single(c => c.Id == replyId); Assert.That(commentReply.InReplyToCommentId, Is.EqualTo(meetingCommentId)); Assert.That(commentReply.Comment, Is.EqualTo(reply)); Assert.That(commentReply.AuthorId, Is.EqualTo(ExecutionContext.UserId)); Assert.That(commentReply.CreateDate, Is.EqualTo(date)); Assert.That(commentReply.EditDate, Is.Null); } [Test] public void AddReply_WhenParentCommentNotExists_ThrowsInvalidCommandException() { // Assert Assert.CatchAsync(async () => { // Act await MeetingsModule.ExecuteCommandAsync(new AddReplyToMeetingCommentCommand( inReplyToCommentId: Guid.NewGuid(), "Reply for a nonexistent comment.")); }); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingComments/EditMeetingCommentTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingComment; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.EditMeetingComment; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; using static CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Meetings.MeetingHelper; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingComments { [TestFixture] public class EditMeetingCommentTests : TestBase { [Test] public async Task EditMeetingComment_WhenDataIsValid_IsSuccessful() { // Arrange var meetingId = await CreateMeetingAsync(MeetingsModule, ExecutionContext); var meetingCommentId = await MeetingsModule.ExecuteCommandAsync(new AddMeetingCommentCommand( meetingId, "The meeting was great.")); var editedComment = "It was very interesting!"; var meetingCommentsBefore = await MeetingsModule.ExecuteQueryAsync(new GetMeetingCommentsQuery(meetingId)); var originalMeetingComment = meetingCommentsBefore.Single(); var date = new DateTime(2020, 1, 1, 01, 00, 00); SystemClock.Set(date); // Act await MeetingsModule.ExecuteCommandAsync(new EditMeetingCommentCommand(meetingCommentId, editedComment)); // Assert var meetingCommentsAfter = await MeetingsModule.ExecuteQueryAsync(new GetMeetingCommentsQuery(meetingId)); Assert.That(meetingCommentsAfter.Count, Is.EqualTo(1)); var editedMeetingComment = meetingCommentsAfter.Single(); Assert.That(editedMeetingComment.Comment, Is.EqualTo(editedComment)); Assert.That(editedMeetingComment.EditDate, Is.EqualTo(date)); Assert.That(editedMeetingComment.AuthorId, Is.EqualTo(originalMeetingComment.AuthorId)); Assert.That(editedMeetingComment.CreateDate, Is.EqualTo(originalMeetingComment.CreateDate)); } [Test] public void EditMeetingComment_WhenItIsNonexistent_ThrowsInvalidCommandException() { // Assert Assert.CatchAsync(async () => { // Act await MeetingsModule.ExecuteCommandAsync(new EditMeetingCommentCommand( meetingCommentId: Guid.NewGuid(), "Edit a nonexistent comment.")); }); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingComments/GetMeetingCommentsTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingComment; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingComments; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Meetings; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingComments { [TestFixture] public class GetMeetingCommentsTests : TestBase { [Test] public async Task GetMeetingComments_Test() { // Arrange var meetingId = await MeetingHelper.CreateMeetingAsync(MeetingsModule, ExecutionContext); await MeetingsModule.ExecuteCommandAsync( new AddMeetingCommentCommand(meetingId, "The meeting was great.")); await MeetingsModule.ExecuteCommandAsync( new AddMeetingCommentCommand(meetingId, "The meeting was wonderful.")); await MeetingsModule.ExecuteCommandAsync( new AddMeetingCommentCommand(meetingId, "The meeting was amazing.")); // Act var meetingComments = await MeetingsModule.ExecuteQueryAsync(new GetMeetingCommentsQuery(meetingId)); // Assert Assert.That(meetingComments.Count, Is.EqualTo(3)); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingComments/RemoveMeetingCommentTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.AddMeetingComment; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.GetMeetingComments; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.RemoveMeetingComment; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Meetings; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingComments { public class RemoveMeetingCommentTests : TestBase { [Test] public async Task RemoveMeetingComment_ByAuthor_WhenDataIsValid_IsSuccessful() { // Arrange var meetingId = await MeetingHelper.CreateMeetingAsync(MeetingsModule, ExecutionContext); var meetingCommentId = await MeetingsModule.ExecuteCommandAsync(new AddMeetingCommentCommand( meetingId, "The meeting was great.")); // Act await MeetingsModule.ExecuteCommandAsync(new RemoveMeetingCommentCommand( meetingCommentId, reason: string.Empty)); // Assert var meetingComments = await MeetingsModule.ExecuteQueryAsync(new GetMeetingCommentsQuery(meetingId)); Assert.That(meetingComments, Is.Empty); } [Test] public void RemoveMeetingComment_WhenItIsNonexistent_ThrowsInvalidCommandException() { // Assert Assert.CatchAsync(async () => { // Act await MeetingsModule.ExecuteCommandAsync(new RemoveMeetingCommentCommand( meetingCommentId: Guid.NewGuid(), reason: string.Empty)); }); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingGroupProposals/GetMeetingGroupProposalsTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetAllMeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.ProposeMeetingGroup; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingGroupProposals { [TestFixture] public class GetMeetingGroupProposalsTests : TestBase { [Test] public async Task GetMeetingGroupProposals_Test() { await MeetingsModule.ExecuteCommandAsync(new ProposeMeetingGroupCommand( "Name 1", "Desc 1", "Warsaw", "PL")); await MeetingsModule.ExecuteCommandAsync(new ProposeMeetingGroupCommand( "Name 2", "Desc 2", "London", "GB")); await MeetingsModule.ExecuteCommandAsync(new ProposeMeetingGroupCommand( "Name 3", "Desc 3", "Rome", "IT")); await MeetingsModule.ExecuteCommandAsync(new ProposeMeetingGroupCommand( "Name 4", "Desc 4", "Madrid", "ES")); await MeetingsModule.ExecuteCommandAsync(new ProposeMeetingGroupCommand( "Name 5", "Desc 5", "Berlin", "DE")); var allProposals = await MeetingsModule.ExecuteQueryAsync(new GetAllMeetingGroupProposalsQuery(null, null)); Assert.That(allProposals.Count, Is.EqualTo(5)); var proposalsPaged = await MeetingsModule.ExecuteQueryAsync(new GetAllMeetingGroupProposalsQuery(2, 2)); Assert.That(proposalsPaged.Count, Is.EqualTo(2)); Assert.That(proposalsPaged[0].Name, Is.EqualTo("Name 3")); Assert.That(proposalsPaged[1].Name, Is.EqualTo("Name 4")); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingGroupProposals/MeetingGroupProposalSampleData.cs ================================================ namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingGroupProposals { public struct MeetingGroupProposalSampleData { public static Guid MeetingGroupProposalId = Guid.NewGuid(); public static string Name = "Great Meeting"; public static string Description = "Great Meeting description"; public static string LocationCity = "Warsaw"; public static string LocationCountryCode = "PL"; public static Guid ProposalUserId = Guid.NewGuid(); public static DateTime ProposalDate = new DateTime(2020, 1, 1, 10, 20, 00); } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingGroupProposals/ProposeMeetingGroupTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.AcceptMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.GetMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.ProposeMeetingGroup; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingGroupProposals { [TestFixture] public class ProposeMeetingGroupTests : TestBase { [Test] public async Task ProposeAndAcceptMeetingGroup_WhenDataIsValid_IsSuccessful() { var proposalId = await MeetingsModule.ExecuteCommandAsync(new ProposeMeetingGroupCommand( MeetingGroupProposalSampleData.Name, MeetingGroupProposalSampleData.Description, MeetingGroupProposalSampleData.LocationCity, MeetingGroupProposalSampleData.LocationCountryCode)); await MeetingsModule.ExecuteCommandAsync(new AcceptMeetingGroupProposalCommand(Guid.NewGuid(), proposalId)); var meetingGroupProposal = await MeetingsModule.ExecuteQueryAsync(new GetMeetingGroupProposalQuery(proposalId)); Assert.That(meetingGroupProposal.Id, Is.EqualTo(proposalId)); Assert.That(meetingGroupProposal.Name, Is.EqualTo(MeetingGroupProposalSampleData.Name)); Assert.That(meetingGroupProposal.Description, Is.EqualTo(MeetingGroupProposalSampleData.Description)); Assert.That(meetingGroupProposal.LocationCity, Is.EqualTo(MeetingGroupProposalSampleData.LocationCity)); Assert.That(meetingGroupProposal.LocationCountryCode, Is.EqualTo(MeetingGroupProposalSampleData.LocationCountryCode)); Assert.That(meetingGroupProposal.StatusCode, Is.EqualTo("Accepted")); } [Test] public void ProposeMeetingGroup_WhenNoLocationProvided_ThrowsInvalidCommandException() { Assert.CatchAsync(async () => { await MeetingsModule.ExecuteCommandAsync(new ProposeMeetingGroupCommand( MeetingGroupProposalSampleData.Name, MeetingGroupProposalSampleData.Description, null, null)); }); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/MeetingGroups/CreateNewMeetingGroupTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.AcceptMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.ProposeMeetingGroup; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.CreateNewMeetingGroup; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetAuthenticationMemberMeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetMeetingGroupDetails; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.MeetingGroups { [TestFixture] public class CreateNewMeetingGroupTests : TestBase { [Test] public async Task CreateNewMeetingGroup_Test() { // Arrange var proposalId = await MeetingsModule.ExecuteCommandAsync(new ProposeMeetingGroupCommand( MeetingGroupProposalSampleData.Name, MeetingGroupProposalSampleData.Description, MeetingGroupProposalSampleData.LocationCity, MeetingGroupProposalSampleData.LocationCountryCode)); await MeetingsModule.ExecuteCommandAsync(new AcceptMeetingGroupProposalCommand(Guid.NewGuid(), proposalId)); // Act await MeetingsModule.ExecuteCommandAsync( new CreateNewMeetingGroupCommand( Guid.NewGuid(), new MeetingGroupProposalId(proposalId))); // Assert var meetingGroups = await MeetingsModule.ExecuteQueryAsync(new GetAuthenticationMemberMeetingGroupsQuery()); Assert.That(meetingGroups, Is.Not.Empty); var meetingGroupDetails = await MeetingsModule.ExecuteQueryAsync(new GetMeetingGroupDetailsQuery(proposalId)); Assert.That(meetingGroupDetails.MembersCount, Is.EqualTo(1)); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/Meetings/MeetingCreateTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetMeetingAttendees; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.GetMeetingDetails; using CompanyName.MyMeetings.Modules.Meetings.Application.Members.CreateMember; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Meetings { [TestFixture] public class MeetingCreateTests : TestBase { [Test] public async Task CreateMeeting_Test() { // Arrange await MeetingsModule.ExecuteCommandAsync(new CreateMemberCommand( Guid.NewGuid(), ExecutionContext.UserId, "johndoe", "johndoe@mail.com", "John", "Doe", "John Doe")); // Act var meetingId = await MeetingHelper.CreateMeetingAsync( MeetingsModule, ExecutionContext); // Assert var meetingDetails = await MeetingsModule.ExecuteQueryAsync(new GetMeetingDetailsQuery(meetingId)); Assert.That(meetingDetails, Is.Not.Null); var meetingAttendees = await MeetingsModule.ExecuteQueryAsync(new GetMeetingAttendeesQuery(meetingId)); Assert.That(meetingAttendees.Count, Is.EqualTo(1)); } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/Meetings/MeetingHelper.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.ProposeMeetingGroup; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.CreateNewMeetingGroup; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetAllMeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.SetMeetingGroupExpirationDate; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.CreateMeeting; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; using CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.Meetings { internal static class MeetingHelper { public static async Task CreateMeetingAsync( IMeetingsModule meetingsModule, ExecutionContextMock executionContext) { var proposalId = await meetingsModule.ExecuteCommandAsync( new ProposeMeetingGroupCommand( "Amazing group", "Absolutely amazing meeting group.", "London", "UK")); await meetingsModule.ExecuteCommandAsync( new CreateNewMeetingGroupCommand( Guid.NewGuid(), new MeetingGroupProposalId(proposalId))); var meetingGroups = await meetingsModule.ExecuteQueryAsync(new GetAllMeetingGroupsQuery()); var meetingGroup = meetingGroups.Single(); await meetingsModule.ExecuteCommandAsync( new SetMeetingGroupExpirationDateCommand( Guid.NewGuid(), meetingGroup.Id, SystemClock.Now.AddMonths(1))); var meetingId = await meetingsModule.ExecuteCommandAsync( new CreateMeetingCommand( meetingGroup.Id, "Some meeting", DateTime.UtcNow.AddDays(1), DateTime.UtcNow.AddDays(10), "Some very nice meeting.", "UK", "Baker street", "W2 2SZ", "London", 25, 1, null, null, null, null, [executionContext.UserId])); return meetingId; } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/SeedWork/EventsBusMock.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork { public class EventsBusMock : IEventsBus { private readonly IList _publishedEvents; public EventsBusMock() { _publishedEvents = new List(); } public void Dispose() { } public Task Publish(T @event) where T : IntegrationEvent { _publishedEvents.Add(@event); return Task.CompletedTask; } public T GetLastPublishedEvent() where T : IntegrationEvent { return _publishedEvents.OfType().Last(); } public void Subscribe(IIntegrationEventHandler handler) where T : IntegrationEvent { } public void StartConsuming() { } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/SeedWork/ExecutionContextMock.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork { public class ExecutionContextMock : IExecutionContextAccessor { public ExecutionContextMock(Guid userId) { UserId = userId; } public Guid UserId { get; } public Guid CorrelationId { get; } public bool IsAvailable { get; } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs ================================================ using System.Data; using System.Reflection; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.AcceptMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration.Processing.Outbox; using Dapper; using MediatR; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Meetings.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 [meetings].[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(MeetingGroupProposalAcceptedNotification)).GetType(message.Type); return JsonConvert.DeserializeObject(message.Data, type) as T; } } } ================================================ FILE: src/Modules/Meetings/Tests/IntegrationTests/SeedWork/TestBase.cs ================================================ using System.Data; using System.Data.SqlClient; using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests; using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration; using Dapper; using MediatR; using NSubstitute; using NUnit.Framework; using Serilog; namespace CompanyName.MyMeetings.Modules.Meetings.IntegrationTests.SeedWork { public class TestBase { protected string ConnectionString { get; private set; } protected ILogger Logger { get; private set; } protected IMeetingsModule MeetingsModule { get; private set; } protected IEmailSender EmailSender { get; private set; } protected ExecutionContextMock ExecutionContext { 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 = new LoggerConfiguration() .Enrich.FromLogContext() .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{Module}] [{Context}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); EmailSender = Substitute.For(); ExecutionContext = new ExecutionContextMock(Guid.NewGuid()); MeetingsStartup.Initialize( ConnectionString, ExecutionContext, Logger, new EmailsConfiguration("from@email.com"), new EventsBusMock()); MeetingsModule = new MeetingsModule(); } [TearDown] public void AfterEachTest() { MeetingsStartup.Stop(); SystemClock.Reset(); } protected async Task ExecuteScript(string scriptPath) { var sql = await File.ReadAllTextAsync(scriptPath); await using var sqlConnection = new SqlConnection(ConnectionString); await sqlConnection.ExecuteScalarAsync(sql); } 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()); } } protected static void AssertBrokenRule(AsyncTestDelegate testDelegate) where TRule : class, IBusinessRule { var message = $"Expected {typeof(TRule).Name} broken rule"; var businessRuleValidationException = Assert.CatchAsync(testDelegate, message); if (businessRuleValidationException != null) { Assert.That(businessRuleValidationException.BrokenRule, Is.TypeOf(), message); } } protected static async Task AssertEventually(IProbe probe, int timeout) { await new Poller(timeout).CheckAsync(probe); } private static async Task ClearDatabase(IDbConnection connection) { const string sql = "DELETE FROM [meetings].[InboxMessages] " + "DELETE FROM [meetings].[InternalCommands] " + "DELETE FROM [meetings].[OutboxMessages] " + "DELETE FROM [meetings].[MeetingAttendees] " + "DELETE FROM [meetings].[MeetingGroupMembers] " + "DELETE FROM [meetings].[MeetingGroupProposals] " + "DELETE FROM [meetings].[MeetingGroups] " + "DELETE FROM [meetings].[MeetingNotAttendees] " + "DELETE FROM [meetings].[MeetingCommentingConfigurations] " + "DELETE FROM [meetings].[Meetings] " + "DELETE FROM [meetings].[MeetingWaitlistMembers] " + "DELETE FROM [meetings].[MeetingMemberCommentLikes] " + "DELETE FROM [meetings].[MeetingComments] " + "DELETE FROM [meetings].[Countries] " + "DELETE FROM [meetings].[Members] "; await connection.ExecuteScalarAsync(sql); } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.csproj ================================================  ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/MeetingGroupProposals/MeetingGroupProposalTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.SeedWork; using FluentAssertions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.MeetingGroupProposals { [TestFixture] public class MeetingGroupProposalTests : TestBase { [Test] public void ProposeNewMeetingGroup_IsSuccessful() { var proposalMemberId = new MemberId(Guid.NewGuid()); var meetingProposal = MeetingGroupProposal.ProposeNew( "name", "description", MeetingGroupLocation.CreateNew("Warsaw", "PL"), proposalMemberId); var meetingGroupProposed = AssertPublishedDomainEvent(meetingProposal); meetingGroupProposed.MeetingGroupProposalId.Should().Be(meetingProposal.Id); } [Test] public void AcceptProposal_WhenIsNotAccepted_IsSuccessful() { var proposalMemberId = new MemberId(Guid.NewGuid()); var meetingProposal = MeetingGroupProposal.ProposeNew( "name", "description", MeetingGroupLocation.CreateNew("Warsaw", "PL"), proposalMemberId); meetingProposal.Accept(); var meetingGroupProposalAccepted = AssertPublishedDomainEvent(meetingProposal); meetingGroupProposalAccepted.MeetingGroupProposalId.Should().Be(meetingProposal.Id); } [Test] public void AcceptProposal_WhenIsAlreadyAccepted_BreaksProposalCannotBeAcceptedMoreThanOnceRule() { var proposalMemberId = new MemberId(Guid.NewGuid()); var meetingProposal = MeetingGroupProposal.ProposeNew( "name", "description", MeetingGroupLocation.CreateNew("Warsaw", "PL"), proposalMemberId); meetingProposal.Accept(); AssertBrokenRule(() => { meetingProposal.Accept(); }); } [Test] public void CreateMeetingGroup_IsSuccessful_And_CreatorIsAHost() { var proposalMemberId = new MemberId(Guid.NewGuid()); var name = "name"; var description = "description"; var meetingGroupLocation = MeetingGroupLocation.CreateNew("Warsaw", "PL"); var meetingProposal = MeetingGroupProposal.ProposeNew( name, description, meetingGroupLocation, proposalMemberId); var meetingGroup = meetingProposal.CreateMeetingGroup(); var meetingGroupCreated = AssertPublishedDomainEvent(meetingGroup); var newMeetingGroupMemberJoined = AssertPublishedDomainEvent(meetingGroup); meetingGroupCreated.MeetingGroupId.Should().Be(meetingProposal.Id); newMeetingGroupMemberJoined.MemberId.Should().Be(proposalMemberId); newMeetingGroupMemberJoined.Role.Should().Be(MeetingGroupMemberRole.Organizer); } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/MeetingGroups/MeetingGroupTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.SeedWork; using FluentAssertions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.MeetingGroups { [TestFixture] public class MeetingGroupTests : TestBase { [Test] public void EditGeneralAttributes_IsSuccessful() { var meetingGroup = CreateMeetingGroup(); var meetingGroupLocation = MeetingGroupLocation.CreateNew("London", "GB"); meetingGroup.EditGeneralAttributes("newName", "newDescription", meetingGroupLocation); var meetingGroupGeneralAttributesEdited = AssertPublishedDomainEvent(meetingGroup); meetingGroupGeneralAttributesEdited.NewName.Should().Be("newName"); meetingGroupGeneralAttributesEdited.NewDescription.Should().Be("newDescription"); meetingGroupGeneralAttributesEdited.NewLocation.Should().Be(meetingGroupLocation); } [Test] public void JoinToGroup_WhenMemberHasNotJoinedYet_IsSuccessful() { var meetingGroup = CreateMeetingGroup(); MemberId newMemberId = new MemberId(Guid.NewGuid()); meetingGroup.JoinToGroupMember(newMemberId); var newMeetingGroupMemberJoined = AssertPublishedDomainEvent(meetingGroup); newMeetingGroupMemberJoined.MeetingGroupId.Should().Be(meetingGroup.Id); newMeetingGroupMemberJoined.MemberId.Should().Be(newMemberId); newMeetingGroupMemberJoined.Role.Should().Be(MeetingGroupMemberRole.Member); } [Test] public void JoinToGroup_WhenMemberHasAlreadyJoined_BreaksMeetingGroupMemberCannotBeAddedTwiceRule() { var meetingGroup = CreateMeetingGroup(); MemberId newMemberId = new MemberId(Guid.NewGuid()); meetingGroup.JoinToGroupMember(newMemberId); AssertBrokenRule(() => { meetingGroup.JoinToGroupMember(newMemberId); }); } [Test] public void LeaveGroup_WhenMemberIsActiveMemberOfGroup_IsSuccessful() { var meetingGroup = CreateMeetingGroup(); var newMemberId = new MemberId(Guid.NewGuid()); meetingGroup.JoinToGroupMember(newMemberId); meetingGroup.LeaveGroup(newMemberId); var meetingGroupMemberLeft = AssertPublishedDomainEvent(meetingGroup); meetingGroupMemberLeft.MemberId.Should().Be(newMemberId); } [Test] public void LeaveGroup_WhenMemberIsNotActiveMemberOfGroup_BreaksNotActualGroupMemberCannotLeaveGroupRule() { var meetingGroup = CreateMeetingGroup(); var newMemberId = new MemberId(Guid.NewGuid()); AssertBrokenRule(() => { meetingGroup.LeaveGroup(newMemberId); }); } [Test] public void UpdatePaymentDateTo_IsSuccessful() { var meetingGroup = CreateMeetingGroup(); var dateTo = DateTime.UtcNow; meetingGroup.SetExpirationDate(dateTo); var meetingGroupPaymentInfoUpdated = AssertPublishedDomainEvent(meetingGroup); meetingGroupPaymentInfoUpdated.MeetingGroupId.Should().Be(meetingGroup.Id); meetingGroupPaymentInfoUpdated.PaymentDateTo.Should().Be(dateTo); } [Test] public void CreateMeeting_WhenGroupIsNotPayed_IsNotPossible() { var meetingGroup = CreateMeetingGroup(); MemberId creatorId = new MemberId(Guid.NewGuid()); AssertBrokenRule(() => { meetingGroup.CreateMeeting( "title", MeetingTerm.CreateNewBetweenDates( new DateTime(2019, 1, 1, 10, 0, 0), new DateTime(2019, 1, 1, 12, 0, 0)), "description", MeetingLocation.CreateNew("Name", "Address", "PostalCode", "City"), null, 0, Term.NoTerm, MoneyValue.Undefined, [], creatorId); }); } [Test] public void CreateMeeting_WhenCreatorIsMemberOfGroupAndHostsAreNotDefined_IsSuccessful() { var definedProposalMemberId = new MemberId(Guid.NewGuid()); var meetingGroup = CreateMeetingGroup(definedProposalMemberId); meetingGroup.SetExpirationDate(DateTime.UtcNow.AddDays(1)); var meeting = meetingGroup.CreateMeeting( "title", MeetingTerm.CreateNewBetweenDates( new DateTime(2019, 1, 1, 10, 0, 0), new DateTime(2019, 1, 1, 12, 0, 0)), "description", MeetingLocation.CreateNew("Name", "Address", "PostalCode", "City"), null, 0, Term.NoTerm, MoneyValue.Undefined, [], definedProposalMemberId); AssertPublishedDomainEvent(meeting); } [Test] public void CreateMeeting_WhenHostsAreDefinedAndTheyAreGroupMembers_DefinedHostsAreHostsOfMeeting() { var definedProposalMemberId = new MemberId(Guid.NewGuid()); var meetingGroup = CreateMeetingGroup(definedProposalMemberId); meetingGroup.SetExpirationDate(DateTime.UtcNow.AddDays(1)); var hostOne = new MemberId(Guid.NewGuid()); var hostTwo = new MemberId(Guid.NewGuid()); List hosts = [ hostOne, hostTwo ]; meetingGroup.JoinToGroupMember(hostOne); meetingGroup.JoinToGroupMember(hostTwo); var meeting = meetingGroup.CreateMeeting( "title", MeetingTerm.CreateNewBetweenDates( new DateTime(2019, 1, 1, 10, 0, 0), new DateTime(2019, 1, 1, 12, 0, 0)), "description", MeetingLocation.CreateNew("Name", "Address", "PostalCode", "City"), null, 0, Term.NoTerm, MoneyValue.Undefined, hosts, definedProposalMemberId); var meetingAttendeeAddedEvents = AssertPublishedDomainEvents(meeting); meetingAttendeeAddedEvents.Should().HaveCount(2); meetingAttendeeAddedEvents[0].AttendeeId.Should().Be(hostOne); meetingAttendeeAddedEvents[0].Role.Should().Be(MeetingAttendeeRole.Host.Value); meetingAttendeeAddedEvents[1].AttendeeId.Should().Be(hostTwo); meetingAttendeeAddedEvents[1].Role.Should().Be(MeetingAttendeeRole.Host.Value); } [Test] public void CreateMeeting_WhenHostsAreDefinedAndTheyAreNotGroupMembers_BreaksMeetingHostMustBeAMeetingGroupMemberRule() { var definedProposalMemberId = new MemberId(Guid.NewGuid()); var meetingGroup = CreateMeetingGroup(definedProposalMemberId); meetingGroup.SetExpirationDate(DateTime.UtcNow.AddDays(1)); var hostOne = new MemberId(Guid.NewGuid()); var hostTwo = new MemberId(Guid.NewGuid()); List hosts = [ hostOne, hostTwo ]; AssertBrokenRule(() => { meetingGroup.CreateMeeting( "title", MeetingTerm.CreateNewBetweenDates( new DateTime(2019, 1, 1, 10, 0, 0), new DateTime(2019, 1, 1, 12, 0, 0)), "description", MeetingLocation.CreateNew("Name", "Address", "PostalCode", "City"), null, 0, Term.NoTerm, MoneyValue.Undefined, hosts, definedProposalMemberId); }); } [Test] public void CreateMeeting_WhenCreatorIsNotMemberOfGroup_BreaksMeetingHostMustBeAMeetingGroupMemberRule() { var definedProposalMemberId = new MemberId(Guid.NewGuid()); var creatorId = new MemberId(Guid.NewGuid()); var meetingGroup = CreateMeetingGroup(definedProposalMemberId); meetingGroup.SetExpirationDate(DateTime.UtcNow.AddDays(1)); AssertBrokenRule(() => { meetingGroup.CreateMeeting( "title", MeetingTerm.CreateNewBetweenDates( new DateTime(2019, 1, 1, 10, 0, 0), new DateTime(2019, 1, 1, 12, 0, 0)), "description", MeetingLocation.CreateNew("Name", "Address", "PostalCode", "City"), null, 0, Term.NoTerm, MoneyValue.Undefined, [], creatorId); }); } private static MeetingGroup CreateMeetingGroup(MemberId definedProposalMemberId = null) { var proposalMemberId = definedProposalMemberId ?? new MemberId(Guid.NewGuid()); var meetingProposal = MeetingGroupProposal.ProposeNew( "name", "description", MeetingGroupLocation.CreateNew("Warsaw", "PL"), proposalMemberId); meetingProposal.Accept(); var meetingGroup = meetingProposal.CreateMeetingGroup(); DomainEventsTestHelper.ClearAllDomainEvents(meetingGroup); return meetingGroup; } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/Meetings/MeetingAddAttendeeTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using FluentAssertions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.Meetings { [TestFixture] public class MeetingAddAttendeeTests : MeetingTestsBase { [Test] public void AddAttendee_WhenMeetingHasStared_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, MeetingTerm = MeetingTerm.CreateNewBetweenDates(DateTime.UtcNow.AddDays(-2), DateTime.UtcNow.AddDays(-1)) }); AssertBrokenRule(() => { meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, creatorId, 0); }); } [Test] public void AddAttendee_WhenRsvpTermEnded_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, RvspTerm = Term.CreateNewBetweenDates(DateTime.UtcNow.AddDays(-2), DateTime.UtcNow.AddDays(-1)) }); AssertBrokenRule(() => { meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, creatorId, 0); }); } [Test] public void AddAttendee_WhenAttendeeIsNotAMemberOfMeetingGroup_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); AssertBrokenRule(() => { meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, new MemberId(Guid.NewGuid()), 0); }); } [Test] public void AddAttendee_WhenMemberIsAlreadyAttendeeOfMeeting_IsNotPossible() { // Arrange var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var newMemberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newMemberId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newMemberId, 0); // Assert AssertBrokenRule(() => { // Act meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newMemberId, 0); }); } [Test] public void AddAttendee_WhenGuestsNumberIsAboveTheLimit_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, GuestsLimit = 5 }); var newMemberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newMemberId); AssertBrokenRule(() => { meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newMemberId, 6); }); } [Test] public void AddAttendee_WhenAttendeeLimitIsReached_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, AttendeesLimit = 5 }); var newMemberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newMemberId); var aboveLimitMember = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(aboveLimitMember); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newMemberId, 3); // for now: creator or meeting (automatically) and newMember with 3 guests = 5 AssertBrokenRule(() => { meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, aboveLimitMember, 0); }); } [Test] public void AddAttendee_WhenAllConditionsAllowsNewAttendee_IsSuccessful() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var newMemberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newMemberId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newMemberId, 3); var meetingAttendeesAddedEvents = AssertPublishedDomainEvents(meetingTestData.Meeting); meetingAttendeesAddedEvents.Should().HaveCount(2); meetingAttendeesAddedEvents[0].AttendeeId.Should().Be(creatorId); meetingAttendeesAddedEvents[0].Role.Should().Be(MeetingAttendeeRole.Host.Value); meetingAttendeesAddedEvents[1].AttendeeId.Should().Be(newMemberId); meetingAttendeesAddedEvents[1].Role.Should().Be(MeetingAttendeeRole.Attendee.Value); } [Test] public void AddAttendee_WhenMemberIsNotAttendeeAndChangedDecision_IsSuccessful() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var newMemberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newMemberId); meetingTestData.Meeting.AddNotAttendee(newMemberId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newMemberId, 0); var meetingNotAttendeeChangedDecision = AssertPublishedDomainEvent(meetingTestData.Meeting); meetingNotAttendeeChangedDecision.MemberId.Should().Be(newMemberId); } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/Meetings/MeetingAddNotAttendeeTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using FluentAssertions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.Meetings { [TestFixture] public class MeetingAddNotAttendeeTests : MeetingTestsBase { [Test] public void AddNotAttendee_WhenMeetingHasStarted_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, MeetingTerm = MeetingTerm.CreateNewBetweenDates(DateTime.UtcNow.AddDays(-2), DateTime.UtcNow.AddDays(-1)) }); AssertBrokenRule(() => { meetingTestData.Meeting.AddNotAttendee(creatorId); }); } [Test] public void AddNotAttendee_WhenMemberIsAlreadyNotAttendee_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); meetingTestData.Meeting.AddNotAttendee(creatorId); AssertBrokenRule(() => { meetingTestData.Meeting.AddNotAttendee(creatorId); }); } [Test] public void AddNotAttendee_WhenMemberIsNotNotAttendee_IsSuccessful() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var memberId = new MemberId(Guid.NewGuid()); meetingTestData.Meeting.AddNotAttendee(memberId); var meetingNotAttendeeAdded = AssertPublishedDomainEvent(meetingTestData.Meeting); meetingNotAttendeeAdded.MemberId.Should().Be(memberId); } [Test] public void AddNotAttendee_WhenMemberIsAttendeeAndChangedDecision_IsSuccessful() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var newMemberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newMemberId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newMemberId, 0); meetingTestData.Meeting.AddNotAttendee(newMemberId); var meetingAttendeeChangedDecision = AssertPublishedDomainEvent(meetingTestData.Meeting); meetingAttendeeChangedDecision.MemberId.Should().Be(newMemberId); } [Test] public void ChangeNotAttendeeDecision_WhenMeetingHasStared_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, MeetingTerm = MeetingTerm.CreateNewBetweenDates(DateTime.UtcNow.AddDays(-2), DateTime.UtcNow.AddDays(-1)) }); var newMemberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newMemberId); AssertBrokenRule(() => { meetingTestData.Meeting.ChangeNotAttendeeDecision(newMemberId); }); } [Test] public void ChangeNotAttendeeDecision_WhenMemberIsNotActiveNotAttendee_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var newMemberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newMemberId); AssertBrokenRule(() => { meetingTestData.Meeting.ChangeNotAttendeeDecision(newMemberId); }); } [Test] public void ChangeNotAttendeeDecision_WhenMemberIsNotAttendee_IsSuccessful() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var newMemberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newMemberId); meetingTestData.Meeting.AddNotAttendee(newMemberId); meetingTestData.Meeting.ChangeNotAttendeeDecision(newMemberId); var meetingNotAttendeeChangedDecision = AssertPublishedDomainEvent(meetingTestData.Meeting); meetingNotAttendeeChangedDecision.MemberId.Should().Be(newMemberId); } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/Meetings/MeetingCommentTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingComments.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingMemberCommentLikes.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using FluentAssertions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.Meetings { [TestFixture] public class MeetingCommentTests : MeetingTestsBase { [Test] public void AddComment_WhenDataIsValid_IsSuccessful() { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { commentAuthorId } }); var comment = "Great meeting!"; // Act var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, comment, meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); // Assert var meetingCommentCreatedEvent = AssertPublishedDomainEvent(meetingComment); meetingCommentCreatedEvent.MeetingCommentId.Should().Be(meetingComment.Id); meetingCommentCreatedEvent.MeetingId.Should().Be(meetingTestData.Meeting.Id); meetingCommentCreatedEvent.Comment.Should().Be(comment); } [Test] public void AddComment_WhenAuthorIsNotMeetingGroupMember_BreaksCommentCanBeAddedOnlyByMeetingGroupMemberRule() { // Arrange var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions()); // Assert AssertBrokenRule(() => { // Act meetingTestData.Meeting.AddComment(new MemberId(Guid.NewGuid()), "Bad meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); }); } [Test] [TestCase(null)] [TestCase("")] public void AddComment_WhenTextIsEmpty_BreaksCommentTextMustBeProvidedRule(string missingComment) { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { commentAuthorId } }); // Assert AssertBrokenRule(() => { // Act meetingTestData.Meeting.AddComment(commentAuthorId, missingComment, meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); }); } [Test] public void AddComment_WhenMeetingCommentingDisabled_BreaksCommentCanBeCreatedOnlyIfCommentingForMeetingEnabledRule() { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData( new MeetingTestDataOptions { Attendees = new[] { commentAuthorId }, IsMeetingCommentingEnabled = false }); // Assert AssertBrokenRule(() => { // Act meetingTestData.Meeting.AddComment(commentAuthorId, "I appreciate your work!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); }); } [Test] public void EditComment_IsSuccessful() { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { commentAuthorId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); meetingComment.ClearDomainEvents(); var editedComment = "Wonderful!"; // Act meetingComment.Edit(commentAuthorId, editedComment, meetingTestData.MeetingCommentingConfiguration); // Assert var meetingCommentEditedEvent = AssertPublishedDomainEvent(meetingComment); meetingCommentEditedEvent.MeetingCommentId.Should().Be(meetingComment.Id); meetingCommentEditedEvent.EditedComment.Should().Be(editedComment); } [Test] public void EditComment_ByNoAuthor_BreaksMeetingCommentCanBeEditedOnlyByAuthor() { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { commentAuthorId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); meetingComment.ClearDomainEvents(); var editedComment = "Wonderful!"; // Assert AssertBrokenRule(() => { // Act meetingComment.Edit(new MemberId(Guid.NewGuid()), editedComment, meetingTestData.MeetingCommentingConfiguration); }); } [Test] [TestCase(null)] [TestCase("")] public void EditComment_WhenNewTextIsEmpty_BreaksCommentTextMustBeProvidedRule(string missingComment) { // Arrange var authorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { authorId } }); var meetingComment = meetingTestData.Meeting.AddComment(authorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); meetingComment.ClearDomainEvents(); // Assert AssertBrokenRule(() => { // Act meetingComment.Edit(new MemberId(Guid.NewGuid()), missingComment, meetingTestData.MeetingCommentingConfiguration); }); } [Test] public void EditComment_WhenMeetingCommentingDisabled_BreaksCommentCanBeEditedOnlyIfCommentingForMeetingEnabledRule() { // Arrange var groupOrganizerId = new MemberId(Guid.NewGuid()); var commentAuthorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData( new MeetingTestDataOptions { CreatorId = groupOrganizerId, Attendees = new[] { commentAuthorId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "It was good.", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); meetingTestData.MeetingCommentingConfiguration.DisableCommenting(groupOrganizerId, meetingTestData.MeetingGroup); // Assert AssertBrokenRule(() => { // Act meetingComment.Edit(commentAuthorId, "It was bad.", meetingTestData.MeetingCommentingConfiguration); }); } [Test] public void RemoveComment_IsSuccessful() { // Arrange var removingMemberId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { removingMemberId } }); var meetingComment = meetingTestData.Meeting.AddComment(removingMemberId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); meetingComment.ClearDomainEvents(); // Act meetingComment.Remove(removingMemberId, meetingTestData.MeetingGroup); // Assert var meetingCommentRemovedEvent = AssertPublishedDomainEvent(meetingComment); meetingCommentRemovedEvent.MeetingCommentId.Should().Be(meetingComment.Id); } [Test] public void RemoveComment_ByNoAuthorNoOrganizer_BreaksMeetingCommentCanBeRemovedOnlyByAuthorOrGroupOrganizerRule() { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var groupCreatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = groupCreatorId, Attendees = new[] { commentAuthorId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); meetingComment.ClearDomainEvents(); // Assert AssertBrokenRule(() => { // Act meetingComment.Remove(removingMemberId: new MemberId(Guid.NewGuid()), meetingTestData.MeetingGroup); }); } [Test] public void RemoveComment_ByAuthor_BreaksRemovingReasonCanBeProvidedOnlyByGroupOrganizer() { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { commentAuthorId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); meetingComment.ClearDomainEvents(); // Assert AssertBrokenRule(() => { // Act meetingComment.Remove( commentAuthorId, meetingTestData.MeetingGroup, "I don't like the comment."); }); } [Test] public void AddReplyToComment_WhenDataIsValid_IsSuccessful() { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var replyAuthorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { commentAuthorId, replyAuthorId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); var reply = "Exactly!"; // Act var replyToComment = meetingComment.Reply(replyAuthorId, reply, meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); // Assert var replyToMeetingCommentAddedEvent = AssertPublishedDomainEvent(replyToComment); replyToMeetingCommentAddedEvent.MeetingCommentId.Should().Be(replyToComment.Id); replyToMeetingCommentAddedEvent.InReplyToCommentId.Should().Be(meetingComment.Id); replyToMeetingCommentAddedEvent.Reply.Should().Be(reply); } [Test] public void AddReplyToComment_WhenAuthorIsNotMeetingGroupMember_BreaksCommentCanBeAddedOnlyByMeetingGroupMemberRule() { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { commentAuthorId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); // Assert AssertBrokenRule(() => { // Act meetingComment.Reply(replierId: new MemberId(Guid.NewGuid()), "Exactly!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); }); } [Test] [TestCase(null)] [TestCase("")] public void AddReplyToComment_WhenTextIsEmpty_BreaksCommentTextMustBeProvidedRule(string missingReply) { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var replyAuthorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { commentAuthorId, replyAuthorId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); // Assert AssertBrokenRule(() => { // Act meetingComment.Reply(replyAuthorId, missingReply, meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); }); } [Test] public void AddReplyToComment_WhenMeetingCommentingDisabled_BreaksCommentCanBeCreatedOnlyIfCommentingForMeetingEnabledRule() { // Arrange var creatorId = new MemberId(Guid.NewGuid()); var commentAuthorId = new MemberId(Guid.NewGuid()); var replyAuthorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, Attendees = new[] { commentAuthorId, replyAuthorId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); meetingTestData.MeetingCommentingConfiguration.DisableCommenting(creatorId, meetingTestData.MeetingGroup); // Assert AssertBrokenRule(() => { // Act meetingComment.Reply(replyAuthorId, "Exactly!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); }); } [Test] public void AddLikeToComment_WhenDataIsValid_IsSuccessful() { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var likerId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { commentAuthorId, likerId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); // Act var like = meetingComment.Like( likerId, likerMeetingGroupMember: new MeetingGroupMemberData(meetingTestData.MeetingGroup.Id, likerId), meetingMemberCommentLikesCount: 0); // Assert var meetingCommentLikedEvent = AssertPublishedDomainEvent(like); meetingCommentLikedEvent.MeetingCommentId.Should().Be(meetingComment.Id); meetingCommentLikedEvent.LikerId.Should().Be(likerId); } [Test] public void AddLikeToComment_WhenLikerIsNotGroupMember_BreaksCommentCanBeLikedOnlyByMeetingGroupMemberRule() { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { commentAuthorId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); // Assert AssertBrokenRule(() => { // Act meetingComment.Like( likerId: new MemberId(Guid.NewGuid()), likerMeetingGroupMember: null, meetingMemberCommentLikesCount: 0); }); } [Test] public void AddLikeToComment_WhenTheCommentIsAlreadyLikedByTheMember_BreaksCommentCannotBeLikedByTheSameMemberMoreThanOnceRule() { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var likerId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { commentAuthorId, likerId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); // Assert AssertBrokenRule(() => { // Act meetingComment.Like( likerId, likerMeetingGroupMember: new MeetingGroupMemberData(meetingTestData.MeetingGroup.Id, likerId), meetingMemberCommentLikesCount: 1); }); } [Test] public void RemoveLike_WhenDataIsValid_IsSuccessful() { // Arrange var commentAuthorId = new MemberId(Guid.NewGuid()); var likerId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { Attendees = new[] { commentAuthorId, likerId } }); var meetingComment = meetingTestData.Meeting.AddComment(commentAuthorId, "Great meeting!", meetingTestData.MeetingGroup, meetingTestData.MeetingCommentingConfiguration); var commentLike = meetingComment.Like( likerId, likerMeetingGroupMember: new MeetingGroupMemberData(meetingTestData.MeetingGroup.Id, likerId), meetingMemberCommentLikesCount: 0); commentLike.ClearDomainEvents(); // Act commentLike.Remove(); // Assert var meetingCommentUnlikedEvent = AssertPublishedDomainEvent(commentLike); meetingCommentUnlikedEvent.MeetingCommentId.Should().Be(meetingComment.Id); meetingCommentUnlikedEvent.LikerId.Should().Be(likerId); } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/Meetings/MeetingCommentingConfigurationTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using FluentAssertions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.Meetings { [TestFixture] public class MeetingCommentingConfigurationTests : MeetingTestsBase { [Test] public void CreateMeetingCommentingConfiguration_IsSuccessful() { // Arrange var meeting = CreateMeetingTestData(new MeetingTestDataOptions()); // Act var meetingCommentingConfiguration = meeting.Meeting.CreateCommentingConfiguration(); // Assert var meetingCommentingConfigurationCreatedEvent = AssertPublishedDomainEvent(meetingCommentingConfiguration); meetingCommentingConfigurationCreatedEvent.MeetingId.Should().Be(meeting.Meeting.Id); meetingCommentingConfigurationCreatedEvent.IsEnabled.Should().BeTrue(); } [Test] public void DisableCommenting_IsSuccessfull() { // Arrange var organizerId = new MemberId(Guid.NewGuid()); var meeting = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = organizerId }); var meetingCommentingConfiguration = meeting.Meeting.CreateCommentingConfiguration(); meetingCommentingConfiguration.ClearDomainEvents(); // Act meetingCommentingConfiguration.DisableCommenting(organizerId, meeting.MeetingGroup); // Assert var meetingCommentingDisabledEvent = AssertPublishedDomainEvent(meetingCommentingConfiguration); meetingCommentingDisabledEvent.MeetingId.Should().Be(meeting.Meeting.Id); } [Test] public void DisableCommenting_WhenMemberIsNotGroupOrganizer_BreakMeetingCommentingCanBeDisabledOnlyByGroupOrganizerRule() { // Arrange var meeting = CreateMeetingTestData(new MeetingTestDataOptions()); var meetingCommentingConfiguration = meeting.Meeting.CreateCommentingConfiguration(); AssertBrokenRule(() => { meetingCommentingConfiguration.DisableCommenting(new MemberId(Guid.NewGuid()), meeting.MeetingGroup); }); } [Test] public void DisableCommenting_WhenCommentingAlreadyDisabled_IsIgnored() { // Arrange var organizerId = new MemberId(Guid.NewGuid()); var meeting = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = organizerId }); var meetingCommentingConfiguration = meeting.Meeting.CreateCommentingConfiguration(); meetingCommentingConfiguration.DisableCommenting(organizerId, meeting.MeetingGroup); meetingCommentingConfiguration.ClearDomainEvents(); // Act meetingCommentingConfiguration.DisableCommenting(organizerId, meeting.MeetingGroup); // Assert AssertDomainEventNotPublished(meetingCommentingConfiguration); } [Test] public void EnableCommenting_IsSuccessfull() { // Arrange var organizerId = new MemberId(Guid.NewGuid()); var meeting = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = organizerId }); var meetingCommentingConfiguration = meeting.Meeting.CreateCommentingConfiguration(); meetingCommentingConfiguration.DisableCommenting(organizerId, meeting.MeetingGroup); meetingCommentingConfiguration.ClearDomainEvents(); // Act meetingCommentingConfiguration.EnableCommenting(organizerId, meeting.MeetingGroup); // Assert var meetingCommentingEnabledEvent = AssertPublishedDomainEvent(meetingCommentingConfiguration); meetingCommentingEnabledEvent.MeetingId.Should().Be(meeting.Meeting.Id); } [Test] public void EnableCommenting_WhenMemberIsNotGroupOrganizer_BreakMeetingCommentingCanBeEnabledOnlyByGroupOrganizerRule() { // Arrange var organizerId = new MemberId(Guid.NewGuid()); var meeting = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = organizerId }); var meetingCommentingConfiguration = meeting.Meeting.CreateCommentingConfiguration(); meetingCommentingConfiguration.DisableCommenting(organizerId, meeting.MeetingGroup); meetingCommentingConfiguration.ClearDomainEvents(); AssertBrokenRule(() => { meetingCommentingConfiguration.EnableCommenting(new MemberId(Guid.NewGuid()), meeting.MeetingGroup); }); } [Test] public void EnableCommenting_WhenCommentingAlreadyEnabled_IsIgnored() { // Arrange var organizerId = new MemberId(Guid.NewGuid()); var meeting = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = organizerId }); var meetingCommentingConfiguration = meeting.Meeting.CreateCommentingConfiguration(); meetingCommentingConfiguration.ClearDomainEvents(); // Act meetingCommentingConfiguration.EnableCommenting(organizerId, meeting.MeetingGroup); // Assert AssertDomainEventNotPublished(meetingCommentingConfiguration); } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/Meetings/MeetingLimitsTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules; using FluentAssertions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.Meetings { [TestFixture] public class MeetingLimitsTests : MeetingTestsBase { [Test] public void CreateMeetingLimits_WhenAttendeesLimitIsGreaterThanGuestsLimit_IsSuccessful() { var meetingLimits = MeetingLimits.Create(15, 5); meetingLimits.AttendeesLimit.Should().Be(15); meetingLimits.GuestsLimit.Should().Be(5); } [Test] public void CreateMeetingLimits_WhenAttendeesLimitIsLessThanGuestsLimit_BreaksMeetingAttendeesLimitMustBeGreaterThanGuestsLimitRule() { AssertBrokenRule(() => { MeetingLimits.Create(5, 8); }); } [Test] public void CreateMeetingLimits_WhenAttendeesLimitIsNotDefined_GuestsLimitCanBeAny() { var meetingLimits = MeetingLimits.Create(null, 5); meetingLimits.AttendeesLimit.Should().BeNull(); meetingLimits.GuestsLimit.Should().Be(5); } [Test] public void CreateMeetingLimits_WhenAttendeesLimitIsNegative_BreaksMeetingAttendeesLimitCannotBeNegativeRule() { AssertBrokenRule(() => { MeetingLimits.Create(-2, 8); }); } [Test] public void CreateMeetingLimits_WhenGuestsLimitIsNegative_BreaksMeetingGuestsLimitCannotBeNegativeRule() { AssertBrokenRule(() => { MeetingLimits.Create(20, -9); }); } [Test] public void CreateMeetingLimits_WhenAttendeesLimitIsEqualToGuestsLimit_BreaksMeetingAttendeesLimitMustBeGreaterThanGuestsLimitRule() { AssertBrokenRule(() => { MeetingLimits.Create(5, 5); }); } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/Meetings/MeetingRolesTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.SeedWork; using FluentAssertions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.Meetings { [TestFixture] public class MeetingRolesTests : MeetingTestsBase { [Test] public void SetHostRole_WhenMeetingHasStarted_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, MeetingTerm = MeetingTerm.CreateNewBetweenDates(DateTime.UtcNow.AddDays(-2), DateTime.UtcNow.AddDays(-1)) }); AssertBrokenRule(() => { meetingTestData.Meeting.SetHostRole(meetingTestData.MeetingGroup, creatorId, creatorId); }); } [Test] public void SetHostRole_WhenSettingMemberIsNotAOrganizerOrHostMeeting_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var settingMemberId = new MemberId(Guid.NewGuid()); var newOrganizerId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newOrganizerId); meetingTestData.MeetingGroup.JoinToGroupMember(settingMemberId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newOrganizerId, 0); AssertBrokenRule(() => { meetingTestData.Meeting.SetHostRole(meetingTestData.MeetingGroup, settingMemberId, newOrganizerId); }); } [Test] public void SetHostRole_WhenSettingMemberIsGroupOrganizer_IsSuccessful() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var newOrganizerId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newOrganizerId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newOrganizerId, 0); meetingTestData.Meeting.SetHostRole(meetingTestData.MeetingGroup, creatorId, newOrganizerId); var newMeetingHostSet = AssertPublishedDomainEvent(meetingTestData.Meeting); newMeetingHostSet.HostId.Should().Be(newOrganizerId); } [Test] public void SetHostRole_WhenSettingMemberIsMeetingHost_IsSuccessful() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var newOrganizerId = new MemberId(Guid.NewGuid()); var settingMemberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newOrganizerId); meetingTestData.MeetingGroup.JoinToGroupMember(settingMemberId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newOrganizerId, 0); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, settingMemberId, 0); meetingTestData.Meeting.SetHostRole(meetingTestData.MeetingGroup, creatorId, settingMemberId); DomainEventsTestHelper.ClearAllDomainEvents(meetingTestData.Meeting); meetingTestData.Meeting.SetHostRole(meetingTestData.MeetingGroup, settingMemberId, newOrganizerId); var newMeetingHostSetEvent = AssertPublishedDomainEvent(meetingTestData.Meeting); newMeetingHostSetEvent.HostId.Should().Be(newOrganizerId); } [Test] public void SetAttendeeRole_WhenMeetingHasStarted_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, MeetingTerm = MeetingTerm.CreateNewBetweenDates(DateTime.UtcNow.AddDays(-2), DateTime.UtcNow.AddDays(-1)) }); AssertBrokenRule(() => { meetingTestData.Meeting.SetAttendeeRole(meetingTestData.MeetingGroup, creatorId, creatorId); }); } [Test] public void SetAttendeeRole_WhenSettingMemberIsNotAOrganizerOrHostMeeting_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var settingMemberId = new MemberId(Guid.NewGuid()); var newOrganizerId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newOrganizerId); meetingTestData.MeetingGroup.JoinToGroupMember(settingMemberId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newOrganizerId, 0); AssertBrokenRule(() => { meetingTestData.Meeting.SetAttendeeRole(meetingTestData.MeetingGroup, settingMemberId, newOrganizerId); }); } [Test] public void SetAttendeeRole_WhenMemberIsOrganizer_IsSuccessful() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var newOrganizerId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(newOrganizerId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, newOrganizerId, 0); meetingTestData.Meeting.SetHostRole(meetingTestData.MeetingGroup, creatorId, newOrganizerId); meetingTestData.Meeting.SetAttendeeRole(meetingTestData.MeetingGroup, creatorId, newOrganizerId); var newMeetingHostSet = AssertPublishedDomainEvent(meetingTestData.Meeting); newMeetingHostSet.HostId.Should().Be(newOrganizerId); } [Test] public void SetAttendeeRole_WhenMemberIsAlreadyAttendee_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var attendeeId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(attendeeId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, attendeeId, 0); AssertBrokenRule(() => { meetingTestData.Meeting.SetAttendeeRole(meetingTestData.MeetingGroup, creatorId, attendeeId); }); } [Test] public void SetAttendeeRole_ForLastOrganizer_BreaksMeetingMustHaveAtLeastOneHostRule() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); AssertBrokenRule(() => { meetingTestData.Meeting.SetAttendeeRole(meetingTestData.MeetingGroup, creatorId, creatorId); }); } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/Meetings/MeetingTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; using FluentAssertions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.Meetings { [TestFixture] public class MeetingTests : MeetingTestsBase { [Test] public void CancelMeeting_WhenMeetingHasStarted_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, MeetingTerm = MeetingTerm.CreateNewBetweenDates(DateTime.UtcNow.AddDays(-2), DateTime.UtcNow.AddDays(-1)) }); AssertBrokenRule(() => { meetingTestData.Meeting.Cancel(creatorId); }); } [Test] public void CancelMeeting_WhenMeetingHasNotStarted_IsSuccessful() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var date = DateTime.UtcNow; SystemClock.Set(date); meetingTestData.Meeting.Cancel(creatorId); var meetingCanceled = AssertPublishedDomainEvent(meetingTestData.Meeting); meetingCanceled.MeetingId.Should().Be(meetingTestData.Meeting.Id); meetingCanceled.CancelMemberId.Should().Be(creatorId); meetingCanceled.CancelDate.Should().Be(date); } [Test] public void RemoveAttendee_WhenMeetingHasStarted_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, MeetingTerm = MeetingTerm.CreateNewBetweenDates(DateTime.UtcNow.AddDays(-2), DateTime.UtcNow.AddDays(-1)) }); AssertBrokenRule(() => { meetingTestData.Meeting.RemoveAttendee(new MemberId(Guid.NewGuid()), creatorId, null); }); } [Test] public void RemoveAttendee_WhenMemberIsNotAttendee_BreaksOnlyActiveAttendeeCanBeRemovedFromMeetingRule() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var attendeeToRemoveId = new MemberId(Guid.NewGuid()); AssertBrokenRule(() => { meetingTestData.Meeting.RemoveAttendee(attendeeToRemoveId, creatorId, null); }); } [Test] public void RemoveAttendee_WhenMemberIsAttendee_AndReasonIsProvided_IsSuccessful() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var attendeeToRemoveId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(attendeeToRemoveId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, attendeeToRemoveId, 0); const string reason = "reasonOfRemoval"; meetingTestData.Meeting.RemoveAttendee(attendeeToRemoveId, creatorId, reason); var meetingAttendeeRemoved = AssertPublishedDomainEvent(meetingTestData.Meeting); meetingAttendeeRemoved.MemberId.Should().Be(attendeeToRemoveId); meetingAttendeeRemoved.MeetingId.Should().Be(meetingTestData.Meeting.Id); meetingAttendeeRemoved.Reason.Should().Be(reason); } [Test] public void RemoveAttendee_WhenMemberIsAttendee_AndReasonIsNotProvided_BreaksReasonOfRemovingAttendeeFromMeetingMustBeProvidedRule() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var attendeeToRemoveId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(attendeeToRemoveId); meetingTestData.Meeting.AddAttendee(meetingTestData.MeetingGroup, attendeeToRemoveId, 0); AssertBrokenRule(() => { meetingTestData.Meeting.RemoveAttendee(attendeeToRemoveId, creatorId, null); }); } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/Meetings/MeetingTestsBase.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingCommentingConfigurations; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Domain.MeetingGroups; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.SeedWork; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.Meetings { public class MeetingTestsBase : TestBase { protected class MeetingTestDataOptions { internal MemberId CreatorId { get; set; } internal MeetingTerm MeetingTerm { get; set; } internal Term RvspTerm { get; set; } internal int GuestsLimit { get; set; } internal int? AttendeesLimit { get; set; } internal bool IsMeetingCommentingEnabled { get; set; } = true; internal IEnumerable Attendees { get; set; } = Enumerable.Empty(); } protected class MeetingTestData { public MeetingTestData(MeetingGroup meetingGroup, Meeting meeting, MeetingCommentingConfiguration meetingCommentingConfiguration) { MeetingGroup = meetingGroup; Meeting = meeting; MeetingCommentingConfiguration = meetingCommentingConfiguration; } internal MeetingGroup MeetingGroup { get; } internal Meeting Meeting { get; } internal MeetingCommentingConfiguration MeetingCommentingConfiguration { get; } } protected MeetingTestData CreateMeetingTestData(MeetingTestDataOptions options) { var proposalMemberId = options.CreatorId ?? new MemberId(Guid.NewGuid()); var meetingProposal = MeetingGroupProposal.ProposeNew( "name", "description", MeetingGroupLocation.CreateNew("Warsaw", "PL"), proposalMemberId); meetingProposal.Accept(); var meetingGroup = meetingProposal.CreateMeetingGroup(); meetingGroup.SetExpirationDate(DateTime.Now.AddDays(1)); var meetingTerm = options.MeetingTerm ?? MeetingTerm.CreateNewBetweenDates(DateTime.UtcNow.AddDays(1), DateTime.UtcNow.AddDays(2)); var rsvpTerm = options.RvspTerm ?? Term.NoTerm; var meeting = meetingGroup.CreateMeeting( "title", meetingTerm, "description", MeetingLocation.CreateNew("Name", "Address", "PostalCode", "City"), options.AttendeesLimit, options.GuestsLimit, rsvpTerm, MoneyValue.Undefined, new List(), proposalMemberId); foreach (var attendee in options.Attendees) { meetingGroup.JoinToGroupMember(attendee); meeting.AddAttendee(meetingGroup, attendee, 0); } var meetingCommentingConfiguration = meeting.CreateCommentingConfiguration(); if (options.IsMeetingCommentingEnabled) { meetingCommentingConfiguration.EnableCommenting(proposalMemberId, meetingGroup); } else { meetingCommentingConfiguration.DisableCommenting(proposalMemberId, meetingGroup); } DomainEventsTestHelper.ClearAllDomainEvents(meetingGroup); return new MeetingTestData(meetingGroup, meeting, meetingCommentingConfiguration); } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/Meetings/MeetingWaitlistTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings.Rules; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using FluentAssertions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.Meetings { [TestFixture] public class MeetingWaitlistTests : MeetingTestsBase { [Test] public void SignUpMemberToWaitList_WhenMeetingHasStared_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, MeetingTerm = MeetingTerm.CreateNewBetweenDates(DateTime.UtcNow.AddDays(-2), DateTime.UtcNow.AddDays(-1)) }); var memberId = new MemberId(Guid.NewGuid()); AssertBrokenRule(() => { meetingTestData.Meeting.SignUpMemberToWaitlist(meetingTestData.MeetingGroup, memberId); }); } [Test] public void SignUpMemberToWaitList_WhenRsvpTermEnded_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, RvspTerm = Term.CreateNewBetweenDates(DateTime.UtcNow.AddDays(-2), DateTime.UtcNow.AddDays(-1)) }); var memberId = new MemberId(Guid.NewGuid()); AssertBrokenRule(() => { meetingTestData.Meeting.SignUpMemberToWaitlist(meetingTestData.MeetingGroup, memberId); }); } [Test] public void SignUpMemberToWaitList_WhenMemberIsNotAMemberOfMeetingGroup_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); AssertBrokenRule(() => { meetingTestData.Meeting.SignUpMemberToWaitlist(meetingTestData.MeetingGroup, new MemberId(Guid.NewGuid())); }); } [Test] public void SignUpMemberToWaitList_WhenMemberIsOnTheListAlready_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var memberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(memberId); meetingTestData.Meeting.SignUpMemberToWaitlist(meetingTestData.MeetingGroup, memberId); AssertBrokenRule(() => { meetingTestData.Meeting.SignUpMemberToWaitlist(meetingTestData.MeetingGroup, memberId); }); } [Test] public void SignUpMemberToWaitList_WhenAllConditionsAreSatisfied_IsSuccessful() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var memberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(memberId); meetingTestData.Meeting.SignUpMemberToWaitlist(meetingTestData.MeetingGroup, memberId); var meetingWaitlistMemberAdded = AssertPublishedDomainEvent(meetingTestData.Meeting); meetingWaitlistMemberAdded.MemberId.Should().Be(memberId); } [Test] public void SignOffMemberFromWaitList_WhenMeetingHasStared_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId, MeetingTerm = MeetingTerm.CreateNewBetweenDates(DateTime.UtcNow.AddDays(-2), DateTime.UtcNow.AddDays(-1)) }); var memberId = new MemberId(Guid.NewGuid()); AssertBrokenRule(() => { meetingTestData.Meeting.SignOffMemberFromWaitlist(memberId); }); } [Test] public void SignOffMemberFromWaitList_WhenMemberIsNotActiveWaitlistMember_IsNotPossible() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var memberId = new MemberId(Guid.NewGuid()); AssertBrokenRule(() => { meetingTestData.Meeting.SignOffMemberFromWaitlist(memberId); }); } [Test] public void SignOffMemberFromWaitList_WhenMemberIsOnWaitList_IsSuccessful() { var creatorId = new MemberId(Guid.NewGuid()); var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions { CreatorId = creatorId }); var memberId = new MemberId(Guid.NewGuid()); meetingTestData.MeetingGroup.JoinToGroupMember(memberId); meetingTestData.Meeting.SignUpMemberToWaitlist(meetingTestData.MeetingGroup, memberId); meetingTestData.Meeting.SignOffMemberFromWaitlist(memberId); var memberSignedOffFromMeetingWaitlist = AssertPublishedDomainEvent(meetingTestData.Meeting); memberSignedOffFromMeetingWaitlist.MemberId.Should().Be(memberId); } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/Members/MemberTests.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Domain.Members; using CompanyName.MyMeetings.Modules.Meetings.Domain.Members.Events; using CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.SeedWork; using FluentAssertions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.Domain.UnitTests.Members { [TestFixture] public class MemberTests : TestBase { [Test] public void CreateMember_IsSuccessful() { var memberId = new MemberId(Guid.NewGuid()); var member = Member.Create( memberId.Value, "memberLogin", "memberEmail@mail.com", "John", "Doe", "John Doe"); var memberCreated = AssertPublishedDomainEvent(member); memberCreated.MemberId.Should().Be(memberId); } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/SeedWork/DomainEventsTestHelper.cs ================================================ using System.Collections; using System.Reflection; using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Meetings.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; } public static void ClearAllDomainEvents(Entity aggregate) { aggregate.ClearDomainEvents(); 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 = field.FieldType.IsAssignableFrom(typeof(Entity)); if (isEntity) { var entity = field.GetValue(aggregate) as Entity; ClearAllDomainEvents(entity); } 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) { ClearAllDomainEvents(entityItem); } } } } } } } } ================================================ FILE: src/Modules/Meetings/Tests/UnitTests/SeedWork/TestBase.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; using FluentAssertions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Meetings.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 AssertDomainEventNotPublished(Entity aggregate) where T : IDomainEvent { var domainEvent = DomainEventsTestHelper.GetAllDomainEvents(aggregate).OfType().SingleOrDefault(); Assert.That(domainEvent, Is.Null); } public static List AssertPublishedDomainEvents(Entity aggregate) where T : IDomainEvent { var domainEvents = DomainEventsTestHelper.GetAllDomainEvents(aggregate).OfType().ToList(); if (!domainEvents.Any()) { throw new Exception($"{typeof(T).Name} event not published"); } return domainEvents; } 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) { businessRuleValidationException.BrokenRule.Should().BeOfType(); } } public static void AssertBrokenRule(AsyncTestDelegate testDelegate) where TRule : class, IBusinessRule { var message = $"Expected {typeof(TRule).Name} broken rule"; var businessRuleValidationException = Assert.CatchAsync(testDelegate, message); if (businessRuleValidationException != null) { Assert.That(businessRuleValidationException.BrokenRule, Is.TypeOf(), message); } } [TearDown] public void AfterEachTest() { SystemClock.Reset(); } } } ================================================ FILE: src/Modules/Payments/Application/CompanyName.MyMeetings.Modules.Payments.Application.csproj ================================================  ================================================ FILE: src/Modules/Payments/Application/Configuration/Commands/ICommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands { public interface ICommandHandler : IRequestHandler where TCommand : ICommand { } public interface ICommandHandler : IRequestHandler where TCommand : ICommand { } } ================================================ FILE: src/Modules/Payments/Application/Configuration/Commands/ICommandsScheduler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands { public interface ICommandsScheduler { Task EnqueueAsync(ICommand command); Task EnqueueAsync(ICommand command); } } ================================================ FILE: src/Modules/Payments/Application/Configuration/Commands/InternalCommandBase.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.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; } } } ================================================ FILE: src/Modules/Payments/Application/Configuration/Projections/IProjector.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Projections { public interface IProjector { Task Project(IDomainEvent @event); } } ================================================ FILE: src/Modules/Payments/Application/Configuration/Projections/ProjectorBase.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Projections { internal abstract class ProjectorBase { protected static Task When(IDomainEvent @event) { return Task.CompletedTask; } } } ================================================ FILE: src/Modules/Payments/Application/Configuration/Queries/IQueryHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Queries { public interface IQueryHandler : IRequestHandler where TQuery : IQuery { } } ================================================ FILE: src/Modules/Payments/Application/Contracts/CommandBase.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.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; } } } ================================================ FILE: src/Modules/Payments/Application/Contracts/ICommand.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.Contracts { public interface ICommand : IRequest { Guid Id { get; } } public interface ICommand : IRequest { Guid Id { get; } } } ================================================ FILE: src/Modules/Payments/Application/Contracts/IPaymentsModule.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Application.Contracts { public interface IPaymentsModule { Task ExecuteCommandAsync(ICommand command); Task ExecuteCommandAsync(ICommand command); Task ExecuteQueryAsync(IQuery query); } } ================================================ FILE: src/Modules/Payments/Application/Contracts/IQuery.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.Contracts { public interface IQuery : IRequest { } } ================================================ FILE: src/Modules/Payments/Application/Contracts/IRecurringCommand.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Application.Contracts { public interface IRecurringCommand { } } ================================================ FILE: src/Modules/Payments/Application/Contracts/QueryBase.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Application.Contracts { public abstract class QueryBase : IQuery { public Guid Id { get; } protected QueryBase() { Id = Guid.NewGuid(); } protected QueryBase(Guid id) { Id = id; } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/CreateMeetingFee/CreateMeetingFeeCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.CreateMeetingFee { public class CreateMeetingFeeCommand : InternalCommandBase { [JsonConstructor] public CreateMeetingFeeCommand( Guid id, Guid payerId, Guid meetingId, decimal value, string currency) : base(id) { PayerId = payerId; MeetingId = meetingId; Value = value; Currency = currency; } internal decimal Value { get; } internal string Currency { get; } internal Guid PayerId { get; } internal Guid MeetingId { get; } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/CreateMeetingFee/CreateMeetingFeeCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.CreateMeetingFee { internal class CreateMeetingFeeCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; internal CreateMeetingFeeCommandHandler( IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public Task Handle(CreateMeetingFeeCommand command, CancellationToken cancellationToken) { var meetingFee = MeetingFee.Create( new PayerId(command.PayerId), new MeetingId(command.MeetingId), MoneyValue.Of(command.Value, command.Currency)); _aggregateStore.AppendChanges(meetingFee); return Task.FromResult(meetingFee.Id); } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/CreateMeetingFeePayment/CreateMeetingFeePaymentCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.CreateMeetingFeePayment { public class CreateMeetingFeePaymentCommand : CommandBase { public CreateMeetingFeePaymentCommand(Guid meetingFeeId) { MeetingFeeId = meetingFeeId; } public Guid MeetingFeeId { get; } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/CreateMeetingFeePayment/CreateMeetingFeePaymentCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.CreateMeetingFeePayment { internal class CreateMeetingFeePaymentCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; internal CreateMeetingFeePaymentCommandHandler(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public Task Handle(CreateMeetingFeePaymentCommand command, CancellationToken cancellationToken) { var meetingFeePayment = MeetingFeePayment.Create(new MeetingFeeId(command.MeetingFeeId)); _aggregateStore.AppendChanges(meetingFeePayment); return Task.FromResult(meetingFeePayment.Id); } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/GetMeetingFees/GetMeetingFeesQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.GetMeetingFees { public class GetMeetingFeesQuery : QueryBase> { public GetMeetingFeesQuery(Guid meetingId) { MeetingId = meetingId; } public Guid MeetingId { get; } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/GetMeetingFees/GetMeetingFeesQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.GetMeetingFees { internal class GetMeetingFeesQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetMeetingFeesQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle(GetMeetingFeesQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [MeetingFee].MeetingFeeId AS [{nameof(MeetingFeeDto.MeetingFeeId)}], [MeetingFee].PayerId AS [{nameof(MeetingFeeDto.PayerId)}], [MeetingFee].FeeCurrency AS [{nameof(MeetingFeeDto.FeeCurrency)}], [MeetingFee].FeeValue AS [{nameof(MeetingFeeDto.FeeValue)}], [MeetingFee].MeetingId AS [{nameof(MeetingFeeDto.MeetingId)}], [MeetingFee].Status AS [{nameof(MeetingFeeDto.Status)}] FROM [payments].[MeetingFees] AS [MeetingFee] WHERE [MeetingFee].MeetingId = @MeetingId """; var meetingFees = await connection.QueryAsync( sql, new { query.MeetingId }); return meetingFees.AsList(); } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/GetMeetingFees/MeetingFeeDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.GetMeetingFees { public class MeetingFeeDto { public Guid MeetingFeeId { get; } public Guid PayerId { get; } public Guid MeetingId { get; } public decimal FeeValue { get; } public string FeeCurrency { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/GetMeetingFees/MeetingFeesProjector.cs ================================================ using System.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Projections; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees.Events; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.GetMeetingFees { internal class MeetingFeesProjector : ProjectorBase, IProjector { private readonly IDbConnection _connection; public MeetingFeesProjector(ISqlConnectionFactory sqlConnectionFactory) { _connection = sqlConnectionFactory.GetOpenConnection(); } public async Task Project(IDomainEvent @event) { await When((dynamic)@event); } private async Task When(MeetingFeeCreatedDomainEvent meetingFeeCreated) { await _connection.ExecuteScalarAsync( "INSERT INTO payments.MeetingFees " + "([MeetingFeeId], [PayerId], [MeetingId], [FeeValue], [FeeCurrency], [Status]) " + "VALUES (@MeetingFeeId, @PayerId, @MeetingId, @FeeValue, @FeeCurrency, @Status)", new { meetingFeeCreated.MeetingFeeId, meetingFeeCreated.PayerId, meetingFeeCreated.MeetingId, meetingFeeCreated.FeeValue, meetingFeeCreated.FeeCurrency, meetingFeeCreated.Status }); } private async Task When(MeetingFeePaidDomainEvent meetingFeePaid) { await UpdateStatus(meetingFeePaid.MeetingFeeId, meetingFeePaid.Status); } private async Task When(MeetingFeeExpiredDomainEvent meetingFeeExpired) { await UpdateStatus(meetingFeeExpired.MeetingFeeId, meetingFeeExpired.Status); } private async Task When(MeetingFeeCanceledDomainEvent meetingFeeCanceled) { await UpdateStatus(meetingFeeCanceled.MeetingFeeId, meetingFeeCanceled.Status); } private async Task UpdateStatus(Guid meetingFeeId, string status) { await _connection.ExecuteScalarAsync( "UPDATE payments.MeetingFees " + "SET " + "[Status] = @Status " + "WHERE [MeetingFeeId] = @MeetingFeeId", new { meetingFeeId, status }); } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/MarkMeetingFeeAsPaid/MarkMeetingFeeAsPaidCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeeAsPaid { public class MarkMeetingFeeAsPaidCommand : InternalCommandBase { [JsonConstructor] public MarkMeetingFeeAsPaidCommand(Guid meetingFeeId) : base(Guid.Empty) { MeetingFeeId = meetingFeeId; } public Guid MeetingFeeId { get; } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/MarkMeetingFeeAsPaid/MarkMeetingFeeAsPaidCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeeAsPaid { internal class MarkMeetingFeeAsPaidCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; internal MarkMeetingFeeAsPaidCommandHandler(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public async Task Handle(MarkMeetingFeeAsPaidCommand command, CancellationToken cancellationToken) { var meetingFee = await _aggregateStore.Load(new MeetingFeeId(command.MeetingFeeId)); meetingFee.MarkAsPaid(); _aggregateStore.AppendChanges(meetingFee); } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/MarkMeetingFeeAsPaid/MeetingFeePaidNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees.Events; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeeAsPaid { public class MeetingFeePaidNotification : DomainNotificationBase { public MeetingFeePaidNotification(MeetingFeePaidDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/MarkMeetingFeeAsPaid/MeetingFeePaidNotificationHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeeAsPaid { public class MeetingFeePaidNotificationHandler : INotificationHandler { private readonly IEventsBus _eventsBus; private readonly IAggregateStore _aggregateStore; public MeetingFeePaidNotificationHandler(IEventsBus eventsBus, IAggregateStore aggregateStore) { _eventsBus = eventsBus; _aggregateStore = aggregateStore; } public async Task Handle(MeetingFeePaidNotification notification, CancellationToken cancellationToken) { var meetingFee = await _aggregateStore.Load(new MeetingFeeId(notification.DomainEvent.MeetingFeeId)); var meetingFeeSnapshot = meetingFee.GetSnapshot(); await _eventsBus.Publish(new MeetingFeePaidIntegrationEvent( notification.Id, notification.DomainEvent.OccurredOn, meetingFeeSnapshot.PayerId, meetingFeeSnapshot.MeetingId)); } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/MarkMeetingFeePaymentAsPaid/MarkMeetingFeePaymentAsPaidCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeePaymentAsPaid { public class MarkMeetingFeePaymentAsPaidCommand : CommandBase { public MarkMeetingFeePaymentAsPaidCommand(Guid meetingFeePaymentId) { MeetingFeePaymentId = meetingFeePaymentId; } public Guid MeetingFeePaymentId { get; } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/MarkMeetingFeePaymentAsPaid/MarkMeetingFeePaymentAsPaidCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeePaymentAsPaid { internal class MarkMeetingFeePaymentAsPaidCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; internal MarkMeetingFeePaymentAsPaidCommandHandler(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public async Task Handle(MarkMeetingFeePaymentAsPaidCommand command, CancellationToken cancellationToken) { var meetingFeePayment = await _aggregateStore.Load(new MeetingFeePaymentId(command.MeetingFeePaymentId)); meetingFeePayment.MarkAsPaid(); _aggregateStore.AppendChanges(meetingFeePayment); } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/MarkMeetingFeePaymentAsPaid/MeetingFeePaymentPaidNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments.Events; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeePaymentAsPaid { public class MeetingFeePaymentPaidNotification : DomainNotificationBase { public MeetingFeePaymentPaidNotification(MeetingFeePaymentPaidDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/MarkMeetingFeePaymentAsPaid/MeetingFeePaymentPaidNotificationHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeeAsPaid; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeePaymentAsPaid { public class MeetingFeePaymentPaidNotificationHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; private readonly IAggregateStore _aggregateStore; public MeetingFeePaymentPaidNotificationHandler( ICommandsScheduler commandsScheduler, IAggregateStore aggregateStore) { _commandsScheduler = commandsScheduler; _aggregateStore = aggregateStore; } public async Task Handle(MeetingFeePaymentPaidNotification notification, CancellationToken cancellationToken) { var meetingFeePayment = await _aggregateStore.Load(new MeetingFeePaymentId(notification.DomainEvent.MeetingFeePaymentId)); var meetingFeePaymentSnapshot = meetingFeePayment.GetSnapshot(); await _commandsScheduler.EnqueueAsync( new MarkMeetingFeeAsPaidCommand( meetingFeePaymentSnapshot.MeetingFeeId)); } } } ================================================ FILE: src/Modules/Payments/Application/MeetingFees/MeetingAttendeeAddedIntegrationEventHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.CreateMeetingFee; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees { internal class MeetingAttendeeAddedIntegrationEventHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; internal MeetingAttendeeAddedIntegrationEventHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(MeetingAttendeeAddedIntegrationEvent notification, CancellationToken cancellationToken) { if (notification.FeeValue.HasValue) { await _commandsScheduler.EnqueueAsync(new CreateMeetingFeeCommand( Guid.NewGuid(), notification.AttendeeId, notification.MeetingId, notification.FeeValue.Value, notification.FeeCurrency)); } } } } ================================================ FILE: src/Modules/Payments/Application/Payers/CreatePayer/CreatePayerCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.Application.Payers.CreatePayer { public class CreatePayerCommand : InternalCommandBase { internal Guid UserId { get; } internal string Login { get; } internal string Email { get; } internal string FirstName { get; } internal string LastName { get; } internal string Name { get; } [JsonConstructor] public CreatePayerCommand( Guid id, Guid userId, string login, string email, string firstName, string lastName, string name) : base(id) { UserId = userId; Login = login; Email = email; FirstName = firstName; LastName = lastName; Name = name; } } } ================================================ FILE: src/Modules/Payments/Application/Payers/CreatePayer/CreatePayerCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Application.Payers.CreatePayer { internal class CreatePayerCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; public CreatePayerCommandHandler(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public Task Handle(CreatePayerCommand request, CancellationToken cancellationToken) { var payer = Payer.Create( request.UserId, request.Login, request.Email, request.FirstName, request.LastName, request.Name); _aggregateStore.AppendChanges(payer); return Task.FromResult(payer.Id); } } } ================================================ FILE: src/Modules/Payments/Application/Payers/CreatePayer/NewUserRegisteredIntegrationEventHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.Payers.CreatePayer { internal class NewUserRegisteredIntegrationEventHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; internal NewUserRegisteredIntegrationEventHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(NewUserRegisteredIntegrationEvent notification, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync(new CreatePayerCommand( Guid.NewGuid(), notification.UserId, notification.Login, notification.Email, notification.FirstName, notification.LastName, notification.Name)); } } } ================================================ FILE: src/Modules/Payments/Application/Payers/GetPayer/GetPayerQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.Payers.GetPayer { public class GetPayerQuery : QueryBase { public GetPayerQuery(Guid payerId) { PayerId = payerId; } public Guid PayerId { get; } } } ================================================ FILE: src/Modules/Payments/Application/Payers/GetPayer/GetPayerQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.Payers.GetPayer { internal class GetPayerQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetPayerQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(GetPayerQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [Payer].[Id] as [{nameof(PayerDto.Id)}], [Payer].[Login] as [{nameof(PayerDto.Login)}], [Payer].[Email] as [{nameof(PayerDto.Email)}], [Payer].[FirstName] as [{nameof(PayerDto.FirstName)}], [Payer].[LastName] as [{nameof(PayerDto.LastName)}], [Payer].[Name] as [{nameof(PayerDto.Name)}] FROM [payments].[Payers] AS [Payer] WHERE [Payer].[Id] = @PayerId """; return await connection.QuerySingleAsync(sql, new { query.PayerId }); } } } ================================================ FILE: src/Modules/Payments/Application/Payers/GetPayer/PayerDetailsProjector.cs ================================================ using System.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Projections; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers.Events; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.Payers.GetPayer { internal class PayerDetailsProjector : ProjectorBase, IProjector { private readonly IDbConnection _connection; public PayerDetailsProjector(ISqlConnectionFactory sqlConnectionFactory) { _connection = sqlConnectionFactory.GetOpenConnection(); } public async Task Project(IDomainEvent @event) { await When((dynamic)@event); } private async Task When(PayerCreatedDomainEvent payerCreated) { await _connection.ExecuteScalarAsync( "INSERT INTO payments.Payers " + "([Id], [Login], [Email], [FirstName], [LastName], " + "[Name]) " + "VALUES (@PayerId, @Login, @Email, @FirstName, @LastName," + "@Name)", new { payerCreated.PayerId, payerCreated.FirstName, payerCreated.LastName, payerCreated.Email, payerCreated.Login, payerCreated.Name }); } } } ================================================ FILE: src/Modules/Payments/Application/Payers/GetPayer/PayerDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Application.Payers.GetPayer { public class PayerDto { public Guid Id { get; set; } public string Login { get; set; } public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Name { get; set; } } } ================================================ FILE: src/Modules/Payments/Application/Payers/GetPayerEmail/PayerEmailProvider.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.Payers.GetPayerEmail { public static class PayerEmailProvider { public static async Task GetPayerEmail(Guid payerId, ISqlConnectionFactory sqlConnectionFactory) { var connection = sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [Payer].[Email] FROM [payments].[Payers] AS [Payer] WHERE [Payer].[Id] = @PayerId """; return await connection.QuerySingleAsync(sql, new { payerId }); } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/ActivatePriceListItem/ActivatePriceListItemCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.ActivatePriceListItem { public class ActivatePriceListItemCommand : CommandBase { public ActivatePriceListItemCommand(Guid priceListItemId) { PriceListItemId = priceListItemId; } public Guid PriceListItemId { get; } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/ActivatePriceListItem/ActivatePriceListItemCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.ActivatePriceListItem { internal class ActivatePriceListItemCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; public ActivatePriceListItemCommandHandler(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public async Task Handle(ActivatePriceListItemCommand command, CancellationToken cancellationToken) { var priceListItem = await _aggregateStore.Load(new PriceListItemId(command.PriceListItemId)); if (priceListItem == null) { throw new InvalidCommandException(["Pricelist item for activation must exist."]); } priceListItem.Activate(); _aggregateStore.AppendChanges(priceListItem); } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/ChangePriceListItemAttributes/ChangePriceListItemAttributesCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.ChangePriceListItemAttributes { public class ChangePriceListItemAttributesCommand : CommandBase { public ChangePriceListItemAttributesCommand(Guid priceListItemId, string countryCode, string subscriptionPeriodCode, string categoryCode, decimal priceValue, string priceCurrency) { PriceListItemId = priceListItemId; CountryCode = countryCode; SubscriptionPeriodCode = subscriptionPeriodCode; CategoryCode = categoryCode; PriceValue = priceValue; PriceCurrency = priceCurrency; } public Guid PriceListItemId { get; } public string CountryCode { get; } public string SubscriptionPeriodCode { get; } public string CategoryCode { get; } public decimal PriceValue { get; } public string PriceCurrency { get; } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/ChangePriceListItemAttributes/ChangePriceListItemAttributesCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.ChangePriceListItemAttributes { internal class ChangePriceListItemAttributesCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; public ChangePriceListItemAttributesCommandHandler(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public async Task Handle(ChangePriceListItemAttributesCommand command, CancellationToken cancellationToken) { var priceListItem = await _aggregateStore.Load(new PriceListItemId(command.PriceListItemId)); if (priceListItem == null) { throw new InvalidCommandException(["Pricelist item for changing must exist."]); } priceListItem.ChangeAttributes( command.CountryCode, SubscriptionPeriod.Of(command.SubscriptionPeriodCode), PriceListItemCategory.Of(command.CategoryCode), MoneyValue.Of(command.PriceValue, command.PriceCurrency)); _aggregateStore.AppendChanges(priceListItem); } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/CreatePriceListItem/CreatePriceListItemCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.CreatePriceListItem { public class CreatePriceListItemCommand : CommandBase { public string CountryCode { get; } public string SubscriptionPeriodCode { get; } public string CategoryCode { get; } public decimal PriceValue { get; } public string PriceCurrency { get; } public CreatePriceListItemCommand(string subscriptionPeriodCode, string categoryCode, string countryCode, decimal priceValue, string priceCurrency) { SubscriptionPeriodCode = subscriptionPeriodCode; CategoryCode = categoryCode; CountryCode = countryCode; PriceValue = priceValue; PriceCurrency = priceCurrency; } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/CreatePriceListItem/CreatePriceListItemCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.CreatePriceListItem { internal class CreatePriceListItemCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; internal CreatePriceListItemCommandHandler(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public Task Handle(CreatePriceListItemCommand command, CancellationToken cancellationToken) { var priceListItem = PriceListItem.Create( command.CountryCode, SubscriptionPeriod.Of(command.SubscriptionPeriodCode), PriceListItemCategory.Of(command.CategoryCode), MoneyValue.Of(command.PriceValue, command.PriceCurrency)); _aggregateStore.AppendChanges(priceListItem); return Task.FromResult(priceListItem.Id); } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/DeactivatePriceListItem/DeactivatePriceListItemCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.DeactivatePriceListItem { public class DeactivatePriceListItemCommand : CommandBase { public DeactivatePriceListItemCommand(Guid priceListItemId) { PriceListItemId = priceListItemId; } public Guid PriceListItemId { get; } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/DeactivatePriceListItem/DeactivatePriceListItemCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.DeactivatePriceListItem { internal class DeactivatePriceListItemCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; public DeactivatePriceListItemCommandHandler(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public async Task Handle(DeactivatePriceListItemCommand command, CancellationToken cancellationToken) { var priceListItem = await _aggregateStore.Load(new PriceListItemId(command.PriceListItemId)); if (priceListItem == null) { throw new InvalidCommandException(["Pricelist item for deactivation must exist."]); } priceListItem.Deactivate(); _aggregateStore.AppendChanges(priceListItem); } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/GetPriceListItem/GetPriceListItemQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.GetPriceListItem { public class GetPriceListItemQuery : QueryBase { public GetPriceListItemQuery(string countryCode, string categoryCode, string periodTypeCode) { CountryCode = countryCode; CategoryCode = categoryCode; PeriodTypeCode = periodTypeCode; } public string CountryCode { get; } public string CategoryCode { get; } public string PeriodTypeCode { get; } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/GetPriceListItem/GetPriceListItemQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.GetPriceListItem { internal class GetPriceListItemQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetPriceListItemQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle( GetPriceListItemQuery query, CancellationToken cancellationToken) { using var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [PriceListItem].[MoneyCurrency] AS [{nameof(PriceListItemMoneyValueDto.Currency)}], [PriceListItem].[MoneyValue] AS [{nameof(PriceListItemMoneyValueDto.Value)}] FROM [payments].[PriceListItems] AS [PriceListItem] WHERE [PriceListItem].[IsActive] = 1 AND [PriceListItem].[SubscriptionPeriodCode] = @PeriodTypeCode AND [PriceListItem].[CategoryCode] = @CategoryCode AND [PriceListItem].[CountryCode] = @CountryCode """; return await connection.QuerySingleAsync( sql, new { query.CategoryCode, query.PeriodTypeCode, query.CountryCode }); } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/GetPriceListItem/PriceListItemMoneyValueDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.GetPriceListItem { public class PriceListItemMoneyValueDto { public decimal Value { get; set; } public string Currency { get; set; } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/GetPriceListItem/PriceListItemsProjector.cs ================================================ using System.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Projections; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.Events; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.GetPriceListItem { internal class PriceListItemsProjector : ProjectorBase, IProjector { private readonly IDbConnection _connection; public PriceListItemsProjector(ISqlConnectionFactory sqlConnectionFactory) { _connection = sqlConnectionFactory.GetOpenConnection(); } public async Task Project(IDomainEvent @event) { await When((dynamic)@event); } private async Task When(PriceListItemCreatedDomainEvent @event) { await _connection.ExecuteScalarAsync( "INSERT INTO payments.PriceListItems " + "([Id], [SubscriptionPeriodCode], [CategoryCode], [CountryCode], [MoneyValue], [MoneyCurrency], [IsActive])" + "VALUES (@PriceListItemId, @SubscriptionPeriodCode, @CategoryCode, @CountryCode, @Price, @Currency, @IsActive)", new { @event.PriceListItemId, @event.SubscriptionPeriodCode, @event.CategoryCode, @event.CountryCode, @event.Price, @event.Currency, @event.IsActive }); } private async Task When(PriceListItemActivatedDomainEvent @event) { await _connection.ExecuteScalarAsync( "UPDATE payments.PriceListItems " + "SET [IsActive] = 'true' " + "WHERE [Id] = @PriceListItemId", new { @event.PriceListItemId }); } private async Task When(PriceListItemDeactivatedDomainEvent @event) { await _connection.ExecuteScalarAsync( "UPDATE payments.PriceListItems " + "SET [IsActive] = 'false' " + "WHERE [Id] = @PriceListItemId", new { @event.PriceListItemId }); } private async Task When(PriceListItemAttributesChangedDomainEvent @event) { await _connection.ExecuteScalarAsync( "UPDATE payments.PriceListItems " + "SET " + "[SubscriptionPeriodCode] = @SubscriptionPeriodCode," + "[CountryCode] = @CountryCode," + "[CategoryCode] = @CategoryCode," + "[MoneyValue] = @Price," + "[MoneyCurrency] = @Currency " + "WHERE [Id] = @PriceListItemId", new { @event.PriceListItemId, @event.SubscriptionPeriodCode, @event.CountryCode, @event.CategoryCode, @event.Price, @event.Currency }); } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/GetPriceListItems/GetPriceListItemsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.GetPriceListItems { public class GetPriceListItemsQuery : QueryBase> { } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/GetPriceListItems/GetPriceListItemsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Queries; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.GetPriceListItems { internal class GetPriceListItemsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetPriceListItemsQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle(GetPriceListItemsQuery request, CancellationToken cancellationToken) { return await PriceListFactory.GetPriceListItems(_sqlConnectionFactory.GetOpenConnection()); } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/PriceListFactory.cs ================================================ using System.Data; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.PricingStrategies; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems { public static class PriceListFactory { public static async Task CreatePriceList(IDbConnection connection) { var priceListItemList = await GetPriceListItems(connection); var priceListItems = priceListItemList .Select(x => new PriceListItemData( x.CountryCode, SubscriptionPeriod.Of(x.SubscriptionPeriodCode), MoneyValue.Of(x.MoneyValue, x.MoneyCurrency), PriceListItemCategory.Of(x.CategoryCode))) .ToList(); // This is place for selecting pricing strategy based on provided data and the system state. IPricingStrategy pricingStrategy = new DirectValueFromPriceListPricingStrategy(priceListItems); return PriceList.Create( priceListItems, pricingStrategy); } public static async Task> GetPriceListItems(IDbConnection connection) { const string sql = $""" SELECT [PriceListItem].[CountryCode] AS [{nameof(PriceListItemDto.CountryCode)}], [PriceListItem].[SubscriptionPeriodCode] AS [{nameof(PriceListItemDto.SubscriptionPeriodCode)}], [PriceListItem].[MoneyValue] AS [{nameof(PriceListItemDto.MoneyValue)}], [PriceListItem].[MoneyCurrency] AS [{nameof(PriceListItemDto.MoneyCurrency)}], [PriceListItem].[CategoryCode] AS [{nameof(PriceListItemDto.CategoryCode)}] FROM [payments].[PriceListItems] AS [PriceListItem] WHERE [PriceListItem].[IsActive] = 1 """; var priceListItems = await connection.QueryAsync( sql); var priceListItemList = priceListItems.AsList(); return priceListItemList; } } } ================================================ FILE: src/Modules/Payments/Application/PriceListItems/PriceListItemDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems { public class PriceListItemDto { public string CountryCode { get; set; } public string SubscriptionPeriodCode { get; set; } public decimal MoneyValue { get; set; } public string MoneyCurrency { get; set; } public string CategoryCode { get; set; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/BuySubscription/BuySubscriptionCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.BuySubscription { public class BuySubscriptionCommand : CommandBase { public BuySubscriptionCommand( string subscriptionTypeCode, string countryCode, decimal value, string currency) { SubscriptionTypeCode = subscriptionTypeCode; CountryCode = countryCode; Value = value; Currency = currency; } public string SubscriptionTypeCode { get; } public string CountryCode { get; } public decimal Value { get; } public string Currency { get; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/BuySubscription/BuySubscriptionCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.BuySubscription { internal class BuySubscriptionCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; private readonly IPayerContext _payerContext; private readonly ISqlConnectionFactory _sqlConnectionFactory; internal BuySubscriptionCommandHandler( IAggregateStore aggregateStore, IPayerContext payerContext, ISqlConnectionFactory sqlConnectionFactory) { _aggregateStore = aggregateStore; _payerContext = payerContext; _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(BuySubscriptionCommand command, CancellationToken cancellationToken) { var priceList = await PriceListFactory.CreatePriceList(_sqlConnectionFactory.GetOpenConnection()); var subscription = SubscriptionPayment.Buy( _payerContext.PayerId, SubscriptionPeriod.Of(command.SubscriptionTypeCode), command.CountryCode, MoneyValue.Of(command.Value, command.Currency), priceList); _aggregateStore.AppendChanges(subscription); return subscription.Id; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/BuySubscriptionRenewal/BuySubscriptionRenewalCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.BuySubscriptionRenewal { public class BuySubscriptionRenewalCommand : CommandBase { public BuySubscriptionRenewalCommand( Guid subscriptionId, string subscriptionTypeCode, string countryCode, decimal value, string currency) { SubscriptionId = subscriptionId; SubscriptionTypeCode = subscriptionTypeCode; CountryCode = countryCode; Value = value; Currency = currency; } public Guid SubscriptionId { get; } public string SubscriptionTypeCode { get; } public string CountryCode { get; } public decimal Value { get; } public string Currency { get; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/BuySubscriptionRenewal/BuySubscriptionRenewalCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.BuySubscriptionRenewal { internal class BuySubscriptionRenewalCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; private readonly IPayerContext _payerContext; private readonly ISqlConnectionFactory _sqlConnectionFactory; internal BuySubscriptionRenewalCommandHandler( IAggregateStore aggregateStore, IPayerContext payerContext, ISqlConnectionFactory sqlConnectionFactory) { _aggregateStore = aggregateStore; _payerContext = payerContext; _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(BuySubscriptionRenewalCommand command, CancellationToken cancellationToken) { var priceList = await PriceListFactory.CreatePriceList(_sqlConnectionFactory.GetOpenConnection()); var subscriptionId = new SubscriptionId(command.SubscriptionId); var subscription = await _aggregateStore.Load(new SubscriptionId(command.SubscriptionId)); if (subscription == null) { throw new InvalidCommandException(["Subscription for renewal must exist."]); } var subscriptionRenewalPayment = SubscriptionRenewalPayment.Buy( _payerContext.PayerId, subscriptionId, SubscriptionPeriod.Of(command.SubscriptionTypeCode), command.CountryCode, MoneyValue.Of(command.Value, command.Currency), priceList); _aggregateStore.AppendChanges(subscriptionRenewalPayment); return subscriptionRenewalPayment.Id; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/CreateSubscription/CreateSubscriptionCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.CreateSubscription { public class CreateSubscriptionCommand : InternalCommandBase { public Guid SubscriptionPaymentId { get; } [JsonConstructor] public CreateSubscriptionCommand( Guid id, Guid subscriptionPaymentId) : base(id) { SubscriptionPaymentId = subscriptionPaymentId; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/CreateSubscription/CreateSubscriptionCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.CreateSubscription { internal class CreateSubscriptionCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; internal CreateSubscriptionCommandHandler( IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public async Task Handle(CreateSubscriptionCommand command, CancellationToken cancellationToken) { var subscriptionPayment = await _aggregateStore.Load(new SubscriptionPaymentId(command.SubscriptionPaymentId)); var subscription = Subscription.Create(subscriptionPayment.GetSnapshot()); _aggregateStore.AppendChanges(subscription); return subscription.Id; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/CreateSubscription/SubscriptionCreatedEnqueueEmailConfirmationHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Payers.GetPayerEmail; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.SendSubscriptionCreationConfirmationEmail; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.CreateSubscription { public class SubscriptionCreatedEnqueueEmailConfirmationHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; private readonly ISqlConnectionFactory _sqlConnectionFactory; public SubscriptionCreatedEnqueueEmailConfirmationHandler( ICommandsScheduler commandsScheduler, ISqlConnectionFactory sqlConnectionFactory) { _commandsScheduler = commandsScheduler; _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(SubscriptionCreatedNotification notification, CancellationToken cancellationToken) { var payerEmail = await PayerEmailProvider.GetPayerEmail( notification.DomainEvent.PayerId, _sqlConnectionFactory); await _commandsScheduler.EnqueueAsync(new SendSubscriptionCreationConfirmationEmailCommand( Guid.NewGuid(), new SubscriptionId(notification.DomainEvent.SubscriptionId), payerEmail)); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/CreateSubscription/SubscriptionCreatedNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions.Events; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.CreateSubscription { public class SubscriptionCreatedNotification : DomainNotificationBase { [JsonConstructor] protected SubscriptionCreatedNotification(SubscriptionCreatedDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/CreateSubscription/SubscriptionCreatedNotificationHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Payments.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.CreateSubscription { public class SubscriptionCreatedNotificationHandler : INotificationHandler { private readonly IEventsBus _eventsBus; public SubscriptionCreatedNotificationHandler(IEventsBus eventsBus) { _eventsBus = eventsBus; } public async Task Handle(SubscriptionCreatedNotification notification, CancellationToken cancellationToken) { await _eventsBus.Publish(new SubscriptionExpirationDateChangedIntegrationEvent( notification.Id, notification.DomainEvent.OccurredOn, notification.DomainEvent.PayerId, notification.DomainEvent.ExpirationDate)); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/ExpireSubscription/ExpireSubscriptionCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscription { public class ExpireSubscriptionCommand : InternalCommandBase { public Guid SubscriptionId { get; } [JsonConstructor] public ExpireSubscriptionCommand( Guid id, Guid subscriptionId) : base(id) { SubscriptionId = subscriptionId; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/ExpireSubscription/ExpireSubscriptionCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscription { internal class ExpireSubscriptionCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; public ExpireSubscriptionCommandHandler(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public async Task Handle(ExpireSubscriptionCommand command, CancellationToken cancellationToken) { var subscription = await _aggregateStore.Load(new SubscriptionId(command.SubscriptionId)); subscription.Expire(); _aggregateStore.AppendChanges(subscription); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/ExpireSubscriptionPayment/ExpireSubscriptionPaymentCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscriptionPayment { public class ExpireSubscriptionPaymentCommand : CommandBase { public ExpireSubscriptionPaymentCommand(Guid paymentId) { PaymentId = paymentId; } public Guid PaymentId { get; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/ExpireSubscriptionPayment/ExpireSubscriptionPaymentCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscriptionPayment { internal class ExpireSubscriptionPaymentCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; public ExpireSubscriptionPaymentCommandHandler(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public async Task Handle(ExpireSubscriptionPaymentCommand command, CancellationToken cancellationToken) { var subscriptionPayment = await _aggregateStore.Load(new SubscriptionPaymentId(command.PaymentId)); subscriptionPayment.Expire(); _aggregateStore.AppendChanges(subscriptionPayment); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/ExpireSubscriptionPayments/ExpireSubscriptionPaymentsCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscriptionPayments { public class ExpireSubscriptionPaymentsCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/ExpireSubscriptionPayments/ExpireSubscriptionPaymentsCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscriptionPayment; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscriptionPayments { internal class ExpireSubscriptionPaymentsCommandHandler : ICommandHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly ICommandsScheduler _commandsScheduler; public ExpireSubscriptionPaymentsCommandHandler( ISqlConnectionFactory sqlConnectionFactory, ICommandsScheduler commandsScheduler) { _sqlConnectionFactory = sqlConnectionFactory; _commandsScheduler = commandsScheduler; } public async Task Handle(ExpireSubscriptionPaymentsCommand request, CancellationToken cancellationToken) { const string sql = """ SELECT [SubscriptionPayment].PaymentId FROM [payments].[SubscriptionPayments] AS [SubscriptionPayment] WHERE [SubscriptionPayment].Date > @Date AND [SubscriptionPayment].[Status] = @Status """; var connection = _sqlConnectionFactory.GetOpenConnection(); var timeForPayment = TimeSpan.FromMinutes(20); var date = SystemClock.Now.Add(timeForPayment); var expiredSubscriptionPaymentsIds = await connection.QueryAsync(sql, new { Date = date, Status = SubscriptionPaymentStatus.WaitingForPayment.Code }); var expiredSubscriptionsPaymentsIdsList = expiredSubscriptionPaymentsIds.AsList(); foreach (var subscriptionPaymentId in expiredSubscriptionsPaymentsIdsList) { await _commandsScheduler.EnqueueAsync( new ExpireSubscriptionPaymentCommand(subscriptionPaymentId)); } } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/ExpireSubscriptions/ExpireSubscriptionsCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscriptions { public class ExpireSubscriptionsCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/ExpireSubscriptions/ExpireSubscriptionsCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscription; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscriptions { internal class ExpireSubscriptionsCommandHandler : ICommandHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly ICommandsScheduler _commandsScheduler; public ExpireSubscriptionsCommandHandler( ISqlConnectionFactory sqlConnectionFactory, ICommandsScheduler commandsScheduler) { _sqlConnectionFactory = sqlConnectionFactory; _commandsScheduler = commandsScheduler; } public async Task Handle(ExpireSubscriptionsCommand request, CancellationToken cancellationToken) { const string sql = """ SELECT [SubscriptionDetails].Id FROM [payments].[SubscriptionDetails] AS [SubscriptionDetails] WHERE [SubscriptionDetails].ExpirationDate < @Date """; var connection = _sqlConnectionFactory.GetOpenConnection(); var expiredSubscriptionsIds = await connection.QueryAsync(sql, new { Date = SystemClock.Now }); var expiredSubscriptionsIdsList = expiredSubscriptionsIds.AsList(); foreach (var subscriptionId in expiredSubscriptionsIdsList) { await _commandsScheduler.EnqueueAsync( new ExpireSubscriptionCommand( Guid.NewGuid(), subscriptionId)); } } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/GetPayerSubscription/GetAuthenticatedPayerSubscriptionQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionDetails; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetPayerSubscription { public class GetAuthenticatedPayerSubscriptionQuery : QueryBase { } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/GetPayerSubscription/GetAuthenticatedPayerSubscriptionQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Queries; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionDetails; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetPayerSubscription { internal class GetAuthenticatedPayerSubscriptionQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly IExecutionContextAccessor _executionContextAccessor; public GetAuthenticatedPayerSubscriptionQueryHandler( ISqlConnectionFactory sqlConnectionFactory, IExecutionContextAccessor executionContextAccessor) { _sqlConnectionFactory = sqlConnectionFactory; _executionContextAccessor = executionContextAccessor; } public async Task Handle(GetAuthenticatedPayerSubscriptionQuery query, CancellationToken cancellationToken) { const string sql = $""" SELECT [SubscriptionDetails].[Id] AS [{nameof(SubscriptionDetailsDto.SubscriptionId)}], [SubscriptionDetails].[Period] AS [{nameof(SubscriptionDetailsDto.Period)}], [SubscriptionDetails].[Status] AS [{nameof(SubscriptionDetailsDto.Status)}], [SubscriptionDetails].[ExpirationDate] AS [{nameof(SubscriptionDetailsDto.ExpirationDate)}] FROM [payments].[SubscriptionDetails] AS [SubscriptionDetails] WHERE [SubscriptionDetails].[PayerId] = @PayerId """; var connection = _sqlConnectionFactory.GetOpenConnection(); return await connection.QuerySingleOrDefaultAsync( sql, new { PayerId = _executionContextAccessor.UserId }); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/GetSubscriptionDetails/GetSubscriptionDetailsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionDetails { public class GetSubscriptionDetailsQuery : QueryBase { public GetSubscriptionDetailsQuery(Guid subscriptionId) { SubscriptionId = subscriptionId; } public Guid SubscriptionId { get; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/GetSubscriptionDetails/GetSubscriptionDetailsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionDetails { internal class GetSubscriptionDetailsQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetSubscriptionDetailsQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(GetSubscriptionDetailsQuery query, CancellationToken cancellationToken) { const string sql = $""" SELECT [SubscriptionDetails].[Id] AS [{nameof(SubscriptionDetailsDto.SubscriptionId)}], [SubscriptionDetails].[Period] AS [{nameof(SubscriptionDetailsDto.Period)}], [SubscriptionDetails].[Status] AS [{nameof(SubscriptionDetailsDto.Status)}], [SubscriptionDetails].[ExpirationDate] AS [{nameof(SubscriptionDetailsDto.ExpirationDate)}] FROM [payments].[SubscriptionDetails] AS [SubscriptionDetails] WHERE [SubscriptionDetails].[Id] = @SubscriptionId """; var connection = _sqlConnectionFactory.GetOpenConnection(); return await connection.QuerySingleAsync( sql, new { query.SubscriptionId }); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/GetSubscriptionDetails/SubscriptionDetailsDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionDetails { public class SubscriptionDetailsDto { public Guid SubscriptionId { get; set; } public string Period { get; set; } public DateTime ExpirationDate { get; set; } public string Status { get; set; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/GetSubscriptionDetails/SubscriptionDetailsProjector.cs ================================================ using System.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Projections; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions.Events; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionDetails { internal class SubscriptionDetailsProjector : ProjectorBase, IProjector { private readonly IDbConnection _connection; public SubscriptionDetailsProjector(ISqlConnectionFactory sqlConnectionFactory) { _connection = sqlConnectionFactory.GetOpenConnection(); } public async Task Project(IDomainEvent @event) { await When((dynamic)@event); } private async Task When(SubscriptionRenewedDomainEvent subscriptionRenewed) { var period = SubscriptionPeriod.GetName(subscriptionRenewed.SubscriptionPeriodCode); await _connection.ExecuteScalarAsync( "UPDATE payments.SubscriptionDetails " + "SET " + "[Status] = @Status, " + "[ExpirationDate] = @ExpirationDate, " + "[Period] = @Period " + "WHERE [Id] = @SubscriptionId", new { subscriptionRenewed.SubscriptionId, subscriptionRenewed.Status, subscriptionRenewed.ExpirationDate, period }); } private async Task When(SubscriptionExpiredDomainEvent subscriptionExpired) { await _connection.ExecuteScalarAsync( "UPDATE payments.SubscriptionDetails " + "SET " + "[Status] = @Status " + "WHERE [Id] = @SubscriptionId", new { subscriptionExpired.SubscriptionId, subscriptionExpired.Status }); } private async Task When(SubscriptionCreatedDomainEvent subscriptionCreated) { var period = SubscriptionPeriod.GetName(subscriptionCreated.SubscriptionPeriodCode); await _connection.ExecuteScalarAsync( "INSERT INTO payments.SubscriptionDetails " + "([Id], [PayerId], [Period], [Status], [CountryCode], [ExpirationDate]) " + "VALUES (@SubscriptionId, @PayerId, @Period, @Status, @CountryCode, @ExpirationDate)", new { subscriptionCreated.SubscriptionId, subscriptionCreated.PayerId, period, subscriptionCreated.Status, subscriptionCreated.CountryCode, subscriptionCreated.ExpirationDate }); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/GetSubscriptionPayments/GetSubscriptionPaymentsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionPayments { public class GetSubscriptionPaymentsQuery : QueryBase> { public GetSubscriptionPaymentsQuery(Guid payerId) { PayerId = payerId; } public Guid PayerId { get; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/GetSubscriptionPayments/GetSubscriptionPaymentsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionPayments { internal class GetSubscriptionPaymentsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetSubscriptionPaymentsQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle( GetSubscriptionPaymentsQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [SubscriptionPayment].[PaymentId] AS [{nameof(SubscriptionPaymentDto.PaymentId)}], [SubscriptionPayment].[PayerId] AS [{nameof(SubscriptionPaymentDto.PayerId)}], [SubscriptionPayment].[Status] AS [{nameof(SubscriptionPaymentDto.Status)}], [SubscriptionPayment].[MoneyCurrency] AS [{nameof(SubscriptionPaymentDto.MoneyCurrency)}], [SubscriptionPayment].[MoneyValue] AS [{nameof(SubscriptionPaymentDto.MoneyValue)}], [SubscriptionPayment].[Date] AS [{nameof(SubscriptionPaymentDto.Date)}], [SubscriptionPayment].[SubscriptionId] AS [{nameof(SubscriptionPaymentDto.SubscriptionId)}], [SubscriptionPayment].[Type] AS [{nameof(SubscriptionPaymentDto.Type)}], [SubscriptionPayment].[Period] AS [{nameof(SubscriptionPaymentDto.Period)}] FROM [payments].[SubscriptionPayments] AS [SubscriptionPayment] WHERE [SubscriptionPayment].PayerId = @PayerId """; var subscriptionPayments = await connection.QueryAsync( sql, new { query.PayerId }); return subscriptionPayments.AsList(); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/GetSubscriptionPayments/SubscriptionPaymentDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionPayments { public class SubscriptionPaymentDto { public Guid PaymentId { get; set; } public Guid PayerId { get; set; } public string Type { get; set; } public string Status { get; set; } public string Period { get; set; } public DateTime Date { get; set; } public Guid? SubscriptionId { get; set; } public decimal MoneyValue { get; set; } public string MoneyCurrency { get; set; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/GetSubscriptionPayments/SubscriptionPaymentsProjector.cs ================================================ using System.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Projections; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions.Events; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionPayments { internal class SubscriptionPaymentsProjector : ProjectorBase, IProjector { private readonly IDbConnection _connection; public SubscriptionPaymentsProjector(ISqlConnectionFactory sqlConnectionFactory) { _connection = sqlConnectionFactory.GetOpenConnection(); } public async Task Project(IDomainEvent @event) { await When((dynamic)@event); } private async Task When(SubscriptionPaymentCreatedDomainEvent subscriptionPaymentCreated) { string period = SubscriptionPeriod.GetName(subscriptionPaymentCreated.SubscriptionPeriodCode); await _connection.ExecuteScalarAsync( "INSERT INTO payments.SubscriptionPayments " + "([PaymentId], [PayerId], [Type], [Status], [Period], [Date], " + "[SubscriptionId], [MoneyValue], [MoneyCurrency]) " + "VALUES (@SubscriptionPaymentId, @PayerId, @Type, @Status, @Period, " + "@OccurredOn, NULL, @Value, @Currency)", new { subscriptionPaymentCreated.SubscriptionPaymentId, subscriptionPaymentCreated.PayerId, Type = "Initial Payment", subscriptionPaymentCreated.Status, period, subscriptionPaymentCreated.OccurredOn, subscriptionPaymentCreated.Value, subscriptionPaymentCreated.Currency }); } private async Task When(SubscriptionPaymentPaidDomainEvent subscriptionPaymentPaid) { await _connection.ExecuteScalarAsync( "UPDATE payments.SubscriptionPayments SET Status = @Status " + "WHERE PaymentId = @SubscriptionPaymentId ", new { subscriptionPaymentPaid.SubscriptionPaymentId, subscriptionPaymentPaid.Status }); } private async Task When(SubscriptionRenewalPaymentCreatedDomainEvent subscriptionRenewalPaymentCreated) { string period = SubscriptionPeriod.GetName(subscriptionRenewalPaymentCreated.SubscriptionPeriodCode); await _connection.ExecuteScalarAsync( "INSERT INTO payments.SubscriptionPayments " + "([PaymentId], [PayerId], [Type], [Status], [Period], [Date], " + "[SubscriptionId], [MoneyValue], [MoneyCurrency]) " + "VALUES (@SubscriptionRenewalPaymentId, @PayerId, @Type, @Status, @Period, " + "@OccurredOn, @SubscriptionId, @Value, @Currency)", new { subscriptionRenewalPaymentCreated.SubscriptionRenewalPaymentId, subscriptionRenewalPaymentCreated.PayerId, Type = "Renewal Payment", subscriptionRenewalPaymentCreated.Status, period, subscriptionRenewalPaymentCreated.OccurredOn, subscriptionRenewalPaymentCreated.SubscriptionId, subscriptionRenewalPaymentCreated.Value, subscriptionRenewalPaymentCreated.Currency }); } private async Task When(SubscriptionRenewalPaymentPaidDomainEvent subscriptionRenewalPaymentPaid) { await _connection.ExecuteScalarAsync( "UPDATE payments.SubscriptionPayments SET Status = @Status " + "WHERE PaymentId = @SubscriptionRenewalPaymentId", new { subscriptionRenewalPaymentPaid.SubscriptionRenewalPaymentId, subscriptionRenewalPaymentPaid.Status }); } private async Task When(SubscriptionCreatedDomainEvent subscriptionCreated) { await _connection.ExecuteScalarAsync( "UPDATE payments.SubscriptionPayments SET SubscriptionId = @SubscriptionId " + "WHERE PaymentId = @SubscriptionPaymentId ", new { subscriptionCreated.SubscriptionId, subscriptionCreated.SubscriptionPaymentId }); } private async Task When(SubscriptionPaymentExpiredDomainEvent subscriptionPaymentExpired) { await _connection.ExecuteScalarAsync( "UPDATE payments.SubscriptionPayments SET Status = @Status " + "WHERE PaymentId = @SubscriptionPaymentId ", new { subscriptionPaymentExpired.SubscriptionPaymentId, subscriptionPaymentExpired.Status }); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/MarkSubscriptionPaymentAsPaid/MarkSubscriptionPaymentAsPaidCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionPaymentAsPaid { public class MarkSubscriptionPaymentAsPaidCommand : CommandBase { public MarkSubscriptionPaymentAsPaidCommand(Guid subscriptionPaymentId) { SubscriptionPaymentId = subscriptionPaymentId; } public Guid SubscriptionPaymentId { get; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/MarkSubscriptionPaymentAsPaid/MarkSubscriptionPaymentAsPaidCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionPaymentAsPaid { internal class MarkSubscriptionPaymentAsPaidCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; internal MarkSubscriptionPaymentAsPaidCommandHandler(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public async Task Handle(MarkSubscriptionPaymentAsPaidCommand command, CancellationToken cancellationToken) { var subscriptionPayment = await _aggregateStore.Load(new SubscriptionPaymentId(command.SubscriptionPaymentId)); subscriptionPayment.MarkAsPaid(); _aggregateStore.AppendChanges(subscriptionPayment); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/MarkSubscriptionPaymentAsPaid/SubscriptionPaymentPaidNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Events; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionPaymentAsPaid { public class SubscriptionPaymentPaidNotification : DomainNotificationBase { public SubscriptionPaymentPaidNotification(SubscriptionPaymentPaidDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/MarkSubscriptionPaymentAsPaid/SubscriptionPaymentPaidNotificationHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.CreateSubscription; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionPaymentAsPaid { public class SubscriptionPaymentPaidNotificationHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; public SubscriptionPaymentPaidNotificationHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(SubscriptionPaymentPaidNotification notification, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync( new CreateSubscriptionCommand( Guid.NewGuid(), notification.DomainEvent.SubscriptionPaymentId)); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/MarkSubscriptionRenewalPaymentAsPaid/MarkSubscriptionRenewalPaymentAsPaidCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionRenewalPaymentAsPaid { public class MarkSubscriptionRenewalPaymentAsPaidCommand : CommandBase { public Guid SubscriptionRenewalPaymentId { get; } public MarkSubscriptionRenewalPaymentAsPaidCommand(Guid subscriptionRenewalPaymentId) { SubscriptionRenewalPaymentId = subscriptionRenewalPaymentId; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/MarkSubscriptionRenewalPaymentAsPaid/MarkSubscriptionRenewalPaymentAsPaidCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionRenewalPaymentAsPaid { internal class MarkSubscriptionRenewalPaymentAsPaidCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; internal MarkSubscriptionRenewalPaymentAsPaidCommandHandler(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public async Task Handle(MarkSubscriptionRenewalPaymentAsPaidCommand command, CancellationToken cancellationToken) { var subscriptionRenewalPayment = await _aggregateStore.Load( new SubscriptionRenewalPaymentId(command.SubscriptionRenewalPaymentId)); subscriptionRenewalPayment.MarkRenewalAsPaid(); _aggregateStore.AppendChanges(subscriptionRenewalPayment); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/MarkSubscriptionRenewalPaymentAsPaid/SubscriptionRenewalPaymentAsPaidNotificationHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.RenewSubscription; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionRenewalPaymentAsPaid { public class SubscriptionRenewalPaymentAsPaidNotificationHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; public SubscriptionRenewalPaymentAsPaidNotificationHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(SubscriptionRenewalPaymentPaidNotification notification, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync( new RenewSubscriptionCommand( Guid.NewGuid(), notification.DomainEvent.SubscriptionId, notification.DomainEvent.SubscriptionRenewalPaymentId)); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/MarkSubscriptionRenewalPaymentAsPaid/SubscriptionRenewalPaymentPaidNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments.Events; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionRenewalPaymentAsPaid { public class SubscriptionRenewalPaymentPaidNotification : DomainNotificationBase { public SubscriptionRenewalPaymentPaidNotification(SubscriptionRenewalPaymentPaidDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/RenewSubscription/RenewSubscriptionCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.RenewSubscription { public class RenewSubscriptionCommand : InternalCommandBase { public Guid SubscriptionId { get; } public Guid SubscriptionRenewalPaymentId { get; } [JsonConstructor] public RenewSubscriptionCommand( Guid id, Guid subscriptionId, Guid subscriptionRenewalPaymentId) : base(id) { SubscriptionId = subscriptionId; SubscriptionRenewalPaymentId = subscriptionRenewalPaymentId; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/RenewSubscription/RenewSubscriptionCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.RenewSubscription { internal class RenewSubscriptionCommandHandler : ICommandHandler { private readonly IAggregateStore _aggregateStore; public RenewSubscriptionCommandHandler( IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public async Task Handle(RenewSubscriptionCommand command, CancellationToken cancellationToken) { var subscriptionRenewalPayment = await _aggregateStore.Load(new SubscriptionRenewalPaymentId(command.SubscriptionRenewalPaymentId)); var subscription = await _aggregateStore.Load(new SubscriptionId(command.SubscriptionId)); subscription.Renew(subscriptionRenewalPayment.GetSnapshot()); _aggregateStore.AppendChanges(subscription); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/RenewSubscription/SubscriptionRenewedEnqueueEmailConfirmationHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Payers.GetPayerEmail; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.SendSubscriptionRenewalConfirmationEmail; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.RenewSubscription { public class SubscriptionRenewedEnqueueEmailConfirmationHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; private readonly ISqlConnectionFactory _sqlConnectionFactory; public SubscriptionRenewedEnqueueEmailConfirmationHandler( ICommandsScheduler commandsScheduler, ISqlConnectionFactory sqlConnectionFactory) { _commandsScheduler = commandsScheduler; _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(SubscriptionRenewedNotification notification, CancellationToken cancellationToken) { var payerEmail = await PayerEmailProvider.GetPayerEmail( notification.DomainEvent.PayerId, _sqlConnectionFactory); await _commandsScheduler.EnqueueAsync(new SendSubscriptionRenewalConfirmationEmailCommand( Guid.NewGuid(), new SubscriptionId(notification.DomainEvent.SubscriptionId), payerEmail)); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/RenewSubscription/SubscriptionRenewedNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions.Events; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.RenewSubscription { public class SubscriptionRenewedNotification : DomainNotificationBase { [JsonConstructor] public SubscriptionRenewedNotification(SubscriptionRenewedDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/RenewSubscription/SubscriptionRenewedNotificationHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Payments.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.RenewSubscription { public class SubscriptionRenewedNotificationHandler : INotificationHandler { private readonly IEventsBus _eventsBus; public SubscriptionRenewedNotificationHandler(IEventsBus eventsBus) { _eventsBus = eventsBus; } public async Task Handle(SubscriptionRenewedNotification notification, CancellationToken cancellationToken) { await _eventsBus.Publish(new SubscriptionExpirationDateChangedIntegrationEvent( notification.Id, notification.DomainEvent.OccurredOn, notification.DomainEvent.PayerId, notification.DomainEvent.ExpirationDate)); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/SendSubscriptionCreationConfirmationEmail/SendSubscriptionCreationConfirmationEmailCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.SendSubscriptionCreationConfirmationEmail { public class SendSubscriptionCreationConfirmationEmailCommand : InternalCommandBase { internal SubscriptionId SubscriptionId { get; } internal string Email { get; } [JsonConstructor] public SendSubscriptionCreationConfirmationEmailCommand( Guid id, SubscriptionId subscriptionId, string email) : base(id) { SubscriptionId = subscriptionId; Email = email; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/SendSubscriptionCreationConfirmationEmail/SendSubscriptionCreationConfirmationEmailCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.SendSubscriptionCreationConfirmationEmail { internal class SendSubscriptionCreationConfirmationEmailCommandHandler : ICommandHandler { private readonly IEmailSender _emailSender; public SendSubscriptionCreationConfirmationEmailCommandHandler(IEmailSender emailSender) { _emailSender = emailSender; } public async Task Handle(SendSubscriptionCreationConfirmationEmailCommand request, CancellationToken cancellationToken) { var emailMessage = new EmailMessage( request.Email, "MyMeetings - Subscription purchased", $"Subscription {request.SubscriptionId.Value} was successfully paid and created with ❤ for you!"); await _emailSender.SendEmail(emailMessage); } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/SendSubscriptionRenewalConfirmationEmail/SendSubscriptionRenewalConfirmationEmailCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.SendSubscriptionRenewalConfirmationEmail { public class SendSubscriptionRenewalConfirmationEmailCommand : InternalCommandBase { internal SubscriptionId SubscriptionId { get; } internal string Email { get; } [JsonConstructor] public SendSubscriptionRenewalConfirmationEmailCommand( Guid id, SubscriptionId subscriptionId, string email) : base(id) { SubscriptionId = subscriptionId; Email = email; } } } ================================================ FILE: src/Modules/Payments/Application/Subscriptions/SendSubscriptionRenewalConfirmationEmail/SendSubscriptionRenewalConfirmationEmailCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; namespace CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.SendSubscriptionRenewalConfirmationEmail { internal class SendSubscriptionRenewalConfirmationEmailCommandHandler : ICommandHandler { private readonly IEmailSender _emailSender; public SendSubscriptionRenewalConfirmationEmailCommandHandler(IEmailSender emailSender) { _emailSender = emailSender; } public async Task Handle(SendSubscriptionRenewalConfirmationEmailCommand request, CancellationToken cancellationToken) { var emailMessage = new EmailMessage( request.Email, "MyMeetings - Subscription renewed", $"Subscription {request.SubscriptionId.Value} was successfully paid and renewed with ❤ for you!"); await _emailSender.SendEmail(emailMessage); } } } ================================================ FILE: src/Modules/Payments/Domain/CompanyName.MyMeetings.Modules.Payments.Domain.csproj ================================================  ================================================ FILE: src/Modules/Payments/Domain/MeetingFeePayments/Events/MeetingFeePaymentCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments.Events { public class MeetingFeePaymentCreatedDomainEvent : DomainEventBase { public MeetingFeePaymentCreatedDomainEvent( Guid meetingFeePaymentId, Guid meetingFeeId, string status) { MeetingFeeId = meetingFeeId; Status = status; MeetingFeePaymentId = meetingFeePaymentId; } public Guid MeetingFeeId { get; } public Guid MeetingFeePaymentId { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFeePayments/Events/MeetingFeePaymentExpiredDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments.Events { public class MeetingFeePaymentExpiredDomainEvent : DomainEventBase { public MeetingFeePaymentExpiredDomainEvent(Guid meetingFeePaymentId, string status) { MeetingFeePaymentId = meetingFeePaymentId; Status = status; } public Guid MeetingFeePaymentId { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFeePayments/Events/MeetingFeePaymentPaidDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments.Events { public class MeetingFeePaymentPaidDomainEvent : DomainEventBase { public MeetingFeePaymentPaidDomainEvent(Guid meetingFeePaymentId, string status) { MeetingFeePaymentId = meetingFeePaymentId; Status = status; } public Guid MeetingFeePaymentId { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFeePayments/MeetingFeePayment.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments { public class MeetingFeePayment : AggregateRoot { private MeetingFeeId _meetingFeeId; private MeetingFeePaymentStatus _status; public static MeetingFeePayment Create( MeetingFeeId meetingFeeId) { var meetingFeePayment = new MeetingFeePayment(); var meetingFeePaymentCreated = new MeetingFeePaymentCreatedDomainEvent( Guid.NewGuid(), meetingFeeId.Value, MeetingFeePaymentStatus.WaitingForPayment.Code); meetingFeePayment.Apply(meetingFeePaymentCreated); meetingFeePayment.AddDomainEvent(meetingFeePaymentCreated); return meetingFeePayment; } public void Expire() { MeetingFeePaymentPaidDomainEvent @event = new MeetingFeePaymentPaidDomainEvent( this.Id, MeetingFeePaymentStatus.Expired.Code); this.Apply(@event); this.AddDomainEvent(@event); } public void MarkAsPaid() { MeetingFeePaymentPaidDomainEvent @event = new MeetingFeePaymentPaidDomainEvent( this.Id, MeetingFeePaymentStatus.Paid.Code); this.Apply(@event); this.AddDomainEvent(@event); } public MeetingFeePaymentSnapshot GetSnapshot() { return new MeetingFeePaymentSnapshot(this.Id, _meetingFeeId.Value); } protected override void Apply(IDomainEvent @event) { this.When((dynamic)@event); } private void When(MeetingFeePaymentCreatedDomainEvent @event) { this.Id = @event.MeetingFeePaymentId; _meetingFeeId = new MeetingFeeId(@event.MeetingFeeId); _status = MeetingFeePaymentStatus.Of(@event.Status); } private void When(MeetingFeePaymentExpiredDomainEvent @event) { _status = MeetingFeePaymentStatus.Of(@event.Status); } private void When(MeetingFeePaymentPaidDomainEvent @event) { _status = MeetingFeePaymentStatus.Of(@event.Status); } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFeePayments/MeetingFeePaymentId.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments { public class MeetingFeePaymentId : AggregateId { public MeetingFeePaymentId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFeePayments/MeetingFeePaymentSnapshot.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments { public class MeetingFeePaymentSnapshot { public MeetingFeePaymentSnapshot(Guid meetingFeePaymentId, Guid meetingFeeId) { MeetingFeePaymentId = meetingFeePaymentId; MeetingFeeId = meetingFeeId; } public Guid MeetingFeePaymentId { get; } public Guid MeetingFeeId { get; } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFeePayments/MeetingFeePaymentStatus.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments { public class MeetingFeePaymentStatus : ValueObject { public static MeetingFeePaymentStatus WaitingForPayment => new MeetingFeePaymentStatus(nameof(WaitingForPayment)); public static MeetingFeePaymentStatus Paid => new MeetingFeePaymentStatus(nameof(Paid)); public static MeetingFeePaymentStatus Expired => new MeetingFeePaymentStatus(nameof(Expired)); public string Code { get; } private MeetingFeePaymentStatus(string code) { Code = code; } public static MeetingFeePaymentStatus Of(string code) { return new MeetingFeePaymentStatus(code); } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFees/Events/MeetingFeeCanceledDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees.Events { public class MeetingFeeCanceledDomainEvent : DomainEventBase { public MeetingFeeCanceledDomainEvent(Guid meetingFeeId, string status) { MeetingFeeId = meetingFeeId; Status = status; } public Guid MeetingFeeId { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFees/Events/MeetingFeeCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees.Events { public class MeetingFeeCreatedDomainEvent : DomainEventBase { public MeetingFeeCreatedDomainEvent( Guid meetingFeeId, Guid payerId, Guid meetingId, decimal feeValue, string feeCurrency, string status) { PayerId = payerId; MeetingId = meetingId; FeeValue = feeValue; FeeCurrency = feeCurrency; Status = status; MeetingFeeId = meetingFeeId; } public Guid MeetingFeeId { get; } public Guid PayerId { get; } public Guid MeetingId { get; } public decimal FeeValue { get; } public string FeeCurrency { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFees/Events/MeetingFeeExpiredDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees.Events { public class MeetingFeeExpiredDomainEvent : DomainEventBase { public MeetingFeeExpiredDomainEvent(Guid meetingFeeId, string status) { MeetingFeeId = meetingFeeId; Status = status; } public Guid MeetingFeeId { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFees/Events/MeetingFeePaidDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees.Events { public class MeetingFeePaidDomainEvent : DomainEventBase { public MeetingFeePaidDomainEvent(Guid meetingFeeId, string status) { MeetingFeeId = meetingFeeId; Status = status; } public Guid MeetingFeeId { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFees/MeetingFee.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees { public class MeetingFee : AggregateRoot { private PayerId _payerId; private MeetingId _meetingId; private MoneyValue _fee; private MeetingFeeStatus _status; protected override void Apply(IDomainEvent @event) { this.When((dynamic)@event); } private MeetingFee() { } public static MeetingFee Create( PayerId payerId, MeetingId meetingId, MoneyValue fee) { var meetingFee = new MeetingFee(); var meetingFeeCreated = new MeetingFeeCreatedDomainEvent( Guid.NewGuid(), payerId.Value, meetingId.Value, fee.Value, fee.Currency, MeetingFeeStatus.WaitingForPayment.Code); meetingFee.Apply(meetingFeeCreated); meetingFee.AddDomainEvent(meetingFeeCreated); return meetingFee; } public void MarkAsPaid() { var @event = new MeetingFeePaidDomainEvent( this.Id, MeetingFeeStatus.Paid.Code); this.Apply(@event); this.AddDomainEvent(@event); } public MeetingFeeSnapshot GetSnapshot() { return new MeetingFeeSnapshot(this.Id, _payerId.Value, _meetingId.Value); } private void When(MeetingFeeCreatedDomainEvent meetingFeeCreated) { this.Id = meetingFeeCreated.MeetingFeeId; _payerId = new PayerId(meetingFeeCreated.PayerId); _meetingId = new MeetingId(meetingFeeCreated.MeetingId); _fee = MoneyValue.Of(meetingFeeCreated.FeeValue, meetingFeeCreated.FeeCurrency); _status = MeetingFeeStatus.Of(meetingFeeCreated.Status); } private void When(MeetingFeeCanceledDomainEvent meetingFeeCanceled) { _status = MeetingFeeStatus.Of(meetingFeeCanceled.Status); } private void When(MeetingFeeExpiredDomainEvent meetingFeeExpired) { _status = MeetingFeeStatus.Of(meetingFeeExpired.Status); } private void When(MeetingFeePaidDomainEvent meetingFeePaid) { _status = MeetingFeeStatus.Of(meetingFeePaid.Status); } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFees/MeetingFeeId.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees { public class MeetingFeeId : AggregateId { public MeetingFeeId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFees/MeetingFeeSnapshot.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees { public class MeetingFeeSnapshot { public MeetingFeeSnapshot(Guid meetingFeeId, Guid payerId, Guid meetingId) { MeetingFeeId = meetingFeeId; PayerId = payerId; MeetingId = meetingId; } public Guid MeetingFeeId { get; } public Guid PayerId { get; } public Guid MeetingId { get; } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFees/MeetingFeeStatus.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees { public class MeetingFeeStatus : ValueObject { public static MeetingFeeStatus WaitingForPayment => new MeetingFeeStatus(nameof(WaitingForPayment)); public static MeetingFeeStatus Paid => new MeetingFeeStatus(nameof(Paid)); public static MeetingFeeStatus Expired => new MeetingFeeStatus(nameof(Expired)); public static MeetingFeeStatus Canceled => new MeetingFeeStatus(nameof(Canceled)); public string Code { get; } private MeetingFeeStatus(string code) { Code = code; } public static MeetingFeeStatus Of(string code) { return new MeetingFeeStatus(code); } } } ================================================ FILE: src/Modules/Payments/Domain/MeetingFees/MeetingId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees { public class MeetingId : TypedIdValueBase { public MeetingId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Payments/Domain/Payers/Events/PayerCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Payers.Events { public class PayerCreatedDomainEvent : DomainEventBase { public Guid PayerId { get; } public string Login { get; } public string FirstName { get; } public string LastName { get; } public string Name { get; } public string Email { get; } public PayerCreatedDomainEvent( Guid payerId, string login, string firstName, string lastName, string name, string email) { PayerId = payerId; Login = login; FirstName = firstName; LastName = lastName; Name = name; Email = email; } } } ================================================ FILE: src/Modules/Payments/Domain/Payers/IPayerContext.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Domain.Payers { public interface IPayerContext { PayerId PayerId { get; } } } ================================================ FILE: src/Modules/Payments/Domain/Payers/IPayerRepository.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Domain.Payers { public interface IPayerRepository { Task AddAsync(Payer payer); } } ================================================ FILE: src/Modules/Payments/Domain/Payers/Payer.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Payers { public class Payer : AggregateRoot, IAggregateRoot { protected override void Apply(IDomainEvent @event) { this.When((dynamic)@event); } private string _login; private string _email; private string _firstName; private string _lastName; private string _name; private DateTime _createDate; public static Payer Create( Guid id, string login, string email, string firstName, string lastName, string name) { var payer = new Payer(); var payerCreated = new PayerCreatedDomainEvent( id, login, firstName, lastName, name, email); payer.Apply(payerCreated); payer.AddDomainEvent(payerCreated); return payer; } private Payer() { } private void When(PayerCreatedDomainEvent @event) { this.Id = @event.PayerId; _login = @event.Login; _createDate = @event.OccurredOn; _email = @event.Email; _firstName = @event.FirstName; _lastName = @event.LastName; _name = @event.Name; } } } ================================================ FILE: src/Modules/Payments/Domain/Payers/PayerId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Payers { public class PayerId : TypedIdValueBase { public PayerId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/Events/PriceListItemActivatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.Events { public class PriceListItemActivatedDomainEvent : DomainEventBase { public PriceListItemActivatedDomainEvent(Guid priceListItemId) { PriceListItemId = priceListItemId; } public Guid PriceListItemId { get; } } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/Events/PriceListItemAttributesChangedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.Events { public class PriceListItemAttributesChangedDomainEvent : DomainEventBase { public PriceListItemAttributesChangedDomainEvent(Guid priceListItemId, string countryCode, string subscriptionPeriodCode, string categoryCode, decimal price, string currency) { PriceListItemId = priceListItemId; CountryCode = countryCode; SubscriptionPeriodCode = subscriptionPeriodCode; CategoryCode = categoryCode; Price = price; Currency = currency; } public Guid PriceListItemId { get; } public string CountryCode { get; } public string SubscriptionPeriodCode { get; } public string CategoryCode { get; } public decimal Price { get; } public string Currency { get; } } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/Events/PriceListItemCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.Events { public class PriceListItemCreatedDomainEvent : DomainEventBase { public PriceListItemCreatedDomainEvent( Guid priceListItemId, string countryCode, string subscriptionPeriodCode, string categoryCode, decimal price, string currency, bool isActive) { PriceListItemId = priceListItemId; CountryCode = countryCode; SubscriptionPeriodCode = subscriptionPeriodCode; CategoryCode = categoryCode; Price = price; Currency = currency; IsActive = isActive; } public Guid PriceListItemId { get; } public string CountryCode { get; } public string SubscriptionPeriodCode { get; } public string CategoryCode { get; } public decimal Price { get; } public string Currency { get; } public bool IsActive { get; } } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/Events/PriceListItemDeactivatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.Events { public class PriceListItemDeactivatedDomainEvent : DomainEventBase { public PriceListItemDeactivatedDomainEvent(Guid priceListItemId) { PriceListItemId = priceListItemId; } public Guid PriceListItemId { get; } } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/PriceList.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.PricingStrategies; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Rules; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems { public class PriceList : ValueObject { private readonly List _items; private readonly IPricingStrategy _pricingStrategy; private PriceList( List items, IPricingStrategy pricingStrategy) { _items = items; _pricingStrategy = pricingStrategy; } public static PriceList Create( List items, IPricingStrategy pricingStrategy) { return new PriceList(items, pricingStrategy); } public MoneyValue GetPrice( string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category) { CheckRule(new PriceForSubscriptionMustBeDefinedRule(countryCode, subscriptionPeriod, _items, category)); return _pricingStrategy.GetPrice(countryCode, subscriptionPeriod, category); } } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/PriceListItem.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems { public class PriceListItem : AggregateRoot { private string _countryCode; private SubscriptionPeriod _subscriptionPeriod; private PriceListItemCategory _category; private MoneyValue _price; private bool _isActive; private PriceListItem() { } public static PriceListItem Create( string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category, MoneyValue price) { var priceListItem = new PriceListItem(); var priceListItemCreatedEvent = new PriceListItemCreatedDomainEvent( Guid.NewGuid(), countryCode, subscriptionPeriod.Code, category.Code, price.Value, price.Currency, isActive: true); priceListItem.Apply(priceListItemCreatedEvent); priceListItem.AddDomainEvent(priceListItemCreatedEvent); return priceListItem; } public void Activate() { if (!_isActive) { var priceListItemActivatedEvent = new PriceListItemActivatedDomainEvent(this.Id); this.Apply(priceListItemActivatedEvent); this.AddDomainEvent(priceListItemActivatedEvent); } } public void Deactivate() { if (_isActive) { var priceListItemDeactivatedEvent = new PriceListItemDeactivatedDomainEvent(this.Id); this.Apply(priceListItemDeactivatedEvent); this.AddDomainEvent(priceListItemDeactivatedEvent); } } public void ChangeAttributes( string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category, MoneyValue price) { var priceListItemChangedDomainEvent = new PriceListItemAttributesChangedDomainEvent(this.Id, countryCode, subscriptionPeriod.Code, category.Code, price.Value, price.Currency); this.Apply(priceListItemChangedDomainEvent); this.AddDomainEvent(priceListItemChangedDomainEvent); } protected override void Apply(IDomainEvent @event) => When((dynamic)@event); private void When(PriceListItemActivatedDomainEvent @event) { this._isActive = true; } private void When(PriceListItemCreatedDomainEvent @event) { this.Id = @event.PriceListItemId; _countryCode = @event.CountryCode; _subscriptionPeriod = SubscriptionPeriod.Of(@event.SubscriptionPeriodCode); _category = PriceListItemCategory.Of(@event.CategoryCode); _price = MoneyValue.Of(@event.Price, @event.Currency); _isActive = true; } private void When(PriceListItemDeactivatedDomainEvent @event) { this._isActive = false; } private void When(PriceListItemAttributesChangedDomainEvent @event) { this._countryCode = @event.CountryCode; this._subscriptionPeriod = SubscriptionPeriod.Of(@event.SubscriptionPeriodCode); this._category = PriceListItemCategory.Of(@event.CategoryCode); this._price = MoneyValue.Of(@event.Price, @event.Currency); } } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/PriceListItemCategory.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems { public class PriceListItemCategory : ValueObject { public static PriceListItemCategory New => new PriceListItemCategory(nameof(New)); public static PriceListItemCategory Renewal => new PriceListItemCategory(nameof(Renewal)); public string Code { get; } private PriceListItemCategory(string code) { Code = code; } public static PriceListItemCategory Of(string code) => new PriceListItemCategory(code); } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/PriceListItemData.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems { public class PriceListItemData { public PriceListItemData( string countryCode, SubscriptionPeriod subscriptionPeriod, MoneyValue value, PriceListItemCategory category) { CountryCode = countryCode; Value = value; Category = category; SubscriptionPeriod = subscriptionPeriod; } public string CountryCode { get; } public SubscriptionPeriod SubscriptionPeriod { get; } public MoneyValue Value { get; } public PriceListItemCategory Category { get; } } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/PriceListItemId.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems { public class PriceListItemId : AggregateId { public PriceListItemId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/PricingStrategies/DirectValueFromPriceListPricingStrategy.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.PricingStrategies { public class DirectValueFromPriceListPricingStrategy : IPricingStrategy { private readonly List _items; public DirectValueFromPriceListPricingStrategy(List items) { _items = items; } public MoneyValue GetPrice( string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category) { var priceListItem = _items.Single(x => x.CountryCode == countryCode && x.SubscriptionPeriod == subscriptionPeriod && x.Category == category); return priceListItem.Value; } } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/PricingStrategies/DirectValuePricingStrategy.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.PricingStrategies { public class DirectValuePricingStrategy : IPricingStrategy { private readonly MoneyValue _directValue; public DirectValuePricingStrategy(MoneyValue directValue) { _directValue = directValue; } public MoneyValue GetPrice(string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category) { return _directValue; } } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/PricingStrategies/DiscountedValueFromPriceListPricingStrategy.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.PricingStrategies { public class DiscountedValueFromPriceListPricingStrategy : IPricingStrategy { private readonly List _items; private readonly MoneyValue _discountValue; public DiscountedValueFromPriceListPricingStrategy( List items, MoneyValue discountValue) { _items = items; _discountValue = discountValue; } public MoneyValue GetPrice(string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category) { var priceListItem = _items.Single(x => x.CountryCode == countryCode && x.SubscriptionPeriod == subscriptionPeriod && x.Category == category); return priceListItem.Value - _discountValue; } } } ================================================ FILE: src/Modules/Payments/Domain/PriceListItems/PricingStrategies/IPricingStrategy.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.PricingStrategies { public interface IPricingStrategy { MoneyValue GetPrice( string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category); } } ================================================ FILE: src/Modules/Payments/Domain/SeedWork/AggregateId.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork { public abstract class AggregateId where T : AggregateRoot { protected AggregateId(Guid value) { Value = value; } public Guid Value { get; } } } ================================================ FILE: src/Modules/Payments/Domain/SeedWork/AggregateRoot.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork { public abstract class AggregateRoot { private readonly List _domainEvents; public Guid Id { get; protected set; } public int Version { get; private set; } public IReadOnlyCollection GetDomainEvents() => _domainEvents.AsReadOnly(); protected void AddDomainEvent(IDomainEvent @event) { _domainEvents.Add(@event); } protected AggregateRoot() { _domainEvents = []; Version = -1; } public void Load(IEnumerable history) { foreach (var e in history) { Apply(e); Version++; } } protected abstract void Apply(IDomainEvent @event); protected static void CheckRule(IBusinessRule rule) { if (rule.IsBroken()) { throw new BusinessRuleValidationException(rule); } } } } ================================================ FILE: src/Modules/Payments/Domain/SeedWork/IAggregateStore.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork { public interface IAggregateStore { Task Save(); Task Load(AggregateId aggregateId) where T : AggregateRoot; List GetChanges(); void AppendChanges(T aggregate) where T : AggregateRoot; void ClearChanges(); } } ================================================ FILE: src/Modules/Payments/Domain/SeedWork/MoneyValue.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork.Rules; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork { public class MoneyValue : ValueObject { public decimal Value { get; } public string Currency { get; } private MoneyValue(decimal value, string currency) { this.Value = value; this.Currency = currency; } public static MoneyValue Of(decimal value, string currency) { CheckRule(new ValueOfMoneyMustNotBeNegativeRule(value)); return new MoneyValue(value, currency); } public static bool operator >(decimal left, MoneyValue right) => left > right.Value; public static bool operator <(decimal left, MoneyValue right) => left < right.Value; public static bool operator >=(decimal left, MoneyValue right) => left >= right.Value; public static bool operator <=(decimal left, MoneyValue right) => left <= right.Value; public static bool operator >(MoneyValue left, decimal right) => left.Value > right; public static bool operator <(MoneyValue left, decimal right) => left.Value < right; public static bool operator >=(MoneyValue left, decimal right) => left.Value >= right; public static bool operator <=(MoneyValue left, decimal right) => left.Value <= right; public static MoneyValue operator -(MoneyValue left, MoneyValue right) { CheckRule(new MoneyMustHaveTheSameCurrencyRule(left, right)); return Of(left.Value - right.Value, left.Currency); } } } ================================================ FILE: src/Modules/Payments/Domain/SeedWork/Rules/MoneyMustHaveTheSameCurrencyRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork.Rules { public class MoneyMustHaveTheSameCurrencyRule : IBusinessRule { private readonly MoneyValue _left; private readonly MoneyValue _right; public MoneyMustHaveTheSameCurrencyRule(MoneyValue left, MoneyValue right) { _left = left; _right = right; } public bool IsBroken() => _left.Currency != _right.Currency; public string Message => "Currency of money must be the same."; } } ================================================ FILE: src/Modules/Payments/Domain/SeedWork/Rules/ValueOfMoneyMustNotBeNegativeRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork.Rules { public class ValueOfMoneyMustNotBeNegativeRule : IBusinessRule { private readonly decimal _value; public ValueOfMoneyMustNotBeNegativeRule(decimal value) { _value = value; } public bool IsBroken() => _value < 0; public string Message => "Value of money must not be negative."; } } ================================================ FILE: src/Modules/Payments/Domain/SeedWork/SystemClock.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork { public static class SystemClock { private static DateTime? _customDate; public static DateTime Now { get { if (_customDate.HasValue) { return _customDate.Value; } return DateTime.UtcNow; } } public static void Set(DateTime customDate) => _customDate = customDate; public static void Reset() => _customDate = null; } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionPayments/Events/SubscriptionPaymentCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Events { public class SubscriptionPaymentCreatedDomainEvent : DomainEventBase { public SubscriptionPaymentCreatedDomainEvent( Guid subscriptionPaymentId, Guid payerId, string subscriptionPeriodCode, string countryCode, string status, decimal value, string currency) { SubscriptionPaymentId = subscriptionPaymentId; PayerId = payerId; SubscriptionPeriodCode = subscriptionPeriodCode; CountryCode = countryCode; Status = status; Value = value; Currency = currency; } public Guid SubscriptionPaymentId { get; } public Guid PayerId { get; } public string SubscriptionPeriodCode { get; } public string CountryCode { get; } public string Status { get; } public decimal Value { get; } public string Currency { get; } } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionPayments/Events/SubscriptionPaymentExpiredDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Events { public class SubscriptionPaymentExpiredDomainEvent : DomainEventBase { public SubscriptionPaymentExpiredDomainEvent(Guid subscriptionPaymentId, string status) { SubscriptionPaymentId = subscriptionPaymentId; Status = status; } public Guid SubscriptionPaymentId { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionPayments/Events/SubscriptionPaymentPaidDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Events { public class SubscriptionPaymentPaidDomainEvent : DomainEventBase { public SubscriptionPaymentPaidDomainEvent(Guid subscriptionPaymentId, string status) { SubscriptionPaymentId = subscriptionPaymentId; Status = status; } public Guid SubscriptionPaymentId { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionPayments/Rules/PriceForSubscriptionMustBeDefinedRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Rules { public class PriceForSubscriptionMustBeDefinedRule : IBusinessRule { private readonly string _countryCode; private readonly SubscriptionPeriod _subscriptionPeriod; private readonly IList _priceListItems; private readonly PriceListItemCategory _category; public PriceForSubscriptionMustBeDefinedRule( string countryCode, SubscriptionPeriod subscriptionPeriod, IList priceListItems, PriceListItemCategory category) { _countryCode = countryCode; _subscriptionPeriod = subscriptionPeriod; _priceListItems = priceListItems; _category = category; } public bool IsBroken() => _priceListItems.Count(x => x.CountryCode == _countryCode && x.Category == _category && x.SubscriptionPeriod == _subscriptionPeriod) != 1; public string Message => "Price for subscription must be defined"; } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionPayments/Rules/PriceOfferMustMatchPriceInPriceListRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Rules { public class PriceOfferMustMatchPriceInPriceListRule : IBusinessRule { private readonly MoneyValue _priceOffer; private readonly MoneyValue _priceInPriceList; public PriceOfferMustMatchPriceInPriceListRule( MoneyValue priceOffer, MoneyValue priceInPriceList) { _priceOffer = priceOffer; _priceInPriceList = priceInPriceList; } public bool IsBroken() => _priceOffer != _priceInPriceList; public string Message => "Price offer must match price in Price List"; } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionPayments/SubscriptionPayment.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Rules; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments { public class SubscriptionPayment : AggregateRoot { private PayerId _payerId; private SubscriptionPeriod _subscriptionPeriod; private string _countryCode; private SubscriptionPaymentStatus _subscriptionPaymentStatus; private MoneyValue _value; public static SubscriptionPayment Buy( PayerId payerId, SubscriptionPeriod period, string countryCode, MoneyValue priceOffer, PriceList priceList) { var priceInPriceList = priceList.GetPrice(countryCode, period, PriceListItemCategory.New); CheckRule(new PriceOfferMustMatchPriceInPriceListRule(priceOffer, priceInPriceList)); var subscriptionPayment = new SubscriptionPayment(); var subscriptionPaymentCreated = new SubscriptionPaymentCreatedDomainEvent( Guid.NewGuid(), payerId.Value, period.Code, countryCode, SubscriptionPaymentStatus.WaitingForPayment.Code, priceOffer.Value, priceOffer.Currency); subscriptionPayment.Apply(subscriptionPaymentCreated); subscriptionPayment.AddDomainEvent(subscriptionPaymentCreated); return subscriptionPayment; } public SubscriptionPaymentSnapshot GetSnapshot() { return new SubscriptionPaymentSnapshot(new SubscriptionPaymentId(this.Id), _payerId, _subscriptionPeriod, _countryCode); } public void MarkAsPaid() { SubscriptionPaymentPaidDomainEvent @event = new SubscriptionPaymentPaidDomainEvent( this.Id, SubscriptionPaymentStatus.Paid.Code); this.Apply(@event); this.AddDomainEvent(@event); } public void Expire() { SubscriptionPaymentExpiredDomainEvent @event = new SubscriptionPaymentExpiredDomainEvent( this.Id, SubscriptionPaymentStatus.Expired.Code); this.Apply(@event); this.AddDomainEvent(@event); } protected override void Apply(IDomainEvent @event) { this.When((dynamic)@event); } private void When(SubscriptionPaymentPaidDomainEvent @event) { _subscriptionPaymentStatus = SubscriptionPaymentStatus.Of(@event.Status); } private void When(SubscriptionPaymentCreatedDomainEvent @event) { this.Id = @event.SubscriptionPaymentId; _payerId = new PayerId(@event.PayerId); _subscriptionPeriod = SubscriptionPeriod.Of(@event.SubscriptionPeriodCode); _countryCode = @event.CountryCode; _subscriptionPaymentStatus = SubscriptionPaymentStatus.Of(@event.Status); _value = MoneyValue.Of(@event.Value, @event.Currency); } private void When(SubscriptionPaymentExpiredDomainEvent @event) { _subscriptionPaymentStatus = SubscriptionPaymentStatus.Of(@event.Status); } } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionPayments/SubscriptionPaymentId.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments { public class SubscriptionPaymentId : AggregateId { public SubscriptionPaymentId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionPayments/SubscriptionPaymentSnapshot.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments { public class SubscriptionPaymentSnapshot { public SubscriptionPaymentSnapshot( SubscriptionPaymentId id, PayerId payerId, SubscriptionPeriod subscriptionPeriod, string countryCode) { PayerId = payerId; SubscriptionPeriod = subscriptionPeriod; CountryCode = countryCode; Id = id; } public PayerId PayerId { get; } public SubscriptionPeriod SubscriptionPeriod { get; } public string CountryCode { get; } public SubscriptionPaymentId Id { get; } } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionPayments/SubscriptionPaymentStatus.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments { public class SubscriptionPaymentStatus : ValueObject { public static SubscriptionPaymentStatus WaitingForPayment => new SubscriptionPaymentStatus(nameof(WaitingForPayment)); public static SubscriptionPaymentStatus Paid => new SubscriptionPaymentStatus(nameof(Paid)); public static SubscriptionPaymentStatus Expired => new SubscriptionPaymentStatus(nameof(Expired)); public string Code { get; } private SubscriptionPaymentStatus(string code) { Code = code; } public static SubscriptionPaymentStatus Of(string code) { return new SubscriptionPaymentStatus(code); } } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionRenewalPayments/Events/SubscriptionRenewalPaymentCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments.Events { public class SubscriptionRenewalPaymentCreatedDomainEvent : DomainEventBase { public SubscriptionRenewalPaymentCreatedDomainEvent( Guid subscriptionRenewalPaymentId, Guid payerId, Guid subscriptionId, string subscriptionPeriodCode, string countryCode, string status, decimal value, string currency) { SubscriptionRenewalPaymentId = subscriptionRenewalPaymentId; PayerId = payerId; SubscriptionId = subscriptionId; SubscriptionPeriodCode = subscriptionPeriodCode; CountryCode = countryCode; Status = status; Value = value; Currency = currency; } public Guid SubscriptionRenewalPaymentId { get; } public Guid PayerId { get; } public Guid SubscriptionId { get; } public string SubscriptionPeriodCode { get; } public string CountryCode { get; } public string Status { get; } public decimal Value { get; } public string Currency { get; } } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionRenewalPayments/Events/SubscriptionRenewalPaymentPaidDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments.Events { public class SubscriptionRenewalPaymentPaidDomainEvent : DomainEventBase { public SubscriptionRenewalPaymentPaidDomainEvent( Guid subscriptionRenewalPaymentId, Guid subscriptionId, string status) { SubscriptionRenewalPaymentId = subscriptionRenewalPaymentId; SubscriptionId = subscriptionId; Status = status; } public Guid SubscriptionRenewalPaymentId { get; } public Guid SubscriptionId { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionRenewalPayments/Rules/PriceOfferMustMatchPriceInPriceListRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments.Rules { public class PriceOfferMustMatchPriceInPriceListRule : IBusinessRule { private readonly MoneyValue _priceOffer; private readonly MoneyValue _priceInPriceList; public PriceOfferMustMatchPriceInPriceListRule( MoneyValue priceOffer, MoneyValue priceInPriceList) { _priceOffer = priceOffer; _priceInPriceList = priceInPriceList; } public bool IsBroken() => _priceOffer != _priceInPriceList; public string Message => "Price offer must match price in Price List"; } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionRenewalPayments/SubscriptionRenewalPayment.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments.Rules; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments { public class SubscriptionRenewalPayment : AggregateRoot { private PayerId _payerId; private SubscriptionId _subscriptionId; private SubscriptionPeriod _subscriptionPeriod; private string _countryCode; private SubscriptionRenewalPaymentStatus _subscriptionRenewalPaymentStatus; private MoneyValue _value; public static SubscriptionRenewalPayment Buy( PayerId payerId, SubscriptionId subscriptionId, SubscriptionPeriod period, string countryCode, MoneyValue priceOffer, PriceList priceList) { var priceInPriceList = priceList.GetPrice(countryCode, period, PriceListItemCategory.Renewal); CheckRule(new PriceOfferMustMatchPriceInPriceListRule(priceOffer, priceInPriceList)); var subscriptionRenewalPayment = new SubscriptionRenewalPayment(); var subscriptionRenewalPaymentCreated = new SubscriptionRenewalPaymentCreatedDomainEvent( Guid.NewGuid(), payerId.Value, subscriptionId.Value, period.Code, countryCode, SubscriptionRenewalPaymentStatus.WaitingForPayment.Code, priceOffer.Value, priceOffer.Currency); subscriptionRenewalPayment.Apply(subscriptionRenewalPaymentCreated); subscriptionRenewalPayment.AddDomainEvent(subscriptionRenewalPaymentCreated); return subscriptionRenewalPayment; } public SubscriptionRenewalPaymentSnapshot GetSnapshot() { return new SubscriptionRenewalPaymentSnapshot(new SubscriptionRenewalPaymentId(this.Id), _payerId, _subscriptionPeriod, _countryCode); } public void MarkRenewalAsPaid() { SubscriptionRenewalPaymentPaidDomainEvent @event = new SubscriptionRenewalPaymentPaidDomainEvent( this.Id, this._subscriptionId.Value, SubscriptionRenewalPaymentStatus.Paid.Code); this.Apply(@event); this.AddDomainEvent(@event); } protected override void Apply(IDomainEvent @event) { this.When((dynamic)@event); } private void When(SubscriptionRenewalPaymentCreatedDomainEvent @event) { this.Id = @event.SubscriptionRenewalPaymentId; _payerId = new PayerId(@event.PayerId); _subscriptionId = new SubscriptionId(@event.SubscriptionId); _subscriptionPeriod = SubscriptionPeriod.Of(@event.SubscriptionPeriodCode); _countryCode = @event.CountryCode; _subscriptionRenewalPaymentStatus = SubscriptionRenewalPaymentStatus.Of(@event.Status); _value = MoneyValue.Of(@event.Value, @event.Currency); } private void When(SubscriptionRenewalPaymentPaidDomainEvent @event) { _subscriptionRenewalPaymentStatus = SubscriptionRenewalPaymentStatus.Of(@event.Status); } } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionRenewalPayments/SubscriptionRenewalPaymentId.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments { public class SubscriptionRenewalPaymentId : AggregateId { public SubscriptionRenewalPaymentId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionRenewalPayments/SubscriptionRenewalPaymentSnapshot.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments { public class SubscriptionRenewalPaymentSnapshot { public SubscriptionRenewalPaymentSnapshot( SubscriptionRenewalPaymentId id, PayerId payerId, SubscriptionPeriod subscriptionPeriod, string countryCode) { PayerId = payerId; SubscriptionPeriod = subscriptionPeriod; CountryCode = countryCode; Id = id; } public PayerId PayerId { get; } public SubscriptionPeriod SubscriptionPeriod { get; } public string CountryCode { get; } public SubscriptionRenewalPaymentId Id { get; } } } ================================================ FILE: src/Modules/Payments/Domain/SubscriptionRenewalPayments/SubscriptionRenewalPaymentStatus.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments { public class SubscriptionRenewalPaymentStatus { public static SubscriptionRenewalPaymentStatus WaitingForPayment => new SubscriptionRenewalPaymentStatus(nameof(WaitingForPayment)); public static SubscriptionRenewalPaymentStatus Paid => new SubscriptionRenewalPaymentStatus(nameof(Paid)); public static SubscriptionRenewalPaymentStatus Expired => new SubscriptionRenewalPaymentStatus(nameof(Expired)); public string Code { get; } private SubscriptionRenewalPaymentStatus(string code) { Code = code; } public static SubscriptionRenewalPaymentStatus Of(string code) { return new SubscriptionRenewalPaymentStatus(code); } } } ================================================ FILE: src/Modules/Payments/Domain/Subscriptions/Events/SubscriptionCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions.Events { public class SubscriptionCreatedDomainEvent : DomainEventBase { public SubscriptionCreatedDomainEvent( Guid subscriptionPaymentId, Guid subscriptionId, Guid payerId, string subscriptionPeriodCode, string countryCode, DateTime expirationDate, string status) { SubscriptionPaymentId = subscriptionPaymentId; SubscriptionId = subscriptionId; SubscriptionPeriodCode = subscriptionPeriodCode; CountryCode = countryCode; ExpirationDate = expirationDate; Status = status; PayerId = payerId; } public Guid PayerId { get; } public Guid SubscriptionPaymentId { get; } public Guid SubscriptionId { get; } public string SubscriptionPeriodCode { get; } public string CountryCode { get; } public DateTime ExpirationDate { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/Subscriptions/Events/SubscriptionExpiredDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions.Events { public class SubscriptionExpiredDomainEvent : DomainEventBase { public SubscriptionExpiredDomainEvent( Guid subscriptionId, string status) { SubscriptionId = subscriptionId; Status = status; } public Guid SubscriptionId { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/Subscriptions/Events/SubscriptionRenewedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions.Events { public class SubscriptionRenewedDomainEvent : DomainEventBase { public SubscriptionRenewedDomainEvent( Guid subscriptionId, DateTime expirationDate, Guid payerId, string subscriptionPeriodCode, string status) { SubscriptionId = subscriptionId; ExpirationDate = expirationDate; PayerId = payerId; SubscriptionPeriodCode = subscriptionPeriodCode; Status = status; } public Guid SubscriptionId { get; } public DateTime ExpirationDate { get; } public Guid PayerId { get; } public string SubscriptionPeriodCode { get; } public string Status { get; } } } ================================================ FILE: src/Modules/Payments/Domain/Subscriptions/SubscriberId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions { public class SubscriberId : TypedIdValueBase { public SubscriberId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Payments/Domain/Subscriptions/Subscription.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions.Events; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions { public class Subscription : AggregateRoot { private SubscriberId _subscriberId; private SubscriptionPeriod _subscriptionPeriod; private SubscriptionStatus _status; private string _countryCode; private DateTime _expirationDate; private Subscription() { } public void Renew( SubscriptionRenewalPaymentSnapshot subscriptionRenewalPayment) { var expirationDate = SubscriptionDateExpirationCalculator.CalculateForRenewal( _expirationDate, subscriptionRenewalPayment.SubscriptionPeriod); SubscriptionRenewedDomainEvent subscriptionRenewedDomainEvent = new SubscriptionRenewedDomainEvent( this.Id, expirationDate, subscriptionRenewalPayment.PayerId.Value, subscriptionRenewalPayment.SubscriptionPeriod.Code, SubscriptionStatus.Active.Code); this.Apply(subscriptionRenewedDomainEvent); this.AddDomainEvent(subscriptionRenewedDomainEvent); } public void Expire() { if (_expirationDate < SystemClock.Now) { SubscriptionExpiredDomainEvent subscriptionExpiredDomainEvent = new SubscriptionExpiredDomainEvent(this.Id, SubscriptionStatus.Expired.Code); this.When(subscriptionExpiredDomainEvent); this.AddDomainEvent(subscriptionExpiredDomainEvent); } } public static Subscription Create( SubscriptionPaymentSnapshot subscriptionPayment) { var subscription = new Subscription(); var expirationDate = SubscriptionDateExpirationCalculator.CalculateForNew(subscriptionPayment.SubscriptionPeriod); var subscriptionCreatedDomainEvent = new SubscriptionCreatedDomainEvent( subscriptionPayment.Id.Value, Guid.NewGuid(), subscriptionPayment.PayerId.Value, subscriptionPayment.SubscriptionPeriod.Code, subscriptionPayment.CountryCode, expirationDate, SubscriptionStatus.Active.Code); subscription.Apply(subscriptionCreatedDomainEvent); subscription.AddDomainEvent(subscriptionCreatedDomainEvent); return subscription; } protected sealed override void Apply(IDomainEvent @event) { this.When((dynamic)@event); } private void When(SubscriptionCreatedDomainEvent @event) { this.Id = @event.SubscriptionId; _subscriberId = new SubscriberId(@event.PayerId); _subscriptionPeriod = SubscriptionPeriod.Of(@event.SubscriptionPeriodCode); _countryCode = @event.CountryCode; _status = SubscriptionStatus.Of(@event.Status); _expirationDate = @event.ExpirationDate; } private void When(SubscriptionRenewedDomainEvent @event) { this.Id = @event.SubscriptionId; _subscriptionPeriod = SubscriptionPeriod.Of(@event.SubscriptionPeriodCode); _status = SubscriptionStatus.Of(@event.Status); _expirationDate = @event.ExpirationDate; } private void When(SubscriptionExpiredDomainEvent @event) { _status = SubscriptionStatus.Of(@event.Status); } } } ================================================ FILE: src/Modules/Payments/Domain/Subscriptions/SubscriptionDateExpirationCalculator.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions { public class SubscriptionDateExpirationCalculator { public static DateTime CalculateForNew(SubscriptionPeriod period) { return SystemClock.Now.AddMonths(period.GetMonthsNumber()); } public static DateTime CalculateForRenewal(DateTime expirationDate, SubscriptionPeriod period) { if (expirationDate > SystemClock.Now) { return expirationDate.AddMonths(period.GetMonthsNumber()); } return SystemClock.Now.AddMonths(period.GetMonthsNumber()); } } } ================================================ FILE: src/Modules/Payments/Domain/Subscriptions/SubscriptionId.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions { public class SubscriptionId : AggregateId { public SubscriptionId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Payments/Domain/Subscriptions/SubscriptionPeriod.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions { public class SubscriptionPeriod : ValueObject { public string Code { get; } public static SubscriptionPeriod Month => new SubscriptionPeriod(nameof(Month)); public static SubscriptionPeriod HalfYear => new SubscriptionPeriod(nameof(HalfYear)); public static SubscriptionPeriod Of(string code) { return new SubscriptionPeriod(code); } public static string GetName(string code) { return code == Month.Code ? "Month" : "6 months"; } private SubscriptionPeriod(string code) { Code = code; } public int GetMonthsNumber() => this == Month ? 1 : 6; } } ================================================ FILE: src/Modules/Payments/Domain/Subscriptions/SubscriptionStatus.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions { public class SubscriptionStatus : ValueObject { public static SubscriptionStatus Active => new SubscriptionStatus(nameof(Active)); public static SubscriptionStatus Expired => new SubscriptionStatus(nameof(Expired)); public string Code { get; } private SubscriptionStatus(string code) { Code = code; } public static SubscriptionStatus Of(string code) { return new SubscriptionStatus(code); } } } ================================================ FILE: src/Modules/Payments/Domain/Users/IUserContext.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Domain.Users { public interface IUserContext { UserId UserId { get; } } } ================================================ FILE: src/Modules/Payments/Domain/Users/UserId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.Domain.Users { public class UserId : TypedIdValueBase { public UserId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Payments/Infrastructure/AggregateStore/AggregateStoreDomainEventsAccessor.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.AggregateStore { public class AggregateStoreDomainEventsAccessor : IDomainEventsAccessor { private readonly IAggregateStore _aggregateStore; public AggregateStoreDomainEventsAccessor(IAggregateStore aggregateStore) { _aggregateStore = aggregateStore; } public IReadOnlyCollection GetAllDomainEvents() { return _aggregateStore .GetChanges() .ToList() .AsReadOnly(); } public void ClearAllDomainEvents() { _aggregateStore.ClearChanges(); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/AggregateStore/DomainEventTypeMappings.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions.Events; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.AggregateStore { internal static class DomainEventTypeMappings { internal static IDictionary Dictionary { get; } static DomainEventTypeMappings() { Dictionary = new Dictionary(); Dictionary.Add("SubscriptionPaymentCreated", typeof(SubscriptionPaymentCreatedDomainEvent)); Dictionary.Add("SubscriptionPaymentPaid", typeof(SubscriptionPaymentPaidDomainEvent)); Dictionary.Add("SubscriptionPaymentExpired", typeof(SubscriptionPaymentExpiredDomainEvent)); Dictionary.Add("SubscriptionRenewalPaymentCreated", typeof(SubscriptionRenewalPaymentCreatedDomainEvent)); Dictionary.Add("SubscriptionRenewalPaymentPaid", typeof(SubscriptionRenewalPaymentPaidDomainEvent)); Dictionary.Add("SubscriptionCreated", typeof(SubscriptionCreatedDomainEvent)); Dictionary.Add("SubscriptionRenewed", typeof(SubscriptionRenewedDomainEvent)); Dictionary.Add("SubscriptionExpired", typeof(SubscriptionExpiredDomainEvent)); Dictionary.Add("PayerCreated", typeof(PayerCreatedDomainEvent)); Dictionary.Add("PriceListItemCreated", typeof(PriceListItemCreatedDomainEvent)); Dictionary.Add("PriceListItemActivated", typeof(PriceListItemActivatedDomainEvent)); Dictionary.Add("PriceListItemDeactivated", typeof(PriceListItemDeactivatedDomainEvent)); Dictionary.Add("PriceListItemAttributesChanged", typeof(PriceListItemAttributesChangedDomainEvent)); Dictionary.Add("MeetingFeeCanceled", typeof(MeetingFeeCanceledDomainEvent)); Dictionary.Add("MeetingFeeCreated", typeof(MeetingFeeCreatedDomainEvent)); Dictionary.Add("MeetingFeeExpired", typeof(MeetingFeeExpiredDomainEvent)); Dictionary.Add("MeetingFeePaid", typeof(MeetingFeePaidDomainEvent)); Dictionary.Add("MeetingFeePaymentCreated", typeof(MeetingFeePaymentCreatedDomainEvent)); Dictionary.Add("MeetingFeePaymentExpired", typeof(MeetingFeePaymentExpiredDomainEvent)); Dictionary.Add("MeetingFeePaymentPaid", typeof(MeetingFeePaymentPaidDomainEvent)); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/AggregateStore/ICheckpointStore.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.AggregateStore { public interface ICheckpointStore { long? GetCheckpoint(SubscriptionCode subscriptionCode); Task StoreCheckpoint(SubscriptionCode subscriptionCode, long checkpoint); } } ================================================ FILE: src/Modules/Payments/Infrastructure/AggregateStore/SqlOutboxAccessor.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.AggregateStore { public class SqlOutboxAccessor : IOutbox { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly List _messages; public SqlOutboxAccessor(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; _messages = []; } public void Add(OutboxMessage message) { _messages.Add(message); } public async Task Save() { if (_messages.Any()) { using var connection = _sqlConnectionFactory.CreateNewConnection(); const string sql = "INSERT INTO [payments].[OutboxMessages] " + "([Id], [OccurredOn], [Type], [Data]) VALUES " + "(@Id, @OccurredOn, @Type, @Data)"; foreach (var message in _messages) { await connection.ExecuteScalarAsync(sql, new { message.Id, message.OccurredOn, message.Type, message.Data }); } _messages.Clear(); } } } } ================================================ FILE: src/Modules/Payments/Infrastructure/AggregateStore/SqlServerCheckpointStore.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.AggregateStore { public class SqlServerCheckpointStore : ICheckpointStore { private readonly ISqlConnectionFactory _sqlConnectionFactory; public SqlServerCheckpointStore(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public long? GetCheckpoint(SubscriptionCode subscriptionCode) { using var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = """ SELECT [SubscriptionCheckpoint].Position FROM [payments].[SubscriptionCheckpoints] AS [SubscriptionCheckpoint] WHERE [Code] = @Code """; var checkpoint = connection.QuerySingleOrDefault( sql, new { Code = subscriptionCode }); return checkpoint; } public async Task StoreCheckpoint(SubscriptionCode subscriptionCode, long checkpoint) { var actualCheckpoint = GetCheckpoint(subscriptionCode); using var connection = _sqlConnectionFactory.GetOpenConnection(); if (actualCheckpoint == null) { await connection.ExecuteScalarAsync( "INSERT INTO [payments].[SubscriptionCheckpoints] VALUES (@Code, @Position)", new { Code = subscriptionCode, Position = checkpoint }); } else { await connection.ExecuteScalarAsync( "UPDATE [payments].[SubscriptionCheckpoints] " + "SET Position = @Position " + "WHERE Code = @Code", new { Code = subscriptionCode, Position = checkpoint }); } } } } ================================================ FILE: src/Modules/Payments/Infrastructure/AggregateStore/SqlStreamAggregateStore.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Serialization; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using Newtonsoft.Json; using SqlStreamStore; using SqlStreamStore.Streams; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.AggregateStore { public class SqlStreamAggregateStore : IAggregateStore { private readonly IStreamStore _streamStore; private readonly List _appendedChanges; private readonly List _aggregatesToSave; public SqlStreamAggregateStore( ISqlConnectionFactory sqlConnectionFactory, IStreamStore streamStore) { _appendedChanges = []; _aggregatesToSave = []; _streamStore = streamStore; } public async Task Save() { foreach (var aggregateToSave in _aggregatesToSave) { await _streamStore.AppendToStream( GetStreamId(aggregateToSave.Aggregate), aggregateToSave.Aggregate.Version, aggregateToSave.Messages.ToArray()); } _aggregatesToSave.Clear(); } public async Task Load(AggregateId aggregateId) where T : AggregateRoot { var streamId = GetStreamId(aggregateId); List domainEvents = []; ReadStreamPage readStreamPage; int position = StreamVersion.Start; int take = 100; do { readStreamPage = await _streamStore.ReadStreamForwards(streamId, position, take); var messages = readStreamPage.Messages; foreach (var streamMessage in messages) { Type type = DomainEventTypeMappings.Dictionary[streamMessage.Type]; var jsonData = await streamMessage.GetJsonData(); var domainEvent = JsonConvert.DeserializeObject(jsonData, type) as IDomainEvent; domainEvents.Add(domainEvent); } position += take; } while (!readStreamPage.IsEnd); if (!domainEvents.Any()) { return null; } var aggregate = (T)Activator.CreateInstance(typeof(T), true); aggregate.Load(domainEvents); return aggregate; } public List GetChanges() { return _appendedChanges; } public void AppendChanges(T aggregate) where T : AggregateRoot { _aggregatesToSave.Add(new AggregateToSave(aggregate, CreateStreamMessages(aggregate).ToList())); } public void ClearChanges() { _appendedChanges.Clear(); } private class AggregateToSave { public AggregateToSave(AggregateRoot aggregate, List messages) { Aggregate = aggregate; Messages = messages; } public AggregateRoot Aggregate { get; } public List Messages { get; } } private NewStreamMessage[] CreateStreamMessages( T aggregate) where T : AggregateRoot { List newStreamMessages = []; var domainEvents = aggregate.GetDomainEvents(); foreach (var domainEvent in domainEvents) { string jsonData = JsonConvert.SerializeObject(domainEvent, new JsonSerializerSettings { ContractResolver = new AllPropertiesContractResolver() }); var message = new NewStreamMessage( domainEvent.Id, MapDomainEventToType(domainEvent), jsonData); newStreamMessages.Add(message); _appendedChanges.Add(domainEvent); } return newStreamMessages.ToArray(); } private string MapDomainEventToType(IDomainEvent domainEvent) { foreach (var key in DomainEventTypeMappings.Dictionary.Keys) { if (DomainEventTypeMappings.Dictionary[key] == domainEvent.GetType()) { return key; } } throw new ArgumentException("Invalid Domain Event type", nameof(domainEvent)); } private static string GetStreamId(T aggregate) where T : AggregateRoot { return $"{aggregate.GetType().Name}-{aggregate.Id:N}"; } private static string GetStreamId(AggregateId aggregateId) where T : AggregateRoot => $"{typeof(T).Name}-{aggregateId.Value:N}"; } } ================================================ FILE: src/Modules/Payments/Infrastructure/AggregateStore/SubscriptionCode.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.AggregateStore { public enum SubscriptionCode { /// /// All. /// All } } ================================================ FILE: src/Modules/Payments/Infrastructure/AggregateStore/SubscriptionsManager.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Projections; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration; using Newtonsoft.Json; using SqlStreamStore; using SqlStreamStore.Streams; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.AggregateStore { public class SubscriptionsManager { private readonly IStreamStore _streamStore; public SubscriptionsManager( IStreamStore streamStore) { _streamStore = streamStore; } public void Start() { long? actualPosition; using (var scope = PaymentsCompositionRoot.BeginLifetimeScope()) { var checkpointStore = scope.Resolve(); actualPosition = checkpointStore.GetCheckpoint(SubscriptionCode.All); } _streamStore.SubscribeToAll(actualPosition, StreamMessageReceived); } public void Stop() { _streamStore.Dispose(); } private static async Task StreamMessageReceived( IAllStreamSubscription subscription, StreamMessage streamMessage, CancellationToken cancellationToken) { var type = DomainEventTypeMappings.Dictionary[streamMessage.Type]; var jsonData = await streamMessage.GetJsonData(cancellationToken); var domainEvent = JsonConvert.DeserializeObject(jsonData, type) as IDomainEvent; using var scope = PaymentsCompositionRoot.BeginLifetimeScope(); var projectors = scope.Resolve>(); foreach (var projector in projectors) { await projector.Project(domainEvent); } var checkpointStore = scope.Resolve(); await checkpointStore.StoreCheckpoint(SubscriptionCode.All, streamMessage.Position); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/CompanyName.MyMeetings.Modules.Payments.Infrastructure.csproj ================================================  ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/AllConstructorFinder.cs ================================================ using System.Collections.Concurrent; using System.Reflection; using Autofac.Core.Activators.Reflection; namespace CompanyName.MyMeetings.Modules.Payments.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); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Assemblies.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration { internal static class Assemblies { public static readonly Assembly Application = typeof(IPaymentsModule).Assembly; } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Authentication/AuthenticationModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Authentication { internal class AuthenticationModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType() .As() .InstancePerLifetimeScope(); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Authentication/PayerContext.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Authentication { internal class PayerContext : IPayerContext { private readonly IExecutionContextAccessor _executionContextAccessor; public PayerContext(IExecutionContextAccessor executionContextAccessor) { this._executionContextAccessor = executionContextAccessor; } public PayerId PayerId => new PayerId(_executionContextAccessor.UserId); } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/DataAccess/DataAccessModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Projections; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.AggregateStore; using Microsoft.Extensions.Logging; using SqlStreamStore; namespace CompanyName.MyMeetings.Modules.Payments.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(a => new MsSqlStreamStore(new MsSqlStreamStoreSettings(_databaseConnectionString) { Schema = DatabaseSchema.Name })) .As(); builder.RegisterType() .As() .InstancePerLifetimeScope(); builder.RegisterType() .As() .InstancePerLifetimeScope(); var applicationAssembly = typeof(IProjector).Assembly; builder.RegisterAssemblyTypes(applicationAssembly) .Where(type => type.Name.EndsWith("Projector")) .AsImplementedInterfaces() .InstancePerLifetimeScope() .FindConstructorsWith(new AllConstructorFinder()); builder.RegisterType() .As() .SingleInstance(); var infrastructureAssembly = ThisAssembly; builder.RegisterAssemblyTypes(infrastructureAssembly) .Where(type => type.Name.EndsWith("Repository")) .AsImplementedInterfaces() .InstancePerLifetimeScope() .FindConstructorsWith(new AllConstructorFinder()); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/DatabaseSchema.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration { public class DatabaseSchema { public const string Name = "payments"; } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Email/EmailModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Email { internal class EmailModule : Module { private readonly EmailsConfiguration _configuration; public EmailModule(EmailsConfiguration configuration) { _configuration = configuration; } protected override void Load(ContainerBuilder builder) { builder.RegisterType() .As() .WithParameter("configuration", _configuration) .InstancePerLifetimeScope(); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/EventsBus/EventsBusModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.Payments.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) .As(); } else { builder.RegisterType() .As() .SingleInstance(); } } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Administration.IntegrationEvents.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents; using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using Serilog; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.EventsBus { public static class EventsBusStartup { public static void Initialize( ILogger logger) { SubscribeToIntegrationEvents(logger); } private static void SubscribeToIntegrationEvents(ILogger logger) { var eventBus = PaymentsCompositionRoot.BeginLifetimeScope().Resolve(); SubscribeToIntegrationEvent(eventBus, logger); SubscribeToIntegrationEvent(eventBus, logger); 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()); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs ================================================ 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.Payments.Infrastructure.Configuration.EventsBus { internal class IntegrationEventGenericHandler : IIntegrationEventHandler where T : IntegrationEvent { public async Task Handle(T @event) { using (var scope = PaymentsCompositionRoot.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 [payments].[InboxMessages] (Id, OccurredOn, Type, Data) " + "VALUES (@Id, @OccurredOn, @Type, @Data)"; await connection.ExecuteScalarAsync(sql, new { @event.Id, @event.OccurredOn, type, data }); } } } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Logging/LoggingModule.cs ================================================ using Autofac; using Serilog; namespace CompanyName.MyMeetings.Modules.Payments.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(); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Mediation/MediatorModule.cs ================================================ using System.Reflection; using Autofac; using Autofac.Core; using Autofac.Features.Variance; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using FluentValidation; using MediatR; using MediatR.Pipeline; namespace CompanyName.MyMeetings.Modules.Payments.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; } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/PaymentsCompositionRoot.cs ================================================ using Autofac; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration { public static class PaymentsCompositionRoot { private static IContainer _container; public static void SetContainer(IContainer container) { _container = container; } public static ILifetimeScope BeginLifetimeScope() { return _container.BeginLifetimeScope(); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/PaymentsStartup.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeeAsPaid; using CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeePaymentAsPaid; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.CreateSubscription; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionPaymentAsPaid; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionRenewalPaymentAsPaid; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.RenewSubscription; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.AggregateStore; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Authentication; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.DataAccess; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Email; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.EventsBus; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Logging; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Mediation; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.Outbox; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Quartz; using ILogger = Serilog.ILogger; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration { public class PaymentsStartup { private static IContainer _container; private static SubscriptionsManager _subscriptionsManager; public static void Initialize( string connectionString, IExecutionContextAccessor executionContextAccessor, ILogger logger, EmailsConfiguration emailsConfiguration, IEventsBus eventsBus, bool runQuartz = true, long? internalProcessingPoolingInterval = null) { var moduleLogger = logger.ForContext("Module", "Payments"); ConfigureCompositionRoot(connectionString, executionContextAccessor, moduleLogger, emailsConfiguration, eventsBus, runQuartz); if (runQuartz) { QuartzStartup.Initialize(moduleLogger, internalProcessingPoolingInterval); } EventsBusStartup.Initialize(moduleLogger); } public static void Stop() { _subscriptionsManager.Stop(); QuartzStartup.StopQuartz(); } private static void ConfigureCompositionRoot( string connectionString, IExecutionContextAccessor executionContextAccessor, ILogger logger, EmailsConfiguration emailsConfiguration, IEventsBus eventsBus, bool runQuartz = true) { var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterModule(new LoggingModule(logger)); var loggerFactory = new Serilog.Extensions.Logging.SerilogLoggerFactory(logger); containerBuilder.RegisterModule(new DataAccessModule(connectionString, loggerFactory)); containerBuilder.RegisterModule(new ProcessingModule()); containerBuilder.RegisterModule(new EmailModule(emailsConfiguration)); containerBuilder.RegisterModule(new EventsBusModule(eventsBus)); containerBuilder.RegisterModule(new MediatorModule()); containerBuilder.RegisterModule(new AuthenticationModule()); BiDictionary domainNotificationsMap = new BiDictionary(); domainNotificationsMap.Add("MeetingFeePaidNotification", typeof(MeetingFeePaidNotification)); domainNotificationsMap.Add("MeetingFeePaymentPaidNotification", typeof(MeetingFeePaymentPaidNotification)); domainNotificationsMap.Add("SubscriptionCreatedNotification", typeof(SubscriptionCreatedNotification)); domainNotificationsMap.Add("SubscriptionPaymentPaidNotification", typeof(SubscriptionPaymentPaidNotification)); domainNotificationsMap.Add("SubscriptionRenewalPaymentPaidNotification", typeof(SubscriptionRenewalPaymentPaidNotification)); domainNotificationsMap.Add("SubscriptionRenewedNotification", typeof(SubscriptionRenewedNotification)); containerBuilder.RegisterModule(new OutboxModule(domainNotificationsMap)); if (runQuartz) { containerBuilder.RegisterModule(new QuartzModule()); } containerBuilder.RegisterInstance(executionContextAccessor); _container = containerBuilder.Build(); PaymentsCompositionRoot.SetContainer(_container); RunEventsProjectors(); } private static void RunEventsProjectors() { _subscriptionsManager = _container.Resolve(); _subscriptionsManager.Start(); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/CommandsExecutor.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing { internal static class CommandsExecutor { internal static async Task Execute(ICommand command) { using (var scope = PaymentsCompositionRoot.BeginLifetimeScope()) { var mediator = scope.Resolve(); await mediator.Send(command); } } internal static async Task Execute(ICommand command) { using (var scope = PaymentsCompositionRoot.BeginLifetimeScope()) { var mediator = scope.Resolve(); return await mediator.Send(command); } } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/Inbox/InboxMessageDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.Inbox { public class InboxMessageDto { public Guid Id { get; set; } public string Type { get; set; } public string Data { get; set; } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.Inbox { public class ProcessInboxCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using Dapper; using MediatR; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.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 [payments].[InboxMessages] AS [InboxMessage] WHERE [InboxMessage].[ProcessedDate] IS NULL ORDER BY [InboxMessage].[OccurredOn] """; var messages = await connection.QueryAsync(sql); const string sqlUpdateProcessedDate = """ UPDATE [payments].[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.ExecuteScalarAsync(sqlUpdateProcessedDate, new { Date = DateTime.UtcNow, message.Id }); } } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/Inbox/ProcessInboxJob.cs ================================================ using Quartz; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.Inbox { [DisallowConcurrentExecution] public class ProcessInboxJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ProcessInboxCommand()); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Serialization; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using Dapper; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.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 [payments].[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 [payments].[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() }) }); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.InternalCommands { internal class ProcessInternalCommandsCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using Dapper; using Newtonsoft.Json; using Polly; namespace CompanyName.MyMeetings.Modules.Payments.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 [payments].[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 [payments].[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; } } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsJob.cs ================================================ using Quartz; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.InternalCommands { [DisallowConcurrentExecution] public class ProcessInternalCommandsJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ProcessInternalCommandsCommand()); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using Serilog; using Serilog.Context; using Serilog.Core; using Serilog.Events; namespace CompanyName.MyMeetings.Modules.Payments.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); return; } 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))); } } } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using Serilog; using Serilog.Context; using Serilog.Core; using Serilog.Events; namespace CompanyName.MyMeetings.Modules.Payments.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) { if (command is IRecurringCommand) { return await _decorated.Handle(command, 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))); } } } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/Outbox/OutboxMessageDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.Outbox { public class OutboxMessageDto { public Guid Id { get; set; } public string Type { get; set; } public string Data { get; set; } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/Outbox/OutboxModule.cs ================================================ 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.Payments.Infrastructure.AggregateStore; namespace CompanyName.MyMeetings.Modules.Payments.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(); this.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"); } } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.Outbox { public class ProcessOutboxCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using Dapper; using MediatR; using Newtonsoft.Json; using Serilog.Context; using Serilog.Core; using Serilog.Events; namespace CompanyName.MyMeetings.Modules.Payments.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 [payments].[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 [payments].[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()}"))); } } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxJob.cs ================================================ using Quartz; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.Outbox { [DisallowConcurrentExecution] public class ProcessOutboxJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ProcessOutboxCommand()); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/PaymentsUnitOfWork.cs ================================================ using System.Transactions; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using Dapper; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing { public class PaymentsUnitOfWork : IUnitOfWork { private readonly IOutbox _outbox; private readonly IAggregateStore _aggregateStore; private readonly IDomainEventsDispatcher _domainEventsDispatcher; private readonly ISqlConnectionFactory _sqlConnectionFactory; public PaymentsUnitOfWork( IOutbox outbox, IAggregateStore aggregateStore, IDomainEventsDispatcher domainEventsDispatcher, ISqlConnectionFactory sqlConnectionFactory) { _outbox = outbox; _aggregateStore = aggregateStore; _domainEventsDispatcher = domainEventsDispatcher; _sqlConnectionFactory = sqlConnectionFactory; } public async Task CommitAsync( CancellationToken cancellationToken = default, Guid? internalCommandId = null) { await _domainEventsDispatcher.DispatchEventsAsync(); var options = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }; using var transaction = new TransactionScope( TransactionScopeOption.Required, options, TransactionScopeAsyncFlowOption.Enabled); await _aggregateStore.Save(); await _outbox.Save(); if (internalCommandId.HasValue) { using var connection = _sqlConnectionFactory.CreateNewConnection(); await connection.ExecuteScalarAsync( "UPDATE payments.InternalCommands " + "SET ProcessedDate = @Date " + "WHERE Id = @Id", new { Date = DateTime.UtcNow, Id = internalCommandId.Value }); } transaction.Complete(); return 0; } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/ProcessingModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.AggregateStore; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.InternalCommands; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.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.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()); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing { internal class UnitOfWorkCommandHandlerDecorator : ICommandHandler where T : ICommand { private readonly ICommandHandler _decorated; private readonly IUnitOfWork _unitOfWork; public UnitOfWorkCommandHandlerDecorator( ICommandHandler decorated, IUnitOfWork unitOfWork) { _decorated = decorated; _unitOfWork = unitOfWork; } public async Task Handle(T command, CancellationToken cancellationToken) { await this._decorated.Handle(command, cancellationToken); Guid? internalCommandId = null; if (command is InternalCommandBase) { internalCommandId = command.Id; } await this._unitOfWork.CommitAsync(cancellationToken, internalCommandId); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerWithResultDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing { internal class UnitOfWorkCommandHandlerWithResultDecorator : ICommandHandler where T : ICommand { private readonly ICommandHandler _decorated; private readonly IUnitOfWork _unitOfWork; public UnitOfWorkCommandHandlerWithResultDecorator( ICommandHandler decorated, IUnitOfWork unitOfWork) { _decorated = decorated; _unitOfWork = unitOfWork; } public async Task Handle(T command, CancellationToken cancellationToken) { var result = await this._decorated.Handle(command, cancellationToken); Guid? internalCommandId = null; if (command is InternalCommandBase) { internalCommandId = command.Id; } await _unitOfWork.CommitAsync(cancellationToken, internalCommandId); return result; } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/ValidationCommandHandlerDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using FluentValidation; namespace CompanyName.MyMeetings.Modules.Payments.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); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Processing/ValidationCommandHandlerWithResultDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using FluentValidation; namespace CompanyName.MyMeetings.Modules.Payments.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); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Quartz/Jobs/ExpireSubscriptionPaymentsJob.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscriptionPayments; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing; using Quartz; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Quartz.Jobs { public class ExpireSubscriptionPaymentsJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ExpireSubscriptionPaymentsCommand()); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Quartz/Jobs/ExpireSubscriptionsJob.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscriptions; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing; using Quartz; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Quartz.Jobs { public class ExpireSubscriptionsJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ExpireSubscriptionsCommand()); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Quartz/QuartzModule.cs ================================================ using Autofac; using Quartz; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Quartz { public class QuartzModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { builder.RegisterAssemblyTypes(ThisAssembly) .Where(x => typeof(IJob).IsAssignableFrom(x)).InstancePerDependency(); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Quartz/QuartzStartup.cs ================================================ using System.Collections.Specialized; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.Inbox; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.InternalCommands; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.Outbox; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Quartz.Jobs; using Quartz; using Quartz.Impl; using Quartz.Logging; using Serilog; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Quartz { internal static class QuartzStartup { private static IScheduler _scheduler; internal static void StopQuartz() { _scheduler.Shutdown(); } internal static void Initialize(ILogger logger, long? internalProcessingPoolingInterval) { logger.Information("Quartz starting..."); var schedulerConfiguration = new NameValueCollection(); schedulerConfiguration.Add("quartz.scheduler.instanceName", "Meetings"); ISchedulerFactory schedulerFactory = new StdSchedulerFactory(schedulerConfiguration); _scheduler = schedulerFactory.GetScheduler().GetAwaiter().GetResult(); LogProvider.SetCurrentLogProvider(new SerilogLogProvider(logger)); _scheduler.Start().GetAwaiter().GetResult(); ScheduleProcessOutboxJob(_scheduler, internalProcessingPoolingInterval); ScheduleProcessInboxJob(_scheduler, internalProcessingPoolingInterval); ScheduleProcessInternalCommandsJob(_scheduler, internalProcessingPoolingInterval); ScheduleExpireSubscriptionsJob(_scheduler); ScheduleExpireSubscriptionPaymentsJob(_scheduler); logger.Information("Quartz started."); } private static void ScheduleExpireSubscriptionPaymentsJob(IScheduler scheduler) { var expireSubscriptionPaymentsJob = JobBuilder.Create().Build(); var triggerCommandsProcessing = TriggerBuilder .Create() .StartNow() .WithCronSchedule("0/59 * * ? * *") .Build(); scheduler.ScheduleJob(expireSubscriptionPaymentsJob, triggerCommandsProcessing).GetAwaiter().GetResult(); } private static void ScheduleExpireSubscriptionsJob(IScheduler scheduler) { var expireSubscriptionsJob = JobBuilder.Create().Build(); var triggerCommandsProcessing = TriggerBuilder .Create() .StartNow() .WithCronSchedule("0/59 * * ? * *") .Build(); scheduler.ScheduleJob(expireSubscriptionsJob, triggerCommandsProcessing).GetAwaiter().GetResult(); } private static void ScheduleProcessInternalCommandsJob( IScheduler scheduler, long? internalProcessingPoolingInterval) { var processInternalCommandsJob = JobBuilder.Create().Build(); ITrigger triggerCommandsProcessing; if (internalProcessingPoolingInterval.HasValue) { triggerCommandsProcessing = TriggerBuilder .Create() .StartNow() .WithSimpleSchedule(x => x.WithInterval(TimeSpan.FromMilliseconds(internalProcessingPoolingInterval.Value)) .RepeatForever()) .Build(); } else { triggerCommandsProcessing = TriggerBuilder .Create() .StartNow() .WithCronSchedule("0/2 * * ? * *") .Build(); } scheduler.ScheduleJob(processInternalCommandsJob, triggerCommandsProcessing).GetAwaiter().GetResult(); } private static void ScheduleProcessInboxJob(IScheduler scheduler, long? internalProcessingPoolingInterval) { 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(); } private static void ScheduleProcessOutboxJob(IScheduler scheduler, long? internalProcessingPoolingInterval) { 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(); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/Configuration/Quartz/SerilogLogProvider.cs ================================================ using Quartz.Logging; using Serilog; namespace CompanyName.MyMeetings.Modules.Payments.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(); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/InternalCommands/InternalCommandEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure.InternalCommands { internal class InternalCommandEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("InternalCommands", "payments"); builder.HasKey(b => b.Id); builder.Property(b => b.Id).ValueGeneratedNever(); } } } ================================================ FILE: src/Modules/Payments/Infrastructure/PaymentsModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing; using MediatR; namespace CompanyName.MyMeetings.Modules.Payments.Infrastructure { public class PaymentsModule : IPaymentsModule { 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 = PaymentsCompositionRoot.BeginLifetimeScope()) { var mediator = scope.Resolve(); return await mediator.Send(query); } } } } ================================================ FILE: src/Modules/Payments/IntegrationEvents/CompanyName.MyMeetings.Modules.Payments.IntegrationEvents.csproj ================================================  ================================================ FILE: src/Modules/Payments/IntegrationEvents/MeetingFeePaidIntegrationEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationEvents { public class MeetingFeePaidIntegrationEvent : IntegrationEvent { public MeetingFeePaidIntegrationEvent( Guid id, DateTime occurredOn, Guid payerId, Guid meetingId) : base(id, occurredOn) { PayerId = payerId; MeetingId = meetingId; } public Guid PayerId { get; } public Guid MeetingId { get; } } } ================================================ FILE: src/Modules/Payments/IntegrationEvents/SubscriptionExpirationDateChangedIntegrationEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationEvents { public class SubscriptionExpirationDateChangedIntegrationEvent : IntegrationEvent { public SubscriptionExpirationDateChangedIntegrationEvent( Guid id, DateTime occurredOn, Guid payerId, DateTime expirationDate) : base(id, occurredOn) { PayerId = payerId; ExpirationDate = expirationDate; } public Guid PayerId { get; } public DateTime ExpirationDate { get; } } } ================================================ FILE: src/Modules/Payments/Tests/ArchTests/Application/ApplicationTests.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Payments.Application.Configuration.Queries; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.ArchTests.SeedWork; using FluentValidation; using MediatR; using NetArchTest.Rules; using Newtonsoft.Json; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.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); } } } ================================================ FILE: src/Modules/Payments/Tests/ArchTests/CompanyName.MyMeetings.Modules.Payments.ArchTests.csproj ================================================  ================================================ FILE: src/Modules/Payments/Tests/ArchTests/Domain/DomainTests.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.ArchTests.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.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() { List excludedFromCheck = [typeof(PriceList)]; var valueObjects = Types.InAssembly(DomainAssembly) .That() .Inherit(typeof(ValueObject)) .GetTypes() .Where(x => !excludedFromCheck.Contains(x)) .ToList(); 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); } } } ================================================ FILE: src/Modules/Payments/Tests/ArchTests/Module/LayersTests.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.ArchTests.SeedWork; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.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); } } } ================================================ FILE: src/Modules/Payments/Tests/ArchTests/SeedWork/TestBase.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.ArchTests.SeedWork { public abstract class TestBase { protected static Assembly ApplicationAssembly => typeof(CommandBase).Assembly; protected static Assembly DomainAssembly => typeof(Subscription).Assembly; protected static Assembly InfrastructureAssembly => typeof(PaymentsStartup).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); } } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/AssemblyInfo.cs ================================================ using NUnit.Framework; [assembly: NonParallelizable] [assembly: LevelOfParallelism(1)] namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests { public class AssemblyInfo { } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/CompanyName.MyMeetings.Modules.Payments.IntegrationTests.csproj ================================================  ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/MeetingFees/MeetingFeesTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.CreateMeetingFee; using CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.CreateMeetingFeePayment; using CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.GetMeetingFees; using CompanyName.MyMeetings.Modules.Payments.Application.MeetingFees.MarkMeetingFeePaymentAsPaid; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFeePayments; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees; using CompanyName.MyMeetings.Modules.Payments.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests.MeetingFees { [NonParallelizable] [TestFixture] public class MeetingFeesTests : TestBase { [Test] public async Task Create_Then_Pay_MeetingFee_Test() { var payerId = Guid.NewGuid(); var meetingId = Guid.NewGuid(); var meetingFeeId = await PaymentsModule.ExecuteCommandAsync(new CreateMeetingFeeCommand( Guid.NewGuid(), payerId, meetingId, 100, "PLN")); var meetingFees = await GetEventually(new GetMeetingFeesProbe(PaymentsModule, meetingId, x => x != null && x.Count > 0), 5000); Assert.That(meetingFees[0].Status, Is.EqualTo(MeetingFeeStatus.WaitingForPayment.Code)); var meetingFeePaymentId = await PaymentsModule.ExecuteCommandAsync(new CreateMeetingFeePaymentCommand(meetingFeeId)); await PaymentsModule.ExecuteCommandAsync(new MarkMeetingFeePaymentAsPaidCommand(meetingFeePaymentId)); meetingFees = await GetEventually(new GetMeetingFeesProbe(PaymentsModule, meetingId, x => x.Any(y => y.Status == MeetingFeePaymentStatus.Paid.Code)), 10000); Assert.That(meetingFees[0].Status, Is.EqualTo(MeetingFeeStatus.Paid.Code)); } private class GetMeetingFeesProbe : IProbe> { private readonly IPaymentsModule _paymentsModule; private readonly Guid _meetingId; private readonly Func, bool> _condition; public GetMeetingFeesProbe( IPaymentsModule paymentsModule, Guid meetingId, Func, bool> condition) { _paymentsModule = paymentsModule; _meetingId = meetingId; _condition = condition; } public bool IsSatisfied(List sample) { return sample != null && _condition(sample); } public async Task> GetSampleAsync() { return await _paymentsModule.ExecuteQueryAsync(new GetMeetingFeesQuery(_meetingId)); } public string DescribeFailureTo() { return $"Cannot get meeting fees for MeetingId: {_meetingId}"; } } } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/Payers/PayerSampleData.cs ================================================ namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests.Payers { public struct PayerSampleData { public static Guid Id => Guid.Parse("ed2991be-a8d2-4b09-9c1f-0d76e27b96ef"); public static string Login => "jdoe"; public static string Email => "johndoe@mail.com"; public static string FirstName => "John"; public static string LastName => "Doe"; public static string Name => "John Doe"; } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/Payers/PayerTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.Payers.CreatePayer; using CompanyName.MyMeetings.Modules.Payments.Application.Payers.GetPayer; using CompanyName.MyMeetings.Modules.Payments.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests.Payers { [NonParallelizable] [TestFixture] public class PayerTests : TestBase { [Test] public async Task CreatePayer_Test() { var payerId = await PaymentsModule.ExecuteCommandAsync( new CreatePayerCommand( Guid.NewGuid(), PayerSampleData.Id, PayerSampleData.Login, PayerSampleData.Email, PayerSampleData.FirstName, PayerSampleData.LastName, PayerSampleData.Name)); var payer = await GetEventually( new GetPayerProbe(PaymentsModule, payerId), 5000); Assert.That(payer.Id, Is.EqualTo(PayerSampleData.Id)); Assert.That(payer.Login, Is.EqualTo(PayerSampleData.Login)); Assert.That(payer.Name, Is.EqualTo(PayerSampleData.Name)); Assert.That(payer.FirstName, Is.EqualTo(PayerSampleData.FirstName)); Assert.That(payer.LastName, Is.EqualTo(PayerSampleData.LastName)); Assert.That(payer.Email, Is.EqualTo(PayerSampleData.Email)); } private class GetPayerProbe : IProbe { private readonly IPaymentsModule _paymentsModule; private readonly Guid _payerId; public GetPayerProbe( IPaymentsModule paymentsModule, Guid payerId) { _paymentsModule = paymentsModule; _payerId = payerId; } public bool IsSatisfied(PayerDto sample) { return sample != null; } public async Task GetSampleAsync() { return await _paymentsModule.ExecuteQueryAsync(new GetPayerQuery(_payerId)); } public string DescribeFailureTo() { return $"Cannot get payer for PayerId: {_payerId}"; } } } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/PriceList/PriceListHelper.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.CreatePriceListItem; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.GetPriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using CompanyName.MyMeetings.Modules.Payments.IntegrationTests.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests.PriceList { public static class PriceListHelper { public static async Task AddPriceListItems(IPaymentsModule paymentsModule) { await paymentsModule.ExecuteCommandAsync(new CreatePriceListItemCommand( SubscriptionPeriod.Month.Code, PriceListItemCategory.New.Code, "PL", 60, "PLN")); await paymentsModule.ExecuteCommandAsync(new CreatePriceListItemCommand( SubscriptionPeriod.HalfYear.Code, PriceListItemCategory.New.Code, "PL", 320, "PLN")); await paymentsModule.ExecuteCommandAsync(new CreatePriceListItemCommand( SubscriptionPeriod.Month.Code, PriceListItemCategory.New.Code, "US", 15, "USD")); await paymentsModule.ExecuteCommandAsync(new CreatePriceListItemCommand( SubscriptionPeriod.HalfYear.Code, PriceListItemCategory.New.Code, "US", 80, "USD")); await paymentsModule.ExecuteCommandAsync(new CreatePriceListItemCommand( SubscriptionPeriod.HalfYear.Code, PriceListItemCategory.Renewal.Code, "PL", 320, "PLN")); await TestBase.GetEventually(new GetPriceListProbe(paymentsModule, x => x.Count == 5), 5000); } private class GetPriceListProbe : IProbe> { private readonly IPaymentsModule _paymentsModule; private readonly Func, bool> _condition; public GetPriceListProbe( IPaymentsModule paymentsModule, Func, bool> condition) { _paymentsModule = paymentsModule; _condition = condition; } public bool IsSatisfied(List sample) { return sample != null && _condition(sample); } public async Task> GetSampleAsync() { return await _paymentsModule.ExecuteQueryAsync(new GetPriceListItemsQuery()); } public string DescribeFailureTo() { return "Cannot get price list for specified condition"; } } } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/SeedWork/EventsBusMock.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests.SeedWork { public class EventsBusMock : IEventsBus { private readonly IList _publishedEvents; public EventsBusMock() { _publishedEvents = new List(); } public void Dispose() { } public Task Publish(T @event) where T : IntegrationEvent { _publishedEvents.Add(@event); return Task.CompletedTask; } public T GetLastPublishedEvent() where T : IntegrationEvent { return _publishedEvents.OfType().Last(); } public void Subscribe(IIntegrationEventHandler handler) where T : IntegrationEvent { } public void StartConsuming() { } } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/SeedWork/ExecutionContextMock.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests.SeedWork { public class ExecutionContextMock : IExecutionContextAccessor { public ExecutionContextMock(Guid userId) { UserId = userId; } public Guid UserId { get; } public Guid CorrelationId { get; } public bool IsAvailable { get; } } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs ================================================ using System.Data; using System.Reflection; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.CreateSubscription; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration.Processing.Outbox; using Dapper; using MediatR; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests.SeedWork { public class OutboxMessagesHelper { public static async Task> GetOutboxMessages(IDbConnection connection) { const string sql = """ SELECT [OutboxMessage].[Id], [OutboxMessage].[Type], [OutboxMessage].[Data] FROM [payments].[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(SubscriptionCreatedNotification)).GetType(message.Type); return JsonConvert.DeserializeObject(message.Data, type) as T; } } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/SeedWork/TestBase.cs ================================================ 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.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Infrastructure; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration; using Dapper; using MediatR; using NSubstitute; using NUnit.Framework; using Serilog; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests.SeedWork { public class TestBase { protected string ConnectionString { get; private set; } protected ILogger Logger { get; private set; } protected IPaymentsModule PaymentsModule { get; private set; } protected IEmailSender EmailSender { get; private set; } protected EmailsConfiguration EmailsConfiguration { get; private set; } protected EventsBusMock EventsBus { get; private set; } protected ExecutionContextMock ExecutionContext { 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 = new LoggerConfiguration() .Enrich.FromLogContext() .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{Module}] [{Context}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); EmailSender = Substitute.For(); EmailsConfiguration = new EmailsConfiguration("from@email.com"); EventsBus = new EventsBusMock(); ExecutionContext = new ExecutionContextMock(Guid.NewGuid()); PaymentsStartup.Initialize( ConnectionString, ExecutionContext, Logger, EmailsConfiguration, EventsBus, true); PaymentsModule = new PaymentsModule(); } public static async Task GetEventually(IProbe probe, int timeout) where T : class { var poller = new Poller(timeout); return await poller.GetAsync(probe); } [TearDown] public void AfterEachTest() { PaymentsStartup.Stop(); SystemClock.Reset(); } 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()); } } protected static async Task AssertEventually(IProbe probe, int timeout) { await new Poller(timeout).CheckAsync(probe); } private static async Task ClearDatabase(IDbConnection connection) { const string sql = "DELETE FROM [payments].[InboxMessages] " + "DELETE FROM [payments].[InternalCommands] " + "DELETE FROM [payments].[OutboxMessages] " + "DELETE FROM payments.Messages " + "DBCC CHECKIDENT ('payments.Messages', RESEED, 0); " + "DELETE FROM payments.Streams " + "DBCC CHECKIDENT ('payments.Streams', RESEED, 0); " + "DELETE FROM payments.SubscriptionDetails " + "DELETE FROM [payments].[SubscriptionCheckpoints] " + "DELETE FROM [payments].PriceListItems " + "DELETE FROM [payments].SubscriptionPayments " + "DELETE FROM [payments].MeetingFees " + "DELETE FROM [payments].[Payers] "; await connection.ExecuteScalarAsync(sql); } } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/Subscriptions/BuySubscriptionTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.BuySubscription; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetPayerSubscription; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionDetails; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionPaymentAsPaid; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using CompanyName.MyMeetings.Modules.Payments.IntegrationTests.PriceList; using CompanyName.MyMeetings.Modules.Payments.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests.Subscriptions { [TestFixture] public class BuySubscriptionTests : TestBase { [Test] public async Task BuySubscription_Test() { await PriceListHelper.AddPriceListItems(PaymentsModule); var subscriptionPaymentId = await PaymentsModule.ExecuteCommandAsync( new BuySubscriptionCommand( SubscriptionPeriod.Month.Code, "PL", 60, "PLN")); var subscriptionPayments = await GetEventually( new GetSubscriptionPaymentsProbe( PaymentsModule, ExecutionContext.UserId, x => true), 10000); var subscriptionPayment = subscriptionPayments.Single(x => x.PaymentId == subscriptionPaymentId); Assert.That(subscriptionPayment.Status, Is.EqualTo(SubscriptionPaymentStatus.WaitingForPayment.Code)); await PaymentsModule.ExecuteCommandAsync( new MarkSubscriptionPaymentAsPaidCommand(subscriptionPaymentId)); var subscription = await GetEventually( new GetPayerSubscriptionProbe( PaymentsModule, ExecutionContext.UserId), 10000); Assert.That(subscription.Period, Is.EqualTo(SubscriptionPeriod.Month.Code)); Assert.That(subscription.Status, Is.EqualTo(SubscriptionStatus.Active.Code)); } private class GetPayerSubscriptionProbe : IProbe { private readonly IPaymentsModule _paymentsModule; public GetPayerSubscriptionProbe( IPaymentsModule paymentsModule, Guid payerId) { _paymentsModule = paymentsModule; } public bool IsSatisfied(SubscriptionDetailsDto sample) { return sample != null; } public async Task GetSampleAsync() { return await _paymentsModule.ExecuteQueryAsync(new GetAuthenticatedPayerSubscriptionQuery()); } public string DescribeFailureTo() => "Subscription read model is not in expected state"; } } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/Subscriptions/GetSubscriptionPaymentsProbe.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionPayments; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests.Subscriptions { internal class GetSubscriptionPaymentsProbe : IProbe> { private readonly IPaymentsModule _paymentsModule; private readonly Guid _payerId; private readonly Func, bool> _condition; public GetSubscriptionPaymentsProbe( IPaymentsModule paymentsModule, Guid payerId, Func, bool> condition) { _paymentsModule = paymentsModule; _payerId = payerId; _condition = condition; } public bool IsSatisfied(List sample) { return sample != null && _condition(sample); } public async Task> GetSampleAsync() { return await _paymentsModule.ExecuteQueryAsync(new GetSubscriptionPaymentsQuery(_payerId)); } public string DescribeFailureTo() { return $"Cannot get subscription payments for PayerId: {_payerId}"; } } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/Subscriptions/SubscriptionLifecycleTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.BuySubscription; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.BuySubscriptionRenewal; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscriptions; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionDetails; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionPaymentAsPaid; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionRenewalPaymentAsPaid; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using CompanyName.MyMeetings.Modules.Payments.IntegrationTests.PriceList; using CompanyName.MyMeetings.Modules.Payments.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests.Subscriptions { [NonParallelizable] [TestFixture] [Ignore("Sometimes fails, to check why")] public class SubscriptionLifecycleTests : TestBase { [Test] public async Task Subscription_Buy_ThenRenew_ThenExpire_Test() { await PriceListHelper.AddPriceListItems(PaymentsModule); DateTime referenceDate = new DateTime(2020, 6, 15); SystemClock.Set(referenceDate); var subscriptionPaymentId = await PaymentsModule.ExecuteCommandAsync( new BuySubscriptionCommand( "Month", "PL", 60, "PLN")); var subscriptionPayments = await GetEventually( new GetSubscriptionPaymentsProbe( PaymentsModule, ExecutionContext.UserId, x => true), 10000); var subscriptionPayment = subscriptionPayments.Single(x => x.PaymentId == subscriptionPaymentId); Assert.That(subscriptionPayment.Status, Is.EqualTo(SubscriptionPaymentStatus.WaitingForPayment.Code)); await PaymentsModule.ExecuteCommandAsync( new MarkSubscriptionPaymentAsPaidCommand(subscriptionPaymentId)); subscriptionPayments = await GetEventually( new GetSubscriptionPaymentsProbe( PaymentsModule, ExecutionContext.UserId, x => x.Any(y => y.Status == SubscriptionPaymentStatus.Paid.Code && y.SubscriptionId.HasValue)), 10000); subscriptionPayment = subscriptionPayments.Single(x => x.PaymentId == subscriptionPaymentId); var subscriptionId = subscriptionPayment.SubscriptionId.GetValueOrDefault(); var subscription = await GetEventually( new GetSubscriptionDetailsProbe( PaymentsModule, subscriptionId, x => x.SubscriptionId == subscriptionId && x.Status == SubscriptionStatus.Active.Code && x.Period == SubscriptionPeriod.Month.Code), 5000); Assert.That(subscriptionPayments[0].Status, Is.EqualTo(SubscriptionPaymentStatus.Paid.Code)); Assert.That(subscription.ExpirationDate, Is.EqualTo(referenceDate.AddMonths(1))); var subscriptionRenewalPaymentId = await PaymentsModule.ExecuteCommandAsync( new BuySubscriptionRenewalCommand( subscriptionId, "HalfYear", "PL", 320, "PLN")); subscriptionPayments = await GetEventually( new GetSubscriptionPaymentsProbe( PaymentsModule, ExecutionContext.UserId, x => true), 10000); var renewalPayment = subscriptionPayments .Single(x => x.PaymentId == subscriptionRenewalPaymentId); Assert.That(renewalPayment.Status, Is.EqualTo(SubscriptionRenewalPaymentStatus.WaitingForPayment.Code)); await PaymentsModule.ExecuteCommandAsync( new MarkSubscriptionRenewalPaymentAsPaidCommand(subscriptionRenewalPaymentId)); subscriptionPayments = await GetEventually( new GetSubscriptionPaymentsProbe( PaymentsModule, ExecutionContext.UserId, x => x.Any(y => y.PaymentId == subscriptionRenewalPaymentId)), 10000); renewalPayment = subscriptionPayments .Single(x => x.PaymentId == subscriptionRenewalPaymentId); subscription = await GetEventually( new GetSubscriptionDetailsProbe( PaymentsModule, subscriptionId, x => x.SubscriptionId == subscriptionId && x.Period == SubscriptionPeriod.GetName(SubscriptionPeriod.HalfYear.Code) && x.Status == SubscriptionStatus.Active.Code), 5000); Assert.That(renewalPayment.Status, Is.EqualTo(SubscriptionRenewalPaymentStatus.Paid.Code)); Assert.That(subscription.ExpirationDate, Is.EqualTo(referenceDate.AddMonths(7))); SystemClock.Set(referenceDate.AddMonths(7).AddDays(1)); await PaymentsModule.ExecuteCommandAsync(new ExpireSubscriptionsCommand()); subscription = await GetEventually( new GetSubscriptionDetailsProbe( PaymentsModule, subscriptionId, x => x.SubscriptionId == subscriptionId && x.Status == SubscriptionStatus.Expired.Code), 10000); Assert.That(subscription, Is.Not.Null); } private class GetSubscriptionDetailsProbe : IProbe { private readonly IPaymentsModule _paymentsModule; private readonly Guid _subscriptionId; private readonly Func _condition; public GetSubscriptionDetailsProbe( IPaymentsModule paymentsModule, Guid subscriptionId, Func condition) { _paymentsModule = paymentsModule; _subscriptionId = subscriptionId; _condition = condition; } public bool IsSatisfied(SubscriptionDetailsDto sample) { return sample != null && _condition(sample); } public async Task GetSampleAsync() { return await _paymentsModule.ExecuteQueryAsync(new GetSubscriptionDetailsQuery(_subscriptionId)); } public string DescribeFailureTo() => "Subscription read model is not in expected state"; } } } ================================================ FILE: src/Modules/Payments/Tests/IntegrationTests/Subscriptions/SubscriptionPaymentsTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.BuySubscription; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.ExpireSubscriptionPayments; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments; using CompanyName.MyMeetings.Modules.Payments.IntegrationTests.PriceList; using CompanyName.MyMeetings.Modules.Payments.IntegrationTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.IntegrationTests.Subscriptions { [NonParallelizable] [TestFixture] public class SubscriptionPaymentsTests : TestBase { [Test] public async Task SubscriptionPayment_Expiration_Test() { SystemClock.Set(new DateTime(2020, 6, 15, 10, 0, 0)); await PriceListHelper.AddPriceListItems(PaymentsModule); await PaymentsModule.ExecuteCommandAsync( new BuySubscriptionCommand( "Month", "PL", 60, "PLN")); var subscriptionPayments = await GetEventually( new GetSubscriptionPaymentsProbe( PaymentsModule, ExecutionContext.UserId, x => true), 10000); Assert.That(subscriptionPayments[0].Status, Is.EqualTo(SubscriptionPaymentStatus.WaitingForPayment.Code)); SystemClock.Set(new DateTime(2020, 6, 15, 10, 21, 0)); await PaymentsModule.ExecuteCommandAsync( new ExpireSubscriptionPaymentsCommand()); await GetEventually( new GetSubscriptionPaymentsProbe( PaymentsModule, ExecutionContext.UserId, x => x.Any(y => y.Status == SubscriptionPaymentStatus.Expired.Code)), 10000); } private class GetSubscriptionPaymentsProbe : IProbe> { private readonly IPaymentsModule _paymentsModule; private readonly Guid _payerId; private readonly Func, bool> _condition; public GetSubscriptionPaymentsProbe( IPaymentsModule paymentsModule, Guid payerId, Func, bool> condition) { _paymentsModule = paymentsModule; _payerId = payerId; _condition = condition; } public bool IsSatisfied(List sample) { return sample != null && _condition(sample); } public async Task> GetSampleAsync() { return await _paymentsModule.ExecuteQueryAsync(new GetSubscriptionPaymentsQuery(_payerId)); } public string DescribeFailureTo() { return $"Cannot get subscription payments for PayerId: {_payerId}"; } } } } ================================================ FILE: src/Modules/Payments/Tests/UnitTests/CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.csproj ================================================  ================================================ FILE: src/Modules/Payments/Tests/UnitTests/Payers/PayerTests.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.Payers.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.Payers { [TestFixture] public class PayerTests : TestBase { [Test] public void CreatePayer_IsSuccessful() { var payerId = Guid.NewGuid(); var payer = Payer.Create( payerId, "payerLogin", "payerEmail@mail.com", "John", "Doe", "John Doe"); var payerCreated = AssertPublishedDomainEvent(payer); Assert.That(payerCreated.PayerId, Is.EqualTo(payerId)); } } } ================================================ FILE: src/Modules/Payments/Tests/UnitTests/PriceListItems/PriceListItemTests.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.PriceListItems { [TestFixture] public class PriceListItemTests : TestBase { [Test] public void CreatePriceListItem_IsSuccessful() { // Act var priceListItem = PriceListItem.Create( "BRA", SubscriptionPeriod.Month, PriceListItemCategory.New, MoneyValue.Of(50, "BRL")); // Assert var priceListItemCreated = AssertPublishedDomainEvent(priceListItem); Assert.That(priceListItemCreated.PriceListItemId, Is.EqualTo(priceListItem.Id)); } [Test] public void ActivatePriceListItem_IsSuccessful() { // Arrange var priceListItem = PriceListItem.Create( "BRA", SubscriptionPeriod.Month, PriceListItemCategory.New, MoneyValue.Of(50, "BRL")); priceListItem.Deactivate(); // Act priceListItem.Activate(); // Assert var priceListItemActivated = AssertPublishedDomainEvent(priceListItem); Assert.That(priceListItemActivated.PriceListItemId, Is.EqualTo(priceListItem.Id)); } [Test] public void ActivatePriceListItem_WhenItemIsActive_ThenActivationIgnored() { // Arrange var priceListItem = PriceListItem.Create( "BRA", SubscriptionPeriod.Month, PriceListItemCategory.New, MoneyValue.Of(50, "BRL")); // Act priceListItem.Activate(); // Assert AssertDomainEventNotPublished(priceListItem); } [Test] public void DeactivatePriceListItem_IsSuccessful() { // Arrange var priceListItem = PriceListItem.Create( "BRA", SubscriptionPeriod.Month, PriceListItemCategory.New, MoneyValue.Of(50, "BRL")); // Act priceListItem.Deactivate(); // Assert var priceListItemActivated = AssertPublishedDomainEvent(priceListItem); Assert.That(priceListItemActivated.PriceListItemId, Is.EqualTo(priceListItem.Id)); } [Test] public void DeactivatePriceListItem_WhenItemNotActive_ThenDeactivationIgnored() { // Arrange var priceListItem = PriceListItem.Create( "BRA", SubscriptionPeriod.Month, PriceListItemCategory.New, MoneyValue.Of(50, "BRL")); priceListItem.Deactivate(); // Act priceListItem.Deactivate(); // Assert var priceListItemActivatedEvents = AssertPublishedDomainEvents(priceListItem); Assert.That(priceListItemActivatedEvents.Count, Is.EqualTo(1)); } [Test] public void ChangePriceListItem_IsSuccessful() { // Arrange var priceListItem = PriceListItem.Create( "BRA", SubscriptionPeriod.Month, PriceListItemCategory.New, MoneyValue.Of(50, "BRL")); // Act priceListItem.ChangeAttributes( "ARG", SubscriptionPeriod.HalfYear, PriceListItemCategory.Renewal, MoneyValue.Of(25, "ARS")); // Assert var priceListItemAttributesChangedEvent = AssertPublishedDomainEvent(priceListItem); Assert.That(priceListItemAttributesChangedEvent.PriceListItemId, Is.EqualTo(priceListItem.Id)); } } } ================================================ FILE: src/Modules/Payments/Tests/UnitTests/SeedWork/DomainEventsTestHelper.cs ================================================ using System.Collections; using System.Reflection; using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Payments.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; } } } ================================================ FILE: src/Modules/Payments/Tests/UnitTests/SeedWork/TestBase.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.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 T AssertPublishedDomainEvent(AggregateRoot aggregate) where T : IDomainEvent { var domainEvent = aggregate.GetDomainEvents().OfType().SingleOrDefault(); if (domainEvent == null) { throw new Exception($"{typeof(T).Name} event not published"); } return domainEvent; } public static void AssertDomainEventNotPublished(AggregateRoot aggregate) where T : IDomainEvent { var domainEvent = aggregate.GetDomainEvents().OfType().SingleOrDefault(); Assert.That(domainEvent, Is.Null); } public static List AssertPublishedDomainEvents(Entity aggregate) where T : IDomainEvent { var domainEvents = DomainEventsTestHelper.GetAllDomainEvents(aggregate).OfType().ToList(); if (!domainEvents.Any()) { throw new Exception($"{typeof(T).Name} event not published"); } return domainEvents; } public static List AssertPublishedDomainEvents(AggregateRoot aggregate) where T : IDomainEvent { var domainEvents = aggregate.GetDomainEvents().OfType().ToList(); if (!domainEvents.Any()) { throw new Exception($"{typeof(T).Name} event was not published"); } return domainEvents; } 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); } } [TearDown] public void AfterEachTest() { SystemClock.Reset(); } } } ================================================ FILE: src/Modules/Payments/Tests/UnitTests/SubscriptionPayments/SubscriptionPaymentTests.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments.Rules; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.SubscriptionPayments { [TestFixture] public class SubscriptionPaymentTests : SubscriptionPaymentTestsBase { [Test] public void BuySubscription_IsSuccessful() { // Arrange var subscriptionPaymentTestData = CreateSubscriptionPaymentTestData(); // Act var subscriptionPayment = SubscriptionPayment.Buy( subscriptionPaymentTestData.PayerId, SubscriptionPeriod.Month, "PL", MoneyValue.Of(60, "PLN"), subscriptionPaymentTestData.PriceList); // Assert AssertPublishedDomainEvent(subscriptionPayment); } [Test] public void BuySubscriptionRenewal_WhenPriceDoesNotExist_IsNotPossible() { // Arrange var subscriptionPaymentTestData = CreateSubscriptionPaymentTestData(); // Act & Assert AssertBrokenRule(() => { SubscriptionPayment.Buy( subscriptionPaymentTestData.PayerId, SubscriptionPeriod.Month, "PL", MoneyValue.Of(50, "PLN"), subscriptionPaymentTestData.PriceList); }); } [Test] public void PaySubscription_IsSuccessful() { // Arrange var subscriptionPaymentTestData = CreateSubscriptionPaymentTestData(); var subscriptionPayment = SubscriptionPayment.Buy( subscriptionPaymentTestData.PayerId, SubscriptionPeriod.Month, "PL", MoneyValue.Of(60, "PLN"), subscriptionPaymentTestData.PriceList); // Act subscriptionPayment.MarkAsPaid(); // Assert AssertPublishedDomainEvent(subscriptionPayment); } [Test] public void ExpireSubscription_IsSuccessful() { // Arrange var subscriptionPaymentTestData = CreateSubscriptionPaymentTestData(); var subscriptionPayment = SubscriptionPayment.Buy( subscriptionPaymentTestData.PayerId, SubscriptionPeriod.Month, "PL", MoneyValue.Of(60, "PLN"), subscriptionPaymentTestData.PriceList); // Act subscriptionPayment.Expire(); // Assert AssertPublishedDomainEvent(subscriptionPayment); } } } ================================================ FILE: src/Modules/Payments/Tests/UnitTests/SubscriptionPayments/SubscriptionPaymentTestsBase.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.PricingStrategies; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.SubscriptionPayments { public class SubscriptionPaymentTestsBase : TestBase { protected class SubscriptionPaymentTestData { public SubscriptionPaymentTestData(PriceList priceList, PayerId payerId, SubscriptionId subscriptionId) { PriceList = priceList; PayerId = payerId; SubscriptionId = subscriptionId; } internal PriceList PriceList { get; } internal PayerId PayerId { get; } internal SubscriptionId SubscriptionId { get; } } protected SubscriptionPaymentTestData CreateSubscriptionPaymentTestData() { var payerId = new PayerId(Guid.NewGuid()); var subscriptionId = new SubscriptionId(Guid.NewGuid()); var priceList = CreatePriceList(); var subscriptionPaymentTestData = new SubscriptionPaymentTestData( priceList, payerId, subscriptionId); return subscriptionPaymentTestData; } private PriceList CreatePriceList() { var priceListItem = new PriceListItemData( "PL", SubscriptionPeriod.Month, MoneyValue.Of(60, "PLN"), PriceListItemCategory.New); List priceListItems = [priceListItem]; var priceList = PriceList.Create(priceListItems, new DirectValueFromPriceListPricingStrategy(priceListItems)); return priceList; } } } ================================================ FILE: src/Modules/Payments/Tests/UnitTests/SubscriptionRenewalPayments/SubscriptionRenewalPaymentTests.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments.Rules; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.SubscriptionRenewalPayments { [TestFixture] public class SubscriptionRenewalPaymentTests : SubscriptionRenewalPaymentTestsBase { [Test] public void BuySubscriptionRenewal_IsSuccessful() { // Arrange var subscriptionRenewalPaymentTestData = CreateSubscriptionRenewalPaymentTestData(); // Act var subscriptionRenewalPayment = SubscriptionRenewalPayment.Buy( subscriptionRenewalPaymentTestData.PayerId, subscriptionRenewalPaymentTestData.SubscriptionId, SubscriptionPeriod.Month, "PL", MoneyValue.Of(60, "PLN"), subscriptionRenewalPaymentTestData.PriceList); // Assert AssertPublishedDomainEvent(subscriptionRenewalPayment); } [Test] public void BuySubscriptionRenewal_WhenPriceDoesNotExist_IsNotPossible() { // Arrange var subscriptionRenewalPaymentTestData = CreateSubscriptionRenewalPaymentTestData(); // Act & Assert AssertBrokenRule(() => { SubscriptionRenewalPayment.Buy( subscriptionRenewalPaymentTestData.PayerId, subscriptionRenewalPaymentTestData.SubscriptionId, SubscriptionPeriod.Month, "PL", MoneyValue.Of(50, "PLN"), subscriptionRenewalPaymentTestData.PriceList); }); } [Test] public void PaySubscriptionRenewal_IsSuccessful() { // Arrange var subscriptionRenewalPaymentTestData = CreateSubscriptionRenewalPaymentTestData(); var subscriptionRenewalPayment = SubscriptionRenewalPayment.Buy( subscriptionRenewalPaymentTestData.PayerId, subscriptionRenewalPaymentTestData.SubscriptionId, SubscriptionPeriod.Month, "PL", MoneyValue.Of(60, "PLN"), subscriptionRenewalPaymentTestData.PriceList); // Act subscriptionRenewalPayment.MarkRenewalAsPaid(); // Assert AssertPublishedDomainEvent(subscriptionRenewalPayment); } } } ================================================ FILE: src/Modules/Payments/Tests/UnitTests/SubscriptionRenewalPayments/SubscriptionRenewalPaymentTestsBase.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems.PricingStrategies; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.SeedWork; namespace CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.SubscriptionRenewalPayments { public class SubscriptionRenewalPaymentTestsBase : TestBase { protected class SubscriptionRenewalPaymentTestData { public SubscriptionRenewalPaymentTestData(PriceList priceList, PayerId payerId, SubscriptionId subscriptionId) { PriceList = priceList; PayerId = payerId; SubscriptionId = subscriptionId; } internal PriceList PriceList { get; } internal PayerId PayerId { get; } internal SubscriptionId SubscriptionId { get; } } protected SubscriptionRenewalPaymentTestData CreateSubscriptionRenewalPaymentTestData() { var payerId = new PayerId(Guid.NewGuid()); var subscriptionId = new SubscriptionId(Guid.NewGuid()); var priceList = CreatePriceList(); var subscriptionRenewalPaymentTestData = new SubscriptionRenewalPaymentTestData( priceList, payerId, subscriptionId); return subscriptionRenewalPaymentTestData; } private PriceList CreatePriceList() { var priceListItem = new PriceListItemData( "PL", SubscriptionPeriod.Month, MoneyValue.Of(60, "PLN"), PriceListItemCategory.Renewal); List priceListItems = [priceListItem]; var priceList = PriceList.Create(priceListItems, new DirectValueFromPriceListPricingStrategy(priceListItems)); return priceList; } } } ================================================ FILE: src/Modules/Payments/Tests/UnitTests/Subscriptions/SubscriptionDateExpirationCalculatorTests.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.Subscriptions { [TestFixture] public class SubscriptionDateExpirationCalculatorTests { [Test] public void CalculateForNew_WhenPeriodMonthIsSelected_Test() { // Arrange SubscriptionPeriod period = SubscriptionPeriod.Month; SystemClock.Set(new DateTime(2020, 5, 11)); // Act DateTime expirationDate = SubscriptionDateExpirationCalculator.CalculateForNew(period); // Assert Assert.That(expirationDate, Is.EqualTo(new DateTime(2020, 6, 11))); } [Test] public void CalculateForNew_WhenPeriodHalfYearIsSelected_Test() { // Arrange SubscriptionPeriod period = SubscriptionPeriod.HalfYear; SystemClock.Set(new DateTime(2020, 5, 11)); // Act DateTime expirationDate = SubscriptionDateExpirationCalculator.CalculateForNew(period); // Assert Assert.That(expirationDate, Is.EqualTo(new DateTime(2020, 11, 11))); } [Test] public void CalculateForRenewal_WhenPeriodMonthIsSelected_AndExpireDateIsInTheFuture_ThenMonthsAreAddedToExpireDate() { // Arrange SubscriptionPeriod period = SubscriptionPeriod.Month; SystemClock.Set(new DateTime(2020, 5, 11)); DateTime expirationDate = new DateTime(2020, 7, 1); // Act expirationDate = SubscriptionDateExpirationCalculator.CalculateForRenewal(expirationDate, period); // Assert Assert.That(expirationDate, Is.EqualTo(new DateTime(2020, 8, 1))); } [Test] public void CalculateForRenewal_WhenPeriodMonthIsSelected_AndExpireDatePassed_ThenMonthsAreAddedToNow() { // Arrange SubscriptionPeriod period = SubscriptionPeriod.Month; SystemClock.Set(new DateTime(2020, 5, 11)); DateTime expirationDate = new DateTime(2020, 4, 1); // Act expirationDate = SubscriptionDateExpirationCalculator.CalculateForRenewal(expirationDate, period); // Assert Assert.That(expirationDate, Is.EqualTo(new DateTime(2020, 6, 11))); } } } ================================================ FILE: src/Modules/Payments/Tests/UnitTests/Subscriptions/SubscriptionTests.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Domain.Payers; using CompanyName.MyMeetings.Modules.Payments.Domain.SeedWork; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.SubscriptionRenewalPayments; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions.Events; using CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.Payments.Domain.UnitTests.Subscriptions { [TestFixture] public class SubscriptionTests : TestBase { [Test] public void CreateSubscription_IsSuccessful() { // Arrange var subscriptionPaymentSnapshot = new SubscriptionPaymentSnapshot( new SubscriptionPaymentId(Guid.NewGuid()), new PayerId(Guid.NewGuid()), SubscriptionPeriod.Month, "PL"); // Act var subscription = Subscription.Create(subscriptionPaymentSnapshot); // Assert AssertPublishedDomainEvent(subscription); } [Test] public void RenewSubscription_IsSuccessful() { // Arrange var subscriptionPaymentSnapshot = new SubscriptionPaymentSnapshot( new SubscriptionPaymentId(Guid.NewGuid()), new PayerId(Guid.NewGuid()), SubscriptionPeriod.Month, "PL"); var subscription = Subscription.Create(subscriptionPaymentSnapshot); var subscriptionRenewalPaymentSnapshot = new SubscriptionRenewalPaymentSnapshot( new SubscriptionRenewalPaymentId(Guid.NewGuid()), new PayerId(Guid.NewGuid()), SubscriptionPeriod.Month, "PL"); // Act subscription.Renew(subscriptionRenewalPaymentSnapshot); // Assert AssertPublishedDomainEvent(subscription); } [Test] public void ExpireSubscription_IsSuccessful() { // Arrange var referenceDate = DateTime.UtcNow; SystemClock.Set(referenceDate); var subscriptionPaymentSnapshot = new SubscriptionPaymentSnapshot( new SubscriptionPaymentId(Guid.NewGuid()), new PayerId(Guid.NewGuid()), SubscriptionPeriod.Month, "PL"); var subscription = Subscription.Create(subscriptionPaymentSnapshot); SystemClock.Set(referenceDate.AddMonths(1).AddMilliseconds(1)); // Act subscription.Expire(); // Assert AssertPublishedDomainEvent(subscription); } } } ================================================ FILE: src/Modules/Registrations/Application/CompanyName.MyMeetings.Modules.Registrations.Application.csproj ================================================  ================================================ FILE: src/Modules/Registrations/Application/Configuration/Commands/ICommandHandler.cs ================================================ 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 { } } ================================================ FILE: src/Modules/Registrations/Application/Configuration/Commands/ICommandsScheduler.cs ================================================ 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); } } ================================================ FILE: src/Modules/Registrations/Application/Configuration/Commands/InternalCommandBase.cs ================================================ 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; } } } ================================================ FILE: src/Modules/Registrations/Application/Configuration/Queries/IQueryHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Queries { public interface IQueryHandler : IRequestHandler where TQuery : IQuery { } } ================================================ FILE: src/Modules/Registrations/Application/Contracts/CommandBase.cs ================================================ 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; } } } ================================================ FILE: src/Modules/Registrations/Application/Contracts/CustomClaimTypes.cs ================================================ 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"; } } ================================================ FILE: src/Modules/Registrations/Application/Contracts/ICommand.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts { public interface ICommand : IRequest { Guid Id { get; } } public interface ICommand : IRequest { Guid Id { get; } } } ================================================ FILE: src/Modules/Registrations/Application/Contracts/IQuery.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts { public interface IQuery : IRequest { } } ================================================ FILE: src/Modules/Registrations/Application/Contracts/IRecurringCommand.cs ================================================ namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts { public interface IRecurringCommand { } } ================================================ FILE: src/Modules/Registrations/Application/Contracts/IRegistrationsModule.cs ================================================ namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts { public interface IRegistrationsModule { Task ExecuteCommandAsync(ICommand command); Task ExecuteCommandAsync(ICommand command); Task ExecuteQueryAsync(IQuery query); } } ================================================ FILE: src/Modules/Registrations/Application/Contracts/QueryBase.cs ================================================ 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; } } } ================================================ FILE: src/Modules/Registrations/Application/Contracts/Roles.cs ================================================ namespace CompanyName.MyMeetings.Modules.Registrations.Application.Contracts { public class Roles { public const string Admin = "Admin"; public const string User = "User"; } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.ConfirmUserRegistration { public class ConfirmUserRegistrationCommand : CommandBase { public ConfirmUserRegistrationCommand(Guid userRegistrationId) { UserRegistrationId = userRegistrationId; } public Guid UserRegistrationId { get; } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/ConfirmUserRegistrationCommandHandler.cs ================================================ 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(); } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/IUserCreator.cs ================================================ 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); } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotification.cs ================================================ 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) { } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/ConfirmUserRegistration/UserRegistrationConfirmedNotificationHandler.cs ================================================ 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); } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQuery.cs ================================================ using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.GetUserRegistration { public class GetUserRegistrationQuery : QueryBase { public GetUserRegistrationQuery(Guid userRegistrationId) { UserRegistrationId = userRegistrationId; } public Guid UserRegistrationId { get; } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/GetUserRegistrationQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Queries; namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.GetUserRegistration { internal class GetUserRegistrationQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetUserRegistrationQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public Task Handle(GetUserRegistrationQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); return UserRegistrationProvider.GetById(connection, query.UserRegistrationId); } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.GetUserRegistration { public class UserRegistrationDto { public Guid Id { get; set; } public string Login { get; set; } public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Name { get; set; } public string StatusCode { get; set; } public string Password { get; set; } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/GetUserRegistration/UserRegistrationProvider.cs ================================================ 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 }); } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredEnqueueEmailConfirmationHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.SendUserRegistrationConfirmationEmail; using MediatR; namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser { public class NewUserRegisteredEnqueueEmailConfirmationHandler : INotificationHandler { private readonly ICommandsScheduler _commandsScheduler; public NewUserRegisteredEnqueueEmailConfirmationHandler(ICommandsScheduler commandsScheduler) { _commandsScheduler = commandsScheduler; } public async Task Handle(NewUserRegisteredNotification notification, CancellationToken cancellationToken) { await _commandsScheduler.EnqueueAsync(new SendUserRegistrationConfirmationEmailCommand( Guid.NewGuid(), notification.DomainEvent.UserRegistrationId, notification.DomainEvent.Email, notification.DomainEvent.ConfirmLink)); } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredNotification.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser { public class NewUserRegisteredNotification : DomainNotificationBase { [JsonConstructor] public NewUserRegisteredNotification(NewUserRegisteredDomainEvent domainEvent, Guid id) : base(domainEvent, id) { } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/NewUserRegisteredPublishEventHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; using MediatR; namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser { public class NewUserRegisteredPublishEventHandler : INotificationHandler { private readonly IEventsBus _eventsBus; public NewUserRegisteredPublishEventHandler(IEventsBus eventsBus) { _eventsBus = eventsBus; } public async Task Handle(NewUserRegisteredNotification notification, CancellationToken cancellationToken) { await _eventsBus.Publish(new NewUserRegisteredIntegrationEvent( notification.Id, notification.DomainEvent.OccurredOn, notification.DomainEvent.UserRegistrationId.Value, notification.DomainEvent.Login, notification.DomainEvent.Email, notification.DomainEvent.FirstName, notification.DomainEvent.LastName, notification.DomainEvent.Name)); } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/PasswordManager.cs ================================================ 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; } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser { public class RegisterNewUserCommand : CommandBase { public RegisterNewUserCommand( string login, string password, string email, string firstName, string lastName, string confirmLink) { Login = login; Password = password; Email = email; FirstName = firstName; LastName = lastName; ConfirmLink = confirmLink; } public string Login { get; } public string Password { get; } public string Email { get; } public string FirstName { get; } public string LastName { get; } public string ConfirmLink { get; } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/RegisterNewUser/RegisterNewUserCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.RegisterNewUser { internal class RegisterNewUserCommandHandler : ICommandHandler { private readonly IUserRegistrationRepository _userRegistrationRepository; private readonly IUsersCounter _usersCounter; public RegisterNewUserCommandHandler( IUserRegistrationRepository userRegistrationRepository, IUsersCounter usersCounter) { _userRegistrationRepository = userRegistrationRepository; _usersCounter = usersCounter; } public async Task Handle(RegisterNewUserCommand command, CancellationToken cancellationToken) { var password = PasswordManager.HashPassword(command.Password); var userRegistration = UserRegistration.RegisterNewUser( command.Login, password, command.Email, command.FirstName, command.LastName, _usersCounter, command.ConfirmLink); await _userRegistrationRepository.AddAsync(userRegistration); return userRegistration.Id.Value; } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.SendUserRegistrationConfirmationEmail { public class SendUserRegistrationConfirmationEmailCommand : InternalCommandBase { [JsonConstructor] public SendUserRegistrationConfirmationEmailCommand( Guid id, UserRegistrationId userRegistrationId, string email, string confirmLink) : base(id) { UserRegistrationId = userRegistrationId; Email = email; ConfirmLink = confirmLink; } internal UserRegistrationId UserRegistrationId { get; } internal string Email { get; } internal string ConfirmLink { get; } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/SendUserRegistrationConfirmationEmail/SendUserRegistrationConfirmationEmailCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using CompanyName.MyMeetings.Modules.Registrations.Application.Configuration.Commands; namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations.SendUserRegistrationConfirmationEmail { internal class SendUserRegistrationConfirmationEmailCommandHandler : ICommandHandler { private readonly IEmailSender _emailSender; public SendUserRegistrationConfirmationEmailCommandHandler( IEmailSender emailSender) { _emailSender = emailSender; } public async Task Handle(SendUserRegistrationConfirmationEmailCommand command, CancellationToken cancellationToken) { string link = $"link"; string content = $"Welcome to MyMeetings application! Please confirm your registration using this {link}."; var emailMessage = new EmailMessage( command.Email, "MyMeetings - Please confirm your registration", content); await _emailSender.SendEmail(emailMessage); } } } ================================================ FILE: src/Modules/Registrations/Application/UserRegistrations/UsersCounter.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; using Dapper; namespace CompanyName.MyMeetings.Modules.Registrations.Application.UserRegistrations { public class UsersCounter : IUsersCounter { private readonly ISqlConnectionFactory _sqlConnectionFactory; public UsersCounter(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public int CountUsersWithLogin(string login) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = """ SELECT COUNT(*) FROM [registrations].[v_UserRegistrations] AS [UserRegistration] WHERE [UserRegistration].[Login] = @Login """; return connection.QuerySingle( sql, new { login }); } } } ================================================ FILE: src/Modules/Registrations/Domain/CompanyName.MyMeetings.Modules.Registrations.Domain.csproj ================================================  ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/Events/NewUserRegisteredDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events { public class NewUserRegisteredDomainEvent : DomainEventBase { public UserRegistrationId UserRegistrationId { get; } public string Login { get; } public string Email { get; } public string FirstName { get; } public string LastName { get; } public string Name { get; } public DateTime RegisterDate { get; } public string ConfirmLink { get; } public NewUserRegisteredDomainEvent( UserRegistrationId userRegistrationId, string login, string email, string firstName, string lastName, string name, DateTime registerDate, string confirmLink) { UserRegistrationId = userRegistrationId; Login = login; Email = email; FirstName = firstName; LastName = lastName; Name = name; RegisterDate = registerDate; ConfirmLink = confirmLink; } } } ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/Events/UserRegistrationConfirmedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events { public class UserRegistrationConfirmedDomainEvent : DomainEventBase { public UserRegistrationConfirmedDomainEvent(UserRegistrationId userRegistrationId) { UserRegistrationId = userRegistrationId; } public UserRegistrationId UserRegistrationId { get; } } } ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/Events/UserRegistrationExpiredDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events { public class UserRegistrationExpiredDomainEvent : DomainEventBase { public UserRegistrationExpiredDomainEvent(UserRegistrationId userRegistrationId) { UserRegistrationId = userRegistrationId; } public UserRegistrationId UserRegistrationId { get; } } } ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/IUserRegistrationRepository.cs ================================================ namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations { public interface IUserRegistrationRepository { Task AddAsync(UserRegistration userRegistration); Task GetByIdAsync(UserRegistrationId userRegistrationId); } } ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/IUsersCounter.cs ================================================ namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations { public interface IUsersCounter { int CountUsersWithLogin(string login); } } ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/Rules/UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules { public class UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule : IBusinessRule { private readonly UserRegistrationStatus _actualRegistrationStatus; internal UserCannotBeCreatedWhenRegistrationIsNotConfirmedRule(UserRegistrationStatus actualRegistrationStatus) { this._actualRegistrationStatus = actualRegistrationStatus; } public bool IsBroken() => _actualRegistrationStatus != UserRegistrationStatus.Confirmed; public string Message => "User cannot be created when registration is not confirmed"; } } ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/Rules/UserLoginMustBeUniqueRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules { public class UserLoginMustBeUniqueRule : IBusinessRule { private readonly IUsersCounter _usersCounter; private readonly string _login; internal UserLoginMustBeUniqueRule(IUsersCounter usersCounter, string login) { _usersCounter = usersCounter; _login = login; } public bool IsBroken() => _usersCounter.CountUsersWithLogin(_login) > 0; public string Message => "User Login must be unique"; } } ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedAfterExpirationRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules { public class UserRegistrationCannotBeConfirmedAfterExpirationRule : IBusinessRule { private readonly UserRegistrationStatus _actualRegistrationStatus; internal UserRegistrationCannotBeConfirmedAfterExpirationRule(UserRegistrationStatus actualRegistrationStatus) { this._actualRegistrationStatus = actualRegistrationStatus; } public bool IsBroken() => _actualRegistrationStatus == UserRegistrationStatus.Expired; public string Message => "User Registration cannot be confirmed because it is expired"; } } ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/Rules/UserRegistrationCannotBeConfirmedMoreThanOnceRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules { public class UserRegistrationCannotBeConfirmedMoreThanOnceRule : IBusinessRule { private readonly UserRegistrationStatus _actualRegistrationStatus; internal UserRegistrationCannotBeConfirmedMoreThanOnceRule(UserRegistrationStatus actualRegistrationStatus) { this._actualRegistrationStatus = actualRegistrationStatus; } public bool IsBroken() => _actualRegistrationStatus == UserRegistrationStatus.Confirmed; public string Message => "User Registration cannot be confirmed more than once"; } } ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/Rules/UserRegistrationCannotBeExpiredMoreThanOnceRule.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules { public class UserRegistrationCannotBeExpiredMoreThanOnceRule : IBusinessRule { private readonly UserRegistrationStatus _actualRegistrationStatus; internal UserRegistrationCannotBeExpiredMoreThanOnceRule(UserRegistrationStatus actualRegistrationStatus) { this._actualRegistrationStatus = actualRegistrationStatus; } public bool IsBroken() => _actualRegistrationStatus == UserRegistrationStatus.Expired; public string Message => "User Registration cannot be expired more than once"; } } ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/UserRegistration.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Events; using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations.Rules; namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations { public class UserRegistration : Entity, IAggregateRoot { public UserRegistrationId Id { get; private set; } private string _login; private string _password; private string _email; private string _firstName; private string _lastName; private string _name; private DateTime _registerDate; private UserRegistrationStatus _status; private DateTime? _confirmedDate; private UserRegistration() { // Only EF. } public static UserRegistration RegisterNewUser( string login, string password, string email, string firstName, string lastName, IUsersCounter usersCounter, string confirmLink) { return new UserRegistration(login, password, email, firstName, lastName, usersCounter, confirmLink); } private UserRegistration( string login, string password, string email, string firstName, string lastName, IUsersCounter usersCounter, string confirmLink) { this.CheckRule(new UserLoginMustBeUniqueRule(usersCounter, login)); this.Id = new UserRegistrationId(Guid.NewGuid()); _login = login; _password = password; _email = email; _firstName = firstName; _lastName = lastName; _name = $"{firstName} {lastName}"; _registerDate = DateTime.UtcNow; _status = UserRegistrationStatus.WaitingForConfirmation; this.AddDomainEvent(new NewUserRegisteredDomainEvent( this.Id, _login, _email, _firstName, _lastName, _name, _registerDate, confirmLink)); } public void Confirm() { this.CheckRule(new UserRegistrationCannotBeConfirmedMoreThanOnceRule(_status)); this.CheckRule(new UserRegistrationCannotBeConfirmedAfterExpirationRule(_status)); _status = UserRegistrationStatus.Confirmed; _confirmedDate = DateTime.UtcNow; this.AddDomainEvent(new UserRegistrationConfirmedDomainEvent(this.Id)); } public void Expire() { this.CheckRule(new UserRegistrationCannotBeExpiredMoreThanOnceRule(_status)); _status = UserRegistrationStatus.Expired; this.AddDomainEvent(new UserRegistrationExpiredDomainEvent(this.Id)); } } } ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/UserRegistrationId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations { public class UserRegistrationId : TypedIdValueBase { public UserRegistrationId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/Registrations/Domain/UserRegistrations/UserRegistrationStatus.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations { public class UserRegistrationStatus : ValueObject { public static UserRegistrationStatus WaitingForConfirmation => new UserRegistrationStatus(nameof(WaitingForConfirmation)); public static UserRegistrationStatus Confirmed => new UserRegistrationStatus(nameof(Confirmed)); public static UserRegistrationStatus Expired => new UserRegistrationStatus(nameof(Expired)); public string Value { get; } private UserRegistrationStatus(string value) { Value = value; } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/CompanyName.MyMeetings.Modules.Registrations.Infrastructure.csproj ================================================  ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/AllConstructorFinder.cs ================================================ 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); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Assemblies.cs ================================================ 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; } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Commands/ICommandHandler.cs ================================================ 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 { } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Commands/ICommandsScheduler.cs ================================================ 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); } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Commands/InternalCommandBase.cs ================================================ 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; } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/DataAccess/DataAccessModule.cs ================================================ 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()); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Domain/DomainModule.cs ================================================ 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 { internal class DomainModule : Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType() .As() .InstancePerLifetimeScope(); builder.RegisterType() .As() .InstancePerLifetimeScope(); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Email/EmailModule.cs ================================================ 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(); } } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/EventsBus/EventsBusModule.cs ================================================ 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(); } } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs ================================================ 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()); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs ================================================ 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 [registrations].[InboxMessages] (Id, OccurredOn, Type, Data) " + "VALUES (@Id, @OccurredOn, @Type, @Data)"; await connection.ExecuteScalarAsync(sql, new { @event.Id, @event.OccurredOn, type, data }); } } } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Logging/LoggingModule.cs ================================================ 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(); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Mediation/MediatorModule.cs ================================================ 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; } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/CommandsExecutor.cs ================================================ 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); } } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/IRecurringCommand.cs ================================================ namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing { public interface IRecurringCommand { } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/InboxMessageDto.cs ================================================ 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; } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Inbox { public class ProcessInboxCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs ================================================ 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 [registrations].[InboxMessages] AS [InboxMessage] WHERE [InboxMessage].[ProcessedDate] IS NULL ORDER BY [InboxMessage].[OccurredOn] """; var messages = await connection.QueryAsync(sql); const string sqlUpdateProcessedDate = """ UPDATE [registrations].[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 }); } } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/Inbox/ProcessInboxJob.cs ================================================ 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()); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs ================================================ 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 [registrations].[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 [registrations].[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() }) }); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.InternalCommands { internal class ProcessInternalCommandsCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs ================================================ 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 [registrations].[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 [registrations].[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; } } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsJob.cs ================================================ 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()); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs ================================================ 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); return; } 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))); } } } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs ================================================ 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) { if (command is IRecurringCommand) { return await _decorated.Handle(command, 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))); } } } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/OutboxMessageDto.cs ================================================ 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; } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/OutboxModule.cs ================================================ 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"); } } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommand.cs ================================================ using CompanyName.MyMeetings.Modules.Registrations.Application.Contracts; namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.Processing.Outbox { public class ProcessOutboxCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs ================================================ 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 [registrations].[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 [registrations].[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()}"))); } } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxJob.cs ================================================ 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()); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/ProcessingModule.cs ================================================ 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()); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerDecorator.cs ================================================ 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); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerWithResultDecorator.cs ================================================ 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; } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/ValidationCommandHandlerDecorator.cs ================================================ 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); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Processing/ValidationCommandHandlerWithResultDecorator.cs ================================================ 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); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Quartz/QuartzModule.cs ================================================ 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(); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Quartz/QuartzStartup.cs ================================================ 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."); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/Quartz/SerilogLogProvider.cs ================================================ 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(); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/RegistrationsCompositionRoot.cs ================================================ 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(); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/RegistrationsStartup.cs ================================================ 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.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; 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 CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Configuration.UserAccess; 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", "Registrations"); 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", "Registrations"))); 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()); containerBuilder.RegisterModule(new UserAccessAutofacModule()); var domainNotificationsMap = new BiDictionary(); domainNotificationsMap.Add("NewUserRegisteredNotification", typeof(NewUserRegisteredNotification)); domainNotificationsMap.Add("UserRegistrationConfirmedNotification", typeof(UserRegistrationConfirmedNotification)); 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); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Configuration/UserAccess/UserAccessAutofacModule.cs ================================================ 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(); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Domain/UserRegistrations/UserRegistrationEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.Registrations.Domain.UserRegistrations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.Registrations.Infrastructure.Domain.UserRegistrations { internal class UserRegistrationEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("UserRegistrations", "registrations"); builder.HasKey(x => x.Id); builder.Property("_login").HasColumnName("Login"); builder.Property("_email").HasColumnName("Email"); builder.Property("_password").HasColumnName("Password"); builder.Property("_firstName").HasColumnName("FirstName"); builder.Property("_lastName").HasColumnName("LastName"); builder.Property("_name").HasColumnName("Name"); builder.Property("_registerDate").HasColumnName("RegisterDate"); builder.Property("_confirmedDate").HasColumnName("ConfirmedDate"); builder.OwnsOne("_status", b => { b.Property(x => x.Value).HasColumnName("StatusCode"); }); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Domain/UserRegistrations/UserRegistrationRepository.cs ================================================ 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); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/InternalCommands/InternalCommandEntityTypeConfiguration.cs ================================================ 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(); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Outbox/OutboxAccessor.cs ================================================ 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. } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Outbox/OutboxMessageEntityTypeConfiguration.cs ================================================ 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(); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/RegistrationsContext.cs ================================================ 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()); } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/RegistrationsModule.cs ================================================ 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); } } } } ================================================ FILE: src/Modules/Registrations/Infrastructure/Users/UserAccessGateway.cs ================================================ 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) { await _userAccessModule.ExecuteCommandAsync(new CreateUserCommand( userRegistrationId, login, email, firstName, lastName, password)); } } ================================================ FILE: src/Modules/Registrations/IntegrationEvents/Class1.cs ================================================ namespace CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents; public class Class1 { } ================================================ FILE: src/Modules/Registrations/IntegrationEvents/CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents.csproj ================================================  ================================================ FILE: src/Modules/Registrations/IntegrationEvents/NewUserRegisteredIntegrationEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.Registrations.IntegrationEvents { public class NewUserRegisteredIntegrationEvent : IntegrationEvent { 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 NewUserRegisteredIntegrationEvent(Guid id, DateTime occurredOn, Guid userId, string login, string email, string firstName, string lastName, string name) : base(id, occurredOn) { UserId = userId; Login = login; Email = email; FirstName = firstName; LastName = lastName; Name = name; } } } ================================================ FILE: src/Modules/Registrations/Tests/ArchTests/Application/ApplicationTests.cs ================================================ 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); } } } ================================================ FILE: src/Modules/Registrations/Tests/ArchTests/CompanyName.MyMeetings.Modules.Registrations.ArchTests.csproj ================================================  ================================================ FILE: src/Modules/Registrations/Tests/ArchTests/Domain/DomainTests.cs ================================================ 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); } } } ================================================ FILE: src/Modules/Registrations/Tests/ArchTests/Module/LayersTests.cs ================================================ 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); } } } ================================================ FILE: src/Modules/Registrations/Tests/ArchTests/SeedWork/TestBase.cs ================================================ 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); } } } ================================================ FILE: src/Modules/Registrations/Tests/IntegrationTests/AssemblyInfo.cs ================================================ using NUnit.Framework; [assembly: NonParallelizable] [assembly: LevelOfParallelism(1)] namespace CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests { public class AssemblyInfo { } } ================================================ FILE: src/Modules/Registrations/Tests/IntegrationTests/CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.csproj ================================================  ================================================ FILE: src/Modules/Registrations/Tests/IntegrationTests/SeedWork/ExecutionContextMock.cs ================================================ 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; } } } ================================================ FILE: src/Modules/Registrations/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs ================================================ 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; } } } ================================================ FILE: src/Modules/Registrations/Tests/IntegrationTests/SeedWork/TestBase.cs ================================================ 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); } } } ================================================ FILE: src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/ConfirmUserRegistrationTests.cs ================================================ 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)); } } } ================================================ FILE: src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/SendUserRegistrationConfirmationEmailTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; 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.Registrations.IntegrationTests.UserRegistrations { [TestFixture] public class SendUserRegistrationConfirmationEmailTests : TestBase { [Test] public async Task SendUserRegistrationConfirmationEmail_Test() { var registrationId = Guid.NewGuid(); var confirmLink = "confirmLink/"; await RegistrationsModule.ExecuteCommandAsync(new SendUserRegistrationConfirmationEmailCommand( Guid.NewGuid(), new UserRegistrationId(registrationId), UserRegistrationSampleData.Email, confirmLink)); string link = $"link"; var content = $"Welcome to MyMeetings application! Please confirm your registration using this {link}."; var email = new EmailMessage( UserRegistrationSampleData.Email, "MyMeetings - Please confirm your registration", content); await EmailSender.Received(Quantity.Exactly(1)).SendEmail(email); } } } ================================================ FILE: src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/UserRegistrationSampleData.cs ================================================ namespace CompanyNames.MyMeetings.Modules.Registrations.IntegrationTests.UserRegistrations { public struct UserRegistrationSampleData { 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"; } } ================================================ FILE: src/Modules/Registrations/Tests/IntegrationTests/UserRegistrations/UserRegistrationTests.cs ================================================ using System.Data.SqlClient; 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.Registrations.IntegrationTests.UserRegistrations { [TestFixture] public class UserRegistrationTests : TestBase { [Test] public async Task RegisterNewUserCommand_Test() { var registrationId = await RegistrationsModule.ExecuteCommandAsync(new RegisterNewUserCommand( UserRegistrationSampleData.Login, UserRegistrationSampleData.Password, UserRegistrationSampleData.Email, UserRegistrationSampleData.FirstName, UserRegistrationSampleData.LastName, "confirmLink")); var userRegistration = await RegistrationsModule.ExecuteQueryAsync(new GetUserRegistrationQuery(registrationId)); Assert.That(userRegistration.Email, Is.EqualTo(UserRegistrationSampleData.Email)); Assert.That(userRegistration.Login, Is.EqualTo(UserRegistrationSampleData.Login)); Assert.That(userRegistration.FirstName, Is.EqualTo(UserRegistrationSampleData.FirstName)); Assert.That(userRegistration.LastName, Is.EqualTo(UserRegistrationSampleData.LastName)); var connection = new SqlConnection(ConnectionString); var messagesList = await OutboxMessagesHelper.GetOutboxMessages(connection); Assert.That(messagesList.Count, Is.EqualTo(1)); var newUserRegisteredNotification = await GetLastOutboxMessage(); Assert.That(newUserRegisteredNotification.DomainEvent.Login, Is.EqualTo(UserRegistrationSampleData.Login)); } } } ================================================ FILE: src/Modules/Registrations/Tests/UnitTests/CompanyName.MyMeetings.Modules.Registrations.Domain.UnitTests.csproj ================================================  ================================================ FILE: src/Modules/Registrations/Tests/UnitTests/SeedWork/DomainEventsTestHelper.cs ================================================ 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; } } } ================================================ FILE: src/Modules/Registrations/Tests/UnitTests/SeedWork/TestBase.cs ================================================ 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); } } } } ================================================ FILE: src/Modules/Registrations/Tests/UnitTests/UserRegistrations/UserRegistrationTests.cs ================================================ 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.Registrations.Domain.UnitTests.UserRegistrations { [TestFixture] public class UserRegistrationTests : TestBase { [Test] public void NewUserRegistration_WithUniqueLogin_IsSuccessful() { // Arrange var usersCounter = Substitute.For(); // Act var userRegistration = UserRegistration.RegisterNewUser( "login", "password", "test@email", "firstName", "lastName", usersCounter, "confirmLink"); // Assert var newUserRegisteredDomainEvent = AssertPublishedDomainEvent(userRegistration); Assert.That(newUserRegisteredDomainEvent.UserRegistrationId, Is.EqualTo(userRegistration.Id)); } [Test] public void NewUserRegistration_WithoutUniqueLogin_BreaksUserLoginMustBeUniqueRule() { // Arrange var usersCounter = Substitute.For(); usersCounter.CountUsersWithLogin("login").Returns(x => 1); // Assert AssertBrokenRule(() => { // Act UserRegistration.RegisterNewUser( "login", "password", "test@email", "firstName", "lastName", usersCounter, "confirmLink"); }); } [Test] public void ConfirmingUserRegistration_WhenWaitingForConfirmation_IsSuccessful() { var usersCounter = Substitute.For(); var registration = UserRegistration.RegisterNewUser( "login", "password", "test@email", "firstName", "lastName", usersCounter, "confirmLink"); registration.Confirm(); var userRegistrationConfirmedDomainEvent = AssertPublishedDomainEvent(registration); Assert.That(userRegistrationConfirmedDomainEvent.UserRegistrationId, Is.EqualTo(registration.Id)); } [Test] public void UserRegistration_WhenIsConfirmed_CannotBeConfirmedAgain() { var usersCounter = Substitute.For(); var registration = UserRegistration.RegisterNewUser( "login", "password", "test@email", "firstName", "lastName", usersCounter, "confirmLink"); registration.Confirm(); AssertBrokenRule(() => { registration.Confirm(); }); } [Test] public void UserRegistration_WhenIsExpired_CannotBeConfirmed() { var usersCounter = Substitute.For(); var registration = UserRegistration.RegisterNewUser( "login", "password", "test@email", "firstName", "lastName", usersCounter, "confirmLink"); registration.Expire(); AssertBrokenRule(() => { registration.Confirm(); }); } [Test] public void ExpiringUserRegistration_WhenWaitingForConfirmation_IsSuccessful() { var usersCounter = Substitute.For(); var registration = UserRegistration.RegisterNewUser( "login", "password", "test@email", "firstName", "lastName", usersCounter, "confirmLink"); registration.Expire(); var userRegistrationExpired = AssertPublishedDomainEvent(registration); Assert.That(userRegistrationExpired.UserRegistrationId, Is.EqualTo(registration.Id)); } [Test] public void UserRegistration_WhenIsExpired_CannotBeExpiredAgain() { var usersCounter = Substitute.For(); var registration = UserRegistration.RegisterNewUser( "login", "password", "test@email", "firstName", "lastName", usersCounter, "confirmLink"); registration.Expire(); 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(); }); // } } } ================================================ FILE: src/Modules/UserAccess/Application/Authentication/Authenticate/AuthenticateCommand.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication.Authenticate { public class AuthenticateCommand : CommandBase { public AuthenticateCommand(string login, string password) { Login = login; Password = password; } public string Login { get; } public string Password { get; } } } ================================================ FILE: src/Modules/UserAccess/Application/Authentication/Authenticate/AuthenticateCommandHandler.cs ================================================ using System.Security.Claims; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using Dapper; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication.Authenticate { internal class AuthenticateCommandHandler : ICommandHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; internal AuthenticateCommandHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(AuthenticateCommand request, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [User].[Id] as [{nameof(UserDto.Id)}], [User].[Login] as [{nameof(UserDto.Login)}], [User].[Name] as [{nameof(UserDto.Name)}], [User].[Email] as [{nameof(UserDto.Email)}], [User].[IsActive] as [{nameof(UserDto.IsActive)}], [User].[Password] as [{nameof(UserDto.Password)}] FROM [users].[v_Users] AS [User] WHERE [User].[Login] = @Login """; var user = await connection.QuerySingleOrDefaultAsync( sql, new { request.Login, }); if (user == null) { return new AuthenticationResult("Incorrect login or password"); } if (!user.IsActive) { return new AuthenticationResult("User is not active"); } if (!PasswordManager.VerifyHashedPassword(user.Password, request.Password)) { return new AuthenticationResult("Incorrect login or password"); } user.Claims = [ new Claim(CustomClaimTypes.Name, user.Name), new Claim(CustomClaimTypes.Email, user.Email) ]; return new AuthenticationResult(user); } } } ================================================ FILE: src/Modules/UserAccess/Application/Authentication/Authenticate/AuthenticateCommandValidator.cs ================================================ using FluentValidation; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication.Authenticate { internal class AuthenticateCommandValidator : AbstractValidator { public AuthenticateCommandValidator() { this.RuleFor(x => x.Login).NotEmpty().WithMessage("Login cannot be empty"); this.RuleFor(x => x.Password).NotEmpty().WithMessage("Password cannot be empty"); } } } ================================================ FILE: src/Modules/UserAccess/Application/Authentication/Authenticate/AuthenticationResult.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication.Authenticate { public class AuthenticationResult { public AuthenticationResult(string authenticationError) { IsAuthenticated = false; AuthenticationError = authenticationError; } public AuthenticationResult(UserDto user) { this.IsAuthenticated = true; this.User = user; } public bool IsAuthenticated { get; } public string AuthenticationError { get; } public UserDto User { get; } } } ================================================ FILE: src/Modules/UserAccess/Application/Authentication/Authenticate/PasswordManager.cs ================================================ using System.Runtime.CompilerServices; using System.Security.Cryptography; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication.Authenticate { 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; } } } ================================================ FILE: src/Modules/UserAccess/Application/Authentication/Authenticate/UserDto.cs ================================================ using System.Security.Claims; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication.Authenticate { public class UserDto { public Guid Id { get; set; } public string Login { get; set; } public bool IsActive { get; set; } public string Name { get; set; } public string Email { get; set; } public List Claims { get; set; } public string Password { get; set; } } } ================================================ FILE: src/Modules/UserAccess/Application/Authorization/GetAuthenticatedUserPermissions/GetAuthenticatedUserPermissionsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Authorization.GetUserPermissions; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authorization.GetAuthenticatedUserPermissions { public class GetAuthenticatedUserPermissionsQuery : QueryBase> { } } ================================================ FILE: src/Modules/UserAccess/Application/Authorization/GetAuthenticatedUserPermissions/GetAuthenticatedUserPermissionsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.UserAccess.Application.Authorization.GetUserPermissions; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authorization.GetAuthenticatedUserPermissions { internal class GetAuthenticatedUserPermissionsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly IExecutionContextAccessor _executionContextAccessor; public GetAuthenticatedUserPermissionsQueryHandler( ISqlConnectionFactory sqlConnectionFactory, IExecutionContextAccessor executionContextAccessor) { _sqlConnectionFactory = sqlConnectionFactory; _executionContextAccessor = executionContextAccessor; } public async Task> Handle(GetAuthenticatedUserPermissionsQuery request, CancellationToken cancellationToken) { if (!_executionContextAccessor.IsAvailable) { return []; } var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [UserPermission].[PermissionCode] AS [{nameof(UserPermissionDto.Code)}] FROM [users].[v_UserPermissions] AS [UserPermission] WHERE [UserPermission].UserId = @UserId """; var permissions = await connection.QueryAsync( sql, new { _executionContextAccessor.UserId }); return permissions.AsList(); } } } ================================================ FILE: src/Modules/UserAccess/Application/Authorization/GetUserPermissions/GetUserPermissionsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authorization.GetUserPermissions { public class GetUserPermissionsQuery : QueryBase> { public GetUserPermissionsQuery(Guid userId) { UserId = userId; } public Guid UserId { get; } } } ================================================ FILE: src/Modules/UserAccess/Application/Authorization/GetUserPermissions/GetUserPermissionsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authorization.GetUserPermissions { internal class GetUserPermissionsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetUserPermissionsQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle(GetUserPermissionsQuery request, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [UserPermission].[PermissionCode] AS [{nameof(UserPermissionDto.Code)}] FROM [users].[v_UserPermissions] AS [UserPermission] WHERE [UserPermission].UserId = @UserId """; var permissions = await connection.QueryAsync(sql, new { request.UserId }); return permissions.AsList(); } } } ================================================ FILE: src/Modules/UserAccess/Application/Authorization/GetUserPermissions/UserPermissionDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Authorization.GetUserPermissions { public class UserPermissionDto { public string Code { get; set; } } } ================================================ FILE: src/Modules/UserAccess/Application/CompanyName.MyMeetings.Modules.UserAccess.Application.csproj ================================================  ================================================ FILE: src/Modules/UserAccess/Application/Configuration/Commands/ICommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands { public interface ICommandHandler : IRequestHandler where TCommand : ICommand { } public interface ICommandHandler : IRequestHandler where TCommand : ICommand { } } ================================================ FILE: src/Modules/UserAccess/Application/Configuration/Commands/ICommandsScheduler.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands { public interface ICommandsScheduler { Task EnqueueAsync(ICommand command); Task EnqueueAsync(ICommand command); } } ================================================ FILE: src/Modules/UserAccess/Application/Configuration/Commands/InternalCommandBase.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; namespace CompanyName.MyMeetings.Modules.UserAccess.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; } } } ================================================ FILE: src/Modules/UserAccess/Application/Configuration/Queries/IQueryHandler.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Queries { public interface IQueryHandler : IRequestHandler where TQuery : IQuery { } } ================================================ FILE: src/Modules/UserAccess/Application/Contracts/CommandBase.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.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; } } } ================================================ FILE: src/Modules/UserAccess/Application/Contracts/CustomClaimTypes.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts { public class CustomClaimTypes { public const string Roles = "roles"; public const string Email = "email"; public const string Name = "name"; } } ================================================ FILE: src/Modules/UserAccess/Application/Contracts/ICommand.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts { public interface ICommand : IRequest { Guid Id { get; } } public interface ICommand : IRequest { Guid Id { get; } } } ================================================ FILE: src/Modules/UserAccess/Application/Contracts/IQuery.cs ================================================ using MediatR; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts { public interface IQuery : IRequest { } } ================================================ FILE: src/Modules/UserAccess/Application/Contracts/IRecurringCommand.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts { public interface IRecurringCommand { } } ================================================ FILE: src/Modules/UserAccess/Application/Contracts/IUserAccessModule.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts { public interface IUserAccessModule { Task ExecuteCommandAsync(ICommand command); Task ExecuteCommandAsync(ICommand command); Task ExecuteQueryAsync(IQuery query); } } ================================================ FILE: src/Modules/UserAccess/Application/Contracts/QueryBase.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts { public abstract class QueryBase : IQuery { public Guid Id { get; } protected QueryBase() { Id = Guid.NewGuid(); } protected QueryBase(Guid id) { Id = id; } } } ================================================ FILE: src/Modules/UserAccess/Application/Contracts/Roles.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts { public class Roles { public const string Admin = "Admin"; public const string User = "User"; } } ================================================ FILE: src/Modules/UserAccess/Application/Emails/EmailDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Emails { public class EmailDto { public Guid Id { get; set; } public string From { get; set; } public string To { get; set; } public string Subject { get; set; } public string Content { get; set; } public DateTime Date { get; set; } } } ================================================ FILE: src/Modules/UserAccess/Application/Emails/GetAllEmailsQuery.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Emails { public class GetAllEmailsQuery : QueryBase> { } } ================================================ FILE: src/Modules/UserAccess/Application/Emails/GetAllEmailsQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Emails { internal class GetAllEmailsQueryHandler : IQueryHandler> { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetAllEmailsQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task> Handle(GetAllEmailsQuery query, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [Email].[Id] AS [{nameof(EmailDto.Id)}], [Email].[From] AS [{nameof(EmailDto.From)}], [Email].[To] AS [{nameof(EmailDto.To)}], [Email].[Subject] AS [{nameof(EmailDto.Subject)}], [Email].[Content] AS [{nameof(EmailDto.Content)}], [Email].[Date] AS [{nameof(EmailDto.Date)}] FROM [app].[Emails] AS [Email] ORDER BY [Email].[Date] DESC """; var result = await connection.QueryAsync(sql); return result.AsList(); } } } ================================================ FILE: src/Modules/UserAccess/Application/Users/AddAdminUser/AddAdminUserCommand.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Users.AddAdminUser { public class AddAdminUserCommand : CommandBase { public AddAdminUserCommand( string login, string password, string firstName, string lastName, string name, string email) { Login = login; Password = password; FirstName = firstName; LastName = lastName; Name = name; Email = email; } public string Login { get; } public string FirstName { get; } public string LastName { get; } public string Name { get; } public string Email { get; } public string Password { get; } } } ================================================ FILE: src/Modules/UserAccess/Application/Users/AddAdminUser/AddAdminUserCommandHandler.cs ================================================ 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; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Users.AddAdminUser { internal class AddAdminUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; public AddAdminUserCommandHandler(IUserRepository userRepository) { _userRepository = userRepository; } public async Task Handle(AddAdminUserCommand command, CancellationToken cancellationToken) { var password = PasswordManager.HashPassword(command.Password); var user = User.CreateAdmin( command.Login, password, command.Email, command.FirstName, command.LastName, command.Name); await _userRepository.AddAsync(user); } } } ================================================ FILE: src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommand.cs ================================================ 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 password) { UserId = userId; Login = login; Email = email; FirstName = firstName; LastName = lastName; Password = password; } public Guid UserId { get; } public string Login { get; } public string Email { get; } public string FirstName { get; } public string LastName { get; } public string Password { get; } } ================================================ FILE: src/Modules/UserAccess/Application/Users/CreateUser/CreateUserCommandHandler.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Users.CreateUser; internal class CreateUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; public CreateUserCommandHandler(IUserRepository userRepository) { _userRepository = userRepository; } public async Task Handle(CreateUserCommand command, CancellationToken cancellationToken) { var user = User.CreateUser( command.UserId, command.Login, command.Password, command.Email, command.FirstName, command.LastName); await _userRepository.AddAsync(user); } } ================================================ FILE: src/Modules/UserAccess/Application/Users/GetAuthenticatedUser/GetAuthenticatedUserQuery.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetUser; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetAuthenticatedUser { public class GetAuthenticatedUserQuery : QueryBase { public GetAuthenticatedUserQuery() { } } } ================================================ FILE: src/Modules/UserAccess/Application/Users/GetAuthenticatedUser/GetAuthenticatedUserQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Queries; using CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetUser; using Dapper; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetAuthenticatedUser { internal class GetAuthenticatedUserQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly IExecutionContextAccessor _executionContextAccessor; public GetAuthenticatedUserQueryHandler( ISqlConnectionFactory sqlConnectionFactory, IExecutionContextAccessor executionContextAccessor) { _sqlConnectionFactory = sqlConnectionFactory; _executionContextAccessor = executionContextAccessor; } public async Task Handle(GetAuthenticatedUserQuery request, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [User].[Id] as [{nameof(UserDto.Id)}], [User].[IsActive] as [{nameof(UserDto.IsActive)}], [User].[Login] as [{nameof(UserDto.Login)}], [User].[Email] as [{nameof(UserDto.Email)}], [User].[Name] as [{nameof(UserDto.Name)}] FROM [users].[v_Users] AS [User] WHERE [User].[Id] = @UserId """; return await connection.QuerySingleAsync(sql, new { _executionContextAccessor.UserId }); } } } ================================================ FILE: src/Modules/UserAccess/Application/Users/GetUser/GetUserQuery.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetUser { public class GetUserQuery : QueryBase { public GetUserQuery(Guid userId) { UserId = userId; } public Guid UserId { get; } } } ================================================ FILE: src/Modules/UserAccess/Application/Users/GetUser/GetUserQueryHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Queries; using Dapper; namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetUser { internal class GetUserQueryHandler : IQueryHandler { private readonly ISqlConnectionFactory _sqlConnectionFactory; public GetUserQueryHandler(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public async Task Handle(GetUserQuery request, CancellationToken cancellationToken) { var connection = _sqlConnectionFactory.GetOpenConnection(); const string sql = $""" SELECT [User].[Id] as [{nameof(UserDto.Id)}], [User].[IsActive] as [{nameof(UserDto.IsActive)}], [User].[Login] as [{nameof(UserDto.Login)}], [User].[Email] as [{nameof(UserDto.Email)}], [User].[Name] as [{nameof(UserDto.Name)}] FROM [users].[v_Users] AS [User] WHERE [User].[Id] = @UserId """; return await connection.QuerySingleAsync(sql, new { request.UserId }); } } } ================================================ FILE: src/Modules/UserAccess/Application/Users/GetUser/UserDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Application.Users.GetUser { public class UserDto { public Guid Id { get; set; } public bool IsActive { get; set; } public string Name { get; set; } public string Login { get; set; } public string Email { get; set; } } } ================================================ FILE: src/Modules/UserAccess/Domain/CompanyName.MyMeetings.Modules.UserAccess.Domain.csproj ================================================  ================================================ FILE: src/Modules/UserAccess/Domain/Users/Events/UserCreatedDomainEvent.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.Users.Events { public class UserCreatedDomainEvent : DomainEventBase { public UserCreatedDomainEvent(UserId id) { Id = id; } public new UserId Id { get; } } } ================================================ FILE: src/Modules/UserAccess/Domain/Users/IUserRepository.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.Users { public interface IUserRepository { Task AddAsync(User user); } } ================================================ FILE: src/Modules/UserAccess/Domain/Users/User.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users.Events; namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.Users { public class User : Entity, IAggregateRoot { public UserId Id { get; private set; } private string _login; private string _password; private string _email; #pragma warning disable CS0414 // Field is assigned but its value is never used private bool _isActive; #pragma warning restore CS0414 // Field is assigned but its value is never used private string _firstName; private string _lastName; private string _name; private List _roles; private User() { // Only for EF. } public static User CreateAdmin( string login, string password, string email, string firstName, string lastName, string name) { return new User( Guid.NewGuid(), login, password, email, firstName, lastName, name, UserRole.Administrator); } public static User CreateUser( Guid userId, string login, string password, string email, string firstName, string lastName) { return new User( userId, login, password, email, firstName, lastName, $"{firstName} {lastName}", UserRole.Member); } private User( Guid id, string login, string password, string email, string firstName, string lastName, string name, UserRole role) { this.Id = new UserId(id); _login = login; _password = password; _email = email; _firstName = firstName; _lastName = lastName; _name = name; _isActive = true; _roles = [role]; this.AddDomainEvent(new UserCreatedDomainEvent(this.Id)); } } } ================================================ FILE: src/Modules/UserAccess/Domain/Users/UserId.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.Users { public class UserId : TypedIdValueBase { public UserId(Guid value) : base(value) { } } } ================================================ FILE: src/Modules/UserAccess/Domain/Users/UserRole.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.UserAccess.Domain.Users { public class UserRole : ValueObject { public static UserRole Member => new UserRole(nameof(Member)); public static UserRole Administrator => new UserRole(nameof(Administrator)); public string Value { get; } private UserRole(string value) { this.Value = value; } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.csproj ================================================  ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/AllConstructorFinder.cs ================================================ using System.Collections.Concurrent; using System.Reflection; using Autofac.Core.Activators.Reflection; namespace CompanyName.MyMeetings.Modules.UserAccess.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); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Assemblies.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration { internal static class Assemblies { public static readonly Assembly Application = typeof(IUserAccessModule).Assembly; } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/DataAccess/DataAccessModule.cs ================================================ 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.UserAccess.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 UserAccessContext(dbContextOptionsBuilder.Options, _loggerFactory); }) .AsSelf() .As() .InstancePerLifetimeScope(); var infrastructureAssembly = typeof(UserAccessContext).Assembly; builder.RegisterAssemblyTypes(infrastructureAssembly) .Where(type => type.Name.EndsWith("Repository")) .AsImplementedInterfaces() .InstancePerLifetimeScope() .FindConstructorsWith(new AllConstructorFinder()); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Email/EmailModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; namespace CompanyName.MyMeetings.Modules.UserAccess.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(); } } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/EventsBus/EventsBusModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; namespace CompanyName.MyMeetings.Modules.UserAccess.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(); } } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/EventsBus/EventsBusStartup.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Meetings.IntegrationEvents; using Serilog; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.EventsBus { public static class EventsBusStartup { public static void Initialize( ILogger logger) { SubscribeToIntegrationEvents(logger); } private static void SubscribeToIntegrationEvents(ILogger logger) { var eventBus = UserAccessCompositionRoot.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()); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/EventsBus/IntegrationEventGenericHandler.cs ================================================ 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.UserAccess.Infrastructure.Configuration.EventsBus { internal class IntegrationEventGenericHandler : IIntegrationEventHandler where T : IntegrationEvent { public async Task Handle(T @event) { using (var scope = UserAccessCompositionRoot.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 }); } } } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Identity/IdentityConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.IdentityServer; using IdentityServer4.AccessTokenValidation; using IdentityServer4.Validation; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Identity; public static class IdentityConfiguration { public static IServiceCollection ConfigureIdentityService(this IServiceCollection services) { services.AddIdentityServer() .AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources()) .AddInMemoryApiScopes(IdentityServerConfig.GetApiScopes()) .AddInMemoryApiResources(IdentityServerConfig.GetApis()) .AddInMemoryClients(IdentityServerConfig.GetClients()) .AddInMemoryPersistedGrants() .AddProfileService() .AddDeveloperSigningCredential(); services.AddTransient(); services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) .AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme, x => { x.Authority = "http://localhost:5000"; x.ApiName = "myMeetingsAPI"; x.RequireHttpsMetadata = false; }); return services; } public static IApplicationBuilder AddIdentityService(this IApplicationBuilder app) { app.UseIdentityServer(); return app; } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Logging/LoggingModule.cs ================================================ using Autofac; using Serilog; namespace CompanyName.MyMeetings.Modules.UserAccess.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(); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Mediation/MediatorModule.cs ================================================ using System.Reflection; using Autofac; using Autofac.Core; using Autofac.Features.Variance; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using FluentValidation; using MediatR; using MediatR.Pipeline; namespace CompanyName.MyMeetings.Modules.UserAccess.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; } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/CommandsExecutor.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using MediatR; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing { internal static class CommandsExecutor { internal static async Task Execute(ICommand command) { using (var scope = UserAccessCompositionRoot.BeginLifetimeScope()) { var mediator = scope.Resolve(); await mediator.Send(command); } } internal static async Task Execute(ICommand command) { using (var scope = UserAccessCompositionRoot.BeginLifetimeScope()) { var mediator = scope.Resolve(); return await mediator.Send(command); } } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/IRecurringCommand.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing { public interface IRecurringCommand { } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/Inbox/InboxMessageDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.Inbox { public class InboxMessageDto { public Guid Id { get; set; } public string Type { get; set; } public string Data { get; set; } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommand.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.Inbox { public class ProcessInboxCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/Inbox/ProcessInboxCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using Dapper; using MediatR; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.UserAccess.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 }); } } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/Inbox/ProcessInboxJob.cs ================================================ using Quartz; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.Inbox { [DisallowConcurrentExecution] public class ProcessInboxJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ProcessInboxCommand()); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/InternalCommands/CommandsScheduler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Serialization; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using Dapper; using Newtonsoft.Json; namespace CompanyName.MyMeetings.Modules.UserAccess.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() }) }); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommand.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.InternalCommands { internal class ProcessInternalCommandsCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using Dapper; using Newtonsoft.Json; using Polly; namespace CompanyName.MyMeetings.Modules.UserAccess.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; } } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/InternalCommands/ProcessInternalCommandsJob.cs ================================================ using Quartz; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.InternalCommands { [DisallowConcurrentExecution] public class ProcessInternalCommandsJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ProcessInternalCommandsCommand()); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using Serilog; using Serilog.Context; using Serilog.Core; using Serilog.Events; namespace CompanyName.MyMeetings.Modules.UserAccess.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); return; } 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))); } } } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/LoggingCommandHandlerWithResultDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using Serilog; using Serilog.Context; using Serilog.Core; using Serilog.Events; namespace CompanyName.MyMeetings.Modules.UserAccess.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) { if (command is IRecurringCommand) { return await _decorated.Handle(command, 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))); } } } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/Outbox/OutboxMessageDto.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.Outbox { public class OutboxMessageDto { public Guid Id { get; set; } public string Type { get; set; } public string Data { get; set; } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/Outbox/OutboxModule.cs ================================================ 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.UserAccess.Infrastructure.Outbox; using Module = Autofac.Module; namespace CompanyName.MyMeetings.Modules.UserAccess.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"); } } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommand.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.Outbox { public class ProcessOutboxCommand : CommandBase, IRecurringCommand { } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxCommandHandler.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Data; using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using Dapper; using MediatR; using Newtonsoft.Json; using Serilog.Context; using Serilog.Core; using Serilog.Events; namespace CompanyName.MyMeetings.Modules.UserAccess.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()}"))); } } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/Outbox/ProcessOutboxJob.cs ================================================ using Quartz; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.Outbox { [DisallowConcurrentExecution] public class ProcessOutboxJob : IJob { public async Task Execute(IJobExecutionContext context) { await CommandsExecutor.Execute(new ProcessOutboxCommand()); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/ProcessingModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.BuildingBlocks.Application.Events; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.DomainEventsDispatching; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.InternalCommands; using MediatR; namespace CompanyName.MyMeetings.Modules.UserAccess.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()); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing { internal class UnitOfWorkCommandHandlerDecorator : ICommandHandler where T : ICommand { private readonly ICommandHandler _decorated; private readonly IUnitOfWork _unitOfWork; private readonly UserAccessContext _userAccessContext; public UnitOfWorkCommandHandlerDecorator( ICommandHandler decorated, IUnitOfWork unitOfWork, UserAccessContext userAccessContext) { _decorated = decorated; _unitOfWork = unitOfWork; _userAccessContext = userAccessContext; } public async Task Handle(T command, CancellationToken cancellationToken) { await this._decorated.Handle(command, cancellationToken); if (command is InternalCommandBase) { var internalCommand = await _userAccessContext.InternalCommands.FirstOrDefaultAsync(x => x.Id == command.Id, cancellationToken: cancellationToken); if (internalCommand != null) { internalCommand.ProcessedDate = DateTime.UtcNow; } } await this._unitOfWork.CommitAsync(cancellationToken); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/UnitOfWorkCommandHandlerWithResultDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using Microsoft.EntityFrameworkCore; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing { internal class UnitOfWorkCommandHandlerWithResultDecorator : ICommandHandler where T : ICommand { private readonly ICommandHandler _decorated; private readonly IUnitOfWork _unitOfWork; private readonly UserAccessContext _userAccessContext; public UnitOfWorkCommandHandlerWithResultDecorator( ICommandHandler decorated, IUnitOfWork unitOfWork, UserAccessContext userAccessContext) { _decorated = decorated; _unitOfWork = unitOfWork; _userAccessContext = userAccessContext; } public async Task Handle(T command, CancellationToken cancellationToken) { var result = await this._decorated.Handle(command, cancellationToken); if (command is InternalCommandBase) { var internalCommand = await _userAccessContext.InternalCommands.FirstOrDefaultAsync(x => x.Id == command.Id, cancellationToken: cancellationToken); if (internalCommand != null) { internalCommand.ProcessedDate = DateTime.UtcNow; } } await this._unitOfWork.CommitAsync(cancellationToken); return result; } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/ValidationCommandHandlerDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using FluentValidation; namespace CompanyName.MyMeetings.Modules.UserAccess.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); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Processing/ValidationCommandHandlerWithResultDecorator.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using FluentValidation; namespace CompanyName.MyMeetings.Modules.UserAccess.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); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Quartz/QuartzModule.cs ================================================ using Autofac; using Quartz; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Quartz { public class QuartzModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { builder.RegisterAssemblyTypes(ThisAssembly) .Where(x => typeof(IJob).IsAssignableFrom(x)).InstancePerDependency(); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Quartz/QuartzStartup.cs ================================================ using System.Collections.Specialized; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.Inbox; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.InternalCommands; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.Outbox; using Quartz; using Quartz.Impl; using Quartz.Logging; using Serilog; namespace CompanyName.MyMeetings.Modules.UserAccess.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", "Meetings"); 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."); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Quartz/SerilogLogProvider.cs ================================================ using Quartz.Logging; using Serilog; namespace CompanyName.MyMeetings.Modules.UserAccess.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(); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Security/AesDataProtector.cs ================================================ using System.Security.Cryptography; using System.Text; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Security { public class AesDataProtector : IDataProtector { private readonly string _encryptionKey; public AesDataProtector(string encryptionKey) { _encryptionKey = encryptionKey; } public string Encrypt(string plainText) { var key = Encoding.UTF8.GetBytes(_encryptionKey); using var aesAlg = Aes.Create(); using var encryptor = aesAlg.CreateEncryptor(key, aesAlg.IV); using var msEncrypt = new MemoryStream(); using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { using var swEncrypt = new StreamWriter(csEncrypt); swEncrypt.Write(plainText); } var iv = aesAlg.IV; var decryptedContent = msEncrypt.ToArray(); var result = new byte[iv.Length + decryptedContent.Length]; Buffer.BlockCopy(iv, 0, result, 0, iv.Length); Buffer.BlockCopy(decryptedContent, 0, result, iv.Length, decryptedContent.Length); return Convert.ToBase64String(result); } public string Decrypt(string encryptedText) { var fullCipher = Convert.FromBase64String(encryptedText); var iv = new byte[16]; var cipher = new byte[fullCipher.Length - iv.Length]; Buffer.BlockCopy(fullCipher, 0, iv, 0, iv.Length); Buffer.BlockCopy(fullCipher, iv.Length, cipher, 0, fullCipher.Length - iv.Length); var key = Encoding.UTF8.GetBytes(_encryptionKey); using var aesAlg = Aes.Create(); using var decryptor = aesAlg.CreateDecryptor(key, iv); string result; using (var msDecrypt = new MemoryStream(cipher)) { using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read); using var srDecrypt = new StreamReader(csDecrypt); result = srDecrypt.ReadToEnd(); } return result; } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Security/IDataProtector.cs ================================================ namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Security { public interface IDataProtector { string Encrypt(string plainText); string Decrypt(string encryptedText); } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/Security/SecurityModule.cs ================================================ using Autofac; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Security { internal class SecurityModule : Module { private readonly string _encryptionKey; public SecurityModule(string encryptionKey) { _encryptionKey = encryptionKey; } protected override void Load(ContainerBuilder builder) { builder.RegisterType() .As() .WithParameter("encryptionKey", _encryptionKey); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/UserAccessCompositionRoot.cs ================================================ using Autofac; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration { internal static class UserAccessCompositionRoot { private static IContainer _container; internal static void SetContainer(IContainer container) { _container = container; } internal static ILifetimeScope BeginLifetimeScope() { return _container.BeginLifetimeScope(); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Configuration/UserAccessStartup.cs ================================================ 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; 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; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration { public class UserAccessStartup { 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()); containerBuilder.RegisterModule(new OutboxModule(new BiDictionary())); containerBuilder.RegisterModule(new QuartzModule()); containerBuilder.RegisterModule(new EmailModule(emailsConfiguration, emailSender)); containerBuilder.RegisterModule(new SecurityModule(textEncryptionKey)); containerBuilder.RegisterInstance(executionContextAccessor); _container = containerBuilder.Build(); UserAccessCompositionRoot.SetContainer(_container); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Domain/Users/UserEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Domain.Users { internal class UserEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("Users", "users"); builder.HasKey(x => x.Id); builder.Property("_login").HasColumnName("Login"); builder.Property("_email").HasColumnName("Email"); builder.Property("_password").HasColumnName("Password"); builder.Property("_isActive").HasColumnName("IsActive"); builder.Property("_firstName").HasColumnName("FirstName"); builder.Property("_lastName").HasColumnName("LastName"); builder.Property("_name").HasColumnName("Name"); builder.OwnsMany("_roles", b => { b.WithOwner().HasForeignKey("UserId"); b.ToTable("UserRoles", "users"); b.Property("UserId"); b.Property("Value").HasColumnName("RoleCode"); b.HasKey("UserId", "Value"); }); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Domain/Users/UserRepository.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Domain.Users { public class UserRepository : IUserRepository { private readonly UserAccessContext _userAccessContext; public UserRepository(UserAccessContext userAccessContext) { _userAccessContext = userAccessContext; } public async Task AddAsync(User user) { await _userAccessContext.Users.AddAsync(user); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/IdentityServer/IdentityServerConfig.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using IdentityServer4; using IdentityServer4.Models; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.IdentityServer { internal class IdentityServerConfig { public static IEnumerable GetApiScopes() { return new List { new("all", "Can Do All") }; } public static IEnumerable GetApis() { return new List { new("myMeetingsAPI", "My Meetings API") { Scopes = { "all" } } }; } public static IEnumerable GetIdentityResources() { return new IdentityResource[] { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResource(CustomClaimTypes.Roles, new List { CustomClaimTypes.Roles }) }; } public static IEnumerable GetClients() { return new List { new Client { ClientId = "ro.client", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "all", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } } }; } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/IdentityServer/ProfileService.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using IdentityServer4.Models; using IdentityServer4.Services; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.IdentityServer { internal class ProfileService : IProfileService { public Task GetProfileDataAsync(ProfileDataRequestContext context) { context.IssuedClaims.AddRange(context.Subject.Claims.Where(x => x.Type == CustomClaimTypes.Roles).ToList()); context.IssuedClaims.Add(context.Subject.Claims.Single(x => x.Type == CustomClaimTypes.Name)); context.IssuedClaims.Add(context.Subject.Claims.Single(x => x.Type == CustomClaimTypes.Email)); return Task.CompletedTask; } public Task IsActiveAsync(IsActiveContext context) { return Task.FromResult(context.IsActive); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/IdentityServer/ResourceOwnerPasswordValidator.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication.Authenticate; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using IdentityServer4.Models; using IdentityServer4.Validation; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.IdentityServer { internal class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { private readonly IUserAccessModule _userAccessModule; public ResourceOwnerPasswordValidator(IUserAccessModule userAccessModule) { _userAccessModule = userAccessModule; } public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { var authenticationResult = await _userAccessModule.ExecuteCommandAsync( new AuthenticateCommand(context.UserName, context.Password)); if (!authenticationResult.IsAuthenticated) { context.Result = new GrantValidationResult( TokenRequestErrors.InvalidGrant, authenticationResult.AuthenticationError); return; } context.Result = new GrantValidationResult( authenticationResult.User.Id.ToString(), "forms", authenticationResult.User.Claims); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/InternalCommands/InternalCommandEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.InternalCommands { internal class InternalCommandEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("InternalCommands", "users"); builder.HasKey(b => b.Id); builder.Property(b => b.Id).ValueGeneratedNever(); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Outbox/OutboxAccessor.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Outbox { public class OutboxAccessor : IOutbox { private readonly UserAccessContext _userAccessContext; public OutboxAccessor(UserAccessContext 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. } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/Outbox/OutboxMessageEntityTypeConfiguration.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Outbox { internal class OutboxMessageEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("OutboxMessages", "users"); builder.HasKey(b => b.Id); builder.Property(b => b.Id).ValueGeneratedNever(); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/UserAccessContext.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application.Outbox; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.InternalCommands; using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Domain.Users; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.InternalCommands; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Outbox; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure { public class UserAccessContext : DbContext { public DbSet Users { get; set; } public DbSet OutboxMessages { get; set; } public DbSet InternalCommands { get; set; } private readonly ILoggerFactory _loggerFactory; public UserAccessContext(DbContextOptions options, ILoggerFactory loggerFactory) : base(options) { _loggerFactory = loggerFactory; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new UserEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new OutboxMessageEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new InternalCommandEntityTypeConfiguration()); } } } ================================================ FILE: src/Modules/UserAccess/Infrastructure/UserAccessModule.cs ================================================ using Autofac; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing; using MediatR; namespace CompanyName.MyMeetings.Modules.UserAccess.Infrastructure { public class UserAccessModule : IUserAccessModule { 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 = UserAccessCompositionRoot.BeginLifetimeScope()) { var mediator = scope.Resolve(); return await mediator.Send(query); } } } } ================================================ FILE: src/Modules/UserAccess/IntegrationEvents/CompanyName.MyMeetings.Modules.UserAccess.IntegrationEvents.csproj ================================================  ================================================ FILE: src/Modules/UserAccess/Tests/ArchTests/Application/ApplicationTests.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Commands; using CompanyName.MyMeetings.Modules.UserAccess.Application.Configuration.Queries; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using CompanyName.MyMeetings.Modules.UserAccess.ArchTests.SeedWork; using FluentValidation; using MediatR; using NetArchTest.Rules; using Newtonsoft.Json; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.UserAccess.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); } } } ================================================ FILE: src/Modules/UserAccess/Tests/ArchTests/CompanyName.MyMeetings.Modules.UserAccess.ArchTests.csproj ================================================  ================================================ FILE: src/Modules/UserAccess/Tests/ArchTests/Domain/DomainTests.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.Modules.UserAccess.ArchTests.SeedWork; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.UserAccess.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); } } } ================================================ FILE: src/Modules/UserAccess/Tests/ArchTests/Module/LayersTests.cs ================================================ using CompanyName.MyMeetings.Modules.UserAccess.ArchTests.SeedWork; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.UserAccess.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); } } } ================================================ FILE: src/Modules/UserAccess/Tests/ArchTests/SeedWork/TestBase.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.UserAccess.ArchTests.SeedWork { public abstract class TestBase { protected static Assembly ApplicationAssembly => typeof(CommandBase).Assembly; protected static Assembly DomainAssembly => typeof(User).Assembly; protected static Assembly InfrastructureAssembly => typeof(UserAccessContext).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); } } } ================================================ FILE: src/Modules/UserAccess/Tests/IntegrationTests/AssemblyInfo.cs ================================================ using NUnit.Framework; [assembly: NonParallelizable] [assembly: LevelOfParallelism(1)] namespace CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests { public class AssemblyInfo { } } ================================================ FILE: src/Modules/UserAccess/Tests/IntegrationTests/CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.csproj ================================================  ================================================ FILE: src/Modules/UserAccess/Tests/IntegrationTests/SeedWork/ExecutionContextMock.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; namespace CompanyNames.MyMeetings.Modules.UserAccess.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; } } } ================================================ FILE: src/Modules/UserAccess/Tests/IntegrationTests/SeedWork/OutboxMessagesHelper.cs ================================================ using System.Data; using System.Reflection; using CompanyName.MyMeetings.Modules.UserAccess.Application.Authentication.Authenticate; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration.Processing.Outbox; using Dapper; using MediatR; using Newtonsoft.Json; namespace CompanyNames.MyMeetings.Modules.UserAccess.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 [users].[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(AuthenticateCommand)).GetType(typeof(T).FullName); return JsonConvert.DeserializeObject(message.Data, type) as T; } } } ================================================ FILE: src/Modules/UserAccess/Tests/IntegrationTests/SeedWork/TestBase.cs ================================================ 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.UserAccess.Application.Contracts; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure.Configuration; using Dapper; using MediatR; using NSubstitute; using NUnit.Framework; using Serilog; namespace CompanyNames.MyMeetings.Modules.UserAccess.IntegrationTests.SeedWork { public class TestBase { protected string ConnectionString { get; private set; } protected ILogger Logger { get; private set; } protected IUserAccessModule UserAccessModule { 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(); UserAccessStartup.Initialize( ConnectionString, new ExecutionContextMock(Guid.NewGuid()), Logger, new EmailsConfiguration("from@email.com"), "key", EmailSender, null); UserAccessModule = new UserAccessModule(); } 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 [users].[InboxMessages] " + "DELETE FROM [users].[InternalCommands] " + "DELETE FROM [users].[OutboxMessages] " + "DELETE FROM [users].[Users] " + "DELETE FROM [users].[RolesToPermissions] " + "DELETE FROM [users].[UserRoles] " + "DELETE FROM [users].[Permissions] "; await connection.ExecuteScalarAsync(sql); } } } ================================================ FILE: src/Modules/UserAccess/Tests/IntegrationTests/Users/CreateUserTests.cs ================================================ 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"; } } ================================================ FILE: src/Modules/UserAccess/Tests/UnitTests/CompanyName.MyMeetings.Modules.UserAccess.Domain.UnitTests.csproj ================================================  ================================================ FILE: src/Modules/UserAccess/Tests/UnitTests/SeedWork/DomainEventsTestHelper.cs ================================================ using System.Collections; using System.Reflection; using CompanyName.MyMeetings.BuildingBlocks.Domain; namespace CompanyName.MyMeetings.Modules.UserAccess.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; } } } ================================================ FILE: src/Modules/UserAccess/Tests/UnitTests/SeedWork/TestBase.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Domain; using NUnit.Framework; namespace CompanyName.MyMeetings.Modules.UserAccess.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); } } } } ================================================ FILE: src/Tests/ArchTests/Api/ApiTests.cs ================================================ using CompanyName.MyMeetings.ArchTests.SeedWork; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.ArchTests.Api { [TestFixture] public class ApiTests : TestBase { [Test] public void AdministrationApi_DoesNotHaveDependency_ToOtherModules() { List otherModules = [MeetingsNamespace, PaymentsNamespace, UserAccessNamespace]; var result = Types.InAssembly(ApiAssembly) .That() .ResideInNamespace("CompanyName.MyMeetings.API.Modules.Administration") .Should() .NotHaveDependencyOnAny(otherModules.ToArray()) .GetResult(); AssertArchTestResult(result); } [Test] public void MeetingsApi_DoesNotHaveDependency_ToOtherModules() { List otherModules = [AdministrationNamespace, PaymentsNamespace, UserAccessNamespace]; var result = Types.InAssembly(ApiAssembly) .That() .ResideInNamespace("CompanyName.MyMeetings.API.Modules.Meetings") .Should() .NotHaveDependencyOnAny(otherModules.ToArray()) .GetResult(); AssertArchTestResult(result); } [Test] public void PaymentsApi_DoesNotHaveDependency_ToOtherModules() { List otherModules = [AdministrationNamespace, MeetingsNamespace, UserAccessNamespace]; var result = Types.InAssembly(ApiAssembly) .That() .ResideInNamespace("CompanyName.MyMeetings.API.Modules.Payments") .Should() .NotHaveDependencyOnAny(otherModules.ToArray()) .GetResult(); AssertArchTestResult(result); } [Test] public void UserAccessApi_DoesNotHaveDependency_ToOtherModules() { List otherModules = [AdministrationNamespace, MeetingsNamespace, PaymentsNamespace]; var result = Types.InAssembly(ApiAssembly) .That() .ResideInNamespace("CompanyName.MyMeetings.API.Modules.UserAccess") .Should() .NotHaveDependencyOnAny(otherModules.ToArray()) .GetResult(); AssertArchTestResult(result); } } } ================================================ FILE: src/Tests/ArchTests/CompanyName.MyMeetings.ArchTests.csproj ================================================  ================================================ FILE: src/Tests/ArchTests/Modules/ModuleTests.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.ArchTests.SeedWork; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Administration.Infrastructure; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Domain.Meetings; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Domain.MeetingFees; using CompanyName.MyMeetings.Modules.Payments.Infrastructure.Configuration; using CompanyName.MyMeetings.Modules.UserAccess.Application.Contracts; using CompanyName.MyMeetings.Modules.UserAccess.Domain.Users; using CompanyName.MyMeetings.Modules.UserAccess.Infrastructure; using MediatR; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.ArchTests.Modules { [TestFixture] public class ModuleTests : TestBase { [Test] public void AdministrationModule_DoesNotHave_Dependency_On_Other_Modules() { List otherModules = [MeetingsNamespace, PaymentsNamespace, UserAccessNamespace]; List administrationAssemblies = [ typeof(IAdministrationModule).Assembly, typeof(MeetingGroupLocation).Assembly, typeof(AdministrationContext).Assembly ]; var result = Types.InAssemblies(administrationAssemblies) .That() .DoNotImplementInterface(typeof(INotificationHandler<>)) .And().DoNotHaveNameEndingWith("IntegrationEventHandler") .And().DoNotHaveName("EventsBusStartup") .Should() .NotHaveDependencyOnAny(otherModules.ToArray()) .GetResult(); AssertArchTestResult(result); } [Test] public void MeetingsModule_DoesNotHave_Dependency_On_Other_Modules() { List otherModules = [AdministrationNamespace, PaymentsNamespace, UserAccessNamespace]; List meetingsAssemblies = [ typeof(IMeetingsModule).Assembly, typeof(Meeting).Assembly, typeof(MeetingsContext).Assembly ]; var result = Types.InAssemblies(meetingsAssemblies) .That() .DoNotImplementInterface(typeof(INotificationHandler<>)) .And().DoNotHaveNameEndingWith("IntegrationEventHandler") .And().DoNotHaveName("EventsBusStartup") .Should() .NotHaveDependencyOnAny(otherModules.ToArray()) .GetResult(); AssertArchTestResult(result); } [Test] public void PaymentsModule_DoesNotHave_Dependency_On_Other_Modules() { List otherModules = [AdministrationNamespace, MeetingsNamespace, UserAccessNamespace]; List paymentsAssemblies = [ typeof(IPaymentsModule).Assembly, typeof(MeetingFee).Assembly, typeof(PaymentsStartup).Assembly ]; var result = Types.InAssemblies(paymentsAssemblies) .That() .DoNotImplementInterface(typeof(INotificationHandler<>)) .And().DoNotHaveNameEndingWith("IntegrationEventHandler") .And().DoNotHaveName("EventsBusStartup") .Should() .NotHaveDependencyOnAny(otherModules.ToArray()) .GetResult(); AssertArchTestResult(result); } [Test] public void UserAccessModule_DoesNotHave_Dependency_On_Other_Modules() { List otherModules = [AdministrationNamespace, MeetingsNamespace, PaymentsNamespace]; List userAccessAssemblies = [ typeof(IUserAccessModule).Assembly, typeof(User).Assembly, typeof(UserAccessContext).Assembly ]; var result = Types.InAssemblies(userAccessAssemblies) .That() .DoNotImplementInterface(typeof(INotificationHandler<>)) .And().DoNotHaveNameEndingWith("IntegrationEventHandler") .And().DoNotHaveName("EventsBusStartup") .Should() .NotHaveDependencyOnAny(otherModules.ToArray()) .GetResult(); AssertArchTestResult(result); } } } ================================================ FILE: src/Tests/ArchTests/SeedWork/TestBase.cs ================================================ using System.Reflection; using CompanyName.MyMeetings.API; using NetArchTest.Rules; using NUnit.Framework; namespace CompanyName.MyMeetings.ArchTests.SeedWork { public abstract class TestBase { protected static Assembly ApiAssembly => typeof(Startup).Assembly; public const string MeetingsNamespace = "CompanyName.MyMeetings.Modules.Meetings"; public const string AdministrationNamespace = "CompanyName.MyMeetings.Modules.Administration"; public const string PaymentsNamespace = "CompanyName.MyMeetings.Modules.Payments"; public const string UserAccessNamespace = "CompanyName.MyMeetings.Modules.UserAccess"; 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); } } } ================================================ FILE: src/Tests/IntegrationTests/AssemblyInfo.cs ================================================ using NUnit.Framework; [assembly: NonParallelizable] [assembly: LevelOfParallelism(1)] namespace CompanyName.MyMeetings.IntegrationTests { public class AssemblyInfo { } } ================================================ FILE: src/Tests/IntegrationTests/CompanyName.MyMeetings.IntegrationTests.csproj ================================================  ================================================ FILE: src/Tests/IntegrationTests/CreateMeetingGroup/CreateMeetingGroupTests.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.IntegrationTests.SeedWork; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.AcceptMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.GetMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Administration.Domain.MeetingGroupProposals; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.ProposeMeetingGroup; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.GetAllMeetingGroups; using NUnit.Framework; namespace CompanyName.MyMeetings.IntegrationTests.CreateMeetingGroup { public class CreateMeetingGroupTests : TestBase { [Test] public async Task CreateMeetingGroupScenario_WhenProposalIsAccepted() { var meetingGroupId = await MeetingsModule.ExecuteCommandAsync( new ProposeMeetingGroupCommand( "Name", "Description", "Location", "PL")); await AssertEventually( new GetMeetingGroupProposalFromAdministrationProbe(meetingGroupId, AdministrationModule), 10000); await AdministrationModule.ExecuteCommandAsync(new AcceptMeetingGroupProposalCommand(meetingGroupId)); await AssertEventually( new GetCreatedMeetingGroupFromMeetingsProbe(meetingGroupId, MeetingsModule), 15000); } private class GetCreatedMeetingGroupFromMeetingsProbe : IProbe { private readonly Guid _expectedMeetingGroupId; private readonly IMeetingsModule _meetingsModule; private List _allMeetingGroups; public GetCreatedMeetingGroupFromMeetingsProbe( Guid expectedMeetingGroupId, IMeetingsModule meetingsModule) { _expectedMeetingGroupId = expectedMeetingGroupId; _meetingsModule = meetingsModule; } public bool IsSatisfied() { return _allMeetingGroups != null && _allMeetingGroups.Any(x => x.Id == _expectedMeetingGroupId); } public async Task SampleAsync() { _allMeetingGroups = await _meetingsModule.ExecuteQueryAsync(new GetAllMeetingGroupsQuery()); } public string DescribeFailureTo() => $"Meeting group with ID: {_expectedMeetingGroupId} is not created"; } private class GetMeetingGroupProposalFromAdministrationProbe : IProbe { private readonly Guid _expectedMeetingGroupProposalId; private readonly IAdministrationModule _administrationModule; private MeetingGroupProposalDto _meetingGroupProposal; public GetMeetingGroupProposalFromAdministrationProbe( Guid expectedMeetingGroupProposalId, IAdministrationModule administrationModule) { _expectedMeetingGroupProposalId = expectedMeetingGroupProposalId; _administrationModule = administrationModule; } public bool IsSatisfied() { if (_meetingGroupProposal == null) { return false; } if (_meetingGroupProposal.Id == _expectedMeetingGroupProposalId && _meetingGroupProposal.StatusCode == MeetingGroupProposalStatus.ToVerify.Value) { return true; } return false; } public async Task SampleAsync() { try { _meetingGroupProposal = await _administrationModule.ExecuteQueryAsync( new GetMeetingGroupProposalQuery(_expectedMeetingGroupProposalId)); } catch { // ignored } } public string DescribeFailureTo() => $"Meeting group proposal with ID: {_expectedMeetingGroupProposalId} to verification not created"; } } } ================================================ FILE: src/Tests/IntegrationTests/SeedWork/ExecutionContextMock.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; namespace CompanyName.MyMeetings.IntegrationTests.SeedWork { public class ExecutionContextMock : IExecutionContextAccessor { public ExecutionContextMock(Guid userId) { UserId = userId; } public Guid UserId { get; } public Guid CorrelationId { get; } public bool IsAvailable { get; } } } ================================================ FILE: src/Tests/IntegrationTests/SeedWork/TestBase.cs ================================================ using System.Data; using System.Data.SqlClient; using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using CompanyName.MyMeetings.BuildingBlocks.Domain; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests; using CompanyName.MyMeetings.BuildingBlocks.IntegrationTests.Probing; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using CompanyName.MyMeetings.Modules.Administration.Infrastructure; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration; using Dapper; using NSubstitute; using NUnit.Framework; using Serilog; namespace CompanyName.MyMeetings.IntegrationTests.SeedWork { public class TestBase { protected string ConnectionString { get; private set; } protected ILogger Logger { get; private set; } protected IAdministrationModule AdministrationModule { get; private set; } protected IMeetingsModule MeetingsModule { get; private set; } protected IEmailSender EmailSender { get; private set; } protected ExecutionContextMock ExecutionContext { get; private set; } protected IEventsBus EventsBus { 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 = new LoggerConfiguration() .Enrich.FromLogContext() .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{Module}] [{Context}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); EmailSender = Substitute.For(); ExecutionContext = new ExecutionContextMock(Guid.NewGuid()); EventsBus = new InMemoryEventBusClient(Logger); AdministrationStartup.Initialize( ConnectionString, ExecutionContext, Logger, EventsBus); MeetingsStartup.Initialize( ConnectionString, ExecutionContext, Logger, new EmailsConfiguration("from@email.com"), EventsBus); AdministrationModule = new AdministrationModule(); MeetingsModule = new MeetingsModule(); } [TearDown] public void AfterEachTest() { MeetingsStartup.Stop(); AdministrationStartup.Stop(); SystemClock.Reset(); } protected static void AssertBrokenRule(AsyncTestDelegate testDelegate) where TRule : class, IBusinessRule { var message = $"Expected {typeof(TRule).Name} broken rule"; var businessRuleValidationException = Assert.CatchAsync(testDelegate, message); if (businessRuleValidationException != null) { Assert.That(businessRuleValidationException.BrokenRule, Is.TypeOf(), message); } } protected static async Task AssertEventually(IProbe probe, int timeout) { await new Poller(timeout).CheckAsync(probe); } private static async Task ClearDatabase(IDbConnection connection) { const string sql = "DELETE FROM [administration].[InboxMessages] " + "DELETE FROM [administration].[InternalCommands] " + "DELETE FROM [administration].[OutboxMessages] " + "DELETE FROM [administration].[MeetingGroupProposals] " + "DELETE FROM [administration].[Members] " + "DELETE FROM [meetings].[InboxMessages] " + "DELETE FROM [meetings].[InternalCommands] " + "DELETE FROM [meetings].[OutboxMessages] " + "DELETE FROM [meetings].[MeetingAttendees] " + "DELETE FROM [meetings].[MeetingGroupMembers] " + "DELETE FROM [meetings].[MeetingGroupProposals] " + "DELETE FROM [meetings].[MeetingGroups] " + "DELETE FROM [meetings].[MeetingNotAttendees] " + "DELETE FROM [meetings].[MeetingCommentingConfigurations] " + "DELETE FROM [meetings].[Meetings] " + "DELETE FROM [meetings].[MeetingWaitlistMembers] " + "DELETE FROM [meetings].[Members] "; await connection.ExecuteScalarAsync(sql); } } } ================================================ FILE: src/Tests/SUT/CompanyName.MyMeetings.SUT.csproj ================================================  ================================================ FILE: src/Tests/SUT/Helpers/MeetingGroupsFactory.cs ================================================ using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using CompanyName.MyMeetings.Modules.Administration.Application.MeetingGroupProposals.AcceptMeetingGroupProposal; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.ProposeMeetingGroup; using CompanyName.MyMeetings.SUT.SeedWork; namespace CompanyName.MyMeetings.SUT.Helpers { internal static class MeetingGroupsFactory { public static async Task GivenMeetingGroup( IMeetingsModule meetingsModule, IAdministrationModule administrationModule, string connectionString, string name, string description, string locationCity, string locationCountryCode) { var meetingGroupId = await meetingsModule.ExecuteCommandAsync(new ProposeMeetingGroupCommand( name, description, locationCity, locationCountryCode)); await AsyncOperationsHelper.WaitForProcessing(connectionString); await administrationModule.ExecuteCommandAsync(new AcceptMeetingGroupProposalCommand(meetingGroupId)); await AsyncOperationsHelper.WaitForProcessing(connectionString); return meetingGroupId; } } } ================================================ FILE: src/Tests/SUT/Helpers/TestMeetingFactory.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.CreateMeeting; namespace CompanyName.MyMeetings.SUT.Helpers { internal static class TestMeetingFactory { internal static async Task GivenMeeting( IMeetingsModule meetingsModule, Guid meetingGroupId, string title, DateTime termStartDate, DateTime termEndDate, string description, string meetingLocationName, string meetingLocationAddress, string meetingLocationPostalCode, string meetingLocationCity, int? attendeesLimit, int guestsLimit, DateTime? rsvpTermStartDate, DateTime? rsvpTermEndDate, decimal? eventFeeValue, string eventFeeCurrency, List hostMemberIds) { return await meetingsModule.ExecuteCommandAsync(new CreateMeetingCommand( meetingGroupId, title, termStartDate, termEndDate, description, meetingLocationName, meetingLocationAddress, meetingLocationPostalCode, meetingLocationCity, attendeesLimit, guestsLimit, rsvpTermStartDate, rsvpTermEndDate, eventFeeValue, eventFeeCurrency, hostMemberIds)); } } } ================================================ FILE: src/Tests/SUT/Helpers/TestMeetingGroupManager.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroups.JoinToGroup; namespace CompanyName.MyMeetings.SUT.Helpers { internal static class TestMeetingGroupManager { internal static async Task JoinToGroup(IMeetingsModule meetingsModule, Guid meetingGroupId) { await meetingsModule.ExecuteCommandAsync(new JoinToGroupCommand(meetingGroupId)); } } } ================================================ FILE: src/Tests/SUT/Helpers/TestMeetingManager.cs ================================================ using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Application.Meetings.AddMeetingAttendee; namespace CompanyName.MyMeetings.SUT.Helpers { internal static class TestMeetingManager { internal static async Task AddAttendee(IMeetingsModule meetingsModule, Guid meetingId, int guestsNumber) { await meetingsModule.ExecuteCommandAsync(new AddMeetingAttendeeCommand(meetingId, guestsNumber)); } } } ================================================ FILE: src/Tests/SUT/Helpers/TestPaymentsManager.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.BuySubscription; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetPayerSubscription; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionDetails; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.GetSubscriptionPayments; using CompanyName.MyMeetings.Modules.Payments.Application.Subscriptions.MarkSubscriptionPaymentAsPaid; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using CompanyName.MyMeetings.SUT.SeedWork; using CompanyName.MyMeetings.SUT.SeedWork.Probing; namespace CompanyName.MyMeetings.SUT.Helpers { internal static class TestPaymentsManager { public static async Task BuySubscription( IPaymentsModule paymentsModule, IExecutionContextAccessor executionContextAccessor) { var subscriptionPaymentId = await paymentsModule.ExecuteCommandAsync(new BuySubscriptionCommand( SubscriptionPeriod.Month.Code, "PL", 60, "PLN")); await TestBase.GetEventually( new GetSubscriptionPaymentsProbe( paymentsModule, executionContextAccessor.UserId, x => true), 10000); await paymentsModule.ExecuteCommandAsync( new MarkSubscriptionPaymentAsPaidCommand(subscriptionPaymentId)); await TestBase.GetEventually( new GetPayerSubscriptionProbe( paymentsModule, executionContextAccessor.UserId), 10000); } private class GetSubscriptionPaymentsProbe : IProbe> { private readonly IPaymentsModule _paymentsModule; private readonly Guid _payerId; private readonly Func, bool> _condition; public GetSubscriptionPaymentsProbe( IPaymentsModule paymentsModule, Guid payerId, Func, bool> condition) { _paymentsModule = paymentsModule; _payerId = payerId; _condition = condition; } public bool IsSatisfied(List sample) { return sample != null && _condition(sample); } public async Task> GetSampleAsync() { return await _paymentsModule.ExecuteQueryAsync(new GetSubscriptionPaymentsQuery(_payerId)); } public string DescribeFailureTo() { return $"Cannot get subscription payments for PayerId: {_payerId}"; } } private class GetPayerSubscriptionProbe : IProbe { private readonly IPaymentsModule _paymentsModule; public GetPayerSubscriptionProbe( IPaymentsModule paymentsModule, Guid payerId) { _paymentsModule = paymentsModule; } public bool IsSatisfied(SubscriptionDetailsDto sample) { return sample != null; } public async Task GetSampleAsync() { return await _paymentsModule.ExecuteQueryAsync(new GetAuthenticatedPayerSubscriptionQuery()); } public string DescribeFailureTo() => "Subscription read model is not in expected state"; } } } ================================================ FILE: src/Tests/SUT/Helpers/TestPriceListManager.cs ================================================ using CompanyName.MyMeetings.Modules.Payments.Application.Contracts; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.CreatePriceListItem; using CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.GetPriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.PriceListItems; using CompanyName.MyMeetings.Modules.Payments.Domain.Subscriptions; using CompanyName.MyMeetings.SUT.SeedWork; using CompanyName.MyMeetings.SUT.SeedWork.Probing; namespace CompanyName.MyMeetings.SUT.Helpers { internal static class TestPriceListManager { internal static async Task AddPriceListItems( IPaymentsModule paymentsModule, string connectionString) { await paymentsModule.ExecuteCommandAsync(new CreatePriceListItemCommand( SubscriptionPeriod.Month.Code, PriceListItemCategory.New.Code, "PL", 60, "PLN")); await paymentsModule.ExecuteCommandAsync(new CreatePriceListItemCommand( SubscriptionPeriod.HalfYear.Code, PriceListItemCategory.New.Code, "PL", 320, "PLN")); await paymentsModule.ExecuteCommandAsync(new CreatePriceListItemCommand( SubscriptionPeriod.Month.Code, PriceListItemCategory.New.Code, "US", 15, "USD")); await paymentsModule.ExecuteCommandAsync(new CreatePriceListItemCommand( SubscriptionPeriod.HalfYear.Code, PriceListItemCategory.New.Code, "US", 80, "USD")); await paymentsModule.ExecuteCommandAsync(new CreatePriceListItemCommand( SubscriptionPeriod.HalfYear.Code, PriceListItemCategory.Renewal.Code, "PL", 320, "PLN")); await TestBase.GetEventually(new GetPriceListProbe(paymentsModule, x => x.Count == 5), 5000); await AsyncOperationsHelper.WaitForProcessing(connectionString); } private class GetPriceListProbe : IProbe> { private readonly IPaymentsModule _paymentsModule; private readonly Func, bool> _condition; public GetPriceListProbe( IPaymentsModule paymentsModule, Func, bool> condition) { _paymentsModule = paymentsModule; _condition = condition; } public bool IsSatisfied(List sample) { return sample != null && _condition(sample); } public async Task> GetSampleAsync() { return await _paymentsModule.ExecuteQueryAsync(new GetPriceListItemsQuery()); } public string DescribeFailureTo() { return "Cannot get price list for specified condition"; } } } } ================================================ FILE: src/Tests/SUT/Helpers/UsersFactory.cs ================================================ 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; namespace CompanyName.MyMeetings.SUT.Helpers { internal static class UsersFactory { public static async Task GivenAdmin( IUserAccessModule userAccessModule, string login, string password, string name, string firstName, string lastName, string email) { await userAccessModule.ExecuteCommandAsync(new AddAdminUserCommand( login, password, firstName, lastName, name, email)); } public static async Task GivenUser( IRegistrationsModule registrationsModule, string connectionString, string login, string password, string firstName, string lastName, string email) { var userRegistrationId = await registrationsModule.ExecuteCommandAsync(new RegisterNewUserCommand( login, password, email, firstName, lastName, email)); await registrationsModule.ExecuteCommandAsync(new ConfirmUserRegistrationCommand(userRegistrationId)); await AsyncOperationsHelper.WaitForProcessing(connectionString); return userRegistrationId; } } } ================================================ FILE: src/Tests/SUT/Scripts/SeedPermissions.sql ================================================ -- Permissions INSERT INTO users.[Permissions] ([Code], [Name]) VALUES -- Meetings ('GetMeetingGroupProposals', 'GetMeetingGroupProposals'), ('ProposeMeetingGroup', 'ProposeMeetingGroup'), ('CreateNewMeeting','CreateNewMeeting'), ('EditMeeting','EditMeeting'), ('AddMeetingAttendee','AddMeetingAttendee'), ('RemoveMeetingAttendee','RemoveMeetingAttendee'), ('AddNotAttendee','AddNotAttendee'), ('ChangeNotAttendeeDecision','ChangeNotAttendeeDecision'), ('SignUpMemberToWaitlist','SignUpMemberToWaitlist'), ('SignOffMemberFromWaitlist','SignOffMemberFromWaitlist'), ('SetMeetingHostRole','SetMeetingHostRole'), ('SetMeetingAttendeeRole','SetMeetingAttendeeRole'), ('CancelMeeting','CancelMeeting'), ('GetAllMeetingGroups','GetAllMeetingGroups'), ('EditMeetingGroupGeneralAttributes','EditMeetingGroupGeneralAttributes'), ('JoinToGroup','JoinToGroup'), ('LeaveMeetingGroup','LeaveMeetingGroup'), ('AddMeetingComment','AddMeetingComment'), ('EditMeetingComment','EditMeetingComment'), ('RemoveMeetingComment','RemoveMeetingComment'), ('AddMeetingCommentReply','AddMeetingCommentReply'), ('LikeMeetingComment','LikeMeetingComment'), ('UnlikeMeetingComment','UnlikeMeetingComment'), ('EnableMeetingCommenting','EnableMeetingCommenting'), ('DisableMeetingCommenting','DisableMeetingCommenting'), ('MyMeetingGroupsView','MyMeetingGroupsView'), ('AllMeetingGroupsView','AllMeetingGroupsView'), ('SubscriptionView','SubscriptionView'), ('EmailsView','EmailsView'), ('MyMeetingsView','MyMeetingsView'), ('GetAuthenticatedMemberMeetings','GetAuthenticatedMemberMeetings'), -- Administration ('AcceptMeetingGroupProposal','AcceptMeetingGroupProposal'), ('AdministrationsView','AdministrationsView'), -- Payments ('RegisterPayment','RegisterPayment'), ('BuySubscription','BuySubscription'), ('RenewSubscription','RenewSubscription'), ('CreatePriceListItem','CreatePriceListItem'), ('ActivatePriceListItem','ActivatePriceListItem'), ('DeactivatePriceListItem','DeactivatePriceListItem'), ('ChangePriceListItemAttributes','ChangePriceListItemAttributes'), ('GetAuthenticatedPayerSubscription','GetAuthenticatedPayerSubscription'), ('GetPriceListItem','GetPriceListItem') -- Role To Permissions ---- Meetings INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetMeetingGroupProposals') INSERT INTO users.RolesToPermissions VALUES ('Member', 'ProposeMeetingGroup') INSERT INTO users.RolesToPermissions VALUES ('Member', 'CreateNewMeeting') INSERT INTO users.RolesToPermissions VALUES ('Member', 'EditMeeting') INSERT INTO users.RolesToPermissions VALUES ('Member', 'AddMeetingAttendee') INSERT INTO users.RolesToPermissions VALUES ('Member', 'RemoveMeetingAttendee') INSERT INTO users.RolesToPermissions VALUES ('Member', 'AddNotAttendee') INSERT INTO users.RolesToPermissions VALUES ('Member', 'ChangeNotAttendeeDecision') INSERT INTO users.RolesToPermissions VALUES ('Member', 'SignUpMemberToWaitlist') INSERT INTO users.RolesToPermissions VALUES ('Member', 'SignOffMemberFromWaitlist') INSERT INTO users.RolesToPermissions VALUES ('Member', 'SetMeetingHostRole') INSERT INTO users.RolesToPermissions VALUES ('Member', 'SetMeetingAttendeeRole') INSERT INTO users.RolesToPermissions VALUES ('Member', 'CancelMeeting') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetAllMeetingGroups') INSERT INTO users.RolesToPermissions VALUES ('Member', 'EditMeetingGroupGeneralAttributes') INSERT INTO users.RolesToPermissions VALUES ('Member', 'JoinToGroup') INSERT INTO users.RolesToPermissions VALUES ('Member', 'LeaveMeetingGroup') INSERT INTO users.RolesToPermissions VALUES ('Member', 'AddMeetingComment') INSERT INTO users.RolesToPermissions VALUES ('Member', 'EditMeetingComment') INSERT INTO users.RolesToPermissions VALUES ('Member', 'RemoveMeetingComment') INSERT INTO users.RolesToPermissions VALUES ('Member', 'AddMeetingCommentReply') INSERT INTO users.RolesToPermissions VALUES ('Member', 'LikeMeetingComment') INSERT INTO users.RolesToPermissions VALUES ('Member', 'UnlikeMeetingComment') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetAuthenticatedMemberMeetingGroups') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetMeetingGroupDetails') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetMeetingDetails') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetMeetingAttendees') INSERT INTO users.RolesToPermissions VALUES ('Member', 'MyMeetingsGroupsView') INSERT INTO users.RolesToPermissions VALUES ('Member', 'SubscriptionView') INSERT INTO users.RolesToPermissions VALUES ('Member', 'EmailsView') INSERT INTO users.RolesToPermissions VALUES ('Member', 'AllMeetingGroupsView') INSERT INTO users.RolesToPermissions VALUES ('Member', 'MyMeetingsView') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetAuthenticatedMemberMeetings') ---- Administration INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'AcceptMeetingGroupProposal') INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'AdministrationsView') ---- Payments INSERT INTO users.RolesToPermissions VALUES ('Member', 'RegisterPayment') INSERT INTO users.RolesToPermissions VALUES ('Member', 'BuySubscription') INSERT INTO users.RolesToPermissions VALUES ('Member', 'RenewSubscription') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetAuthenticatedPayerSubscription') INSERT INTO users.RolesToPermissions VALUES ('Member', 'GetPriceListItem') INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'CreatePriceListItem') INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'ActivatePriceListItem') INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'DeactivatePriceListItem') INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'ChangePriceListItemAttributes') INSERT INTO users.RolesToPermissions VALUES ('Administrator', 'GetPriceListItem') ================================================ FILE: src/Tests/SUT/SeedWork/AsyncOperationsHelper.cs ================================================ using System.Data.SqlClient; using System.Diagnostics; using Dapper; namespace CompanyName.MyMeetings.SUT.SeedWork { internal static class AsyncOperationsHelper { public static async Task WaitForProcessing(string connectionString, int timeoutInSeconds = 20) { await using var sqlConnection = new SqlConnection(connectionString); var start = Stopwatch.StartNew(); while (start.Elapsed.Seconds < timeoutInSeconds) { var internalCommandsCountUsers = await sqlConnection.ExecuteScalarAsync( """ SELECT COUNT(*) FROM [users].[InternalCommands] AS [InternalCommand] WHERE [InternalCommand].[ProcessedDate] IS NULL """); var inboxCountUsers = await sqlConnection.ExecuteScalarAsync( """ SELECT COUNT(*) FROM [users].[InboxMessages] AS [InboxMessage] WHERE [InboxMessage].[ProcessedDate] IS NULL """); var outboxCountUsers = await sqlConnection.ExecuteScalarAsync( """ SELECT COUNT(*) FROM [users].[OutboxMessages] AS [OutboxMessage] WHERE [OutboxMessage].[ProcessedDate] IS NULL """); var internalCommandsCountMeetings = await sqlConnection.ExecuteScalarAsync( """ SELECT COUNT(*) FROM [meetings].[InternalCommands] AS [InternalCommand] WHERE [InternalCommand].[ProcessedDate] IS NULL """); var inboxCountMeetings = await sqlConnection.ExecuteScalarAsync( """ SELECT COUNT(*) FROM [meetings].[InboxMessages] AS [InboxMessage] WHERE [InboxMessage].[ProcessedDate] IS NULL """); var outboxCountMeetings = await sqlConnection.ExecuteScalarAsync( """ SELECT COUNT(*) FROM [meetings].[OutboxMessages] AS [OutboxMessage] WHERE [OutboxMessage].[ProcessedDate] IS NULL """); var internalCommandsCountAdministration = await sqlConnection.ExecuteScalarAsync( """ SELECT COUNT(*) FROM [administration].[InternalCommands] AS [InternalCommand] WHERE [InternalCommand].[ProcessedDate] IS NULL """); var inboxCountMeetingsAdministration = await sqlConnection.ExecuteScalarAsync( """ SELECT COUNT(*) FROM [administration].[InboxMessages] AS [InboxMessage] WHERE [InboxMessage].[ProcessedDate] IS NULL """); var outboxCountMeetingsAdministration = await sqlConnection.ExecuteScalarAsync( """ SELECT COUNT(*) FROM [administration].[OutboxMessages] AS [OutboxMessage] WHERE [OutboxMessage].[ProcessedDate] IS NULL """); if (internalCommandsCountUsers == 0 && inboxCountUsers == 0 && outboxCountUsers == 0 && internalCommandsCountMeetings == 0 && inboxCountMeetings == 0 && outboxCountMeetings == 0 && internalCommandsCountAdministration == 0 && inboxCountMeetingsAdministration == 0 && outboxCountMeetingsAdministration == 0) { return; } Thread.Sleep(100); } throw new Exception("Timeout for processing elapsed."); } } } ================================================ FILE: src/Tests/SUT/SeedWork/DatabaseCleaner.cs ================================================ using System.Data; using Dapper; namespace CompanyName.MyMeetings.SUT.SeedWork { internal static class DatabaseCleaner { internal static async Task ClearAllData(IDbConnection connection) { await ClearAdministration(connection); await ClearApp(connection); await ClearMeetings(connection); await ClearPayments(connection); await ClearUsers(connection); await ClearRegistration(connection); } private static async Task ClearUsers(IDbConnection connection) { var clearUsersSql = "DELETE FROM [users].[InboxMessages] " + "DELETE FROM [users].[InternalCommands] " + "DELETE FROM [users].[OutboxMessages] " + "DELETE FROM [users].[Permissions] " + "DELETE FROM [users].[RolesToPermissions] " + "DELETE FROM [users].[UserRoles] " + "DELETE FROM [users].[Users] "; await connection.ExecuteScalarAsync(clearUsersSql); } private static async Task ClearPayments(IDbConnection connection) { var clearPaymentsSql = "DELETE FROM [payments].[InboxMessages] " + "DELETE FROM [payments].[InternalCommands] " + "DELETE FROM [payments].[MeetingFees] " + "DELETE FROM [payments].[Messages] " + "DELETE FROM [payments].[OutboxMessages] " + "DELETE FROM [payments].[Payers] " + "DELETE FROM [payments].[PriceListItems] " + "DELETE FROM [payments].[Streams] " + "DELETE FROM [payments].[SubscriptionCheckpoints] " + "DELETE FROM [payments].[SubscriptionDetails] " + "DELETE FROM [payments].[SubscriptionPayments] "; await connection.ExecuteScalarAsync(clearPaymentsSql); } private static async Task ClearMeetings(IDbConnection connection) { var clearMeetingsSql = "DELETE FROM [meetings].[Countries] " + "DELETE FROM [meetings].[InboxMessages] " + "DELETE FROM [meetings].[InternalCommands] " + "DELETE FROM [meetings].[MeetingAttendees] " + "DELETE FROM [meetings].[MeetingCommentingConfigurations] " + "DELETE FROM [meetings].[MeetingComments] " + "DELETE FROM [meetings].[MeetingGroupMembers] " + "DELETE FROM [meetings].[MeetingGroupProposals] " + "DELETE FROM [meetings].[MeetingGroups] " + "DELETE FROM [meetings].[MeetingMemberCommentLikes] " + "DELETE FROM [meetings].[MeetingNotAttendees] " + "DELETE FROM [meetings].[Meetings] " + "DELETE FROM [meetings].[MeetingWaitlistMembers] " + "DELETE FROM [meetings].[Members] " + "DELETE FROM [meetings].[MemberSubscriptions] " + "DELETE FROM [meetings].[OutboxMessages]"; await connection.ExecuteScalarAsync(clearMeetingsSql); } private static async Task ClearApp(IDbConnection connection) { var clearAppSql = "DELETE FROM [app].[Emails] "; await connection.ExecuteScalarAsync(clearAppSql); } private static async Task ClearAdministration(IDbConnection connection) { var clearAdministrationSql = "DELETE FROM [administration].[InboxMessages] " + "DELETE FROM [administration].[InternalCommands] " + "DELETE FROM [administration].[MeetingGroupProposals] " + "DELETE FROM [administration].[Members] " + "DELETE FROM [administration].[OutboxMessages] "; 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); } } } ================================================ FILE: src/Tests/SUT/SeedWork/ExecutionContextMock.cs ================================================ using CompanyName.MyMeetings.BuildingBlocks.Application; namespace CompanyName.MyMeetings.SUT.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; } } } ================================================ FILE: src/Tests/SUT/SeedWork/Probing/AssertErrorException.cs ================================================ namespace CompanyName.MyMeetings.SUT.SeedWork.Probing { public class AssertErrorException : Exception { public AssertErrorException(string message) : base(message) { } } } ================================================ FILE: src/Tests/SUT/SeedWork/Probing/IProbe.cs ================================================ namespace CompanyName.MyMeetings.SUT.SeedWork.Probing { public interface IProbe { bool IsSatisfied(); Task SampleAsync(); string DescribeFailureTo(); } public interface IProbe { bool IsSatisfied(T sample); Task GetSampleAsync(); string DescribeFailureTo(); } } ================================================ FILE: src/Tests/SUT/SeedWork/Probing/Poller.cs ================================================ namespace CompanyName.MyMeetings.SUT.SeedWork.Probing { public class Poller { private readonly int _timeoutMillis; private readonly int _pollDelayMillis; public Poller(int timeoutMillis) { _timeoutMillis = timeoutMillis; _pollDelayMillis = 1000; } public async Task CheckAsync(IProbe probe) { var timeout = new Timeout(_timeoutMillis); while (!probe.IsSatisfied()) { if (timeout.HasTimedOut()) { throw new AssertErrorException(DescribeFailureOf(probe)); } await Task.Delay(_pollDelayMillis); await probe.SampleAsync(); } } public async Task GetAsync(IProbe probe) where T : class { var timeout = new Timeout(_timeoutMillis); T sample = null; while (!probe.IsSatisfied(sample)) { if (timeout.HasTimedOut()) { throw new AssertErrorException(DescribeFailureOf(probe)); } await Task.Delay(_pollDelayMillis); sample = await probe.GetSampleAsync(); } return sample; } private static string DescribeFailureOf(IProbe probe) { return probe.DescribeFailureTo(); } private static string DescribeFailureOf(IProbe probe) { return probe.DescribeFailureTo(); } } } ================================================ FILE: src/Tests/SUT/SeedWork/Probing/Timeout.cs ================================================ namespace CompanyName.MyMeetings.SUT.SeedWork.Probing { public class Timeout { private readonly DateTime _endTime; public Timeout(int duration) { this._endTime = DateTime.Now.AddMilliseconds(duration); } public bool HasTimedOut() { return DateTime.Now > _endTime; } } } ================================================ FILE: src/Tests/SUT/SeedWork/TestBase.cs ================================================ using System.Data.SqlClient; using CompanyName.MyMeetings.BuildingBlocks.Application.Emails; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.Emails; using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus; using CompanyName.MyMeetings.Modules.Administration.Application.Contracts; using CompanyName.MyMeetings.Modules.Administration.Infrastructure; using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration; using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts; using CompanyName.MyMeetings.Modules.Meetings.Domain.SharedKernel; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure; using CompanyName.MyMeetings.Modules.Meetings.Infrastructure.Configuration; 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; using CompanyName.MyMeetings.SUT.SeedWork.Probing; using Dapper; using NSubstitute; using NUnit.Framework; using Serilog; namespace CompanyName.MyMeetings.SUT.SeedWork { public class TestBase { protected string ConnectionString { get; set; } protected virtual bool PerformDatabaseCleanup => false; protected virtual bool CreatePermissions => true; protected IEmailSender EmailSender { get; private set; } protected ILogger Logger { get; private set; } protected IUserAccessModule UserAccessModule { get; private set; } protected IRegistrationsModule RegistrationsModule { get; private set; } protected IMeetingsModule MeetingsModule { get; private set; } protected IAdministrationModule AdministrationModule { get; private set; } protected IPaymentsModule PaymentsModule { get; private set; } protected ExecutionContextMock ExecutionContextAccessor { get; private set; } protected IEventsBus EventsBus { get; private set; } [SetUp] public async Task BeforeEachTest() { SetConnectionString(); if (PerformDatabaseCleanup) { await this.ClearDatabase(); } if (CreatePermissions) { await this.SeedPermissions(); } ExecutionContextAccessor = new ExecutionContextMock(Guid.NewGuid()); var emailsConfiguration = new EmailsConfiguration("from@email.com"); Logger = Substitute.For(); EventsBus = new InMemoryEventBusClient(Logger); InitializeRegistrationsModule(emailsConfiguration); InitializeUserAccessModule(emailsConfiguration); InitializeMeetingsModule(emailsConfiguration); InitializeAdministrationModule(); PaymentsStartup.Initialize( ConnectionString, ExecutionContextAccessor, Logger, emailsConfiguration, EventsBus, true, 100); PaymentsModule = new PaymentsModule(); } public static async Task GetEventually(IProbe probe, int timeout) where T : class { var poller = new Poller(timeout); return await poller.GetAsync(probe); } [TearDown] public void AfterEachTest() { SystemClock.Reset(); Modules.Payments.Domain.SeedWork.SystemClock.Reset(); } protected async Task WaitForAsyncOperations() { await AsyncOperationsHelper.WaitForProcessing(ConnectionString); } protected void SetDate(DateTime date) { SystemClock.Set(date); Modules.Payments.Domain.SeedWork.SystemClock.Set(date); } protected async Task ExecuteScript(string scriptPath) { var sql = await File.ReadAllTextAsync(scriptPath); await using var sqlConnection = new SqlConnection(ConnectionString); await sqlConnection.ExecuteScalarAsync(sql); } private void InitializeAdministrationModule() { AdministrationStartup.Initialize( ConnectionString, ExecutionContextAccessor, Logger, EventsBus, 100); AdministrationModule = new AdministrationModule(); } private void InitializeMeetingsModule(EmailsConfiguration emailsConfiguration) { MeetingsStartup.Initialize( ConnectionString, ExecutionContextAccessor, Logger, emailsConfiguration, EventsBus, 100); MeetingsModule = new MeetingsModule(); } private async Task SeedPermissions() { await ExecuteScript("Scripts/SeedPermissions.sql"); } private void InitializeUserAccessModule(EmailsConfiguration emailsConfiguration) { Logger = Substitute.For(); EmailSender = Substitute.For(); UserAccessStartup.Initialize( ConnectionString, ExecutionContextAccessor, Logger, emailsConfiguration, "key", EmailSender, EventsBus, 100); 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"; ConnectionString = Environment.GetEnvironmentVariable(connectionStringEnvironmentVariable); if (ConnectionString == null) { throw new ApplicationException( $"Define connection string to SUT database using environment variable: {connectionStringEnvironmentVariable}"); } } private async Task ClearDatabase() { await using var sqlConnection = new SqlConnection(ConnectionString); await DatabaseCleaner.ClearAllData(sqlConnection); } } } ================================================ FILE: src/Tests/SUT/TestCases/CleanDatabaseTestCase.cs ================================================ using CompanyName.MyMeetings.SUT.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.SUT.TestCases { public class CleanDatabaseTestCase : TestBase { protected override bool PerformDatabaseCleanup => true; protected override bool CreatePermissions => false; [Test] public void Prepare() { } } } ================================================ FILE: src/Tests/SUT/TestCases/CreateMeeting.cs ================================================ using CompanyName.MyMeetings.SUT.Helpers; using CompanyName.MyMeetings.SUT.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.SUT.TestCases { public class CreateMeeting : TestBase { protected override bool PerformDatabaseCleanup => true; [Test] public async Task Prepare() { await UsersFactory.GivenAdmin( UserAccessModule, "testAdmin@mail.com", "testAdminPass", "Jane Doe", "Jane", "Doe", "testAdmin@mail.com"); var userId = await UsersFactory.GivenUser( RegistrationsModule, ConnectionString, "adamSmith@mail.com", "adamSmithPass", "Adam", "Smith", "adamSmith@mail.com"); ExecutionContextAccessor.SetUserId(userId); var meetingGroupId = await MeetingGroupsFactory.GivenMeetingGroup( MeetingsModule, AdministrationModule, ConnectionString, "Software Craft", "Group for software craft passionates", "Warsaw", "PL"); await TestPriceListManager.AddPriceListItems(PaymentsModule, ConnectionString); await TestPaymentsManager.BuySubscription( PaymentsModule, ExecutionContextAccessor); SetDate(new DateTime(2022, 7, 1, 10, 0, 0)); var meetingId = await TestMeetingFactory.GivenMeeting( MeetingsModule, meetingGroupId, "Tactical DDD", new DateTime(2022, 7, 10, 18, 0, 0), new DateTime(2022, 7, 10, 20, 0, 0), "Meeting about Tactical DDD patterns", "Location Name", "Location Address", "01-755", "Warsaw", 50, 0, null, null, 0, null, []); var attendeeUserId = await UsersFactory.GivenUser( RegistrationsModule, ConnectionString, "rickmorty@mail.com", "rickmortyPass", "Rick", "Morty", "rickmorty@mail.com"); ExecutionContextAccessor.SetUserId(attendeeUserId); await TestMeetingGroupManager.JoinToGroup(MeetingsModule, meetingGroupId); await TestMeetingManager.AddAttendee(MeetingsModule, meetingId, guestsNumber: 1); } } } ================================================ FILE: src/Tests/SUT/TestCases/OnlyAdminTestCase.cs ================================================ using CompanyName.MyMeetings.SUT.Helpers; using CompanyName.MyMeetings.SUT.SeedWork; using NUnit.Framework; namespace CompanyName.MyMeetings.SUT.TestCases { public class OnlyAdminTestCase : TestBase { protected override bool PerformDatabaseCleanup => true; [Test] public async Task Prepare() { await UsersFactory.GivenAdmin( UserAccessModule, "testAdmin@mail.com", "testAdminPass", "Jane Doe", "Jane", "Doe", "testAdmin@mail.com"); } } } ================================================ FILE: src/entrypoint.sh ================================================ #!/bin/bash echo "Waiting 60 seconds to start backend" sleep 60; echo "Backend starting..." dotnet CompanyName.MyMeetings.API.dll ================================================ FILE: src/global.json ================================================ { "sdk": { "version": "8.0.0", "rollForward": "latestFeature", "allowPrerelease": false } } ================================================ FILE: src/stylecop.json ================================================ { "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", "settings": { "documentationRules": { "companyName": "Kamil Grzybek", "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.", "variables": { "licenseName": "MIT", "licenseFile": "LICENSE" }, "headerDecoration": "-----------------------------------------------------------------------" } } }