├── .gitignore ├── Chapter05 ├── Marketplace.Domain │ ├── ClassifiedAd.cs │ ├── ClassifiedAdId.cs │ ├── ClassifiedAdText.cs │ ├── ClassifiedAdTitle.cs │ ├── Events.cs │ ├── IClassifiedAdRepository.cs │ ├── ICurrencyLookup.cs │ ├── InvalidEntityStateException.cs │ ├── Marketplace.Domain.csproj │ ├── Money.cs │ ├── Price.cs │ └── UserId.cs ├── Marketplace.Framework │ ├── Entity.cs │ ├── IApplicationService.cs │ ├── InvalidValueException.cs │ ├── Marketplace.Framework.csproj │ ├── Value.cs │ └── ValueExtensions.cs ├── Marketplace.Tests │ ├── ClassifiedAd_Publish_Spec.cs │ ├── FakeCurrencyLookup.cs │ ├── Marketplace.Tests.csproj │ └── Money_Spec.cs ├── Marketplace.sln ├── Marketplace │ ├── Api │ │ ├── ClassifiedAdsApplicationService.cs │ │ └── ClassifiedAdsCommandsApi.cs │ ├── ClassifiedAdRepository.cs │ ├── Contracts │ │ └── ClassifiedAds.cs │ ├── DuplicatedEntityIdException.cs │ ├── FixedCurrencyLookup.cs │ ├── Marketplace.csproj │ ├── Program.cs │ └── Startup.cs └── README.md ├── Chapter06 ├── Marketplace.Domain │ ├── ClassifiedAd.cs │ ├── ClassifiedAdId.cs │ ├── ClassifiedAdText.cs │ ├── ClassifiedAdTitle.cs │ ├── Events.cs │ ├── IClassifiedAdRepository.cs │ ├── ICurrencyLookup.cs │ ├── InvalidEntityStateException.cs │ ├── Marketplace.Domain.csproj │ ├── Money.cs │ ├── Price.cs │ └── UserId.cs ├── Marketplace.Framework │ ├── Entity.cs │ ├── IApplicationService.cs │ ├── InvalidValueException.cs │ ├── Marketplace.Framework.csproj │ ├── Value.cs │ └── ValueExtensions.cs ├── Marketplace.Tests │ ├── ClassifiedAd_Publish_Spec.cs │ ├── FakeCurrencyLookup.cs │ ├── Marketplace.Tests.csproj │ └── Money_Spec.cs ├── Marketplace.sln ├── Marketplace │ ├── Api │ │ ├── ClassifiedAdsApplicationService.cs │ │ └── ClassifiedAdsCommandsApi.cs │ ├── ClassifiedAdRepository.cs │ ├── Contracts │ │ └── ClassifiedAds.cs │ ├── DuplicatedEntityIdException.cs │ ├── FixedCurrencyLookup.cs │ ├── Marketplace.csproj │ ├── Program.cs │ └── Startup.cs ├── README.md └── docker-compose.yml ├── Chapter07 ├── Marketplace.Domain │ ├── ClassifiedAd.cs │ ├── ClassifiedAdId.cs │ ├── ClassifiedAdText.cs │ ├── ClassifiedAdTitle.cs │ ├── Events.cs │ ├── IClassifiedAdRepository.cs │ ├── ICurrencyLookup.cs │ ├── InvalidEntityStateException.cs │ ├── Marketplace.Domain.csproj │ ├── Money.cs │ ├── Picture.cs │ ├── PictureRules.cs │ ├── PictureSize.cs │ ├── Price.cs │ └── UserId.cs ├── Marketplace.Framework │ ├── AggregateRoot.cs │ ├── Entity.cs │ ├── IApplicationService.cs │ ├── IInternalEventHandler.cs │ ├── IUnitOfWork.cs │ ├── InvalidValueException.cs │ ├── Marketplace.Framework.csproj │ ├── Value.cs │ └── ValueExtensions.cs ├── Marketplace.Tests │ ├── ClassifiedAd_Publish_Spec.cs │ ├── FakeCurrencyLookup.cs │ ├── Marketplace.Tests.csproj │ └── Money_Spec.cs ├── Marketplace.sln ├── Marketplace │ ├── Api │ │ ├── ClassifiedAdsApplicationService.cs │ │ └── ClassifiedAdsCommandsApi.cs │ ├── Contracts │ │ └── ClassifiedAds.cs │ ├── DuplicatedEntityIdException.cs │ ├── FixedCurrencyLookup.cs │ ├── Infrastructure │ │ ├── ClassifiedAdRepository.cs │ │ └── RavenDbUnitOfWork.cs │ ├── Marketplace.csproj │ ├── Program.cs │ └── Startup.cs └── README.md ├── Chapter08 ├── before │ ├── Marketplace.Domain │ │ ├── ClassifiedAd.cs │ │ ├── ClassifiedAdId.cs │ │ ├── ClassifiedAdText.cs │ │ ├── ClassifiedAdTitle.cs │ │ ├── Events.cs │ │ ├── IClassifiedAdRepository.cs │ │ ├── ICurrencyLookup.cs │ │ ├── InvalidEntityStateException.cs │ │ ├── Marketplace.Domain.csproj │ │ ├── Money.cs │ │ ├── Picture.cs │ │ ├── PictureRules.cs │ │ ├── PictureSize.cs │ │ ├── Price.cs │ │ └── UserId.cs │ ├── Marketplace.Framework │ │ ├── AggregateRoot.cs │ │ ├── Entity.cs │ │ ├── IApplicationService.cs │ │ ├── IInternalEventHandler.cs │ │ ├── IUnitOfWork.cs │ │ ├── InvalidValueException.cs │ │ ├── Marketplace.Framework.csproj │ │ ├── Value.cs │ │ └── ValueExtensions.cs │ ├── Marketplace.Tests │ │ ├── ClassifiedAd_Publish_Spec.cs │ │ ├── FakeCurrencyLookup.cs │ │ ├── Marketplace.Tests.csproj │ │ └── Money_Spec.cs │ ├── Marketplace.sln │ ├── Marketplace │ │ ├── Api │ │ │ ├── ClassifiedAdsApplicationService.cs │ │ │ └── ClassifiedAdsCommandsApi.cs │ │ ├── Contracts │ │ │ └── ClassifiedAds.cs │ │ ├── DuplicatedEntityIdException.cs │ │ ├── FixedCurrencyLookup.cs │ │ ├── Infrastructure │ │ │ ├── ClassifiedAdRepository.cs │ │ │ └── RavenDbUnitOfWork.cs │ │ ├── Marketplace.csproj │ │ ├── Program.cs │ │ └── Startup.cs │ ├── README.md │ ├── docker-compose.yml │ └── init.sql ├── ef-core │ ├── Marketplace.Domain │ │ ├── ClassifiedAd.cs │ │ ├── ClassifiedAdId.cs │ │ ├── ClassifiedAdText.cs │ │ ├── ClassifiedAdTitle.cs │ │ ├── Events.cs │ │ ├── IClassifiedAdRepository.cs │ │ ├── ICurrencyLookup.cs │ │ ├── InvalidEntityStateException.cs │ │ ├── Marketplace.Domain.csproj │ │ ├── Money.cs │ │ ├── Picture.cs │ │ ├── PictureRules.cs │ │ ├── PictureSize.cs │ │ ├── Price.cs │ │ └── UserId.cs │ ├── Marketplace.Framework │ │ ├── AggregateRoot.cs │ │ ├── Entity.cs │ │ ├── IApplicationService.cs │ │ ├── IInternalEventHandler.cs │ │ ├── IUnitOfWork.cs │ │ ├── InvalidValueException.cs │ │ ├── Marketplace.Framework.csproj │ │ ├── Value.cs │ │ └── ValueExtensions.cs │ ├── Marketplace.Tests │ │ ├── ClassifiedAd_Publish_Spec.cs │ │ ├── FakeCurrencyLookup.cs │ │ ├── Marketplace.Tests.csproj │ │ └── Money_Spec.cs │ ├── Marketplace.sln │ ├── Marketplace │ │ ├── Api │ │ │ ├── ClassifiedAdsApplicationService.cs │ │ │ └── ClassifiedAdsCommandsApi.cs │ │ ├── Contracts │ │ │ └── ClassifiedAds.cs │ │ ├── DuplicatedEntityIdException.cs │ │ ├── FixedCurrencyLookup.cs │ │ ├── Infrastructure │ │ │ ├── ClassifiedAdDbContext.cs │ │ │ ├── ClassifiedAdRepository.cs │ │ │ └── EfCoreUnitOfWork.cs │ │ ├── Marketplace.csproj │ │ ├── Program.cs │ │ └── Startup.cs │ ├── README.md │ ├── docker-compose.yml │ └── init.sql └── ravendb │ ├── Marketplace.Domain │ ├── ClassifiedAd.cs │ ├── ClassifiedAdId.cs │ ├── ClassifiedAdText.cs │ ├── ClassifiedAdTitle.cs │ ├── Events.cs │ ├── IClassifiedAdRepository.cs │ ├── ICurrencyLookup.cs │ ├── InvalidEntityStateException.cs │ ├── Marketplace.Domain.csproj │ ├── Money.cs │ ├── Picture.cs │ ├── PictureRules.cs │ ├── PictureSize.cs │ ├── Price.cs │ └── UserId.cs │ ├── Marketplace.Framework │ ├── AggregateRoot.cs │ ├── Entity.cs │ ├── IApplicationService.cs │ ├── IInternalEventHandler.cs │ ├── IUnitOfWork.cs │ ├── InvalidValueException.cs │ ├── Marketplace.Framework.csproj │ ├── Value.cs │ └── ValueExtensions.cs │ ├── Marketplace.Tests │ ├── ClassifiedAd_Publish_Spec.cs │ ├── FakeCurrencyLookup.cs │ ├── Marketplace.Tests.csproj │ └── Money_Spec.cs │ ├── Marketplace.sln │ ├── Marketplace │ ├── Api │ │ ├── ClassifiedAdsApplicationService.cs │ │ └── ClassifiedAdsCommandsApi.cs │ ├── Contracts │ │ └── ClassifiedAds.cs │ ├── DuplicatedEntityIdException.cs │ ├── FixedCurrencyLookup.cs │ ├── Infrastructure │ │ ├── ClassifiedAdRepository.cs │ │ └── RavenDbUnitOfWork.cs │ ├── Marketplace.csproj │ ├── Program.cs │ └── Startup.cs │ ├── README.md │ └── docker-compose.yml ├── Chapter09 ├── ef-core │ ├── Marketplace.Domain │ │ ├── ClassifiedAd │ │ │ ├── ClassifiedAd.cs │ │ │ ├── ClassifiedAdId.cs │ │ │ ├── ClassifiedAdText.cs │ │ │ ├── ClassifiedAdTitle.cs │ │ │ ├── Events.cs │ │ │ ├── IClassifiedAdRepository.cs │ │ │ ├── Picture.cs │ │ │ ├── PictureRules.cs │ │ │ ├── PictureSize.cs │ │ │ └── Price.cs │ │ ├── Marketplace.Domain.csproj │ │ ├── Shared │ │ │ ├── ContentModeration.cs │ │ │ ├── Exceptions.cs │ │ │ ├── ICurrencyLookup.cs │ │ │ ├── Money.cs │ │ │ └── UserId.cs │ │ └── UserProfile │ │ │ ├── DisplayName.cs │ │ │ ├── Events.cs │ │ │ ├── FullName.cs │ │ │ ├── IUserProfileRepository.cs │ │ │ └── UserProfile.cs │ ├── Marketplace.Framework │ │ ├── AggregateRoot.cs │ │ ├── Entity.cs │ │ ├── IApplicationService.cs │ │ ├── IInternalEventHandler.cs │ │ ├── IUnitOfWork.cs │ │ ├── InvalidValueException.cs │ │ ├── Marketplace.Framework.csproj │ │ ├── StringTools.cs │ │ ├── Value.cs │ │ └── ValueExtensions.cs │ ├── Marketplace.Tests │ │ ├── ClassifiedAd_Publish_Spec.cs │ │ ├── FakeCurrencyLookup.cs │ │ ├── Marketplace.Tests.csproj │ │ └── Money_Spec.cs │ ├── Marketplace.sln │ ├── Marketplace │ │ ├── ClassifiedAd │ │ │ ├── ClassifiedAdRepository.cs │ │ │ ├── ClassifiedAdsApplicationService.cs │ │ │ ├── ClassifiedAdsCommandsApi.cs │ │ │ ├── ClassifiedAdsQueryApi.cs │ │ │ ├── Commands.cs │ │ │ ├── Queries.cs │ │ │ ├── QueryModels.cs │ │ │ └── ReadModels.cs │ │ ├── Exceptions.cs │ │ ├── Infrastructure │ │ │ ├── AppBuilderDatabaseExtensions.cs │ │ │ ├── EfCoreUnitOfWork.cs │ │ │ ├── FixedCurrencyLookup.cs │ │ │ ├── MarketplaceDbContext.cs │ │ │ ├── PurgomalumClient.cs │ │ │ └── RequestHandler.cs │ │ ├── Marketplace.csproj │ │ ├── Program.cs │ │ ├── Startup.cs │ │ └── UserProfile │ │ │ ├── Contracts.cs │ │ │ ├── UserProfileApplicationService.cs │ │ │ ├── UserProfileCommandsApi.cs │ │ │ └── UserProfileRepository.cs │ ├── README.md │ ├── docker-compose.yml │ └── init.sql └── ravendb │ ├── Marketplace.Domain │ ├── ClassifiedAd │ │ ├── ClassifiedAd.cs │ │ ├── ClassifiedAdId.cs │ │ ├── ClassifiedAdText.cs │ │ ├── ClassifiedAdTitle.cs │ │ ├── Events.cs │ │ ├── IClassifiedAdRepository.cs │ │ ├── Picture.cs │ │ ├── PictureRules.cs │ │ ├── PictureSize.cs │ │ └── Price.cs │ ├── Marketplace.Domain.csproj │ ├── Shared │ │ ├── ContentModeration.cs │ │ ├── Exceptions.cs │ │ ├── ICurrencyLookup.cs │ │ ├── Money.cs │ │ └── UserId.cs │ └── UserProfile │ │ ├── DisplayName.cs │ │ ├── Events.cs │ │ ├── FullName.cs │ │ ├── IUserProfileRepository.cs │ │ └── UserProfile.cs │ ├── Marketplace.Framework │ ├── AggregateRoot.cs │ ├── Entity.cs │ ├── IApplicationService.cs │ ├── IInternalEventHandler.cs │ ├── IUnitOfWork.cs │ ├── InvalidValueException.cs │ ├── Marketplace.Framework.csproj │ ├── StringTools.cs │ ├── Value.cs │ └── ValueExtensions.cs │ ├── Marketplace.Tests │ ├── ClassifiedAd_Publish_Spec.cs │ ├── FakeCurrencyLookup.cs │ ├── Marketplace.Tests.csproj │ └── Money_Spec.cs │ ├── Marketplace.sln │ ├── Marketplace │ ├── ClassifiedAd │ │ ├── ClassifiedAdRepository.cs │ │ ├── ClassifiedAdsApplicationService.cs │ │ ├── ClassifiedAdsCommandsApi.cs │ │ ├── ClassifiedAdsQueryApi.cs │ │ ├── Commands.cs │ │ ├── Queries.cs │ │ ├── QueryModels.cs │ │ └── ReadModels.cs │ ├── Exceptions.cs │ ├── Infrastructure │ │ ├── FixedCurrencyLookup.cs │ │ ├── PurgomalumClient.cs │ │ ├── RavenDbRepository.cs │ │ ├── RavenDbUnitOfWork.cs │ │ └── RequestHandler.cs │ ├── Marketplace.csproj │ ├── Program.cs │ ├── Startup.cs │ └── UserProfile │ │ ├── Contracts.cs │ │ ├── UserProfileApplicationService.cs │ │ ├── UserProfileCommandsApi.cs │ │ └── UserProfileRepository.cs │ ├── README.md │ └── docker-compose.yml ├── Chapter10 ├── Marketplace.Domain │ ├── ClassifiedAd │ │ ├── ClassifiedAd.cs │ │ ├── ClassifiedAdId.cs │ │ ├── ClassifiedAdText.cs │ │ ├── ClassifiedAdTitle.cs │ │ ├── Events.cs │ │ ├── Picture.cs │ │ ├── PictureRules.cs │ │ ├── PictureSize.cs │ │ └── Price.cs │ ├── Marketplace.Domain.csproj │ ├── Shared │ │ ├── ContentModeration.cs │ │ ├── Exceptions.cs │ │ ├── ICurrencyLookup.cs │ │ ├── Money.cs │ │ └── UserId.cs │ └── UserProfile │ │ ├── DisplayName.cs │ │ ├── Events.cs │ │ ├── FullName.cs │ │ └── UserProfile.cs ├── Marketplace.Framework │ ├── AggregateRoot.cs │ ├── ApplicationServiceExtensions.cs │ ├── Entity.cs │ ├── IAggregateStore.cs │ ├── IApplicationService.cs │ ├── IInternalEventHandler.cs │ ├── InvalidValueException.cs │ ├── Marketplace.Framework.csproj │ ├── StringTools.cs │ ├── Value.cs │ └── ValueExtensions.cs ├── Marketplace.Tests │ ├── ClassifiedAd_Publish_Spec.cs │ ├── FakeCurrencyLookup.cs │ ├── Marketplace.Tests.csproj │ └── Money_Spec.cs ├── Marketplace.sln ├── Marketplace │ ├── ClassifiedAd │ │ ├── ClassifiedAdsApplicationService.cs │ │ ├── ClassifiedAdsCommandsApi.cs │ │ └── Commands.cs │ ├── Exceptions.cs │ ├── HostedService.cs │ ├── Infrastructure │ │ ├── EsAggregateStore.cs │ │ ├── FixedCurrencyLookup.cs │ │ ├── PurgomalumClient.cs │ │ └── RequestHandler.cs │ ├── Marketplace.csproj │ ├── Program.cs │ ├── Startup.cs │ ├── UserProfile │ │ ├── Contracts.cs │ │ ├── UserProfileApplicationService.cs │ │ └── UserProfileCommandsApi.cs │ └── appsettings.json └── docker-compose.yml ├── Chapter11 ├── docker-compose.yml ├── in-database │ ├── Marketplace.Domain │ │ ├── ClassifiedAd │ │ │ ├── ClassifiedAd.cs │ │ │ ├── ClassifiedAdId.cs │ │ │ ├── ClassifiedAdText.cs │ │ │ ├── ClassifiedAdTitle.cs │ │ │ ├── Events.cs │ │ │ ├── Picture.cs │ │ │ ├── PictureRules.cs │ │ │ ├── PictureSize.cs │ │ │ └── Price.cs │ │ ├── Marketplace.Domain.csproj │ │ ├── Shared │ │ │ ├── ContentModeration.cs │ │ │ ├── Exceptions.cs │ │ │ ├── ICurrencyLookup.cs │ │ │ ├── Money.cs │ │ │ └── UserId.cs │ │ └── UserProfile │ │ │ ├── DisplayName.cs │ │ │ ├── Events.cs │ │ │ ├── FullName.cs │ │ │ └── UserProfile.cs │ ├── Marketplace.Framework │ │ ├── AggregateRoot.cs │ │ ├── ApplicationServiceExtensions.cs │ │ ├── Entity.cs │ │ ├── IAggregateStore.cs │ │ ├── IApplicationService.cs │ │ ├── ICheckpointStore.cs │ │ ├── IInternalEventHandler.cs │ │ ├── IProjection.cs │ │ ├── InvalidValueException.cs │ │ ├── Marketplace.Framework.csproj │ │ ├── StringTools.cs │ │ ├── Value.cs │ │ └── ValueExtensions.cs │ ├── Marketplace.Tests │ │ ├── ClassifiedAd_Publish_Spec.cs │ │ ├── FakeCurrencyLookup.cs │ │ ├── Marketplace.Tests.csproj │ │ └── Money_Spec.cs │ ├── Marketplace.sln │ └── Marketplace │ │ ├── ClassifiedAd │ │ ├── ClassifiedAdsApplicationService.cs │ │ ├── ClassifiedAdsCommandsApi.cs │ │ ├── ClassifiedAdsQueryApi.cs │ │ ├── Commands.cs │ │ ├── Queries.cs │ │ └── QueryModels.cs │ │ ├── EventStoreService.cs │ │ ├── Exceptions.cs │ │ ├── Infrastructure │ │ ├── Checkpoint.cs │ │ ├── EsAggregateStore.cs │ │ ├── EventDeserializer.cs │ │ ├── EventMetadata.cs │ │ ├── EventStoreExtensions.cs │ │ ├── FixedCurrencyLookup.cs │ │ ├── ProjectionManager.cs │ │ ├── PurgomalumClient.cs │ │ ├── RavenDbCheckpointStore.cs │ │ ├── RavenDbProjection.cs │ │ └── RequestHandler.cs │ │ ├── Marketplace.csproj │ │ ├── Program.cs │ │ ├── Projections │ │ ├── ClassifiedAdDetailsProjection.cs │ │ ├── ClassifiedAdUpcasters.cs │ │ ├── ReadModels.cs │ │ └── UserDetailsProjection.cs │ │ ├── Properties │ │ └── launchSettings.json │ │ ├── Startup.cs │ │ ├── UserProfile │ │ ├── Contracts.cs │ │ ├── Queries.cs │ │ ├── UserProfileApplicationService.cs │ │ └── UserProfileCommandsApi.cs │ │ └── appsettings.json └── in-memory │ ├── Marketplace.Domain │ ├── ClassifiedAd │ │ ├── ClassifiedAd.cs │ │ ├── ClassifiedAdId.cs │ │ ├── ClassifiedAdText.cs │ │ ├── ClassifiedAdTitle.cs │ │ ├── Events.cs │ │ ├── Picture.cs │ │ ├── PictureRules.cs │ │ ├── PictureSize.cs │ │ └── Price.cs │ ├── Marketplace.Domain.csproj │ ├── Shared │ │ ├── ContentModeration.cs │ │ ├── Exceptions.cs │ │ ├── ICurrencyLookup.cs │ │ ├── Money.cs │ │ └── UserId.cs │ └── UserProfile │ │ ├── DisplayName.cs │ │ ├── Events.cs │ │ ├── FullName.cs │ │ └── UserProfile.cs │ ├── Marketplace.Framework │ ├── AggregateRoot.cs │ ├── ApplicationServiceExtensions.cs │ ├── Entity.cs │ ├── IAggregateStore.cs │ ├── IApplicationService.cs │ ├── IInternalEventHandler.cs │ ├── IProjection.cs │ ├── InvalidValueException.cs │ ├── Marketplace.Framework.csproj │ ├── StringTools.cs │ ├── Value.cs │ └── ValueExtensions.cs │ ├── Marketplace.Tests │ ├── ClassifiedAd_Publish_Spec.cs │ ├── FakeCurrencyLookup.cs │ ├── Marketplace.Tests.csproj │ └── Money_Spec.cs │ ├── Marketplace.sln │ └── Marketplace │ ├── ClassifiedAd │ ├── ClassifiedAdsApplicationService.cs │ ├── ClassifiedAdsCommandsApi.cs │ ├── ClassifiedAdsQueryApi.cs │ ├── Commands.cs │ ├── Queries.cs │ └── QueryModels.cs │ ├── EventStoreService.cs │ ├── Exceptions.cs │ ├── Infrastructure │ ├── EsAggregateStore.cs │ ├── EventDeserializer.cs │ ├── EventMetadata.cs │ ├── EventStoreExtensions.cs │ ├── FixedCurrencyLookup.cs │ ├── ProjectionManager.cs │ ├── PurgomalumClient.cs │ └── RequestHandler.cs │ ├── Marketplace.csproj │ ├── Program.cs │ ├── Projections │ ├── ClassifiedAdDetailsProjection.cs │ ├── ClassifiedAdUpcasters.cs │ ├── ReadModels.cs │ └── UserDetailsProjection.cs │ ├── Properties │ └── launchSettings.json │ ├── Startup.cs │ ├── UserProfile │ ├── Contracts.cs │ ├── UserProfileApplicationService.cs │ └── UserProfileCommandsApi.cs │ └── appsettings.json ├── Chapter13 ├── Marketplace.sln ├── Marketplace.sln.DotSettings ├── docker-compose.yml ├── src │ ├── Marketplace.Ads.Domain │ │ ├── ClassifiedAds │ │ │ ├── ClassifiedAd.cs │ │ │ ├── ClassifiedAdId.cs │ │ │ ├── ClassifiedAdText.cs │ │ │ ├── ClassifiedAdTitle.cs │ │ │ ├── Picture.cs │ │ │ ├── PictureRules.cs │ │ │ ├── PictureSize.cs │ │ │ └── Price.cs │ │ ├── Marketplace.Ads.Domain.csproj │ │ └── Shared │ │ │ ├── ContentModeration.cs │ │ │ ├── Exceptions.cs │ │ │ ├── ICurrencyLookup.cs │ │ │ ├── Money.cs │ │ │ └── UserId.cs │ ├── Marketplace.Ads.Messages │ │ ├── Ads │ │ │ ├── Commands.cs │ │ │ └── Events.cs │ │ └── Marketplace.Ads.Messages.csproj │ ├── Marketplace.Ads │ │ ├── ClassifiedAds │ │ │ ├── ClassifiedAdsCommandService.cs │ │ │ ├── ClassifiedAdsCommandsApi.cs │ │ │ ├── ClassifiedAdsQueryApi.cs │ │ │ ├── Queries.cs │ │ │ └── QueryModels.cs │ │ ├── EventMappings.cs │ │ ├── Marketplace.Ads.csproj │ │ ├── Module.cs │ │ └── Projections │ │ │ ├── ClassifiedAdDetailsProjection.cs │ │ │ ├── MyClassifiedAdsProjection.cs │ │ │ └── ReadModels.cs │ ├── Marketplace.EventSourcing │ │ ├── AggregateId.cs │ │ ├── AggregateRoot.cs │ │ ├── AggregateState.cs │ │ ├── ApplicationService.cs │ │ ├── ApplicationServiceExtensions.cs │ │ ├── CommandService.cs │ │ ├── Entity.cs │ │ ├── IAggregateStore.cs │ │ ├── IApplicationService.cs │ │ ├── ICheckpointStore.cs │ │ ├── IInternalEventHandler.cs │ │ ├── ISubscription.cs │ │ ├── InvalidValueException.cs │ │ ├── Marketplace.EventSourcing.csproj │ │ ├── StringTools.cs │ │ ├── TypeMapper.cs │ │ ├── Value.cs │ │ └── ValueExtensions.cs │ ├── Marketplace.EventStore │ │ ├── EsAggregateStore.cs │ │ ├── EsCheckpointStore.cs │ │ ├── EventDeserializer.cs │ │ ├── EventMetadata.cs │ │ ├── EventStoreExtensions.cs │ │ ├── EventStoreReactor.cs │ │ ├── EventStoreService.cs │ │ ├── FunctionalStore.cs │ │ ├── Marketplace.EventStore.csproj │ │ └── SubscriptionManager.cs │ ├── Marketplace.PaidServices.Domain │ │ ├── ClassifiedAds │ │ │ ├── ActivePaidService.cs │ │ │ ├── ClassifiedAd.cs │ │ │ ├── ClassifiedAdId.cs │ │ │ └── ClassifiedAdState.cs │ │ ├── Exceptions.cs │ │ ├── Marketplace.PaidServices.Domain.csproj │ │ ├── Orders │ │ │ ├── Order.cs │ │ │ ├── OrderId.cs │ │ │ └── OrderState.cs │ │ ├── Services │ │ │ ├── Exceptions.cs │ │ │ └── PaidService.cs │ │ └── Shared │ │ │ └── UserId.cs │ ├── Marketplace.PaidServices.Messages │ │ ├── Ads │ │ │ ├── Commands.cs │ │ │ └── Events.cs │ │ ├── Marketplace.PaidServices.Messages.csproj │ │ └── Orders │ │ │ ├── Commands.cs │ │ │ └── Events.cs │ ├── Marketplace.PaidServices │ │ ├── ClassifiedAds │ │ │ └── ClassifiedAdCommandService.cs │ │ ├── EventMappings.cs │ │ ├── Marketplace.PaidServices.csproj │ │ ├── Module.cs │ │ ├── Orders │ │ │ ├── OrdersCommandApi.cs │ │ │ └── OrdersCommandService.cs │ │ ├── PaidServices │ │ │ ├── Models.cs │ │ │ └── PaidServicesQueryApi.cs │ │ ├── Projections │ │ │ ├── CompletedOrderProjection.cs │ │ │ ├── DraftOrderProjection.cs │ │ │ └── ReadModels.cs │ │ └── Reactors │ │ │ └── OrderReactor.cs │ ├── Marketplace.RavenDb │ │ ├── GetSession.cs │ │ ├── Marketplace.RavenDb.csproj │ │ ├── RavenDbCheckpointStore.cs │ │ ├── RavenDbExtensions.cs │ │ └── RavenDbProjection.cs │ ├── Marketplace.Users.Domain │ │ ├── Marketplace.Users.Domain.csproj │ │ ├── Shared │ │ │ ├── ContentModeration.cs │ │ │ └── Exceptions.cs │ │ └── UserProfiles │ │ │ ├── DisplayName.cs │ │ │ ├── FullName.cs │ │ │ ├── UserId.cs │ │ │ └── UserProfile.cs │ ├── Marketplace.Users.Messages │ │ ├── Marketplace.Users.Messages.csproj │ │ └── UserProfile │ │ │ ├── Commands.cs │ │ │ └── Events.cs │ ├── Marketplace.Users │ │ ├── Auth │ │ │ ├── AuthApi.cs │ │ │ ├── AuthService.cs │ │ │ └── Contracts.cs │ │ ├── EventMappings.cs │ │ ├── Marketplace.Users.csproj │ │ ├── Module.cs │ │ ├── Projections │ │ │ ├── ReadModels.cs │ │ │ └── UserDetailsProjection.cs │ │ └── UserProfiles │ │ │ ├── Queries.cs │ │ │ ├── UserProfileCommandService.cs │ │ │ ├── UserProfileCommandsApi.cs │ │ │ └── UserProfileQueryApi.cs │ ├── Marketplace.WebApi │ │ ├── CommandApi.cs │ │ ├── ControllerBaseExtensions.cs │ │ ├── Conventions.cs │ │ └── Marketplace.WebApi.csproj │ └── Marketplace │ │ ├── ClientApp │ │ ├── .gitignore │ │ ├── README.md │ │ ├── babel.config.js │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ │ ├── favicon.ico │ │ │ └── index.html │ │ └── src │ │ │ ├── App.vue │ │ │ ├── assets │ │ │ ├── list-is-empty.png │ │ │ ├── logo.png │ │ │ └── logo.svg │ │ │ ├── common │ │ │ ├── api.service.js │ │ │ └── config.js │ │ │ ├── components │ │ │ ├── AdImage.vue │ │ │ ├── AdListItem.vue │ │ │ ├── AdPrice.vue │ │ │ ├── AdText.vue │ │ │ ├── AdTitle.vue │ │ │ ├── AdsList.vue │ │ │ ├── Header.vue │ │ │ └── NewAdButton.vue │ │ │ ├── main.js │ │ │ ├── plugins │ │ │ └── vuetify.js │ │ │ ├── router.js │ │ │ ├── store │ │ │ ├── index.js │ │ │ └── modules │ │ │ │ ├── ads │ │ │ │ ├── actions.type.js │ │ │ │ ├── index.js │ │ │ │ └── mutations.type.js │ │ │ │ ├── auth │ │ │ │ ├── actions.type.js │ │ │ │ ├── index.js │ │ │ │ └── mutations.type.js │ │ │ │ └── services │ │ │ │ ├── actions.type.js │ │ │ │ ├── index.js │ │ │ │ └── mutations.type.js │ │ │ └── views │ │ │ ├── About.vue │ │ │ ├── AdServices.vue │ │ │ ├── Home.vue │ │ │ ├── Login.vue │ │ │ ├── NewAd.vue │ │ │ └── Register.vue │ │ ├── Exceptions.cs │ │ ├── Infrastructure │ │ ├── Currency │ │ │ └── FixedCurrencyLookup.cs │ │ ├── Profanity │ │ │ └── PurgomalumClient.cs │ │ ├── RavenDb │ │ │ └── Configuration.cs │ │ └── Vue │ │ │ └── VueDevelopmentServerMiddleware.cs │ │ ├── Marketplace.csproj │ │ ├── Modules │ │ └── Images │ │ │ ├── ImageQueryService.cs │ │ │ ├── ImageStorage.cs │ │ │ └── PictureApi.cs │ │ ├── Program.cs │ │ ├── Properties │ │ └── launchSettings.json │ │ ├── Startup.cs │ │ └── appsettings.json └── tests │ └── Marketplace.Ads.Tests │ ├── ClassifiedAd_Publish_Spec.cs │ ├── FakeCurrencyLookup.cs │ ├── Marketplace.Ads.Tests.csproj │ └── Money_Spec.cs ├── LICENSE ├── Marketplace.Domain ├── ClassifiedAd.cs ├── ClassifiedAdId.cs ├── ClassifiedAdText.cs ├── ClassifiedAdTitle.cs ├── Events.cs ├── ICurrencyLookup.cs ├── InvalidEntityStateException.cs ├── Marketplace.Domain.csproj ├── Money.cs ├── Price.cs └── UserId.cs ├── Marketplace.Framework ├── Entity.cs ├── InvalidValueException.cs ├── Marketplace.Framework.csproj ├── Value.cs └── ValueExtensions.cs ├── Marketplace.Tests ├── ClassifiedAd_Publish_Spec.cs ├── FakeCurrencyLookup.cs ├── Marketplace.Tests.csproj └── Money_Spec.cs ├── Marketplace.sln ├── Marketplace ├── Api │ ├── ClassifiedAdsApplicationService.cs │ └── ClassifiedAdsCommandsApi.cs ├── Contracts │ └── ClassifiedAds.cs ├── Marketplace.csproj ├── Program.cs └── Startup.cs └── README.md /Chapter05/Marketplace.Domain/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | public string Value { get; } 8 | 9 | internal ClassifiedAdText(string text) => Value = text; 10 | 11 | public static ClassifiedAdText FromString(string text) => 12 | new ClassifiedAdText(text); 13 | 14 | public static implicit operator string(ClassifiedAdText text) => 15 | text.Value; 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter05/Marketplace.Domain/IClassifiedAdRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface IClassifiedAdRepository 6 | { 7 | Task Exists(ClassifiedAdId id); 8 | 9 | Task Load(ClassifiedAdId id); 10 | 11 | Task Save(ClassifiedAd entity); 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter05/Marketplace.Domain/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | Currency FindCurrency(string currencyCode); 8 | } 9 | 10 | public class Currency : Value 11 | { 12 | public string CurrencyCode { get; set; } 13 | public bool InUse { get; set; } 14 | public int DecimalPlaces { get; set; } 15 | 16 | public static Currency None = new Currency {InUse = false}; 17 | } 18 | } -------------------------------------------------------------------------------- /Chapter05/Marketplace.Domain/InvalidEntityStateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class InvalidEntityStateException : Exception 6 | { 7 | public InvalidEntityStateException(object entity, string message) 8 | : base($"Entity {entity.GetType().Name} state change rejected, {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter05/Marketplace.Domain/Marketplace.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 8 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter05/Marketplace.Domain/Price.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class Price : Money 6 | { 7 | private Price(decimal amount, string currencyCode, ICurrencyLookup currencyLookup) 8 | : base(amount, currencyCode, currencyLookup) 9 | { 10 | if (amount < 0) 11 | throw new ArgumentException( 12 | "Price cannot be negative", 13 | nameof(amount)); 14 | } 15 | 16 | internal Price(decimal amount, string currencyCode) 17 | : base(amount, new Currency{CurrencyCode = currencyCode}) 18 | { 19 | } 20 | 21 | public new static Price FromDecimal(decimal amount, string currency, 22 | ICurrencyLookup currencyLookup) => 23 | new Price(amount, currency, currencyLookup); 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter05/Marketplace.Domain/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class UserId 6 | { 7 | private Guid Value { get; set; } 8 | 9 | public UserId(Guid value) 10 | { 11 | if (value == default) 12 | throw new ArgumentNullException(nameof(value), "User id cannot be empty"); 13 | 14 | Value = value; 15 | } 16 | 17 | public static implicit operator Guid(UserId self) => self.Value; 18 | } 19 | } -------------------------------------------------------------------------------- /Chapter05/Marketplace.Framework/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Marketplace.Framework 6 | { 7 | public abstract class Entity where TId : IEquatable 8 | { 9 | private readonly List _events; 10 | 11 | protected Entity() => _events = new List(); 12 | 13 | protected void Apply(object @event) 14 | { 15 | When(@event); 16 | EnsureValidState(); 17 | _events.Add(@event); 18 | } 19 | 20 | protected abstract void When(object @event); 21 | 22 | public IEnumerable GetChanges() => _events.AsEnumerable(); 23 | 24 | public void ClearChanges() => _events.Clear(); 25 | 26 | protected abstract void EnsureValidState(); 27 | } 28 | } -------------------------------------------------------------------------------- /Chapter05/Marketplace.Framework/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IApplicationService 6 | { 7 | Task Handle(object command); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter05/Marketplace.Framework/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter05/Marketplace.Framework/Marketplace.Framework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | -------------------------------------------------------------------------------- /Chapter05/Marketplace.Framework/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class ValueExtensions 4 | { 5 | public static void MustNotBeNull(this Value value) where T : Value 6 | { 7 | if (value == null) 8 | throw new InvalidValueException(typeof(T), "cannot be null"); 9 | } 10 | 11 | public static void MustBe(this Value value) where T : Value 12 | { 13 | if (value == null) 14 | throw new InvalidValueException(typeof(T), "cannot be null"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter05/Marketplace.Tests/Marketplace.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter05/Marketplace/DuplicatedEntityIdException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace 4 | { 5 | public class DuplicatedEntityIdException : Exception 6 | { 7 | public DuplicatedEntityIdException(string message) 8 | : base(message) 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter05/README.md: -------------------------------------------------------------------------------- 1 | # Hands-on DDD with C# book code 2 | 3 | The book is **work in progress** and as I am working through 4 | chapters, the code will change. 5 | 6 | The book will contain almost everything from the history of this repository so 7 | I have no plans to have code per chapter. 8 | 9 | The book is currently available as [early access eBook](https://www.packtpub.com/application-development/hands-domain-driven-design-net). 10 | -------------------------------------------------------------------------------- /Chapter06/Marketplace.Domain/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | public string Value { get; } 8 | 9 | internal ClassifiedAdText(string text) => Value = text; 10 | 11 | public static ClassifiedAdText FromString(string text) => 12 | new ClassifiedAdText(text); 13 | 14 | public static implicit operator string(ClassifiedAdText text) => 15 | text.Value; 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter06/Marketplace.Domain/IClassifiedAdRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface IClassifiedAdRepository 6 | { 7 | Task Exists(ClassifiedAdId id); 8 | 9 | Task Load(ClassifiedAdId id); 10 | 11 | Task Save(ClassifiedAd entity); 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter06/Marketplace.Domain/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | Currency FindCurrency(string currencyCode); 8 | } 9 | 10 | public class Currency : Value 11 | { 12 | public string CurrencyCode { get; set; } 13 | public bool InUse { get; set; } 14 | public int DecimalPlaces { get; set; } 15 | 16 | public static Currency None = new Currency {InUse = false}; 17 | } 18 | } -------------------------------------------------------------------------------- /Chapter06/Marketplace.Domain/InvalidEntityStateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class InvalidEntityStateException : Exception 6 | { 7 | public InvalidEntityStateException(object entity, string message) 8 | : base($"Entity {entity.GetType().Name} state change rejected, {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter06/Marketplace.Domain/Marketplace.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 8 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter06/Marketplace.Domain/Price.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class Price : Money 6 | { 7 | private Price(decimal amount, string currencyCode, ICurrencyLookup currencyLookup) 8 | : base(amount, currencyCode, currencyLookup) 9 | { 10 | if (amount < 0) 11 | throw new ArgumentException( 12 | "Price cannot be negative", 13 | nameof(amount)); 14 | } 15 | 16 | internal Price(decimal amount, string currencyCode) 17 | : base(amount, new Currency{CurrencyCode = currencyCode}) 18 | { 19 | } 20 | 21 | public new static Price FromDecimal(decimal amount, string currency, 22 | ICurrencyLookup currencyLookup) => 23 | new Price(amount, currency, currencyLookup); 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter06/Marketplace.Domain/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class UserId 6 | { 7 | private Guid Value { get; set; } 8 | 9 | public UserId(Guid value) 10 | { 11 | if (value == default) 12 | throw new ArgumentNullException(nameof(value), "User id cannot be empty"); 13 | 14 | Value = value; 15 | } 16 | 17 | public static implicit operator Guid(UserId self) => self.Value; 18 | } 19 | } -------------------------------------------------------------------------------- /Chapter06/Marketplace.Framework/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Marketplace.Framework 6 | { 7 | public abstract class Entity where TId : IEquatable 8 | { 9 | private readonly List _events; 10 | 11 | protected Entity() => _events = new List(); 12 | 13 | protected void Apply(object @event) 14 | { 15 | When(@event); 16 | EnsureValidState(); 17 | _events.Add(@event); 18 | } 19 | 20 | protected abstract void When(object @event); 21 | 22 | public IEnumerable GetChanges() => _events.AsEnumerable(); 23 | 24 | public void ClearChanges() => _events.Clear(); 25 | 26 | protected abstract void EnsureValidState(); 27 | } 28 | } -------------------------------------------------------------------------------- /Chapter06/Marketplace.Framework/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IApplicationService 6 | { 7 | Task Handle(object command); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter06/Marketplace.Framework/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter06/Marketplace.Framework/Marketplace.Framework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | -------------------------------------------------------------------------------- /Chapter06/Marketplace.Framework/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class ValueExtensions 4 | { 5 | public static void MustNotBeNull(this Value value) where T : Value 6 | { 7 | if (value == null) 8 | throw new InvalidValueException(typeof(T), "cannot be null"); 9 | } 10 | 11 | public static void MustBe(this Value value) where T : Value 12 | { 13 | if (value == null) 14 | throw new InvalidValueException(typeof(T), "cannot be null"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter06/Marketplace.Tests/Marketplace.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter06/Marketplace/DuplicatedEntityIdException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace 4 | { 5 | public class DuplicatedEntityIdException : Exception 6 | { 7 | public DuplicatedEntityIdException(string message) 8 | : base(message) 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter06/README.md: -------------------------------------------------------------------------------- 1 | # Hands-on DDD with C# book code 2 | 3 | The book is **work in progress** and as I am working through 4 | chapters, the code will change. 5 | 6 | The code is cplit per chapter using folders (despite what I wrote before about using history and branches). 7 | 8 | The book is currently available as [early access eBook](https://www.packtpub.com/application-development/hands-domain-driven-design-net). 9 | -------------------------------------------------------------------------------- /Chapter06/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | ravendb: 6 | container_name: marketplace-ravendb 7 | image: ravendb/ravendb 8 | ports: 9 | - 8080:8080 10 | environment: 11 | - RAVEN_Security_UnsecuredAccessAllowed=PublicNetwork 12 | - RAVEN_ARGS="--Setup.Mode=None" 13 | -------------------------------------------------------------------------------- /Chapter07/Marketplace.Domain/ClassifiedAdId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain 5 | { 6 | public class ClassifiedAdId : Value 7 | { 8 | public Guid Value { get; } 9 | 10 | public ClassifiedAdId(Guid value) 11 | { 12 | if (value == default) 13 | throw new ArgumentNullException(nameof(value), "Classified Ad id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public static implicit operator Guid(ClassifiedAdId self) => self.Value; 19 | 20 | public static implicit operator ClassifiedAdId(string value) 21 | => new ClassifiedAdId(Guid.Parse(value)); 22 | 23 | public override string ToString() => Value.ToString(); 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Domain/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | public string Value { get; } 8 | 9 | internal ClassifiedAdText(string text) => Value = text; 10 | 11 | public static ClassifiedAdText FromString(string text) => 12 | new ClassifiedAdText(text); 13 | 14 | public static implicit operator string(ClassifiedAdText text) => 15 | text.Value; 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Domain/IClassifiedAdRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface IClassifiedAdRepository 6 | { 7 | Task Load(ClassifiedAdId id); 8 | 9 | Task Add(ClassifiedAd entity); 10 | 11 | Task Exists(ClassifiedAdId id); 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Domain/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | Currency FindCurrency(string currencyCode); 8 | } 9 | 10 | public class Currency : Value 11 | { 12 | public string CurrencyCode { get; set; } 13 | public bool InUse { get; set; } 14 | public int DecimalPlaces { get; set; } 15 | 16 | public static Currency None = new Currency {InUse = false}; 17 | } 18 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Domain/InvalidEntityStateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class InvalidEntityStateException : Exception 6 | { 7 | public InvalidEntityStateException(object entity, string message) 8 | : base($"Entity {entity.GetType().Name} state change rejected, {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Domain/Marketplace.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 8 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter07/Marketplace.Domain/PictureRules.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Domain 2 | { 3 | public static class PictureRules 4 | { 5 | public static bool HasCorrectSize(this Picture picture) 6 | => picture != null 7 | && picture.Size.Width >= 800 8 | && picture.Size.Height >= 600; 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Domain/PictureSize.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain 5 | { 6 | public class PictureSize : Value 7 | { 8 | public int Width { get; internal set; } 9 | public int Height { get; internal set; } 10 | 11 | public PictureSize(int width, int height) 12 | { 13 | if (width <= 0) 14 | throw new ArgumentOutOfRangeException( 15 | nameof(width), 16 | "Picture width must be a positive number"); 17 | 18 | if (height <= 0) 19 | throw new ArgumentOutOfRangeException( 20 | nameof(height), 21 | "Picture height must be a positive number"); 22 | 23 | Width = width; 24 | Height = height; 25 | } 26 | 27 | internal PictureSize() { } 28 | } 29 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Domain/Price.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class Price : Money 6 | { 7 | private Price(decimal amount, string currencyCode, ICurrencyLookup currencyLookup) 8 | : base(amount, currencyCode, currencyLookup) 9 | { 10 | if (amount < 0) 11 | throw new ArgumentException( 12 | "Price cannot be negative", 13 | nameof(amount)); 14 | } 15 | 16 | internal Price(decimal amount, string currencyCode) 17 | : base(amount, new Currency{CurrencyCode = currencyCode}) 18 | { 19 | } 20 | 21 | public new static Price FromDecimal(decimal amount, string currency, 22 | ICurrencyLookup currencyLookup) => 23 | new Price(amount, currency, currencyLookup); 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Domain/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class UserId 6 | { 7 | private Guid Value { get; set; } 8 | 9 | public UserId(Guid value) 10 | { 11 | if (value == default) 12 | throw new ArgumentNullException(nameof(value), "User id cannot be empty"); 13 | 14 | Value = value; 15 | } 16 | 17 | public static implicit operator Guid(UserId self) => self.Value; 18 | } 19 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Framework/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public abstract class Entity : IInternalEventHandler 6 | where TId : Value 7 | { 8 | private readonly Action _applier; 9 | 10 | public TId Id { get; protected set; } 11 | 12 | protected Entity(Action applier) => _applier = applier; 13 | 14 | protected abstract void When(object @event); 15 | 16 | protected void Apply(object @event) 17 | { 18 | When(@event); 19 | _applier(@event); 20 | } 21 | 22 | void IInternalEventHandler.Handle(object @event) => When(@event); 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Framework/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IApplicationService 6 | { 7 | Task Handle(object command); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Framework/IInternalEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public interface IInternalEventHandler 4 | { 5 | void Handle(object @event); 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Framework/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IUnitOfWork 6 | { 7 | Task Commit(); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Framework/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Framework/Marketplace.Framework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | -------------------------------------------------------------------------------- /Chapter07/Marketplace.Framework/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class ValueExtensions 4 | { 5 | public static void MustNotBeNull(this Value value) where T : Value 6 | { 7 | if (value == null) 8 | throw new InvalidValueException(typeof(T), "cannot be null"); 9 | } 10 | 11 | public static void MustBe(this Value value) where T : Value 12 | { 13 | if (value == null) 14 | throw new InvalidValueException(typeof(T), "cannot be null"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace.Tests/Marketplace.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter07/Marketplace/DuplicatedEntityIdException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace 4 | { 5 | public class DuplicatedEntityIdException : Exception 6 | { 7 | public DuplicatedEntityIdException(string message) 8 | : base(message) 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter07/Marketplace/Infrastructure/RavenDbUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Marketplace.Framework; 3 | using Raven.Client.Documents.Session; 4 | 5 | namespace Marketplace.Infrastructure 6 | { 7 | public class RavenDbUnitOfWork : IUnitOfWork 8 | { 9 | private readonly IAsyncDocumentSession _session; 10 | 11 | public RavenDbUnitOfWork(IAsyncDocumentSession session) 12 | => _session = session; 13 | 14 | public Task Commit() => _session.SaveChangesAsync(); 15 | } 16 | } -------------------------------------------------------------------------------- /Chapter07/README.md: -------------------------------------------------------------------------------- 1 | # Hands-on DDD with C# book code 2 | 3 | The book is **work in progress** and as I am working through 4 | chapters, the code will change. 5 | 6 | The book will contain almost everything from the history of this repository so 7 | I have no plans to have code per chapter. 8 | 9 | The book is currently available as [early access eBook](https://www.packtpub.com/application-development/hands-domain-driven-design-net). 10 | -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Domain/ClassifiedAdId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain 5 | { 6 | public class ClassifiedAdId : Value 7 | { 8 | public Guid Value { get; } 9 | 10 | public ClassifiedAdId(Guid value) 11 | { 12 | if (value == default) 13 | throw new ArgumentNullException(nameof(value), "Classified Ad id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public static implicit operator Guid(ClassifiedAdId self) => self.Value; 19 | 20 | public static implicit operator ClassifiedAdId(string value) 21 | => new ClassifiedAdId(Guid.Parse(value)); 22 | 23 | public override string ToString() => Value.ToString(); 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Domain/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | public string Value { get; } 8 | 9 | internal ClassifiedAdText(string text) => Value = text; 10 | 11 | public static ClassifiedAdText FromString(string text) => 12 | new ClassifiedAdText(text); 13 | 14 | public static implicit operator string(ClassifiedAdText text) => 15 | text.Value; 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Domain/IClassifiedAdRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface IClassifiedAdRepository 6 | { 7 | Task Load(ClassifiedAdId id); 8 | 9 | Task Add(ClassifiedAd entity); 10 | 11 | Task Exists(ClassifiedAdId id); 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Domain/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | Currency FindCurrency(string currencyCode); 8 | } 9 | 10 | public class Currency : Value 11 | { 12 | public string CurrencyCode { get; set; } 13 | public bool InUse { get; set; } 14 | public int DecimalPlaces { get; set; } 15 | 16 | public static Currency None = new Currency {InUse = false}; 17 | } 18 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Domain/InvalidEntityStateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class InvalidEntityStateException : Exception 6 | { 7 | public InvalidEntityStateException(object entity, string message) 8 | : base($"Entity {entity.GetType().Name} state change rejected, {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Domain/Marketplace.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 7.1 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Domain/PictureRules.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Domain 2 | { 3 | public static class PictureRules 4 | { 5 | public static bool HasCorrectSize(this Picture picture) 6 | => picture != null 7 | && picture.Size.Width >= 800 8 | && picture.Size.Height >= 600; 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Domain/PictureSize.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain 5 | { 6 | public class PictureSize : Value 7 | { 8 | public int Width { get; internal set; } 9 | public int Height { get; internal set; } 10 | 11 | public PictureSize(int width, int height) 12 | { 13 | if (width <= 0) 14 | throw new ArgumentOutOfRangeException( 15 | nameof(width), 16 | "Picture width must be a positive number"); 17 | 18 | if (height <= 0) 19 | throw new ArgumentOutOfRangeException( 20 | nameof(height), 21 | "Picture height must be a positive number"); 22 | 23 | Width = width; 24 | Height = height; 25 | } 26 | 27 | internal PictureSize() { } 28 | } 29 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Domain/Price.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class Price : Money 6 | { 7 | private Price(decimal amount, string currencyCode, ICurrencyLookup currencyLookup) 8 | : base(amount, currencyCode, currencyLookup) 9 | { 10 | if (amount < 0) 11 | throw new ArgumentException( 12 | "Price cannot be negative", 13 | nameof(amount)); 14 | } 15 | 16 | internal Price(decimal amount, string currencyCode) 17 | : base(amount, new Currency{CurrencyCode = currencyCode}) 18 | { 19 | } 20 | 21 | public new static Price FromDecimal(decimal amount, string currency, 22 | ICurrencyLookup currencyLookup) => 23 | new Price(amount, currency, currencyLookup); 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Domain/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class UserId 6 | { 7 | private Guid Value { get; set; } 8 | 9 | public UserId(Guid value) 10 | { 11 | if (value == default) 12 | throw new ArgumentNullException(nameof(value), "User id cannot be empty"); 13 | 14 | Value = value; 15 | } 16 | 17 | public static implicit operator Guid(UserId self) => self.Value; 18 | } 19 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Framework/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public abstract class Entity : IInternalEventHandler 6 | where TId : Value 7 | { 8 | private readonly Action _applier; 9 | 10 | public TId Id { get; protected set; } 11 | 12 | protected Entity(Action applier) => _applier = applier; 13 | 14 | protected abstract void When(object @event); 15 | 16 | protected void Apply(object @event) 17 | { 18 | When(@event); 19 | _applier(@event); 20 | } 21 | 22 | void IInternalEventHandler.Handle(object @event) => When(@event); 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Framework/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IApplicationService 6 | { 7 | Task Handle(object command); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Framework/IInternalEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public interface IInternalEventHandler 4 | { 5 | void Handle(object @event); 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Framework/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IUnitOfWork 6 | { 7 | Task Commit(); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Framework/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Framework/Marketplace.Framework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Framework/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class ValueExtensions 4 | { 5 | public static void MustNotBeNull(this Value value) where T : Value 6 | { 7 | if (value == null) 8 | throw new InvalidValueException(typeof(T), "cannot be null"); 9 | } 10 | 11 | public static void MustBe(this Value value) where T : Value 12 | { 13 | if (value == null) 14 | throw new InvalidValueException(typeof(T), "cannot be null"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace.Tests/Marketplace.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter08/before/Marketplace/DuplicatedEntityIdException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace 4 | { 5 | public class DuplicatedEntityIdException : Exception 6 | { 7 | public DuplicatedEntityIdException(string message) 8 | : base(message) 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter08/before/Marketplace/Infrastructure/RavenDbUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Marketplace.Framework; 3 | using Raven.Client.Documents.Session; 4 | 5 | namespace Marketplace.Infrastructure 6 | { 7 | public class RavenDbUnitOfWork : IUnitOfWork 8 | { 9 | private readonly IAsyncDocumentSession _session; 10 | 11 | public RavenDbUnitOfWork(IAsyncDocumentSession session) 12 | => _session = session; 13 | 14 | public Task Commit() => _session.SaveChangesAsync(); 15 | } 16 | } -------------------------------------------------------------------------------- /Chapter08/before/README.md: -------------------------------------------------------------------------------- 1 | # Hands-on DDD with C# book code 2 | 3 | The book is **work in progress** and as I am working through 4 | chapters, the code will change. 5 | 6 | The book will contain almost everything from the history of this repository so 7 | I have no plans to have code per chapter. 8 | 9 | The book is currently available as [early access eBook](https://www.packtpub.com/application-development/hands-domain-driven-design-net). 10 | -------------------------------------------------------------------------------- /Chapter08/before/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | ravendb: 6 | container_name: marketplace-ch8-ravendb 7 | image: ravendb/ravendb 8 | ports: 9 | - 8080:8080 10 | environment: 11 | - RAVEN_Security_UnsecuredAccessAllowed=PublicNetwork 12 | - RAVEN_ARGS="--Setup.Mode=None" 13 | 14 | postgres: 15 | container_name: marketplace-ch8-postgres 16 | image: postgres 17 | ports: 18 | - 5432:5432 19 | volumes: 20 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql 21 | -------------------------------------------------------------------------------- /Chapter08/before/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE "Marketplace_Chapter8"; 2 | CREATE USER ddd WITH PASSWORD 'book'; 3 | GRANT ALL PRIVILEGES ON DATABASE "Marketplace_Chapter8" to ddd; 4 | -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Domain/ClassifiedAdId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain 5 | { 6 | public class ClassifiedAdId : Value 7 | { 8 | public Guid Value { get; internal set; } 9 | 10 | protected ClassifiedAdId() {} 11 | 12 | public ClassifiedAdId(Guid value) 13 | { 14 | if (value == default) 15 | throw new ArgumentNullException(nameof(value), "Classified Ad id cannot be empty"); 16 | 17 | Value = value; 18 | } 19 | 20 | public static implicit operator Guid(ClassifiedAdId self) => self.Value; 21 | 22 | public static implicit operator ClassifiedAdId(string value) 23 | => new ClassifiedAdId(Guid.Parse(value)); 24 | 25 | public override string ToString() => Value.ToString(); 26 | } 27 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Domain/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | public string Value { get; internal set; } 8 | 9 | protected ClassifiedAdText() {} 10 | 11 | internal ClassifiedAdText(string text) => Value = text; 12 | 13 | public static ClassifiedAdText FromString(string text) => 14 | new ClassifiedAdText(text); 15 | 16 | public static implicit operator string(ClassifiedAdText text) => 17 | text.Value; 18 | 19 | public static ClassifiedAdText NoText => 20 | new ClassifiedAdText(); 21 | } 22 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Domain/IClassifiedAdRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface IClassifiedAdRepository 6 | { 7 | Task Load(ClassifiedAdId id); 8 | 9 | Task Add(ClassifiedAd entity); 10 | 11 | Task Exists(ClassifiedAdId id); 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Domain/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | Currency FindCurrency(string currencyCode); 8 | } 9 | 10 | public class Currency : Value 11 | { 12 | public string CurrencyCode { get; set; } 13 | public bool InUse { get; set; } 14 | public int DecimalPlaces { get; set; } 15 | 16 | public static Currency None = new Currency {InUse = false}; 17 | } 18 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Domain/InvalidEntityStateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class InvalidEntityStateException : Exception 6 | { 7 | public InvalidEntityStateException(object entity, string message) 8 | : base($"Entity {entity.GetType().Name} state change rejected, {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Domain/Marketplace.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 7.1 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Domain/PictureRules.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Domain 2 | { 3 | public static class PictureRules 4 | { 5 | public static bool HasCorrectSize(this Picture picture) 6 | => picture != null 7 | && picture.Size.Width >= 800 8 | && picture.Size.Height >= 600; 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Domain/PictureSize.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain 5 | { 6 | public class PictureSize : Value 7 | { 8 | public int Width { get; internal set; } 9 | public int Height { get; internal set; } 10 | 11 | internal PictureSize() { } 12 | 13 | public PictureSize(int width, int height) 14 | { 15 | if (width <= 0) 16 | throw new ArgumentOutOfRangeException( 17 | nameof(width), 18 | "Picture width must be a positive number"); 19 | 20 | if (height <= 0) 21 | throw new ArgumentOutOfRangeException( 22 | nameof(height), 23 | "Picture height must be a positive number"); 24 | 25 | Width = width; 26 | Height = height; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Domain/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class UserId 6 | { 7 | protected UserId() {} 8 | 9 | public Guid Value { get; internal set; } 10 | 11 | public UserId(Guid value) 12 | { 13 | if (value == default) 14 | throw new ArgumentNullException(nameof(value), "User id cannot be empty"); 15 | 16 | Value = value; 17 | } 18 | 19 | public static implicit operator Guid(UserId self) => self.Value; 20 | 21 | public static UserId NoUser => 22 | new UserId(); 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Framework/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public abstract class Entity : IInternalEventHandler 6 | where TId : Value 7 | { 8 | private readonly Action _applier; 9 | 10 | public TId Id { get; protected set; } 11 | 12 | protected Entity(Action applier) => _applier = applier; 13 | 14 | protected Entity() {} 15 | 16 | protected abstract void When(object @event); 17 | 18 | protected void Apply(object @event) 19 | { 20 | When(@event); 21 | _applier(@event); 22 | } 23 | 24 | void IInternalEventHandler.Handle(object @event) => When(@event); 25 | } 26 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Framework/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IApplicationService 6 | { 7 | Task Handle(object command); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Framework/IInternalEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public interface IInternalEventHandler 4 | { 5 | void Handle(object @event); 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Framework/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IUnitOfWork 6 | { 7 | Task Commit(); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Framework/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Framework/Marketplace.Framework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Framework/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class ValueExtensions 4 | { 5 | public static void MustNotBeNull(this Value value) where T : Value 6 | { 7 | if (value == null) 8 | throw new InvalidValueException(typeof(T), "cannot be null"); 9 | } 10 | 11 | public static void MustBe(this Value value) where T : Value 12 | { 13 | if (value == null) 14 | throw new InvalidValueException(typeof(T), "cannot be null"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace.Tests/Marketplace.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace/DuplicatedEntityIdException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace 4 | { 5 | public class DuplicatedEntityIdException : Exception 6 | { 7 | public DuplicatedEntityIdException(string message) 8 | : base(message) 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/Marketplace/Infrastructure/EfCoreUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Infrastructure 5 | { 6 | public class EfCoreUnitOfWork : IUnitOfWork 7 | { 8 | private readonly ClassifiedAdDbContext _dbContext; 9 | 10 | public EfCoreUnitOfWork(ClassifiedAdDbContext dbContext) 11 | => _dbContext = dbContext; 12 | 13 | public Task Commit() => _dbContext.SaveChangesAsync(); 14 | } 15 | } -------------------------------------------------------------------------------- /Chapter08/ef-core/README.md: -------------------------------------------------------------------------------- 1 | # Hands-on DDD with C# book code 2 | 3 | The book is **work in progress** and as I am working through 4 | chapters, the code will change. 5 | 6 | The book will contain almost everything from the history of this repository so 7 | I have no plans to have code per chapter. 8 | 9 | The book is currently available as [early access eBook](https://www.packtpub.com/application-development/hands-domain-driven-design-net). 10 | -------------------------------------------------------------------------------- /Chapter08/ef-core/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | postgres: 6 | container_name: marketplace-ch8-postgres 7 | image: postgres 8 | ports: 9 | - 5432:5432 10 | volumes: 11 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql 12 | -------------------------------------------------------------------------------- /Chapter08/ef-core/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE "Marketplace_Chapter8"; 2 | CREATE USER ddd WITH PASSWORD 'book'; 3 | GRANT ALL PRIVILEGES ON DATABASE "Marketplace_Chapter8" to ddd; 4 | -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Domain/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | public string Value { get; private set; } 8 | 9 | internal ClassifiedAdText(string text) => Value = text; 10 | 11 | public static ClassifiedAdText FromString(string text) => 12 | new ClassifiedAdText(text); 13 | 14 | public static implicit operator string(ClassifiedAdText text) => 15 | text.Value; 16 | 17 | // Satisfy the serialization requirements 18 | protected ClassifiedAdText() { } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Domain/IClassifiedAdRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface IClassifiedAdRepository 6 | { 7 | Task Load(ClassifiedAdId id); 8 | 9 | Task Add(ClassifiedAd entity); 10 | 11 | Task Exists(ClassifiedAdId id); 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Domain/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | Currency FindCurrency(string currencyCode); 8 | } 9 | 10 | public class Currency : Value 11 | { 12 | public string CurrencyCode { get; set; } 13 | public bool InUse { get; set; } 14 | public int DecimalPlaces { get; set; } 15 | 16 | public static Currency None = new Currency {InUse = false}; 17 | } 18 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Domain/InvalidEntityStateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class InvalidEntityStateException : Exception 6 | { 7 | public InvalidEntityStateException(object entity, string message) 8 | : base($"Entity {entity.GetType().Name} state change rejected, {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Domain/Marketplace.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 8 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Domain/PictureRules.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Domain 2 | { 3 | public static class PictureRules 4 | { 5 | public static bool HasCorrectSize(this Picture picture) 6 | => picture != null 7 | && picture.Size.Width >= 800 8 | && picture.Size.Height >= 600; 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Domain/PictureSize.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain 5 | { 6 | public class PictureSize : Value 7 | { 8 | public int Width { get; internal set; } 9 | public int Height { get; internal set; } 10 | 11 | public PictureSize(int width, int height) 12 | { 13 | if (width <= 0) 14 | throw new ArgumentOutOfRangeException( 15 | nameof(width), 16 | "Picture width must be a positive number"); 17 | 18 | if (height <= 0) 19 | throw new ArgumentOutOfRangeException( 20 | nameof(height), 21 | "Picture height must be a positive number"); 22 | 23 | Width = width; 24 | Height = height; 25 | } 26 | 27 | internal PictureSize() { } 28 | } 29 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Domain/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class UserId 6 | { 7 | private Guid Value { get; set; } 8 | 9 | public UserId(Guid value) 10 | { 11 | if (value == default) 12 | throw new ArgumentNullException(nameof(value), "User id cannot be empty"); 13 | 14 | Value = value; 15 | } 16 | 17 | public static implicit operator Guid(UserId self) => self.Value; 18 | } 19 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Framework/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public abstract class Entity : IInternalEventHandler 6 | where TId : Value 7 | { 8 | private readonly Action _applier; 9 | 10 | public TId Id { get; protected set; } 11 | 12 | protected Entity(Action applier) => _applier = applier; 13 | 14 | protected abstract void When(object @event); 15 | 16 | protected void Apply(object @event) 17 | { 18 | When(@event); 19 | _applier(@event); 20 | } 21 | 22 | void IInternalEventHandler.Handle(object @event) => When(@event); 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Framework/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IApplicationService 6 | { 7 | Task Handle(object command); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Framework/IInternalEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public interface IInternalEventHandler 4 | { 5 | void Handle(object @event); 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Framework/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IUnitOfWork 6 | { 7 | Task Commit(); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Framework/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Framework/Marketplace.Framework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Framework/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class ValueExtensions 4 | { 5 | public static void MustNotBeNull(this Value value) where T : Value 6 | { 7 | if (value == null) 8 | throw new InvalidValueException(typeof(T), "cannot be null"); 9 | } 10 | 11 | public static void MustBe(this Value value) where T : Value 12 | { 13 | if (value == null) 14 | throw new InvalidValueException(typeof(T), "cannot be null"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace.Tests/Marketplace.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace/DuplicatedEntityIdException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace 4 | { 5 | public class DuplicatedEntityIdException : Exception 6 | { 7 | public DuplicatedEntityIdException(string message) 8 | : base(message) 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/Marketplace/Infrastructure/RavenDbUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Marketplace.Framework; 3 | using Raven.Client.Documents.Session; 4 | 5 | namespace Marketplace.Infrastructure 6 | { 7 | public class RavenDbUnitOfWork : IUnitOfWork 8 | { 9 | private readonly IAsyncDocumentSession _session; 10 | 11 | public RavenDbUnitOfWork(IAsyncDocumentSession session) 12 | => _session = session; 13 | 14 | public Task Commit() => _session.SaveChangesAsync(); 15 | } 16 | } -------------------------------------------------------------------------------- /Chapter08/ravendb/README.md: -------------------------------------------------------------------------------- 1 | # Hands-on DDD with C# book code 2 | 3 | The book is **work in progress** and as I am working through 4 | chapters, the code will change. 5 | 6 | The book will contain almost everything from the history of this repository so 7 | I have no plans to have code per chapter. 8 | 9 | The book is currently available as [early access eBook](https://www.packtpub.com/application-development/hands-domain-driven-design-net). 10 | -------------------------------------------------------------------------------- /Chapter08/ravendb/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | ravendb: 6 | container_name: marketplace-ch8-ravendb 7 | image: ravendb/ravendb 8 | ports: 9 | - 8080:8080 10 | environment: 11 | - RAVEN_Security_UnsecuredAccessAllowed=PublicNetwork 12 | - RAVEN_ARGS="--Setup.Mode=None" 13 | -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Domain/ClassifiedAd/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain.ClassifiedAd 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | public string Value { get; internal set; } 8 | 9 | protected ClassifiedAdText() {} 10 | 11 | internal ClassifiedAdText(string text) => Value = text; 12 | 13 | public static ClassifiedAdText FromString(string text) => 14 | new ClassifiedAdText(text); 15 | 16 | public static implicit operator string(ClassifiedAdText text) => 17 | text.Value; 18 | 19 | public static ClassifiedAdText NoText => 20 | new ClassifiedAdText(); 21 | } 22 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Domain/ClassifiedAd/IClassifiedAdRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Domain.ClassifiedAd 4 | { 5 | public interface IClassifiedAdRepository 6 | { 7 | Task Load(ClassifiedAdId id); 8 | 9 | Task Add(ClassifiedAd entity); 10 | 11 | Task Exists(ClassifiedAdId id); 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Domain/ClassifiedAd/PictureRules.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Domain.ClassifiedAd 2 | { 3 | public static class PictureRules 4 | { 5 | public static bool HasCorrectSize(this Picture picture) 6 | => picture != null 7 | && picture.Size.Width >= 800 8 | && picture.Size.Height >= 600; 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Domain/Marketplace.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 8 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Domain/Shared/ContentModeration.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Domain.Shared 2 | { 3 | public delegate bool CheckTextForProfanity(string text); 4 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Domain/Shared/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public static class DomainExceptions 6 | { 7 | public class InvalidEntityState : Exception 8 | { 9 | public InvalidEntityState(object entity, string message) 10 | : base($"Entity {entity.GetType().Name} state change rejected, {message}") 11 | { 12 | } 13 | } 14 | 15 | public class ProfanityFound : Exception 16 | { 17 | public ProfanityFound(string text) 18 | : base($"Profanity found in text: {text}") 19 | { 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Domain/Shared/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | Currency FindCurrency(string currencyCode); 8 | } 9 | 10 | public class Currency : Value 11 | { 12 | public string CurrencyCode { get; set; } 13 | public bool InUse { get; set; } 14 | public int DecimalPlaces { get; set; } 15 | 16 | public static Currency None = new Currency {InUse = false}; 17 | } 18 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Domain/Shared/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.Shared 5 | { 6 | public class UserId : Value 7 | { 8 | protected UserId() {} 9 | 10 | public Guid Value { get; internal set; } 11 | 12 | public UserId(Guid value) 13 | { 14 | if (value == default) 15 | throw new ArgumentNullException(nameof(value), "User id cannot be empty"); 16 | 17 | Value = value; 18 | } 19 | 20 | public static implicit operator Guid(UserId self) => self.Value; 21 | 22 | public static UserId NoUser => 23 | new UserId(); 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Domain/UserProfile/FullName.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.UserProfile 5 | { 6 | public class FullName : Value 7 | { 8 | public string Value { get; private set; } 9 | 10 | internal FullName(string value) => Value = value; 11 | 12 | public static FullName FromString(string fullName) 13 | { 14 | if (fullName.IsEmpty()) 15 | throw new ArgumentNullException(nameof(fullName)); 16 | 17 | return new FullName(fullName); 18 | } 19 | 20 | public static implicit operator string(FullName fullName) 21 | => fullName.Value; 22 | 23 | // Satisfy the serialization requirements 24 | protected FullName() { } 25 | } 26 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Domain/UserProfile/IUserProfileRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Marketplace.Domain.Shared; 3 | 4 | namespace Marketplace.Domain.UserProfile 5 | { 6 | public interface IUserProfileRepository 7 | { 8 | Task Load(UserId id); 9 | 10 | Task Add(UserProfile entity); 11 | 12 | Task Exists(UserId id); 13 | } 14 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Framework/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public abstract class Entity : IInternalEventHandler 6 | where TId : Value 7 | { 8 | private readonly Action _applier; 9 | 10 | public TId Id { get; protected set; } 11 | 12 | protected Entity(Action applier) => _applier = applier; 13 | 14 | protected Entity() {} 15 | 16 | protected abstract void When(object @event); 17 | 18 | protected void Apply(object @event) 19 | { 20 | When(@event); 21 | _applier(@event); 22 | } 23 | 24 | void IInternalEventHandler.Handle(object @event) => When(@event); 25 | } 26 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Framework/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IApplicationService 6 | { 7 | Task Handle(object command); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Framework/IInternalEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public interface IInternalEventHandler 4 | { 5 | void Handle(object @event); 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Framework/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IUnitOfWork 6 | { 7 | Task Commit(); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Framework/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Framework/Marketplace.Framework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Framework/StringTools.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class StringTools 4 | { 5 | public static bool IsEmpty(this string value) 6 | => string.IsNullOrWhiteSpace(value); 7 | } 8 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Framework/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class ValueExtensions 4 | { 5 | public static void MustNotBeNull(this Value value) where T : Value 6 | { 7 | if (value == null) 8 | throw new InvalidValueException(typeof(T), "cannot be null"); 9 | } 10 | 11 | public static void MustBe(this Value value) where T : Value 12 | { 13 | if (value == null) 14 | throw new InvalidValueException(typeof(T), "cannot be null"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace.Tests/Marketplace.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace/ClassifiedAd/QueryModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.ClassifiedAd 4 | { 5 | public static class QueryModels 6 | { 7 | public class GetPublishedClassifiedAds 8 | { 9 | public int Page { get; set; } 10 | public int PageSize { get; set; } 11 | } 12 | 13 | public class GetOwnersClassifiedAd 14 | { 15 | public Guid OwnerId { get; set; } 16 | public int Page { get; set; } 17 | public int PageSize { get; set; } 18 | } 19 | 20 | public class GetPublicClassifiedAd 21 | { 22 | public Guid ClassifiedAdId { get; set; } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace 4 | { 5 | public static class Exceptions 6 | { 7 | public class DuplicatedEntityIdException : Exception 8 | { 9 | public DuplicatedEntityIdException(string message) 10 | : base(message) 11 | { 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/Marketplace/Infrastructure/EfCoreUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Infrastructure 5 | { 6 | public class EfCoreUnitOfWork : IUnitOfWork 7 | { 8 | private readonly MarketplaceDbContext _dbContext; 9 | 10 | public EfCoreUnitOfWork(MarketplaceDbContext dbContext) 11 | => _dbContext = dbContext; 12 | 13 | public Task Commit() => _dbContext.SaveChangesAsync(); 14 | } 15 | } -------------------------------------------------------------------------------- /Chapter09/ef-core/README.md: -------------------------------------------------------------------------------- 1 | # Hands-on DDD with C# book code 2 | -------------------------------------------------------------------------------- /Chapter09/ef-core/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | postgres: 6 | container_name: marketplace-ch9-postgres 7 | image: postgres 8 | ports: 9 | - 5432:5432 10 | volumes: 11 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql 12 | -------------------------------------------------------------------------------- /Chapter09/ef-core/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE "Marketplace_Chapter9"; 2 | CREATE USER ddd WITH PASSWORD 'book'; 3 | GRANT ALL PRIVILEGES ON DATABASE "Marketplace_Chapter9" to ddd; 4 | -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Domain/ClassifiedAd/ClassifiedAdId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.ClassifiedAd 5 | { 6 | public class ClassifiedAdId : Value 7 | { 8 | public Guid Value { get; } 9 | 10 | public ClassifiedAdId(Guid value) 11 | { 12 | if (value == default) 13 | throw new ArgumentNullException(nameof(value), "Classified Ad id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public static implicit operator Guid(ClassifiedAdId self) => self.Value; 19 | 20 | public static implicit operator ClassifiedAdId(string value) 21 | => new ClassifiedAdId(Guid.Parse(value)); 22 | 23 | public override string ToString() => Value.ToString(); 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Domain/ClassifiedAd/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain.ClassifiedAd 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | public string Value { get; private set; } 8 | 9 | internal ClassifiedAdText(string text) => Value = text; 10 | 11 | public static ClassifiedAdText FromString(string text) => 12 | new ClassifiedAdText(text); 13 | 14 | public static implicit operator string(ClassifiedAdText text) => 15 | text.Value; 16 | 17 | // Satisfy the serialization requirements 18 | protected ClassifiedAdText() { } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Domain/ClassifiedAd/IClassifiedAdRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Domain.ClassifiedAd 4 | { 5 | public interface IClassifiedAdRepository 6 | { 7 | Task Load(ClassifiedAdId id); 8 | 9 | Task Add(ClassifiedAd entity); 10 | 11 | Task Exists(ClassifiedAdId id); 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Domain/ClassifiedAd/PictureRules.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Domain.ClassifiedAd 2 | { 3 | public static class PictureRules 4 | { 5 | public static bool HasCorrectSize(this Picture picture) 6 | => picture != null 7 | && picture.Size.Width >= 800 8 | && picture.Size.Height >= 600; 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Domain/Marketplace.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 8 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Domain/Shared/ContentModeration.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Domain.Shared 2 | { 3 | public delegate bool CheckTextForProfanity(string text); 4 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Domain/Shared/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public static class DomainExceptions 6 | { 7 | public class InvalidEntityState : Exception 8 | { 9 | public InvalidEntityState(object entity, string message) 10 | : base($"Entity {entity.GetType().Name} state change rejected, {message}") 11 | { 12 | } 13 | } 14 | 15 | public class ProfanityFound : Exception 16 | { 17 | public ProfanityFound(string text) 18 | : base($"Profanity found in text: {text}") 19 | { 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Domain/Shared/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | Currency FindCurrency(string currencyCode); 8 | } 9 | 10 | public class Currency : Value 11 | { 12 | public string CurrencyCode { get; set; } 13 | public bool InUse { get; set; } 14 | public int DecimalPlaces { get; set; } 15 | 16 | public static Currency None = new Currency {InUse = false}; 17 | } 18 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Domain/Shared/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.Shared 5 | { 6 | public class UserId : Value 7 | { 8 | public Guid Value { get; private set; } 9 | 10 | public UserId(Guid value) 11 | { 12 | if (value == default) 13 | throw new ArgumentNullException(nameof(value), "User id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public static implicit operator Guid(UserId self) => self.Value; 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Domain/UserProfile/FullName.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.UserProfile 5 | { 6 | public class FullName : Value 7 | { 8 | public string Value { get; private set; } 9 | 10 | internal FullName(string value) => Value = value; 11 | 12 | public static FullName FromString(string fullName) 13 | { 14 | if (fullName.IsEmpty()) 15 | throw new ArgumentNullException(nameof(fullName)); 16 | 17 | return new FullName(fullName); 18 | } 19 | 20 | public static implicit operator string(FullName fullName) 21 | => fullName.Value; 22 | 23 | // Satisfy the serialization requirements 24 | protected FullName() { } 25 | } 26 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Domain/UserProfile/IUserProfileRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Marketplace.Domain.Shared; 3 | 4 | namespace Marketplace.Domain.UserProfile 5 | { 6 | public interface IUserProfileRepository 7 | { 8 | Task Load(UserId id); 9 | 10 | Task Add(UserProfile entity); 11 | 12 | Task Exists(UserId id); 13 | } 14 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Framework/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public abstract class Entity : IInternalEventHandler 6 | where TId : Value 7 | { 8 | private readonly Action _applier; 9 | 10 | public TId Id { get; protected set; } 11 | 12 | protected Entity(Action applier) => _applier = applier; 13 | 14 | protected abstract void When(object @event); 15 | 16 | protected void Apply(object @event) 17 | { 18 | When(@event); 19 | _applier(@event); 20 | } 21 | 22 | void IInternalEventHandler.Handle(object @event) => When(@event); 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Framework/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IApplicationService 6 | { 7 | Task Handle(object command); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Framework/IInternalEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public interface IInternalEventHandler 4 | { 5 | void Handle(object @event); 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Framework/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IUnitOfWork 6 | { 7 | Task Commit(); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Framework/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Framework/Marketplace.Framework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Framework/StringTools.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class StringTools 4 | { 5 | public static bool IsEmpty(this string value) 6 | => string.IsNullOrWhiteSpace(value); 7 | } 8 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Framework/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class ValueExtensions 4 | { 5 | public static void MustNotBeNull(this Value value) where T : Value 6 | { 7 | if (value == null) 8 | throw new InvalidValueException(typeof(T), "cannot be null"); 9 | } 10 | 11 | public static void MustBe(this Value value) where T : Value 12 | { 13 | if (value == null) 14 | throw new InvalidValueException(typeof(T), "cannot be null"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace.Tests/Marketplace.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace/ClassifiedAd/ClassifiedAdRepository.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Domain.ClassifiedAd; 2 | using Marketplace.Infrastructure; 3 | using Raven.Client.Documents.Session; 4 | 5 | namespace Marketplace.ClassifiedAd 6 | { 7 | public class ClassifiedAdRepository 8 | : RavenDbRepository, IClassifiedAdRepository 9 | { 10 | public ClassifiedAdRepository(IAsyncDocumentSession session) 11 | : base(session, id => $"ClassifiedAd/{id.Value.ToString()}") { } 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace/ClassifiedAd/QueryModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.ClassifiedAd 4 | { 5 | public static class QueryModels 6 | { 7 | public class GetPublishedClassifiedAds 8 | { 9 | public int Page { get; set; } 10 | public int PageSize { get; set; } 11 | } 12 | 13 | public class GetOwnersClassifiedAd 14 | { 15 | public Guid OwnerId { get; set; } 16 | public int Page { get; set; } 17 | public int PageSize { get; set; } 18 | } 19 | 20 | public class GetPublicClassifiedAd 21 | { 22 | public Guid ClassifiedAdId { get; set; } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace 4 | { 5 | public static class Exceptions 6 | { 7 | public class DuplicatedEntityIdException : Exception 8 | { 9 | public DuplicatedEntityIdException(string message) 10 | : base(message) 11 | { 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace/Infrastructure/RavenDbUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Marketplace.Framework; 3 | using Raven.Client.Documents.Session; 4 | 5 | namespace Marketplace.Infrastructure 6 | { 7 | public class RavenDbUnitOfWork : IUnitOfWork 8 | { 9 | private readonly IAsyncDocumentSession _session; 10 | 11 | public RavenDbUnitOfWork(IAsyncDocumentSession session) 12 | => _session = session; 13 | 14 | public Task Commit() => _session.SaveChangesAsync(); 15 | } 16 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/Marketplace/UserProfile/UserProfileRepository.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Domain.Shared; 2 | using Marketplace.Domain.UserProfile; 3 | using Marketplace.Infrastructure; 4 | using Raven.Client.Documents.Session; 5 | 6 | namespace Marketplace.UserProfile 7 | { 8 | public class UserProfileRepository 9 | : RavenDbRepository, IUserProfileRepository 10 | { 11 | public UserProfileRepository(IAsyncDocumentSession session) 12 | : base(session, id => $"UserProfile/{id.Value.ToString()}") { } 13 | } 14 | } -------------------------------------------------------------------------------- /Chapter09/ravendb/README.md: -------------------------------------------------------------------------------- 1 | # Hands-on DDD with C# book code 2 | 3 | The book is **work in progress** and as I am working through 4 | chapters, the code will change. 5 | 6 | The book will contain almost everything from the history of this repository so 7 | I have no plans to have code per chapter. 8 | 9 | The book is currently available as [early access eBook](https://www.packtpub.com/application-development/hands-domain-driven-design-net). 10 | -------------------------------------------------------------------------------- /Chapter09/ravendb/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | ravendb: 6 | container_name: marketplace-ch9-ravendb 7 | image: ravendb/ravendb 8 | ports: 9 | - 8080:8080 10 | environment: 11 | - RAVEN_Security_UnsecuredAccessAllowed=PublicNetwork 12 | - RAVEN_ARGS="--Setup.Mode=None" 13 | -------------------------------------------------------------------------------- /Chapter10/Marketplace.Domain/ClassifiedAd/ClassifiedAdId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.ClassifiedAd 5 | { 6 | public class ClassifiedAdId : Value 7 | { 8 | public Guid Value { get; } 9 | 10 | public ClassifiedAdId(Guid value) 11 | { 12 | if (value == default) 13 | throw new ArgumentNullException(nameof(value), "Classified Ad id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public static implicit operator Guid(ClassifiedAdId self) => self.Value; 19 | 20 | public static implicit operator ClassifiedAdId(string value) 21 | => new ClassifiedAdId(Guid.Parse(value)); 22 | 23 | public override string ToString() => Value.ToString(); 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Domain/ClassifiedAd/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain.ClassifiedAd 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | public string Value { get; private set; } 8 | 9 | internal ClassifiedAdText(string text) => Value = text; 10 | 11 | public static ClassifiedAdText FromString(string text) => 12 | new ClassifiedAdText(text); 13 | 14 | public static implicit operator string(ClassifiedAdText text) => 15 | text.Value; 16 | 17 | // Satisfy the serialization requirements 18 | protected ClassifiedAdText() { } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Domain/ClassifiedAd/PictureRules.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Domain.ClassifiedAd 2 | { 3 | public static class PictureRules 4 | { 5 | public static bool HasCorrectSize(this Picture picture) 6 | => picture != null 7 | && picture.Size.Width >= 800 8 | && picture.Size.Height >= 600; 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Domain/Marketplace.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 8 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter10/Marketplace.Domain/Shared/ContentModeration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public delegate Task CheckTextForProfanity(string text); 6 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Domain/Shared/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public static class DomainExceptions 6 | { 7 | public class InvalidEntityState : Exception 8 | { 9 | public InvalidEntityState(object entity, string message) 10 | : base($"Entity {entity.GetType().Name} state change rejected, {message}") 11 | { 12 | } 13 | } 14 | 15 | public class ProfanityFound : Exception 16 | { 17 | public ProfanityFound(string text) 18 | : base($"Profanity found in text: {text}") 19 | { 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Domain/Shared/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | Currency FindCurrency(string currencyCode); 8 | } 9 | 10 | public class Currency : Value 11 | { 12 | public string CurrencyCode { get; set; } 13 | public bool InUse { get; set; } 14 | public int DecimalPlaces { get; set; } 15 | 16 | public static Currency None = new Currency {InUse = false}; 17 | } 18 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Domain/Shared/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.Shared 5 | { 6 | public class UserId : Value 7 | { 8 | public Guid Value { get; private set; } 9 | 10 | public UserId(Guid value) 11 | { 12 | if (value == default) 13 | throw new ArgumentNullException(nameof(value), "User id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public static implicit operator Guid(UserId self) => self.Value; 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Domain/UserProfile/FullName.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.UserProfile 5 | { 6 | public class FullName : Value 7 | { 8 | public string Value { get; private set; } 9 | 10 | internal FullName(string value) => Value = value; 11 | 12 | public static FullName FromString(string fullName) 13 | { 14 | if (fullName.IsEmpty()) 15 | throw new ArgumentNullException(nameof(fullName)); 16 | 17 | return new FullName(fullName); 18 | } 19 | 20 | public static implicit operator string(FullName fullName) 21 | => fullName.Value; 22 | 23 | // Satisfy the serialization requirements 24 | protected FullName() { } 25 | } 26 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Framework/ApplicationServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Marketplace.Framework 5 | { 6 | public static class ApplicationServiceExtensions 7 | { 8 | public static async Task HandleUpdate(this IApplicationService service, 9 | IAggregateStore store, TId aggregateId, Action operation) 10 | where T : AggregateRoot 11 | { 12 | var aggregate = await store.Load(aggregateId); 13 | if (aggregate == null) 14 | throw new InvalidOperationException($"Entity with id {aggregateId.ToString()} cannot be found"); 15 | 16 | operation(aggregate); 17 | await store.Save(aggregate); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Framework/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public abstract class Entity : IInternalEventHandler 6 | where TId : Value 7 | { 8 | private readonly Action _applier; 9 | 10 | public TId Id { get; protected set; } 11 | 12 | protected Entity(Action applier) => _applier = applier; 13 | 14 | protected abstract void When(object @event); 15 | 16 | protected void Apply(object @event) 17 | { 18 | When(@event); 19 | _applier(@event); 20 | } 21 | 22 | void IInternalEventHandler.Handle(object @event) => When(@event); 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Framework/IAggregateStore.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IAggregateStore 6 | { 7 | Task Exists(TId aggregateId); 8 | 9 | Task Save(T aggregate) where T : AggregateRoot; 10 | 11 | Task Load(TId aggregateId) where T : AggregateRoot; 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Framework/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IApplicationService 6 | { 7 | Task Handle(object command); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Framework/IInternalEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public interface IInternalEventHandler 4 | { 5 | void Handle(object @event); 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Framework/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Framework/Marketplace.Framework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 7.1 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Chapter10/Marketplace.Framework/StringTools.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class StringTools 4 | { 5 | public static bool IsEmpty(this string value) 6 | => string.IsNullOrWhiteSpace(value); 7 | } 8 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Framework/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class ValueExtensions 4 | { 5 | public static void MustNotBeNull(this Value value) where T : Value 6 | { 7 | if (value == null) 8 | throw new InvalidValueException(typeof(T), "cannot be null"); 9 | } 10 | 11 | public static void MustBe(this Value value) where T : Value 12 | { 13 | if (value == null) 14 | throw new InvalidValueException(typeof(T), "cannot be null"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace.Tests/Marketplace.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter10/Marketplace/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace 4 | { 5 | public static class Exceptions 6 | { 7 | public class DuplicatedEntityIdException : Exception 8 | { 9 | public DuplicatedEntityIdException(string message) 10 | : base(message) 11 | { 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace/HostedService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using EventStore.ClientAPI; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace Marketplace 7 | { 8 | public class HostedService : IHostedService 9 | { 10 | private readonly IEventStoreConnection _esConnection; 11 | 12 | public HostedService(IEventStoreConnection esConnection) 13 | { 14 | _esConnection = esConnection; 15 | } 16 | 17 | public Task StartAsync(CancellationToken cancellationToken) 18 | => _esConnection.ConnectAsync(); 19 | 20 | public Task StopAsync(CancellationToken cancellationToken) 21 | { 22 | _esConnection.Close(); 23 | return Task.CompletedTask; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Chapter10/Marketplace/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventStore": { 3 | "connectionString": "ConnectTo=tcp://admin:changeit@localhost:1113; DefaultUserCredentials=admin:changeit;" 4 | } 5 | } -------------------------------------------------------------------------------- /Chapter10/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | eventstore: 6 | container_name: marketplace-eventstore 7 | image: eventstore/eventstore 8 | ports: 9 | - 2113:2113 10 | - 1113:1113 11 | environment: 12 | - EVENTSTORE_EXT_HTTP_PORT=2113 13 | - EVENTSTORE_EXT_TCP_PORT=1113 14 | -------------------------------------------------------------------------------- /Chapter11/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | eventstore: 6 | container_name: marketplace-eventstore 7 | image: eventstore/eventstore 8 | ports: 9 | - 2113:2113 10 | - 1113:1113 11 | environment: 12 | - EVENTSTORE_EXT_HTTP_PORT=2113 13 | - EVENTSTORE_EXT_TCP_PORT=1113 14 | 15 | ravendb: 16 | container_name: marketplace-ch11-ravendb 17 | image: ravendb/ravendb 18 | ports: 19 | - 8080:8080 20 | environment: 21 | - RAVEN_Security_UnsecuredAccessAllowed=PublicNetwork 22 | - RAVEN_ARGS="--Setup.Mode=None" 23 | -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Domain/ClassifiedAd/ClassifiedAdId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.ClassifiedAd 5 | { 6 | public class ClassifiedAdId : Value 7 | { 8 | public Guid Value { get; } 9 | 10 | public ClassifiedAdId(Guid value) 11 | { 12 | if (value == default) 13 | throw new ArgumentNullException(nameof(value), "Classified Ad id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public static implicit operator Guid(ClassifiedAdId self) => self.Value; 19 | 20 | public static implicit operator ClassifiedAdId(string value) 21 | => new ClassifiedAdId(Guid.Parse(value)); 22 | 23 | public override string ToString() => Value.ToString(); 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Domain/ClassifiedAd/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain.ClassifiedAd 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | public string Value { get; private set; } 8 | 9 | internal ClassifiedAdText(string text) => Value = text; 10 | 11 | public static ClassifiedAdText FromString(string text) => 12 | new ClassifiedAdText(text); 13 | 14 | public static implicit operator string(ClassifiedAdText text) => 15 | text.Value; 16 | 17 | // Satisfy the serialization requirements 18 | protected ClassifiedAdText() { } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Domain/ClassifiedAd/PictureRules.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Domain.ClassifiedAd 2 | { 3 | public static class PictureRules 4 | { 5 | public static bool HasCorrectSize(this Picture picture) 6 | => picture != null 7 | && picture.Size.Width >= 800 8 | && picture.Size.Height >= 600; 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Domain/Marketplace.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 8 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Domain/Shared/ContentModeration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public delegate Task CheckTextForProfanity(string text); 6 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Domain/Shared/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public static class DomainExceptions 6 | { 7 | public class InvalidEntityState : Exception 8 | { 9 | public InvalidEntityState(object entity, string message) 10 | : base($"Entity {entity.GetType().Name} state change rejected, {message}") 11 | { 12 | } 13 | } 14 | 15 | public class ProfanityFound : Exception 16 | { 17 | public ProfanityFound(string text) 18 | : base($"Profanity found in text: {text}") 19 | { 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Domain/Shared/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | Currency FindCurrency(string currencyCode); 8 | } 9 | 10 | public class Currency : Value 11 | { 12 | public string CurrencyCode { get; set; } 13 | public bool InUse { get; set; } 14 | public int DecimalPlaces { get; set; } 15 | 16 | public static Currency None = new Currency {InUse = false}; 17 | } 18 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Domain/Shared/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.Shared 5 | { 6 | public class UserId : Value 7 | { 8 | public Guid Value { get; private set; } 9 | 10 | public UserId(Guid value) 11 | { 12 | if (value == default) 13 | throw new ArgumentNullException(nameof(value), "User id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public static implicit operator Guid(UserId self) => self.Value; 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Domain/UserProfile/FullName.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.UserProfile 5 | { 6 | public class FullName : Value 7 | { 8 | public string Value { get; private set; } 9 | 10 | internal FullName(string value) => Value = value; 11 | 12 | public static FullName FromString(string fullName) 13 | { 14 | if (fullName.IsEmpty()) 15 | throw new ArgumentNullException(nameof(fullName)); 16 | 17 | return new FullName(fullName); 18 | } 19 | 20 | public static implicit operator string(FullName fullName) 21 | => fullName.Value; 22 | 23 | // Satisfy the serialization requirements 24 | protected FullName() { } 25 | } 26 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Framework/ApplicationServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Marketplace.Framework 5 | { 6 | public static class ApplicationServiceExtensions 7 | { 8 | public static async Task HandleUpdate(this IApplicationService service, 9 | IAggregateStore store, TId aggregateId, Action operation) 10 | where T : AggregateRoot 11 | { 12 | var aggregate = await store.Load(aggregateId); 13 | if (aggregate == null) 14 | throw new InvalidOperationException($"Entity with id {aggregateId.ToString()} cannot be found"); 15 | 16 | operation(aggregate); 17 | await store.Save(aggregate); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Framework/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public abstract class Entity : IInternalEventHandler 6 | where TId : Value 7 | { 8 | private readonly Action _applier; 9 | 10 | public TId Id { get; protected set; } 11 | 12 | protected Entity(Action applier) => _applier = applier; 13 | 14 | protected abstract void When(object @event); 15 | 16 | protected void Apply(object @event) 17 | { 18 | When(@event); 19 | _applier(@event); 20 | } 21 | 22 | void IInternalEventHandler.Handle(object @event) => When(@event); 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Framework/IAggregateStore.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IAggregateStore 6 | { 7 | Task Exists(TId aggregateId); 8 | 9 | Task Save(T aggregate) where T : AggregateRoot; 10 | 11 | Task Load(TId aggregateId) where T : AggregateRoot; 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Framework/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IApplicationService 6 | { 7 | Task Handle(object command); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Framework/ICheckpointStore.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using EventStore.ClientAPI; 3 | 4 | namespace Marketplace.Framework 5 | { 6 | public interface ICheckpointStore 7 | { 8 | Task GetCheckpoint(); 9 | Task StoreCheckpoint(Position checkpoint); 10 | } 11 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Framework/IInternalEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public interface IInternalEventHandler 4 | { 5 | void Handle(object @event); 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Framework/IProjection.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IProjection 6 | { 7 | Task Project(object @event); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Framework/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Framework/Marketplace.Framework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | latest 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Framework/StringTools.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class StringTools 4 | { 5 | public static bool IsEmpty(this string value) 6 | => string.IsNullOrWhiteSpace(value); 7 | } 8 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Framework/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class ValueExtensions 4 | { 5 | public static void MustNotBeNull(this Value value) where T : Value 6 | { 7 | if (value == null) 8 | throw new InvalidValueException(typeof(T), "cannot be null"); 9 | } 10 | 11 | public static void MustBe(this Value value) where T : Value 12 | { 13 | if (value == null) 14 | throw new InvalidValueException(typeof(T), "cannot be null"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace.Tests/Marketplace.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace/ClassifiedAd/ClassifiedAdsQueryApi.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Marketplace.Infrastructure; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Raven.Client.Documents.Session; 5 | using Serilog; 6 | 7 | namespace Marketplace.ClassifiedAd 8 | { 9 | [Route("/ad")] 10 | public class ClassifiedAdsQueryApi : Controller 11 | { 12 | private readonly IAsyncDocumentSession _session; 13 | private static ILogger _log = Log.ForContext(); 14 | 15 | public ClassifiedAdsQueryApi(IAsyncDocumentSession session) => 16 | _session = session; 17 | 18 | [HttpGet] 19 | public Task Get(QueryModels.GetPublicClassifiedAd request) 20 | => RequestHandler.HandleQuery(() => _session.Query(request), _log); 21 | } 22 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace/ClassifiedAd/Queries.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Raven.Client.Documents.Session; 3 | using static Marketplace.ClassifiedAd.QueryModels; 4 | using static Marketplace.Projections.ReadModels; 5 | 6 | namespace Marketplace.ClassifiedAd 7 | { 8 | public static class Queries 9 | { 10 | public static Task Query( 11 | this IAsyncDocumentSession session, 12 | GetPublicClassifiedAd query 13 | ) => 14 | session.LoadAsync( 15 | query.ClassifiedAdId.ToString() 16 | ); 17 | } 18 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace/ClassifiedAd/QueryModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.ClassifiedAd 4 | { 5 | public static class QueryModels 6 | { 7 | public class GetPublishedClassifiedAds 8 | { 9 | public int Page { get; set; } 10 | public int PageSize { get; set; } 11 | } 12 | 13 | public class GetOwnersClassifiedAd 14 | { 15 | public Guid OwnerId { get; set; } 16 | public int Page { get; set; } 17 | public int PageSize { get; set; } 18 | } 19 | 20 | public class GetPublicClassifiedAd 21 | { 22 | public Guid ClassifiedAdId { get; set; } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace 4 | { 5 | public static class Exceptions 6 | { 7 | public class DuplicatedEntityIdException : Exception 8 | { 9 | public DuplicatedEntityIdException(string message) 10 | : base(message) 11 | { 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace/Infrastructure/Checkpoint.cs: -------------------------------------------------------------------------------- 1 | using EventStore.ClientAPI; 2 | 3 | namespace Marketplace.Infrastructure 4 | { 5 | public class Checkpoint 6 | { 7 | public string Id { get; set; } 8 | public Position Position { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace/Infrastructure/EventDeserializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using EventStore.ClientAPI; 4 | using Newtonsoft.Json; 5 | 6 | namespace Marketplace.Infrastructure 7 | { 8 | public static class EventDeserializer 9 | { 10 | public static object Deserialzie(this ResolvedEvent resolvedEvent) 11 | { 12 | var meta = JsonConvert.DeserializeObject( 13 | Encoding.UTF8.GetString(resolvedEvent.Event.Metadata)); 14 | var dataType = Type.GetType(meta.ClrType); 15 | var jsonData = Encoding.UTF8.GetString(resolvedEvent.Event.Data); 16 | var data = JsonConvert.DeserializeObject(jsonData, dataType); 17 | return data; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace/Infrastructure/EventMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Infrastructure 2 | { 3 | public class EventMetadata 4 | { 5 | public string ClrType { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Serilog; 4 | 5 | namespace Marketplace 6 | { 7 | public static class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | Log.Logger = new LoggerConfiguration() 12 | .MinimumLevel.Debug() 13 | .WriteTo.Console() 14 | .CreateLogger(); 15 | 16 | CreateWebHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 20 | WebHost.CreateDefaultBuilder(args) 21 | .UseSerilog() 22 | .UseStartup(); 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:53622", 8 | "sslPort": 44380 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "Marketplace": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "swagger", 24 | "applicationUrl": "http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace/UserProfile/Queries.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Raven.Client.Documents.Session; 4 | using static Marketplace.Projections.ReadModels; 5 | 6 | namespace Marketplace.UserProfile 7 | { 8 | public static class Queries 9 | { 10 | public static Task GetUserDetails( 11 | this Func getSession, 12 | Guid id 13 | ) 14 | { 15 | using var session = getSession(); 16 | 17 | return session.LoadAsync(id.ToString()); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter11/in-database/Marketplace/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventStore": { 3 | "connectionString": "ConnectTo=tcp://admin:changeit@localhost:1113; DefaultUserCredentials=admin:changeit;" 4 | }, 5 | "ravenDb": { 6 | "server": "http://localhost:8080", 7 | "database": "Marketplace_Chapter11" 8 | }, 9 | "Logging": { 10 | "LogLevel": { 11 | "Default": "Debug", 12 | "System": "Information", 13 | "Microsoft": "Warning" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Domain/ClassifiedAd/ClassifiedAdId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.ClassifiedAd 5 | { 6 | public class ClassifiedAdId : Value 7 | { 8 | public Guid Value { get; } 9 | 10 | public ClassifiedAdId(Guid value) 11 | { 12 | if (value == default) 13 | throw new ArgumentNullException(nameof(value), "Classified Ad id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public static implicit operator Guid(ClassifiedAdId self) => self.Value; 19 | 20 | public static implicit operator ClassifiedAdId(string value) 21 | => new ClassifiedAdId(Guid.Parse(value)); 22 | 23 | public override string ToString() => Value.ToString(); 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Domain/ClassifiedAd/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain.ClassifiedAd 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | public string Value { get; private set; } 8 | 9 | internal ClassifiedAdText(string text) => Value = text; 10 | 11 | public static ClassifiedAdText FromString(string text) => 12 | new ClassifiedAdText(text); 13 | 14 | public static implicit operator string(ClassifiedAdText text) => 15 | text.Value; 16 | 17 | // Satisfy the serialization requirements 18 | protected ClassifiedAdText() { } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Domain/ClassifiedAd/PictureRules.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Domain.ClassifiedAd 2 | { 3 | public static class PictureRules 4 | { 5 | public static bool HasCorrectSize(this Picture picture) 6 | => picture != null 7 | && picture.Size.Width >= 800 8 | && picture.Size.Height >= 600; 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Domain/Marketplace.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 8 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Domain/Shared/ContentModeration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public delegate Task CheckTextForProfanity(string text); 6 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Domain/Shared/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public static class DomainExceptions 6 | { 7 | public class InvalidEntityState : Exception 8 | { 9 | public InvalidEntityState(object entity, string message) 10 | : base($"Entity {entity.GetType().Name} state change rejected, {message}") 11 | { 12 | } 13 | } 14 | 15 | public class ProfanityFound : Exception 16 | { 17 | public ProfanityFound(string text) 18 | : base($"Profanity found in text: {text}") 19 | { 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Domain/Shared/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain.Shared 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | Currency FindCurrency(string currencyCode); 8 | } 9 | 10 | public class Currency : Value 11 | { 12 | public string CurrencyCode { get; set; } 13 | public bool InUse { get; set; } 14 | public int DecimalPlaces { get; set; } 15 | 16 | public static Currency None = new Currency {InUse = false}; 17 | } 18 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Domain/Shared/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.Shared 5 | { 6 | public class UserId : Value 7 | { 8 | public Guid Value { get; private set; } 9 | 10 | public UserId(Guid value) 11 | { 12 | if (value == default) 13 | throw new ArgumentNullException(nameof(value), "User id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public static implicit operator Guid(UserId self) => self.Value; 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Domain/UserProfile/FullName.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.Framework; 3 | 4 | namespace Marketplace.Domain.UserProfile 5 | { 6 | public class FullName : Value 7 | { 8 | public string Value { get; private set; } 9 | 10 | internal FullName(string value) => Value = value; 11 | 12 | public static FullName FromString(string fullName) 13 | { 14 | if (fullName.IsEmpty()) 15 | throw new ArgumentNullException(nameof(fullName)); 16 | 17 | return new FullName(fullName); 18 | } 19 | 20 | public static implicit operator string(FullName fullName) 21 | => fullName.Value; 22 | 23 | // Satisfy the serialization requirements 24 | protected FullName() { } 25 | } 26 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Framework/ApplicationServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Marketplace.Framework 5 | { 6 | public static class ApplicationServiceExtensions 7 | { 8 | public static async Task HandleUpdate(this IApplicationService service, 9 | IAggregateStore store, TId aggregateId, Action operation) 10 | where T : AggregateRoot 11 | { 12 | var aggregate = await store.Load(aggregateId); 13 | if (aggregate == null) 14 | throw new InvalidOperationException($"Entity with id {aggregateId.ToString()} cannot be found"); 15 | 16 | operation(aggregate); 17 | await store.Save(aggregate); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Framework/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public abstract class Entity : IInternalEventHandler 6 | where TId : Value 7 | { 8 | private readonly Action _applier; 9 | 10 | public TId Id { get; protected set; } 11 | 12 | protected Entity(Action applier) => _applier = applier; 13 | 14 | protected abstract void When(object @event); 15 | 16 | protected void Apply(object @event) 17 | { 18 | When(@event); 19 | _applier(@event); 20 | } 21 | 22 | void IInternalEventHandler.Handle(object @event) => When(@event); 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Framework/IAggregateStore.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IAggregateStore 6 | { 7 | Task Exists(TId aggregateId); 8 | 9 | Task Save(T aggregate) where T : AggregateRoot; 10 | 11 | Task Load(TId aggregateId) where T : AggregateRoot; 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Framework/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IApplicationService 6 | { 7 | Task Handle(object command); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Framework/IInternalEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public interface IInternalEventHandler 4 | { 5 | void Handle(object @event); 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Framework/IProjection.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public interface IProjection 6 | { 7 | Task Project(object @event); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Framework/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Framework/Marketplace.Framework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 7.1 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Framework/StringTools.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class StringTools 4 | { 5 | public static bool IsEmpty(this string value) 6 | => string.IsNullOrWhiteSpace(value); 7 | } 8 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Framework/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class ValueExtensions 4 | { 5 | public static void MustNotBeNull(this Value value) where T : Value 6 | { 7 | if (value == null) 8 | throw new InvalidValueException(typeof(T), "cannot be null"); 9 | } 10 | 11 | public static void MustBe(this Value value) where T : Value 12 | { 13 | if (value == null) 14 | throw new InvalidValueException(typeof(T), "cannot be null"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace.Tests/Marketplace.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace/ClassifiedAd/ClassifiedAdsQueryApi.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Marketplace.Infrastructure; 3 | using Marketplace.Projections; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Serilog; 6 | 7 | namespace Marketplace.ClassifiedAd 8 | { 9 | [Route("/ad")] 10 | public class ClassifiedAdsQueryApi : Controller 11 | { 12 | private static ILogger _log = Log.ForContext(); 13 | 14 | private readonly IEnumerable _items; 15 | 16 | public ClassifiedAdsQueryApi(IEnumerable items) 17 | => _items = items; 18 | 19 | [HttpGet] 20 | public IActionResult Get(QueryModels.GetPublicClassifiedAd request) 21 | => RequestHandler.HandleQuery(() => _items.Query(request), _log); 22 | } 23 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace/ClassifiedAd/Queries.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Marketplace.Projections; 4 | 5 | namespace Marketplace.ClassifiedAd 6 | { 7 | public static class Queries 8 | { 9 | public static ReadModels.ClassifiedAdDetails Query( 10 | this IEnumerable items, 11 | QueryModels.GetPublicClassifiedAd query) 12 | => items.FirstOrDefault(x => x.ClassifiedAdId == query.ClassifiedAdId); 13 | } 14 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace/ClassifiedAd/QueryModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.ClassifiedAd 4 | { 5 | public static class QueryModels 6 | { 7 | public class GetPublishedClassifiedAds 8 | { 9 | public int Page { get; set; } 10 | public int PageSize { get; set; } 11 | } 12 | 13 | public class GetOwnersClassifiedAd 14 | { 15 | public Guid OwnerId { get; set; } 16 | public int Page { get; set; } 17 | public int PageSize { get; set; } 18 | } 19 | 20 | public class GetPublicClassifiedAd 21 | { 22 | public Guid ClassifiedAdId { get; set; } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace 4 | { 5 | public static class Exceptions 6 | { 7 | public class DuplicatedEntityIdException : Exception 8 | { 9 | public DuplicatedEntityIdException(string message) 10 | : base(message) 11 | { 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace/Infrastructure/EventDeserializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using EventStore.ClientAPI; 4 | using Newtonsoft.Json; 5 | 6 | namespace Marketplace.Infrastructure 7 | { 8 | public static class EventDeserializer 9 | { 10 | public static object Deserialzie(this ResolvedEvent resolvedEvent) 11 | { 12 | var meta = JsonConvert.DeserializeObject( 13 | Encoding.UTF8.GetString(resolvedEvent.Event.Metadata)); 14 | var dataType = Type.GetType(meta.ClrType); 15 | var jsonData = Encoding.UTF8.GetString(resolvedEvent.Event.Data); 16 | var data = JsonConvert.DeserializeObject(jsonData, dataType); 17 | return data; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace/Infrastructure/EventMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Infrastructure 2 | { 3 | public class EventMetadata 4 | { 5 | public string ClrType { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Serilog; 4 | 5 | namespace Marketplace 6 | { 7 | public static class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | Log.Logger = new LoggerConfiguration() 12 | .MinimumLevel.Debug() 13 | .WriteTo.Console() 14 | .CreateLogger(); 15 | 16 | CreateWebHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 20 | WebHost.CreateDefaultBuilder(args) 21 | .UseSerilog() 22 | .UseStartup(); 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:53622", 8 | "sslPort": 44380 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "Marketplace": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "swagger", 24 | "applicationUrl": "http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Chapter11/in-memory/Marketplace/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventStore": { 3 | "connectionString": "ConnectTo=tcp://admin:changeit@localhost:1113; DefaultUserCredentials=admin:changeit;" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Debug", 8 | "System": "Information", 9 | "Microsoft": "Warning" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Chapter13/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | eventstore: 6 | container_name: marketplace-ch13-eventstore 7 | image: eventstore/eventstore 8 | ports: 9 | - 2113:2113 10 | - 1113:1113 11 | environment: 12 | - EVENTSTORE_EXT_HTTP_PORT=2113 13 | - EVENTSTORE_EXT_TCP_PORT=1113 14 | 15 | ravendb: 16 | container_name: marketplace-ch13-ravendb 17 | image: ravendb/ravendb 18 | ports: 19 | - 8080:8080 20 | environment: 21 | - RAVEN_Security_UnsecuredAccessAllowed=PublicNetwork 22 | - RAVEN_ARGS="--Setup.Mode=None" 23 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Ads.Domain/ClassifiedAds/ClassifiedAdId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.EventSourcing; 3 | 4 | namespace Marketplace.Ads.Domain.ClassifiedAds 5 | { 6 | public class ClassifiedAdId : AggregateId 7 | { 8 | ClassifiedAdId(Guid value) : base(value) { } 9 | 10 | public static ClassifiedAdId FromGuid(Guid value) 11 | => new ClassifiedAdId(value); 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Ads.Domain/ClassifiedAds/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.EventSourcing; 2 | 3 | namespace Marketplace.Ads.Domain.ClassifiedAds 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | internal ClassifiedAdText(string text) => Value = text; 8 | 9 | // Satisfy the serialization requirements 10 | protected ClassifiedAdText() { } 11 | public string Value { get; } 12 | 13 | public static ClassifiedAdText FromString(string text) => new ClassifiedAdText(text); 14 | 15 | public static implicit operator string(ClassifiedAdText text) => text.Value; 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Ads.Domain/ClassifiedAds/PictureRules.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Ads.Domain.ClassifiedAds 2 | { 3 | public static class PictureRules 4 | { 5 | public static bool HasCorrectSize(this Picture picture) 6 | => picture != null 7 | && picture.Size.Width >= 800 8 | && picture.Size.Height >= 600; 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Ads.Domain/Marketplace.Ads.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 8 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Ads.Domain/Shared/ContentModeration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Ads.Domain.Shared 4 | { 5 | public delegate Task CheckTextForProfanity(string text); 6 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Ads.Domain/Shared/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Ads.Domain.Shared 4 | { 5 | public static class DomainExceptions 6 | { 7 | public class ProfanityFound : Exception 8 | { 9 | public ProfanityFound(string text) 10 | : base($"Profanity found in text: {text}") { } 11 | } 12 | 13 | public class InvalidEntityState : Exception 14 | { 15 | public InvalidEntityState(object entity, string message) 16 | : base( 17 | $"Entity {entity.GetType().Name} state change rejected, {message}" 18 | ) { } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Ads.Domain/Shared/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.EventSourcing; 2 | 3 | namespace Marketplace.Ads.Domain.Shared 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | Currency FindCurrency(string currencyCode); 8 | } 9 | 10 | public class Currency : Value 11 | { 12 | public static Currency None = new Currency {InUse = false}; 13 | public string CurrencyCode { get; set; } 14 | public bool InUse { get; set; } 15 | public int DecimalPlaces { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Ads.Messages/Marketplace.Ads.Messages.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Ads/ClassifiedAds/ClassifiedAdsQueryApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Marketplace.Ads.Projections; 4 | using Marketplace.Modules.ClassifiedAds; 5 | using Marketplace.RavenDb; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Raven.Client.Documents.Session; 8 | 9 | namespace Marketplace.Ads.ClassifiedAds 10 | { 11 | [ApiController, Route("/ad")] 12 | public class ClassifiedAdsQueryApi : ControllerBase 13 | { 14 | readonly Func _getSession; 15 | 16 | public ClassifiedAdsQueryApi(Func getSession) 17 | => _getSession = getSession; 18 | 19 | [HttpGet] 20 | public Task> Get( 21 | [FromQuery] QueryModels.GetPublicClassifiedAd request) 22 | => _getSession.RunApiQuery(s => s.Query(request)); 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Ads/ClassifiedAds/Queries.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Marketplace.Ads.Projections; 3 | using Raven.Client.Documents.Session; 4 | 5 | namespace Marketplace.Modules.ClassifiedAds 6 | { 7 | public static class Queries 8 | { 9 | public static Task Query( 10 | this IAsyncDocumentSession session, 11 | QueryModels.GetPublicClassifiedAd query) 12 | => session.LoadAsync( 13 | query.ClassifiedAdId.ToString() 14 | ); 15 | } 16 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Ads/ClassifiedAds/QueryModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Modules.ClassifiedAds 4 | { 5 | public static class QueryModels 6 | { 7 | public class GetPublishedClassifiedAds 8 | { 9 | public int Page { get; set; } 10 | public int PageSize { get; set; } 11 | } 12 | 13 | public class GetOwnersClassifiedAd 14 | { 15 | public Guid OwnerId { get; set; } 16 | public int Page { get; set; } 17 | public int PageSize { get; set; } 18 | } 19 | 20 | public class GetPublicClassifiedAd 21 | { 22 | public Guid ClassifiedAdId { get; set; } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Ads/Marketplace.Ads.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.2 5 | 8 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/AggregateId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.EventSourcing 4 | { 5 | public abstract class AggregateId : Value> 6 | where T : AggregateRoot 7 | { 8 | protected AggregateId(Guid value) 9 | { 10 | if (value == default) 11 | throw new ArgumentNullException( 12 | nameof(value), 13 | "The Id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public Guid Value { get; } 19 | 20 | public static implicit operator Guid(AggregateId self) => self.Value; 21 | 22 | public override string ToString() => Value.ToString(); 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/ApplicationServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Marketplace.EventSourcing 5 | { 6 | public static class ApplicationServiceExtensions 7 | { 8 | public static async Task HandleUpdate( 9 | this IApplicationService service, 10 | IAggregateStore store, 11 | AggregateId aggregateId, 12 | Action operation) 13 | where T : AggregateRoot 14 | { 15 | var aggregate = await store.Load(aggregateId); 16 | 17 | if (aggregate == null) 18 | throw new InvalidOperationException($"Entity with id {aggregateId} cannot be found"); 19 | 20 | operation(aggregate); 21 | await store.Save(aggregate); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/CommandService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Marketplace.EventSourcing 5 | { 6 | public abstract class CommandService 7 | where T : class, IAggregateState, new() 8 | { 9 | IFunctionalAggregateStore Store { get; } 10 | 11 | protected CommandService(IFunctionalAggregateStore store) 12 | => Store = store; 13 | 14 | protected async Task Handle( 15 | Guid id, 16 | Func.Result> update) 17 | { 18 | var state = await Store.Load(id); 19 | await Store.Save(state.Version, update(state)); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.EventSourcing 4 | { 5 | public abstract class Entity : IInternalEventHandler 6 | where TId : Value 7 | { 8 | readonly Action _applier; 9 | 10 | protected Entity(Action applier) => _applier = applier; 11 | 12 | public TId Id { get; protected set; } 13 | 14 | void IInternalEventHandler.Handle(object @event) => When(@event); 15 | 16 | protected abstract void When(object @event); 17 | 18 | protected void Apply(object @event) 19 | { 20 | When(@event); 21 | _applier(@event); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/IAggregateStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Marketplace.EventSourcing 5 | { 6 | public interface IAggregateStore 7 | { 8 | Task Exists(AggregateId aggregateId) where T : AggregateRoot; 9 | 10 | Task Save(T aggregate) where T : AggregateRoot; 11 | 12 | Task Load(AggregateId aggregateId) where T : AggregateRoot; 13 | } 14 | 15 | public interface IFunctionalAggregateStore 16 | { 17 | Task Save(long version, AggregateState.Result update) 18 | where T : class, IAggregateState, new(); 19 | 20 | Task Load(Guid id) 21 | where T : IAggregateState, new(); 22 | } 23 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.EventSourcing 4 | { 5 | public interface IApplicationService 6 | { 7 | Task Handle(object command); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/ICheckpointStore.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.EventSourcing 4 | { 5 | public interface ICheckpointStore 6 | { 7 | Task GetCheckpoint(); 8 | Task StoreCheckpoint(long? checkpoint); 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/IInternalEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.EventSourcing 2 | { 3 | public interface IInternalEventHandler 4 | { 5 | void Handle(object @event); 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/ISubscription.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.EventSourcing 4 | { 5 | public interface ISubscription 6 | { 7 | Task Project(object @event); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.EventSourcing 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") { } 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/Marketplace.EventSourcing.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 8 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/StringTools.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.EventSourcing 2 | { 3 | public static class StringTools 4 | { 5 | public static bool IsEmpty(this string value) => string.IsNullOrWhiteSpace(value); 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventSourcing/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.EventSourcing 4 | { 5 | public static class ValueExtensions 6 | { 7 | public static void MustNotBeNull(this Value value) where T : Value 8 | { 9 | if (value == null) 10 | throw new InvalidValueException(typeof(T), "cannot be null"); 11 | } 12 | 13 | public static void MustBe(this Value value) where T : Value 14 | { 15 | if (value == null) 16 | throw new InvalidValueException(typeof(T), "cannot be null"); 17 | } 18 | 19 | public static T With(this T instance, Action update) 20 | { 21 | update(instance); 22 | return instance; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventStore/EventDeserializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using EventStore.ClientAPI; 3 | using Marketplace.EventSourcing; 4 | using Newtonsoft.Json; 5 | 6 | namespace Marketplace.EventStore 7 | { 8 | public static class EventDeserializer 9 | { 10 | public static object Deserialze(this ResolvedEvent resolvedEvent) 11 | { 12 | var dataType = TypeMapper.GetType(resolvedEvent.Event.EventType); 13 | var jsonData = Encoding.UTF8.GetString(resolvedEvent.Event.Data); 14 | var data = JsonConvert.DeserializeObject(jsonData, dataType); 15 | return data; 16 | } 17 | 18 | public static T Deserialze(this ResolvedEvent resolvedEvent) 19 | { 20 | var jsonData = Encoding.UTF8.GetString(resolvedEvent.Event.Data); 21 | return JsonConvert.DeserializeObject(jsonData); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventStore/EventMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.EventStore 2 | { 3 | public class EventMetadata 4 | { 5 | public string ClrType { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventStore/EventStoreReactor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Marketplace.EventSourcing; 4 | using Marketplace.EventStore.Logging; 5 | 6 | namespace Marketplace.EventStore 7 | { 8 | public abstract class ReactorBase : ISubscription 9 | { 10 | static readonly ILog Log = LogProvider.GetCurrentClassLogger(); 11 | 12 | public ReactorBase(Reactor reactor) => _reactor = reactor; 13 | 14 | readonly Reactor _reactor; 15 | 16 | public Task Project(object @event) 17 | { 18 | var handler = _reactor(@event); 19 | 20 | if (handler == null) return Task.CompletedTask; 21 | 22 | Log.Debug("Reacting to event {event}", @event); 23 | 24 | return handler(); 25 | } 26 | 27 | public delegate Func Reactor(object @event); 28 | } 29 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.EventStore/Marketplace.EventStore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.PaidServices.Domain/ClassifiedAds/ActivePaidService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.EventSourcing; 3 | using Marketplace.PaidServices.Domain.Services; 4 | 5 | namespace Marketplace.PaidServices.Domain.ClassifiedAds 6 | { 7 | public class ActivePaidService : Value 8 | { 9 | ActivePaidService(PaidService paidService, DateTimeOffset expiresAt) 10 | { 11 | Service = paidService; 12 | ExpiresAt = expiresAt; 13 | } 14 | 15 | PaidService Service { get; } 16 | DateTimeOffset ExpiresAt { get; } 17 | 18 | public static ActivePaidService Create( 19 | PaidService paidService, 20 | DateTimeOffset startFrom) 21 | { 22 | var expiresAt = startFrom + paidService.Duration; 23 | return new ActivePaidService(paidService, expiresAt); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.PaidServices.Domain/ClassifiedAds/ClassifiedAdId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.EventSourcing; 3 | 4 | namespace Marketplace.PaidServices.Domain.ClassifiedAds 5 | { 6 | public class ClassifiedAdId : Value 7 | { 8 | ClassifiedAdId(Guid value) 9 | { 10 | if (value == default) 11 | throw new ArgumentNullException( 12 | nameof(value), 13 | "The Id cannot be empty" 14 | ); 15 | 16 | Value = value; 17 | } 18 | 19 | Guid Value { get; } 20 | 21 | public static ClassifiedAdId FromGuid(Guid value) 22 | => new ClassifiedAdId(value); 23 | 24 | public static implicit operator Guid(ClassifiedAdId self) => self.Value; 25 | 26 | public override string ToString() => Value.ToString(); 27 | } 28 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.PaidServices.Domain/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.PaidServices.Domain 4 | { 5 | public static class DomainExceptions 6 | { 7 | public class OperationNotAllowed : Exception 8 | { 9 | public OperationNotAllowed( 10 | object entity, 11 | string operation, 12 | string state 13 | ) : base( 14 | $"Operation {operation} is not allowed for the " + 15 | $"entity {entity.GetType().Name} in state {state}" 16 | ) { } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.PaidServices.Domain/Marketplace.PaidServices.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 8 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.PaidServices.Domain/Orders/OrderId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.EventSourcing; 3 | 4 | namespace Marketplace.PaidServices.Domain.Orders 5 | { 6 | public class OrderId : Value 7 | { 8 | OrderId(Guid value) 9 | { 10 | if (value == default) 11 | throw new ArgumentNullException( 12 | nameof(value), 13 | "The Id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public static OrderId FromGuid(Guid value) => new OrderId(value); 19 | 20 | Guid Value { get; } 21 | 22 | public static implicit operator Guid(OrderId self) => self.Value; 23 | 24 | public override string ToString() => Value.ToString(); 25 | } 26 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.PaidServices.Domain/Services/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.PaidServices.Domain.Services 4 | { 5 | public static class Exceptions 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.PaidServices.Domain/Shared/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.EventSourcing; 3 | 4 | namespace Marketplace.PaidServices.Domain.Shared 5 | { 6 | public class UserId : Value 7 | { 8 | UserId(Guid value) 9 | { 10 | if (value == default) 11 | throw new ArgumentNullException( 12 | nameof(value), 13 | "The Id cannot be empty"); 14 | 15 | Value = value; 16 | } 17 | 18 | public static UserId FromGuid(Guid value) => new UserId(value); 19 | 20 | Guid Value { get; } 21 | 22 | public static implicit operator Guid(UserId self) => self.Value; 23 | 24 | public override string ToString() => Value.ToString(); 25 | } 26 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.PaidServices.Messages/Ads/Commands.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.PaidServices.Messages.Ads 4 | { 5 | public static class Commands 6 | { 7 | public static class V1 8 | { 9 | public class Create 10 | { 11 | public Guid ClassifiedAdId { get; set; } 12 | public Guid SellerId { get; set; } 13 | } 14 | 15 | public class FulfillOrder 16 | { 17 | public Guid ClassifiedAdId { get; set; } 18 | public DateTimeOffset When { get; set; } 19 | public string[] ServiceTypes { get; set; } 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.PaidServices.Messages/Ads/Events.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.PaidServices.Messages.Ads 4 | { 5 | public static class Events 6 | { 7 | public class V1 8 | { 9 | public class Created 10 | { 11 | public Guid ClassifiedAdId { get; set; } 12 | public Guid SellerId { get; set; } 13 | } 14 | 15 | public class ServiceActivated 16 | { 17 | public Guid ClassifiedAdId { get; set; } 18 | public string ServiceType { get; set; } 19 | public DateTimeOffset ActiveUntil { get; set; } 20 | } 21 | 22 | public class ServiceDeactivated 23 | { 24 | public Guid ClassifiedAdId { get; set; } 25 | public string ServiceType { get; set; } 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.PaidServices.Messages/Marketplace.PaidServices.Messages.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.PaidServices/EventMappings.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.PaidServices.Messages.Ads; 2 | using static Marketplace.EventSourcing.TypeMapper; 3 | using static Marketplace.PaidServices.Messages.Orders.Events; 4 | 5 | namespace Marketplace.PaidServices 6 | { 7 | public static class EventMappings 8 | { 9 | public static void MapEventTypes() 10 | { 11 | Map("OrderCreated"); 12 | Map("ServiceAddedToOrder"); 13 | Map("ServiceRemovedFromOrder"); 14 | 15 | Map("PaidClassifiedAdCreated"); 16 | Map("AdServiceActivated"); 17 | Map("AdServiceDeactivated"); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.PaidServices/PaidServices/PaidServicesQueryApi.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Marketplace.PaidServices.Domain.Services; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Marketplace.PaidServices.PaidServices 7 | { 8 | [Route("/api/services")] 9 | public class PaidServicesQueryApi : ControllerBase 10 | { 11 | [HttpGet] 12 | public ActionResult> Get() 13 | => Ok( 14 | PaidService.AvailableServices.Select( 15 | Models.PaidServiceItem.FromDomain 16 | ) 17 | ); 18 | } 19 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.RavenDb/GetSession.cs: -------------------------------------------------------------------------------- 1 | using Raven.Client.Documents.Session; 2 | 3 | namespace Marketplace.RavenDb 4 | { 5 | public delegate IAsyncDocumentSession GetSession(); 6 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.RavenDb/Marketplace.RavenDb.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 8 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Users.Domain/Marketplace.Users.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 8 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Users.Domain/Shared/ContentModeration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Marketplace.Users.Domain.Shared 4 | { 5 | public delegate Task CheckTextForProfanity(string text); 6 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Users.Domain/Shared/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Users.Domain.Shared 4 | { 5 | public static class DomainExceptions 6 | { 7 | public class ProfanityFound : Exception 8 | { 9 | public ProfanityFound(string text) 10 | : base($"Profanity found in text: {text}") { } 11 | } 12 | 13 | public class InvalidEntityState : Exception 14 | { 15 | public InvalidEntityState(object entity, string message) 16 | : base( 17 | $"Entity {entity.GetType().Name} state change rejected, {message}" 18 | ) { } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Users.Domain/UserProfiles/FullName.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.EventSourcing; 3 | 4 | namespace Marketplace.Users.Domain.UserProfiles 5 | { 6 | public class FullName : Value 7 | { 8 | internal FullName(string value) => Value = value; 9 | 10 | // Satisfy the serialization requirements 11 | protected FullName() { } 12 | public string Value { get; } 13 | 14 | public static FullName FromString(string fullName) 15 | { 16 | if (fullName.IsEmpty()) 17 | throw new ArgumentNullException(nameof(fullName)); 18 | 19 | return new FullName(fullName); 20 | } 21 | 22 | public static implicit operator string(FullName fullName) => fullName.Value; 23 | } 24 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Users.Domain/UserProfiles/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Marketplace.EventSourcing; 3 | 4 | namespace Marketplace.Users.Domain.UserProfiles 5 | { 6 | public class UserId : AggregateId 7 | { 8 | public UserId(Guid value) : base(value) { } 9 | } 10 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Users.Messages/Marketplace.Users.Messages.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 8 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Users/Auth/AuthService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Marketplace.Users.UserProfiles; 4 | 5 | namespace Marketplace.Users.Auth 6 | { 7 | public class AuthService 8 | { 9 | readonly GetUsersModuleSession _getSession; 10 | 11 | public AuthService(GetUsersModuleSession getSession) 12 | => _getSession = getSession; 13 | 14 | public async Task CheckCredentials( 15 | string userName, 16 | string password 17 | ) 18 | { 19 | var userDetails = 20 | await _getSession.GetUserDetails(Guid.Parse(password)); 21 | 22 | return userDetails != null && userDetails.DisplayName == userName; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Users/Auth/Contracts.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Users.Auth 2 | { 3 | public static class Contracts 4 | { 5 | public class Login 6 | { 7 | public string Username { get; set; } 8 | public string Password { get; set; } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Users/EventMappings.cs: -------------------------------------------------------------------------------- 1 | using static Marketplace.EventSourcing.TypeMapper; 2 | using static Marketplace.Users.Messages.UserProfile.Events; 3 | 4 | namespace Marketplace.Users 5 | { 6 | public static class EventMappings 7 | { 8 | public static void MapEventTypes() 9 | { 10 | Map("UserRegistered"); 11 | Map("UserFullNameUpdated"); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Users/Projections/ReadModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Users.Projections 4 | { 5 | public static class ReadModels 6 | { 7 | public class UserDetails 8 | { 9 | public string Id { get; set; } 10 | public string DisplayName { get; set; } 11 | public string FullName { get; set; } 12 | public string PhotoUrl { get; set; } 13 | 14 | public static string GetDatabaseId(Guid id) => $"UserDetails/{id}"; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Users/UserProfiles/Queries.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using static Marketplace.Users.Projections.ReadModels; 4 | 5 | namespace Marketplace.Users.UserProfiles 6 | { 7 | public static class Queries 8 | { 9 | public static Task GetUserDetails( 10 | this GetUsersModuleSession getSession, 11 | Guid id 12 | ) 13 | { 14 | using var session = getSession(); 15 | 16 | return session.LoadAsync( 17 | UserDetails.GetDatabaseId(id) 18 | ); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.Users/UserProfiles/UserProfileQueryApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using static Marketplace.Users.Projections.ReadModels; 5 | 6 | namespace Marketplace.Users.UserProfiles 7 | { 8 | [Route("api/profile")] 9 | public class UserProfileQueryApi : ControllerBase 10 | { 11 | readonly GetUsersModuleSession _getSession; 12 | 13 | public UserProfileQueryApi( 14 | GetUsersModuleSession getSession) 15 | => _getSession = getSession; 16 | 17 | [HttpGet("{userId}")] 18 | public async Task> Get(Guid userId) 19 | { 20 | var user = await _getSession.GetUserDetails(userId); 21 | 22 | if (user == null) return NotFound(); 23 | 24 | return user; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.WebApi/ControllerBaseExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Marketplace.WebApi 6 | { 7 | public static class ControllerBaseExtensions 8 | { 9 | public static async Task HandleCommand( 10 | this ControllerBase _, 11 | Task handler) 12 | { 13 | try 14 | { 15 | await handler; 16 | return new OkResult(); 17 | } 18 | catch (Exception e) 19 | { 20 | return new BadRequestObjectResult( 21 | new 22 | { 23 | error = e.Message, 24 | stackTrace = e.StackTrace 25 | } 26 | ); 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace.WebApi/Marketplace.WebApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 8 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/README.md: -------------------------------------------------------------------------------- 1 | # marketplace 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | npm run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-On-Domain-Driven-Design-with-.NET-Core/bbc760b8964afe02bf62428cac6e0d25cc79b2e3/Chapter13/src/Marketplace/ClientApp/public/favicon.ico -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | marketplace 9 | 10 | 11 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/assets/list-is-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-On-Domain-Driven-Design-with-.NET-Core/bbc760b8964afe02bf62428cac6e0d25cc79b2e3/Chapter13/src/Marketplace/ClientApp/src/assets/list-is-empty.png -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-On-Domain-Driven-Design-with-.NET-Core/bbc760b8964afe02bf62428cac6e0d25cc79b2e3/Chapter13/src/Marketplace/ClientApp/src/assets/logo.png -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/common/config.js: -------------------------------------------------------------------------------- 1 | export const API_URL = "http://localhost:5000/api"; 2 | export default API_URL; -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/components/NewAdButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import "./plugins/vuetify"; 3 | import VueLodash from 'vue-lodash' 4 | import App from "./App.vue"; 5 | import router from "./router"; 6 | import store from "./store"; 7 | import UUID from "vue-uuid"; 8 | import Vuelidate from "vuelidate"; 9 | import "roboto-fontface/css/roboto/roboto-fontface.css"; 10 | import "material-design-icons-iconfont/dist/material-design-icons.css"; 11 | import {CheckAuth} from "./store/modules/auth/actions.type"; 12 | import ApiService from "./common/api.service"; 13 | 14 | Vue.config.productionTip = false; 15 | Vue.use(UUID); 16 | Vue.use(Vuelidate); 17 | Vue.use(VueLodash); 18 | 19 | ApiService.init(); 20 | 21 | router.beforeEach(async (to, from, next) => { 22 | await store.dispatch(CheckAuth); 23 | next(); 24 | }); 25 | 26 | new Vue({ 27 | router, 28 | store, 29 | render: h => h(App) 30 | }).$mount('#app'); 31 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import 'vuetify/dist/vuetify.min.css' 4 | 5 | Vue.use(Vuetify, { 6 | theme: { 7 | primary: '#ee44aa', 8 | secondary: '#424242', 9 | accent: '#82B1FF', 10 | error: '#FF5252', 11 | info: '#2196F3', 12 | success: '#4CAF50', 13 | warning: '#FFC107' 14 | }, 15 | iconfont: 'md', 16 | }); 17 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import Vuex from "vuex" 3 | 4 | import auth from "./modules/auth"; 5 | import ad from "./modules/ads"; 6 | import services from "./modules/services"; 7 | 8 | Vue.use(Vuex); 9 | 10 | export default new Vuex.Store({ 11 | modules: { 12 | auth, 13 | ad, 14 | services 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/store/modules/ads/actions.type.js: -------------------------------------------------------------------------------- 1 | export const CreateAd = "createAd"; 2 | export const RenameAd = "renameAd"; 3 | export const UpdateAdText = "updateAdText"; 4 | export const UpdateAdPrice = "updateAdPrice"; 5 | export const DeleteAdIfEmpty = "deleteAdIfEmpty"; 6 | export const UploadAdImage = "uploadAdImage"; -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/store/modules/ads/mutations.type.js: -------------------------------------------------------------------------------- 1 | export const AdCreated = "adCreated"; 2 | export const AdRenamed = "adRenamed"; 3 | export const AdTextUpdated = "adTextUpdated"; 4 | export const AdPriceUpdated = "adPriceUpdated"; 5 | export const CurrentAdCleared = "currentAdCleared"; 6 | export const AdImageAdded = "addImageAdded"; -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/store/modules/auth/actions.type.js: -------------------------------------------------------------------------------- 1 | export const Login = "login"; 2 | export const Register = "register"; 3 | export const CheckAuth = "checkAuth"; 4 | export const UpdateUser = "updateUser"; 5 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/store/modules/auth/mutations.type.js: -------------------------------------------------------------------------------- 1 | export const Authorized = "authorized"; 2 | export const UserDetailsReceived = "userDetailsReceived"; 3 | export const ErrorOccured = "errorOccured"; 4 | export const Unauthorized = "unauthorized"; 5 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/store/modules/services/actions.type.js: -------------------------------------------------------------------------------- 1 | export const FetchServices = "fetchServices"; 2 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/store/modules/services/index.js: -------------------------------------------------------------------------------- 1 | import ApiService from "../../../common/api.service"; 2 | import { 3 | FetchServices 4 | } from "./actions.type"; 5 | import { 6 | ServicesDataReceived 7 | } from "./mutations.type"; 8 | 9 | const state = { 10 | services: {} 11 | }; 12 | 13 | const getters = { 14 | availableServices: state => state.services 15 | }; 16 | 17 | const actions = { 18 | async [FetchServices](context) { 19 | let services = await ApiService.get("/services"); 20 | context.commit(ServicesDataReceived, services.data); 21 | } 22 | }; 23 | 24 | const mutations = { 25 | [ServicesDataReceived](state, services) { 26 | state.services = services; 27 | } 28 | }; 29 | 30 | export default { 31 | state, 32 | getters, 33 | actions, 34 | mutations, 35 | namespaced: true 36 | } 37 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/store/modules/services/mutations.type.js: -------------------------------------------------------------------------------- 1 | export const ServicesDataReceived = "servicesDataReceived"; 2 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/ClientApp/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace 4 | { 5 | public static class Exceptions 6 | { 7 | public class DuplicatedEntityIdException : Exception 8 | { 9 | public DuplicatedEntityIdException(string message) 10 | : base(message) { } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/Infrastructure/RavenDb/Configuration.cs: -------------------------------------------------------------------------------- 1 | using Raven.Client.Documents; 2 | using Raven.Client.ServerWide; 3 | using Raven.Client.ServerWide.Operations; 4 | 5 | namespace Marketplace.Infrastructure.RavenDb 6 | { 7 | public static class Configuration 8 | { 9 | public static IDocumentStore ConfigureRavenDb( 10 | string serverUrl 11 | ) 12 | { 13 | var store = new DocumentStore 14 | { 15 | Urls = new[] {serverUrl} 16 | }; 17 | store.Initialize(); 18 | 19 | return store; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/Modules/Images/ImageQueryService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Marketplace.Modules.Images 5 | { 6 | public class ImageQueryService 7 | { 8 | readonly Func> _getFile; 9 | 10 | public ImageQueryService(Func> getFile) 11 | => _getFile = getFile; 12 | 13 | public Task GetFile(string fileName) => _getFile(fileName); 14 | } 15 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/Modules/Images/PictureApi.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Marketplace.Modules.Images 5 | { 6 | [ApiController, Route("image")] 7 | public class PictureApi : ControllerBase 8 | { 9 | readonly ImageQueryService _queryService; 10 | 11 | public PictureApi(ImageQueryService queryService) 12 | => _queryService = queryService; 13 | 14 | [HttpGet] 15 | public Task GetFile(string file) => _queryService.GetFile(file); 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter13/src/Marketplace/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:56566", 7 | "sslPort": 44385 8 | } 9 | }, 10 | "profiles": { 11 | "Marketplace": { 12 | "commandName": "Project", 13 | "launchBrowser": true, 14 | "applicationUrl": "http://localhost:5000", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter13/tests/Marketplace.Ads.Tests/Marketplace.Ads.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | latest 5 | false 6 | Marketplace.ClassifiedAds.Tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Marketplace.Domain/ClassifiedAdId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class ClassifiedAdId 6 | { 7 | private readonly Guid _value; 8 | 9 | public ClassifiedAdId(Guid value) 10 | { 11 | if (value == default) 12 | throw new ArgumentNullException(nameof(value), "Classified Ad id cannot be empty"); 13 | 14 | _value = value; 15 | } 16 | 17 | public static implicit operator Guid(ClassifiedAdId self) => self._value; 18 | } 19 | } -------------------------------------------------------------------------------- /Marketplace.Domain/ClassifiedAdText.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class ClassifiedAdText : Value 6 | { 7 | public string Value { get; } 8 | 9 | internal ClassifiedAdText(string text) => Value = text; 10 | 11 | public static ClassifiedAdText FromString(string text) => 12 | new ClassifiedAdText(text); 13 | 14 | public static implicit operator string(ClassifiedAdText text) => 15 | text.Value; 16 | } 17 | } -------------------------------------------------------------------------------- /Marketplace.Domain/ICurrencyLookup.cs: -------------------------------------------------------------------------------- 1 | using Marketplace.Framework; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public interface ICurrencyLookup 6 | { 7 | CurrencyDetails FindCurrency(string currencyCode); 8 | } 9 | 10 | public class CurrencyDetails : Value 11 | { 12 | public string CurrencyCode { get; set; } 13 | public bool InUse { get; set; } 14 | public int DecimalPlaces { get; set; } 15 | 16 | public static CurrencyDetails None = new CurrencyDetails {InUse = false}; 17 | } 18 | } -------------------------------------------------------------------------------- /Marketplace.Domain/InvalidEntityStateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class InvalidEntityStateException : Exception 6 | { 7 | public InvalidEntityStateException(object entity, string message) 8 | : base($"Entity {entity.GetType().Name} state change rejected, {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Marketplace.Domain/Marketplace.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 7.1 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Marketplace.Domain/Price.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class Price : Money 6 | { 7 | private Price(decimal amount, string currencyCode, ICurrencyLookup currencyLookup) 8 | : base(amount, currencyCode, currencyLookup) 9 | { 10 | if (amount < 0) 11 | throw new ArgumentException( 12 | "Price cannot be negative", 13 | nameof(amount)); 14 | } 15 | 16 | internal Price(decimal amount, string currencyCode) 17 | : base(amount, new CurrencyDetails{CurrencyCode = currencyCode}) 18 | { 19 | } 20 | 21 | public static Price FromDecimal(decimal amount, string currency, 22 | ICurrencyLookup currencyLookup) => 23 | new Price(amount, currency, currencyLookup); 24 | } 25 | } -------------------------------------------------------------------------------- /Marketplace.Domain/UserId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Domain 4 | { 5 | public class UserId 6 | { 7 | private readonly Guid _value; 8 | 9 | public UserId(Guid value) 10 | { 11 | if (value == default) 12 | throw new ArgumentNullException(nameof(value), "User id cannot be empty"); 13 | 14 | _value = value; 15 | } 16 | 17 | public static implicit operator Guid(UserId self) => self._value; 18 | } 19 | } -------------------------------------------------------------------------------- /Marketplace.Framework/Entity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Marketplace.Framework 5 | { 6 | public abstract class Entity 7 | { 8 | private readonly List _events; 9 | 10 | protected Entity() => _events = new List(); 11 | 12 | protected void Apply(object @event) 13 | { 14 | When(@event); 15 | EnsureValidState(); 16 | _events.Add(@event); 17 | } 18 | 19 | protected abstract void When(object @event); 20 | 21 | public IEnumerable GetChanges() => _events.AsEnumerable(); 22 | 23 | public void ClearChanges() => _events.Clear(); 24 | 25 | protected abstract void EnsureValidState(); 26 | } 27 | } -------------------------------------------------------------------------------- /Marketplace.Framework/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Framework 4 | { 5 | public class InvalidValueException : Exception 6 | { 7 | public InvalidValueException(Type type, string message) 8 | : base($"Value of {type.Name} {message}") 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Marketplace.Framework/Marketplace.Framework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | -------------------------------------------------------------------------------- /Marketplace.Framework/ValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Framework 2 | { 3 | public static class ValueExtensions 4 | { 5 | public static void MustNotBeNull(this Value value) where T : Value 6 | { 7 | if (value == null) 8 | throw new InvalidValueException(typeof(T), "cannot be null"); 9 | } 10 | 11 | public static void MustBe(this Value value) where T : Value 12 | { 13 | if (value == null) 14 | throw new InvalidValueException(typeof(T), "cannot be null"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Marketplace.Tests/Marketplace.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.1 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Marketplace/Api/ClassifiedAdsApplicationService.cs: -------------------------------------------------------------------------------- 1 | namespace Marketplace.Api 2 | { 3 | public class ClassifiedAdsApplicationService 4 | { 5 | public Task Handle(Contracts.ClassifiedAds.V1.Create command) 6 | { 7 | // we need to create a new Classified Ad here 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /Marketplace/Api/ClassifiedAdsCommandsApi.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Newtonsoft.Json; 5 | 6 | namespace Marketplace.Api 7 | { 8 | [Route("/ad")] 9 | public class ClassifiedAdsCommandsApi : Controller 10 | { 11 | private readonly ClassifiedAdsApplicationService _applicationService; 12 | 13 | public ClassifiedAdsCommandsApi( 14 | ClassifiedAdsApplicationService applicationService) 15 | => _applicationService = applicationService; 16 | 17 | [HttpPost] 18 | public async Task Post( 19 | Contracts.ClassifiedAds.V1.Create request) 20 | { 21 | await _applicationService.Handle(request); 22 | 23 | Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)); 24 | 25 | return Ok(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Marketplace/Contracts/ClassifiedAds.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Marketplace.Contracts 4 | { 5 | public static class ClassifiedAds 6 | { 7 | public static class V1 8 | { 9 | /// 10 | /// Create a new ad command 11 | /// 12 | public class Create 13 | { 14 | /// 15 | /// New ad id 16 | /// 17 | public Guid Id { get; set; } 18 | 19 | /// 20 | /// Ad owner id 21 | /// 22 | public Guid OwnerId { get; set; } 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Marketplace/Marketplace.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.1 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | true 16 | $(NoWarn);1591 17 | 18 | --------------------------------------------------------------------------------