Repository: CanerPatir/aspnet-core-clean-arch
Branch: master
Commit: d985326b38dd
Files: 89
Total size: 113.0 KB
Directory structure:
gitextract_22c5k2bq/
├── .gitignore
├── Directory.build.props
├── LICENSE
├── README.md
├── aspnet-core-clean-arch.sln
├── src/
│ ├── Application/
│ │ ├── Application.csproj
│ │ ├── ICommandHandler.cs
│ │ ├── IEventHandler.cs
│ │ ├── IQueryHandler.cs
│ │ └── UseCases/
│ │ ├── AddContentToProduct/
│ │ │ ├── AddContentToProductCommand.cs
│ │ │ └── AddContentToProductCommandHandler.cs
│ │ └── CreateProduct/
│ │ ├── CreateProductCommand.cs
│ │ └── CreateProductCommandHandler.cs
│ ├── Domain/
│ │ ├── Abstraction/
│ │ │ ├── AggregateRoot.cs
│ │ │ ├── BaseDomainEvent.cs
│ │ │ ├── Entity.cs
│ │ │ ├── IRepository.cs
│ │ │ ├── InstanceEventRouter.cs
│ │ │ └── ValueObject.cs
│ │ ├── BusinessException.cs
│ │ ├── Domain.csproj
│ │ └── ProductContext/
│ │ ├── AttributeRef.cs
│ │ ├── BrandRef.cs
│ │ ├── CategoryRef.cs
│ │ ├── Content.cs
│ │ ├── Events/
│ │ │ ├── AttributeAddedToProduct.cs
│ │ │ ├── BaseProductEvent.cs
│ │ │ ├── ContentAddedToProduct.cs
│ │ │ ├── ImageAddedToProduct.cs
│ │ │ ├── ProductApproved.cs
│ │ │ ├── ProductCreated.cs
│ │ │ └── VariantAddedToProduct.cs
│ │ ├── IProductRepository.cs
│ │ ├── ImageRef.cs
│ │ ├── Product.cs
│ │ └── Variant.cs
│ ├── Infrastructure/
│ │ ├── Infrastructure.csproj
│ │ ├── Logging/
│ │ │ └── WebHostExtensions.cs
│ │ ├── Messaging/
│ │ │ └── Mediator/
│ │ │ ├── IMediator.cs
│ │ │ ├── Mediator.cs
│ │ │ └── ServiceCollectionExtensions.cs
│ │ ├── Persistence/
│ │ │ ├── AggregateNotFoundException.cs
│ │ │ ├── ConcurrencyException.cs
│ │ │ ├── EventStoreProductRepository.cs
│ │ │ ├── IEventStore.cs
│ │ │ ├── InMemory/
│ │ │ │ ├── EventStore/
│ │ │ │ │ └── InMemoryEventStore.cs
│ │ │ │ └── ServiceCollectionExtensions.cs
│ │ │ └── Mongo/
│ │ │ ├── EventStore/
│ │ │ │ └── MongoDbEventStore.cs
│ │ │ ├── IMongoDbContext.cs
│ │ │ ├── MongoDbContext.cs
│ │ │ ├── MongoDbProductRepository.cs
│ │ │ ├── MongoDbProvider.cs
│ │ │ ├── MongoDbRepository.cs
│ │ │ ├── MongoSettings.cs
│ │ │ └── ServiceCollectionExtensions.cs
│ │ └── ServiceCollectionExtensions.cs
│ └── WebApi/
│ ├── Extensions/
│ │ ├── ApplicationBuilderExtensions.cs
│ │ ├── OriginalNameCamelCaseContractResolver.cs
│ │ └── ServiceCollectionExtensions.cs
│ ├── Program.cs
│ ├── Properties/
│ │ └── launchSettings.json
│ ├── Startup.cs
│ ├── UseCases/
│ │ ├── AddContentToProduct/
│ │ │ ├── AddContentToProductCommandValidator.cs
│ │ │ └── ProductController.cs
│ │ └── CreateProduct/
│ │ ├── CreateProductCommandValidator.cs
│ │ └── ProductsController.cs
│ ├── WebApi.csproj
│ ├── app.config
│ ├── appsettings.Development.json
│ ├── appsettings.Production.json
│ ├── appsettings.Staging.json
│ └── appsettings.json
└── test/
├── Application.Tests/
│ ├── Application.Tests.csproj
│ ├── CommandHandlerTestBase.cs
│ └── CreateProductCommandHandlerTests.cs
├── Domain.Specs/
│ ├── Domain.Specs.csproj
│ └── ProductSpecs/
│ ├── WhenContentAddedToProduct.cs
│ ├── WhenCreated.cs
│ ├── WhenProductApproved.cs
│ └── WhenVariantAddedToProduct.cs
├── Infrastructure.Tests/
│ ├── Infrastructure.Tests.csproj
│ └── MediatorTests.cs
└── TestBase/
├── InMemoryDatabaseFixture.cs
├── ScenarioFor.cs
├── ScenarioForExisting.cs
├── SpecBase.cs
├── TestBase.csproj
├── TestBaseWithInMemoryDb.cs
└── TestBaseWithIoC.cs
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
Common IntelliJ Platform excludes
# User specific
.idea/
**/.idea/**/workspace.xml
**/.idea/**/tasks.xml
**/.idea/shelf/*
**/.idea/dictionaries
# Sensitive or high-churn files
**/.idea/**/dataSources/
**/.idea/**/dataSources.ids
**/.idea/**/dataSources.xml
**/.idea/**/dataSources.local.xml
**/.idea/**/sqlDataSources.xml
**/.idea/**/dynamic.xml
# Rider
# Rider auto-generates .iml files, and contentModel.xml
**/.idea/**/*.iml
**/.idea/**/contentModel.xml
**/.idea/**/modules.xml
*.suo
*.user
.vs/
[Bb]in/
[Oo]bj/
_UpgradeReport_Files/
[Pp]ackages/
Thumbs.db
Desktop.ini
.DS_Store
================================================
FILE: Directory.build.props
================================================
<Project>
<PropertyGroup>
<Version>0.0.1</Version>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
## Experimental Product Domain Based On Hexagonal Architecture Principles
This project is a sample application built using .NET Core. The main goal of this project is implementing and better understanding DDD and hexagonal architecture principles.
# Also known as
* Ports and Adapters
* Clean Architecture
* Onion Architecture
# Hexagonal architecture

With hexagonal architecture
* Domain layer contains enterprise wide logic and types and does not depend anything except these
* Application layer contains business logic and types
* Infrastructure layer (including persistence, messaging, logging, presentation) contains all external concerns
* Presentation and Infrastructure layers depend only on Application
* Infrastructure dependencies can be replaced
with minimal effort. For instance, we can switch data store without touching business code.
## Getting Started
Use these instructions to get the project up and running.
### Prerequisites
You will need the following tools:
* [Visual Studio Code or 2019](https://www.visualstudio.com/downloads/) or [JetBrains Rider](https://www.jetbrains.com/rider/download)
* [.NET Core SDK 3.0](https://www.microsoft.com/net/download/dotnet-core/3.0)
### Setup
Follow these steps to get your development environment set up:
1. Clone the repository
2. At the root directory, restore dependencies
```
dotnet restore
```
3. Build the solution
```
dotnet build
```
5. Run tests
```
dotnet test
```
## Technologies
* .NET Core 3.0
* ASP.NET Core 3.0
## License
This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/CanerPatir/aspnet-core-clean-arch/blob/master/LICENSE) file for details.
## References
* https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/
* https://blog.ploeh.dk/2013/12/03/layers-onions-ports-adapters-its-all-the-same/
================================================
FILE: aspnet-core-clean-arch.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{447CDAA9-FE29-40B2-AD2E-5B2AC8284965}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3E7CC539-9831-4E29-A871-B5A817584CCC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "src\Domain\Domain.csproj", "{32905C1D-DCE3-439F-B9A9-CB5765934008}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{8DD844E7-73F5-4EED-A37A-D431303D0FD7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi", "src\WebApi\WebApi.csproj", "{A6F7C54C-494B-44C7-86F0-D73FBC2DBF2B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".solutionItems", ".solutionItems", "{EE69A4C6-E7A9-4B6D-9B2F-CAFB23BDF6DF}"
ProjectSection(SolutionItems) = preProject
Directory.build.props = Directory.build.props
README.md = README.md
LICENSE = LICENSE
hexagonal.png = hexagonal.png
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestBase", "test\TestBase\TestBase.csproj", "{87282F82-F2FE-497E-BC35-2ACEEF151153}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "src\Application\Application.csproj", "{B31F4D82-5454-4E30-9FD6-F520E144FEAC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain.Specs", "test\Domain.Specs\Domain.Specs.csproj", "{CCEC4F14-D4A7-42A6-839D-A3676AED138F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.Tests", "test\Application.Tests\Application.Tests.csproj", "{70A1D656-D4C6-40D8-9622-CB18313DACDA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.Tests", "test\Infrastructure.Tests\Infrastructure.Tests.csproj", "{8C6E9314-A891-42F8-A516-180EE0132175}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{32905C1D-DCE3-439F-B9A9-CB5765934008}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{32905C1D-DCE3-439F-B9A9-CB5765934008}.Debug|Any CPU.Build.0 = Debug|Any CPU
{32905C1D-DCE3-439F-B9A9-CB5765934008}.Release|Any CPU.ActiveCfg = Release|Any CPU
{32905C1D-DCE3-439F-B9A9-CB5765934008}.Release|Any CPU.Build.0 = Release|Any CPU
{8DD844E7-73F5-4EED-A37A-D431303D0FD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8DD844E7-73F5-4EED-A37A-D431303D0FD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DD844E7-73F5-4EED-A37A-D431303D0FD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DD844E7-73F5-4EED-A37A-D431303D0FD7}.Release|Any CPU.Build.0 = Release|Any CPU
{A6F7C54C-494B-44C7-86F0-D73FBC2DBF2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A6F7C54C-494B-44C7-86F0-D73FBC2DBF2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A6F7C54C-494B-44C7-86F0-D73FBC2DBF2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A6F7C54C-494B-44C7-86F0-D73FBC2DBF2B}.Release|Any CPU.Build.0 = Release|Any CPU
{87282F82-F2FE-497E-BC35-2ACEEF151153}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87282F82-F2FE-497E-BC35-2ACEEF151153}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87282F82-F2FE-497E-BC35-2ACEEF151153}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87282F82-F2FE-497E-BC35-2ACEEF151153}.Release|Any CPU.Build.0 = Release|Any CPU
{B31F4D82-5454-4E30-9FD6-F520E144FEAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B31F4D82-5454-4E30-9FD6-F520E144FEAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B31F4D82-5454-4E30-9FD6-F520E144FEAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B31F4D82-5454-4E30-9FD6-F520E144FEAC}.Release|Any CPU.Build.0 = Release|Any CPU
{CCEC4F14-D4A7-42A6-839D-A3676AED138F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CCEC4F14-D4A7-42A6-839D-A3676AED138F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CCEC4F14-D4A7-42A6-839D-A3676AED138F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CCEC4F14-D4A7-42A6-839D-A3676AED138F}.Release|Any CPU.Build.0 = Release|Any CPU
{70A1D656-D4C6-40D8-9622-CB18313DACDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{70A1D656-D4C6-40D8-9622-CB18313DACDA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70A1D656-D4C6-40D8-9622-CB18313DACDA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70A1D656-D4C6-40D8-9622-CB18313DACDA}.Release|Any CPU.Build.0 = Release|Any CPU
{8C6E9314-A891-42F8-A516-180EE0132175}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8C6E9314-A891-42F8-A516-180EE0132175}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C6E9314-A891-42F8-A516-180EE0132175}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C6E9314-A891-42F8-A516-180EE0132175}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{32905C1D-DCE3-439F-B9A9-CB5765934008} = {447CDAA9-FE29-40B2-AD2E-5B2AC8284965}
{8DD844E7-73F5-4EED-A37A-D431303D0FD7} = {447CDAA9-FE29-40B2-AD2E-5B2AC8284965}
{A6F7C54C-494B-44C7-86F0-D73FBC2DBF2B} = {447CDAA9-FE29-40B2-AD2E-5B2AC8284965}
{87282F82-F2FE-497E-BC35-2ACEEF151153} = {3E7CC539-9831-4E29-A871-B5A817584CCC}
{B31F4D82-5454-4E30-9FD6-F520E144FEAC} = {447CDAA9-FE29-40B2-AD2E-5B2AC8284965}
{CCEC4F14-D4A7-42A6-839D-A3676AED138F} = {3E7CC539-9831-4E29-A871-B5A817584CCC}
{70A1D656-D4C6-40D8-9622-CB18313DACDA} = {3E7CC539-9831-4E29-A871-B5A817584CCC}
{8C6E9314-A891-42F8-A516-180EE0132175} = {3E7CC539-9831-4E29-A871-B5A817584CCC}
EndGlobalSection
EndGlobal
================================================
FILE: src/Application/Application.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="8.5.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.0.0" />
</ItemGroup>
</Project>
================================================
FILE: src/Application/ICommandHandler.cs
================================================
using System.Threading;
using System.Threading.Tasks;
namespace Application
{
public interface ICommandHandler<in TMessage>
{
Task Handle(TMessage message, CancellationToken cancellationToken);
}
}
================================================
FILE: src/Application/IEventHandler.cs
================================================
using System.Threading;
using System.Threading.Tasks;
namespace Application
{
public interface IEventHandler <in TEvent>
{
Task Handle(TEvent @event, CancellationToken cancellationToken);
}
}
================================================
FILE: src/Application/IQueryHandler.cs
================================================
using System.Threading;
using System.Threading.Tasks;
namespace Application
{
public interface IQueryHandler<in TMessage, TResult>
{
Task<TResult> Handle(TMessage message, CancellationToken cancellationToken);
}
}
================================================
FILE: src/Application/UseCases/AddContentToProduct/AddContentToProductCommand.cs
================================================
using System;
namespace Application.UseCases.AddContentToProduct
{
public class AddContentToProductCommand
{
public AddContentToProductCommand(Guid productId, string title, string description,
int attributeId, int attributeValueId)
{
ProductId = productId;
Title = title;
Description = description;
AttributeId = attributeId;
AttributeValueId = attributeValueId;
}
public Guid ProductId { get; }
public string Title { get; }
public string Description { get; }
public int AttributeId { get; }
public int AttributeValueId { get; }
}
}
================================================
FILE: src/Application/UseCases/AddContentToProduct/AddContentToProductCommandHandler.cs
================================================
using System.Threading;
using System.Threading.Tasks;
using Domain.ProductContext;
namespace Application.UseCases.AddContentToProduct
{
public class AddContentToProductCommandHandler : ICommandHandler<AddContentToProductCommand>
{
private readonly IProductRepository _productRepository;
public AddContentToProductCommandHandler(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task Handle(AddContentToProductCommand command, CancellationToken cancellationToken)
{
var aggregate = await _productRepository.Load(command.ProductId, cancellationToken);
aggregate.AddContent(command.Title, command.Description,
new AttributeRef(command.AttributeId, command.AttributeValueId));
await _productRepository.Save(aggregate, cancellationToken);
}
}
}
================================================
FILE: src/Application/UseCases/CreateProduct/CreateProductCommand.cs
================================================
namespace Application.UseCases.CreateProduct
{
public class CreateProductCommand
{
public CreateProductCommand(int categoryId, int brandId, string productCode)
{
CategoryId = categoryId;
BrandId = brandId;
ProductCode = productCode;
}
public int CategoryId { get; }
public int BrandId { get; }
public string ProductCode { get; }
}
}
================================================
FILE: src/Application/UseCases/CreateProduct/CreateProductCommandHandler.cs
================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using Domain.ProductContext;
namespace Application.UseCases.CreateProduct
{
public class CreateProductCommandHandler : ICommandHandler<CreateProductCommand>
{
private readonly IProductRepository _productRepository;
public CreateProductCommandHandler(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task Handle(CreateProductCommand command, CancellationToken cancellationToken)
{
var newProduct = Product.Create(Guid.NewGuid(), command.CategoryId, command.BrandId, command.ProductCode);
await _productRepository.Save(newProduct, cancellationToken);
}
}
}
================================================
FILE: src/Domain/Abstraction/AggregateRoot.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Domain
{
public interface IAggregateRoot
{
int Version { get; }
bool HasChanges();
IEnumerable<object> GetUncommittedChanges();
void MarkChangesAsCommitted();
void LoadsFromHistory(IEnumerable<object> history);
}
public abstract class AggregateRoot<TKey> : IAggregateRoot
{
private readonly List<object> _changes = new List<object>();
private readonly InstanceEventRouter _router;
public AggregateRoot()
{
_router = new InstanceEventRouter();
}
public virtual TKey Id { get; protected set; }
public virtual int Version { get; protected set; }
public bool HasChanges() => _changes.Any();
public IEnumerable<object> GetUncommittedChanges()
{
return _changes;
}
public void MarkChangesAsCommitted()
{
_changes.Clear();
}
public void LoadsFromHistory(IEnumerable<object> history)
{
foreach (var e in history) ApplyChange(e, false);
}
protected void Register<TEvent>(Action<TEvent> handler)
{
if (handler == null) throw new ArgumentNullException(nameof(handler));
_router.ConfigureRoute(handler);
}
protected void ApplyChange(object @event)
{
ApplyChange(@event, true);
}
protected void ApplyChange(object @event, bool isNew)
{
if (@event == null) throw new ArgumentNullException(nameof(@event));
_router.Route(@event);
if (isNew)
{
_changes.Add(@event);
Version++;
}
}
#region overrides
/// <inheritdoc />
/// <summary>
/// Checks if this entity is transient (it has not an Id).
/// </summary>
/// <returns>True, if this entity is transient</returns>
public virtual bool IsTransient()
{
if (EqualityComparer<TKey>.Default.Equals(Id, default))
{
return true;
}
//Workaround for EF Core since it sets int/long to min value when attaching to dbcontext
if (typeof(TKey) == typeof(int))
{
return Convert.ToInt32(Id) <= 0;
}
if (typeof(TKey) == typeof(long))
{
return Convert.ToInt64(Id) <= 0;
}
return false;
}
public override bool Equals(object obj)
{
if (!(obj is AggregateRoot<TKey>))
{
return false;
}
//Same instances must be considered as equal
if (ReferenceEquals(this, obj))
{
return true;
}
//Transient objects are not considered as equal
var other = (AggregateRoot<TKey>) obj;
if (IsTransient() && other.IsTransient())
{
return false;
}
//Must have a IS-A relation of types or must be same type
var typeOfThis = GetType();
var typeOfOther = other.GetType();
if (!typeOfThis.GetTypeInfo().IsAssignableFrom(typeOfOther) &&
!typeOfOther.GetTypeInfo().IsAssignableFrom(typeOfThis))
{
return false;
}
return Id.Equals(other.Id);
}
public override int GetHashCode() => Id.GetHashCode();
public static bool operator ==(AggregateRoot<TKey> left, AggregateRoot<TKey> right)
{
if (Equals(left, null))
{
return Equals(right, null);
}
return left.Equals(right);
}
public static bool operator !=(AggregateRoot<TKey> left, AggregateRoot<TKey> right) =>
!(left == right);
#endregion
#region rule helpers
protected void Should(Func<bool> predicate)
{
Should(predicate());
}
protected void Should(Func<bool> predicate, string otherwiseMessage)
{
Should(predicate(), otherwiseMessage);
}
protected void Should(bool clause)
{
if (clause) return;
throw new BusinessException();
}
protected void Should(bool clause, string otherwiseMessage)
{
if (clause) return;
throw new BusinessException(otherwiseMessage);
}
#endregion
}
}
================================================
FILE: src/Domain/Abstraction/BaseDomainEvent.cs
================================================
namespace Domain.Abstraction
{
public class BaseDomainEvent
{
public int Version { get; set; }
}
}
================================================
FILE: src/Domain/Abstraction/Entity.cs
================================================
using System;
namespace Domain
{
public abstract class Entity
{
private readonly InstanceEventRouter _router;
protected Entity() => _router = new InstanceEventRouter();
protected void Register<TEvent>(Action<TEvent> handler)
{
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
_router.ConfigureRoute(handler);
}
public virtual void Route(object @event)
{
if (@event == null)
{
throw new ArgumentNullException(nameof(@event));
}
_router.Route(@event);
}
}
}
================================================
FILE: src/Domain/Abstraction/IRepository.cs
================================================
using System.Threading;
using System.Threading.Tasks;
namespace Domain.Abstraction
{
/// <summary>
/// Provides an abstraction for object sotrage and
/// </summary>
/// <typeparam name="T">Type of aggregate root</typeparam>
/// <typeparam name="TKey">Type of aggregate root's key</typeparam>
public interface IRepository<T, TKey> where T : AggregateRoot<TKey>
{
Task Save(T aggregate, CancellationToken cancellationToken);
Task<T> Load(TKey id, CancellationToken cancellationToken);
}
}
================================================
FILE: src/Domain/Abstraction/InstanceEventRouter.cs
================================================
using System;
using System.Collections.Generic;
namespace Domain
{
/// <summary>
/// Routes an event to a configured state handler.
/// </summary>
internal class InstanceEventRouter
{
private readonly Dictionary<Type, Action<object>> _handlers;
/// <summary>
/// Initializes a new instance of the <see cref="InstanceEventRouter" /> class.
/// </summary>
public InstanceEventRouter() => _handlers = new Dictionary<Type, Action<object>>();
/// <summary>
/// Adds a route for the specified event type to the specified state handler.
/// </summary>
/// <param name="event">The event type the route is for.</param>
/// <param name="handler">The state handler that should be invoked when an event of the specified type is routed.</param>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="event" /> or <paramref name="handler" /> is
/// <c>null</c>.
/// </exception>
public void ConfigureRoute(Type @event, Action<object> handler)
{
if (@event == null)
{
throw new ArgumentNullException(nameof(@event));
}
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
_handlers.Add(@event, handler);
}
/// <summary>
/// Adds a route for the specified event type to the specified state handler.
/// </summary>
/// <typeparam name="TEvent">The event type the route is for.</typeparam>
/// <param name="handler">The state handler that should be invoked when an event of the specified type is routed.</param>
/// <exception cref="T:System.ArgumentNullException">Thrown when <paramref name="handler" /> is <c>null</c>.</exception>
public void ConfigureRoute<TEvent>(Action<TEvent> handler)
{
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
_handlers.Add(typeof(TEvent), @event => handler((TEvent) @event));
}
/// <summary>
/// Routes the specified <paramref name="event" /> to a configured state handler, if any.
/// </summary>
/// <param name="event">The event to route.</param>
/// <exception cref="T:System.ArgumentNullException">Thrown when the <paramref name="event" /> is null.</exception>
public void Route(object @event)
{
if (@event == null)
{
throw new ArgumentNullException(nameof(@event));
}
if (_handlers.TryGetValue(@event.GetType(), out var handler))
{
handler(@event);
}
}
}
}
================================================
FILE: src/Domain/Abstraction/ValueObject.cs
================================================
using System;
using System.Linq;
namespace Domain
{
//Inspired from https://blogs.msdn.microsoft.com/cesardelatorre/2011/06/06/implementing-a-value-object-base-class-supertype-patternddd-patterns-related/
/// <summary>
/// Base class for value objects.
/// </summary>
/// <typeparam name="TValueObject">The type of the value object.</typeparam>
public abstract class ValueObject<TValueObject> : IEquatable<TValueObject>
where TValueObject : ValueObject<TValueObject>
{
public bool Equals(TValueObject other)
{
if ((object) other == null)
{
return false;
}
var publicProperties = GetType().GetProperties();
if (!publicProperties.Any())
{
return true;
}
return publicProperties.All(property =>
Equals(property.GetValue(this, null), property.GetValue(other, null)));
}
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
var item = obj as ValueObject<TValueObject>;
return (object) item != null && Equals((TValueObject) item);
}
public override int GetHashCode()
{
const int index = 1;
const int initialHasCode = 31;
var publicProperties = GetType().GetProperties();
if (!publicProperties.Any())
{
return initialHasCode;
}
var hashCode = initialHasCode;
var changeMultiplier = false;
foreach (var property in publicProperties)
{
var value = property.GetValue(this, null);
if (value == null)
{
//support {"a",null,null,"a"} != {null,"a","a",null}
hashCode = hashCode ^ (index * 13);
continue;
}
hashCode = hashCode * (changeMultiplier ? 59 : 114) + value.GetHashCode();
changeMultiplier = !changeMultiplier;
}
return hashCode;
}
public static bool operator ==(ValueObject<TValueObject> x, ValueObject<TValueObject> y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if ((object) x == null || (object) y == null)
{
return false;
}
return x.Equals(y);
}
public static bool operator !=(ValueObject<TValueObject> x, ValueObject<TValueObject> y) => !(x == y);
}
}
================================================
FILE: src/Domain/BusinessException.cs
================================================
using System;
namespace Domain
{
public class BusinessException : Exception
{
public BusinessException()
{
}
public BusinessException(string message) : base(message)
{
}
public BusinessException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
================================================
FILE: src/Domain/Domain.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
</Project>
================================================
FILE: src/Domain/ProductContext/AttributeRef.cs
================================================
namespace Domain.ProductContext
{
public class AttributeRef : ValueObject<AttributeRef>
{
public AttributeRef(int attributeId, int attributeValueId)
{
AttributeId = attributeId;
AttributeValueId = attributeValueId;
}
/// <summary>
/// Represents attribute for example: color, size, material etc.
/// </summary>
public int AttributeId { get; }
/// <summary>
/// Represents value of attribute for example: Red, Small, Cotton etc.
/// </summary>
public int AttributeValueId { get; }
}
}
================================================
FILE: src/Domain/ProductContext/BrandRef.cs
================================================
namespace Domain.ProductContext
{
public class BrandRef : ValueObject<BrandRef>
{
public BrandRef(int id, string name)
{
Id = id;
Name = name;
}
public int Id { get; }
public string Name { get; }
}
}
================================================
FILE: src/Domain/ProductContext/CategoryRef.cs
================================================
namespace Domain.ProductContext
{
public class CategoryRef : ValueObject<CategoryRef>
{
public CategoryRef(int id, string name)
{
Id = id;
Name = name;
}
public int Id { get; }
public string Name { get; }
}
}
================================================
FILE: src/Domain/ProductContext/Content.cs
================================================
using System.Collections.Generic;
using System.Linq;
using Domain.ProductContext.Events;
namespace Domain.ProductContext
{
/// <summary>
/// Represents group of product variant and can be projection of product detail page.
/// </summary>
public class Content : Entity
{
private ICollection<Variant> _variants = new HashSet<Variant>();
internal Content(string title, string description, AttributeRef slicerAttribute)
{
Title = title;
Description = description;
SlicerAttribute = slicerAttribute;
Register<VariantAddedToProduct>(Apply);
}
private void Apply(VariantAddedToProduct @event)
{
_variants.Add(new Variant(@event.Barcode, @event.VarianterAttribute));
}
public IReadOnlyCollection<Variant> Variants => _variants.ToList();
public string Title { get; private set; }
public string Description { get; private set; }
public AttributeRef SlicerAttribute { get; private set; }
public bool HasSameTypeSlicerAttribute(AttributeRef attribute)
{
return SlicerAttribute.AttributeId == attribute.AttributeId;
}
}
}
================================================
FILE: src/Domain/ProductContext/Events/AttributeAddedToProduct.cs
================================================
using System;
namespace Domain.ProductContext.Events
{
public class AttributeAddedToProduct : BaseProductEvent
{
public AttributeAddedToProduct(Guid productId, AttributeRef attribute)
{
ProductId = productId;
Attribute = attribute;
}
public override Guid ProductId { get; }
public AttributeRef Attribute { get; }
}
}
================================================
FILE: src/Domain/ProductContext/Events/BaseProductEvent.cs
================================================
using System;
using Domain.Abstraction;
namespace Domain.ProductContext.Events
{
public abstract class BaseProductEvent : BaseDomainEvent
{
public abstract Guid ProductId { get; }
}
}
================================================
FILE: src/Domain/ProductContext/Events/ContentAddedToProduct.cs
================================================
using System;
namespace Domain.ProductContext.Events
{
public class ContentAddedToProduct : BaseProductEvent
{
public ContentAddedToProduct(Guid productId, string title, string description, AttributeRef slicerAttribute)
{
ProductId = productId;
Title = title;
Description = description;
SlicerAttribute = slicerAttribute;
}
public override Guid ProductId { get; }
public string Title { get; }
public string Description { get; }
public AttributeRef SlicerAttribute { get; }
}
}
================================================
FILE: src/Domain/ProductContext/Events/ImageAddedToProduct.cs
================================================
using System;
namespace Domain.ProductContext.Events
{
public class ImageAddedToProduct :BaseProductEvent
{
public ImageAddedToProduct(Guid productId, AttributeRef varianterAttr, ImageRef image)
{
ProductId = productId;
VarianterAttr = varianterAttr;
Image = image;
}
public override Guid ProductId { get; }
public AttributeRef VarianterAttr { get; }
public ImageRef Image { get; }
}
}
================================================
FILE: src/Domain/ProductContext/Events/ProductApproved.cs
================================================
using System;
namespace Domain.ProductContext.Events
{
public class ProductApproved : BaseProductEvent
{
public ProductApproved(Guid productId)
{
ProductId = productId;
}
public override Guid ProductId { get; }
}
}
================================================
FILE: src/Domain/ProductContext/Events/ProductCreated.cs
================================================
using System;
namespace Domain.ProductContext.Events
{
public class ProductCreated : BaseProductEvent
{
public ProductCreated(Guid productId, string productCode, int brandId, int categoryId)
{
ProductId = productId;
BrandId = brandId;
ProductCode = productCode;
CategoryId = categoryId;
}
public override Guid ProductId { get; }
public int BrandId { get; }
public string ProductCode { get; }
public int CategoryId { get; }
}
}
================================================
FILE: src/Domain/ProductContext/Events/VariantAddedToProduct.cs
================================================
using System;
namespace Domain.ProductContext.Events
{
public class VariantAddedToProduct: BaseProductEvent
{
public VariantAddedToProduct(Guid productId, string barcode, AttributeRef slicerAttribute, AttributeRef varianterAttribute)
{
ProductId = productId;
Barcode = barcode;
SlicerAttribute = slicerAttribute;
VarianterAttribute = varianterAttribute;
}
public override Guid ProductId { get; }
public string Barcode { get; }
public AttributeRef SlicerAttribute { get; }
public AttributeRef VarianterAttribute { get; }
}
}
================================================
FILE: src/Domain/ProductContext/IProductRepository.cs
================================================
using System;
using Domain.Abstraction;
namespace Domain.ProductContext
{
public interface IProductRepository : IRepository<Product, Guid>
{
}
}
================================================
FILE: src/Domain/ProductContext/ImageRef.cs
================================================
namespace Domain.ProductContext
{
public class ImageRef : ValueObject<ImageRef>
{
public ImageRef(string relativeUrl, string relativeThumbUrl)
{
RelativeUrl = relativeUrl;
RelativeThumbUrl = relativeThumbUrl;
}
public string RelativeUrl { get; }
public string RelativeThumbUrl { get; }
}
}
================================================
FILE: src/Domain/ProductContext/Product.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Domain.ProductContext.Events;
namespace Domain.ProductContext
{
public class Product : AggregateRoot<Guid>
{
private readonly ICollection<AttributeRef> _attributes = new HashSet<AttributeRef>();
private readonly ICollection<Content> _contents = new HashSet<Content>();
public Product()
{
Register<ProductCreated>(Apply);
Register<ContentAddedToProduct>(Apply);
Register<AttributeAddedToProduct>(Apply);
Register<VariantAddedToProduct>(Apply);
Register<ProductApproved>(Apply);
Register<ImageAddedToProduct>(Apply);
}
public static Product Create(Guid productId,
int categoryId,
int brandId,
string productCode)
{
var product = new Product();
product.ApplyChange(
new ProductCreated(productId,
productCode,
brandId, categoryId));
return product;
}
public BrandRef Brand { get; private set; }
public CategoryRef Category { get; private set; }
public string Code { get; private set; }
public IReadOnlyCollection<Content> Contents => _contents.ToList();
public IReadOnlyCollection<AttributeRef> Attributes => _attributes.ToList();
protected IReadOnlyCollection<Variant> Variants => _contents.SelectMany(c => c.Variants).ToList();
protected IReadOnlyCollection<ImageRef> Images => Variants.SelectMany(c => c.Images).ToList();
public bool IsApproved { get; private set; }
private void Apply(ProductApproved @event) => IsApproved = true;
private void Apply(ContentAddedToProduct @event) => _contents.Add(new Content(@event.Title, @event.Description, @event.SlicerAttribute));
private void Apply(AttributeAddedToProduct @event) => _attributes.Add(@event.Attribute);
private void Apply(VariantAddedToProduct @event) => _contents.First(c => c.SlicerAttribute == @event.SlicerAttribute).Route(@event);
private void Apply(ProductCreated @event)
{
Id = @event.ProductId;
Brand = new BrandRef(@event.BrandId, "");
Category = new CategoryRef(@event.CategoryId, "");
Code = @event.ProductCode;
}
private void Apply(ImageAddedToProduct @event)
{
var variant = _contents.SelectMany(c => c.Variants)
.SingleOrDefault(v => v.VarianterAttribute == @event.VarianterAttr);
// ReSharper disable once PossibleNullReferenceException
variant.Route(@event);
}
public void AddContent(string title, string description, AttributeRef slicerAttribute)
{
if (_contents.Any())
{
Should(() => _contents.Any(c => c.HasSameTypeSlicerAttribute(slicerAttribute)),
"Given attribute type should belong to any content of product as slicer");
}
Should(() => _contents.All(c => c.SlicerAttribute != slicerAttribute),
"Same content already exists with given attribute");
ApplyChange(new ContentAddedToProduct(Id, title, description, slicerAttribute));
}
public void AddVariant(string barcode, AttributeRef slicerAttribute, AttributeRef varianterAttribute)
{
var content = _contents.SingleOrDefault(c => c.SlicerAttribute == slicerAttribute);
Should(() => content != null, "No content found with given slicer attribute.");
// ReSharper disable once PossibleNullReferenceException
var variantsOfContent = content.Variants;
if (variantsOfContent.Any())
{
Should(() => variantsOfContent.All(c => c.HasSameTypeVarianterAttribute(varianterAttribute)),
"Given attribute type should belong to any variant of product as varianter");
}
Should(() => variantsOfContent.All(v => v.VarianterAttribute != varianterAttribute) ,
"Same variant already exists with given attribute");
ApplyChange(new VariantAddedToProduct(Id, barcode, slicerAttribute, varianterAttribute));
}
public void AddAttributeToContent(AttributeRef attribute)
{
Should(() => _attributes.Any(a => a != attribute),
"Given attribute had already been added to the product");
ApplyChange(new AttributeAddedToProduct(Id, attribute));
}
public void Approve()
{
Should(() => _contents.Any(), "Product must have at least one content");
Should(() => Variants.Any(), "Product must have at least one variant");
Should(() => Variants.SelectMany(v => v.Images).Any(), "Product must have at least one image");
ApplyChange(new ProductApproved(Id));
}
public void AssignImage(ImageRef image, AttributeRef varianterAttr)
{
Should(() => Variants.Any(v => v.HasSameTypeVarianterAttribute(varianterAttr)),
$"Product does not have any attribute with given attributeId: {varianterAttr.AttributeId}");
ApplyChange(new ImageAddedToProduct(Id, varianterAttr, image));
}
}
}
================================================
FILE: src/Domain/ProductContext/Variant.cs
================================================
using System.Collections.Generic;
using System.Linq;
using Domain.ProductContext.Events;
namespace Domain.ProductContext
{
public class Variant : Entity
{
private readonly ICollection<ImageRef> _images = new HashSet<ImageRef>();
internal Variant(string barcode, AttributeRef varianterAttribute)
{
Barcode = barcode;
VarianterAttribute = varianterAttribute;
Register<ImageAddedToProduct>(Apply);
}
private void Apply(ImageAddedToProduct @event)
{
_images.Add(@event.Image);
}
public IReadOnlyCollection<ImageRef> Images => _images.ToList();
public string Barcode { get; }
public AttributeRef VarianterAttribute { get; }
public bool HasSameTypeVarianterAttribute(AttributeRef attribute)
{
return VarianterAttribute.AttributeId == attribute.AttributeId;
}
}
}
================================================
FILE: src/Infrastructure/Infrastructure.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.9.2" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.10.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="4.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.ColoredConsole" Version="3.0.1" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Graylog" Version="2.0.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Application\Application.csproj" />
</ItemGroup>
</Project>
================================================
FILE: src/Infrastructure/Logging/WebHostExtensions.cs
================================================
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.Graylog;
using Serilog.Sinks.Graylog.Core.Helpers;
using Serilog.Sinks.Graylog.Core.Transport;
namespace Infrastructure.Logging
{
public static class WebHostExtensions
{
public static IWebHostBuilder ConfigureLoggingPlumbing(this IWebHostBuilder webHostBuilder)
{
return webHostBuilder.UseSerilog((context, loggerConfiguration) =>
{
var logLevel = context.Configuration.GetValue<LogEventLevel>("Serilog:MinimumLevel:Default");
loggerConfiguration
.MinimumLevel.Verbose()
.Enrich.FromLogContext()
.Enrich.WithProperty("version",
Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyFileVersionAttribute>().Version)
.Enrich.WithProperty("env", context.HostingEnvironment.EnvironmentName)
.ReadFrom.Configuration(context.Configuration)
.WriteTo.Graylog(
new GraylogSinkOptions
{
HostnameOrAddress = context.Configuration["Serilog:GraylogIp"],
Port = int.Parse(context.Configuration["Serilog:GraylogPort"]),
TransportType = TransportType.Udp,
MinimumLogEventLevel = logLevel,
Facility = context.Configuration["Serilog:FacilityName"],
MessageGeneratorType = MessageIdGeneratortype.Timestamp
});
if (context.Configuration.GetValue<bool>("Serilog:Console"))
{
loggerConfiguration.WriteTo.ColoredConsole(logLevel,
"[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}");
}
});
}
}
}
================================================
FILE: src/Infrastructure/Messaging/Mediator/IMediator.cs
================================================
using System.Threading;
using System.Threading.Tasks;
namespace Infrastructure.Messaging.Mediator
{
public interface IMediator
{
Task SendAsync<TMessage>(TMessage message, CancellationToken cancellationToken = default);
Task PublishAsync<TEvent>(TEvent message, CancellationToken cancellationToken = default);
Task<TResult> SendAsync<TMessage, TResult>(TMessage @event, CancellationToken cancellationToken = default);
}
}
================================================
FILE: src/Infrastructure/Messaging/Mediator/Mediator.cs
================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using Application;
using Microsoft.Extensions.DependencyInjection;
namespace Infrastructure.Messaging.Mediator
{
public class Mediator : IMediator
{
private readonly IServiceProvider _serviceProvider;
public Mediator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Task SendAsync<TMessage>(TMessage message, CancellationToken cancellationToken)
{
var handler = _serviceProvider.GetRequiredService<ICommandHandler<TMessage>>();
if (handler == null) throw new NullReferenceException($"handler of {nameof(TMessage)}");
return handler.Handle(message, cancellationToken);
}
public Task PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default)
{
var handler = _serviceProvider.GetRequiredService<IEventHandler<TEvent>>();
if (handler == null) throw new NullReferenceException($"handler of {nameof(TEvent)}");
return handler.Handle(@event, cancellationToken); }
public Task<TResult> SendAsync<TMessage, TResult>(TMessage message, CancellationToken cancellationToken)
{
var handler = _serviceProvider.GetRequiredService<IQueryHandler<TMessage, TResult>>();
if (handler == null)
throw new NullReferenceException($"handler of {nameof(TMessage)} which returning {nameof(TResult)}");
return handler.Handle(message, cancellationToken);
}
}
}
================================================
FILE: src/Infrastructure/Messaging/Mediator/ServiceCollectionExtensions.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Application;
using Infrastructure.Messaging.Mediator;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddInProcessMessageBus(this IServiceCollection services)
{
services.AddSingleton<IMediator, Mediator>();
var assembliesToScan = new[]
{
Assembly.GetCallingAssembly(),
Assembly.GetEntryAssembly(),
Assembly.GetExecutingAssembly()
}.Distinct().ToArray();
ConnectImplementationsToTypesClosing(typeof(ICommandHandler<>), services, assembliesToScan, false);
ConnectImplementationsToTypesClosing(typeof(IQueryHandler<,>), services, assembliesToScan, false);
var multiOpenInterfaces = new[]
{
typeof(ICommandHandler<>),
typeof(IQueryHandler<,>)
};
foreach (var multiOpenInterface in multiOpenInterfaces)
{
var concretions = assembliesToScan
.SelectMany(a => a.DefinedTypes)
.Where(type => type.FindInterfacesThatClose(multiOpenInterface).Any())
.Where(type => type.IsConcrete() && type.IsOpenGeneric())
.ToList();
foreach (var type in concretions)
{
services.AddTransient(multiOpenInterface, type);
}
}
return services;
}
/// <summary>
/// Helper method use to differentiate behavior between request handlers and notification handlers.
/// Request handlers should only be added once (so set addIfAlreadyExists to false)
/// Notification handlers should all be added (set addIfAlreadyExists to true)
/// </summary>
private static void ConnectImplementationsToTypesClosing(Type openRequestInterface,
IServiceCollection services,
IEnumerable<Assembly> assembliesToScan,
bool addIfAlreadyExists)
{
var concretions = new List<Type>();
var interfaces = new List<Type>();
foreach (var type in assembliesToScan.SelectMany(a => a.DefinedTypes).Where(t => !t.IsOpenGeneric()))
{
var interfaceTypes = type.FindInterfacesThatClose(openRequestInterface).ToArray();
if (!interfaceTypes.Any()) continue;
if (type.IsConcrete())
{
concretions.Add(type);
}
foreach (var interfaceType in interfaceTypes)
{
interfaces.Fill(interfaceType);
}
}
foreach (var @interface in interfaces)
{
var exactMatches = concretions.Where(x => x.CanBeCastTo(@interface)).ToList();
if (addIfAlreadyExists)
{
foreach (var type in exactMatches)
{
services.AddTransient(@interface, type);
}
}
else
{
if (exactMatches.Count > 1)
{
exactMatches.RemoveAll(m => !IsMatchingWithInterface(m, @interface));
}
foreach (var type in exactMatches)
{
services.TryAddTransient(@interface, type);
}
}
if (!@interface.IsOpenGeneric())
{
AddConcretionsThatCouldBeClosed(@interface, concretions, services);
}
}
}
private static bool IsMatchingWithInterface(Type handlerType, Type handlerInterface)
{
if (handlerType == null || handlerInterface == null)
{
return false;
}
if (handlerType.IsInterface)
{
if (handlerType.GenericTypeArguments.SequenceEqual(handlerInterface.GenericTypeArguments))
{
return true;
}
}
else
{
return IsMatchingWithInterface(handlerType.GetInterface(handlerInterface.Name), handlerInterface);
}
return false;
}
private static void AddConcretionsThatCouldBeClosed(Type @interface, List<Type> concretions,
IServiceCollection services)
{
foreach (var type in concretions
.Where(x => x.IsOpenGeneric() && x.CouldCloseTo(@interface)))
{
try
{
services.TryAddTransient(@interface, type.MakeGenericType(@interface.GenericTypeArguments));
}
catch (Exception)
{
// ignored
}
}
}
private static bool CouldCloseTo(this Type openConcretion, Type closedInterface)
{
var openInterface = closedInterface.GetGenericTypeDefinition();
var arguments = closedInterface.GenericTypeArguments;
var concreteArguments = openConcretion.GenericTypeArguments;
return arguments.Length == concreteArguments.Length && openConcretion.CanBeCastTo(openInterface);
}
private static bool CanBeCastTo(this Type pluggedType, Type pluginType)
{
if (pluggedType == null) return false;
if (pluggedType == pluginType) return true;
return pluginType.GetTypeInfo().IsAssignableFrom(pluggedType.GetTypeInfo());
}
private static IEnumerable<Type> FindInterfacesThatClose(this Type pluggedType, Type templateType)
{
return FindInterfacesThatClosesCore(pluggedType, templateType).Distinct();
}
private static IEnumerable<Type> FindInterfacesThatClosesCore(Type pluggedType, Type templateType)
{
if (pluggedType == null) yield break;
if (!pluggedType.IsConcrete()) yield break;
if (templateType.GetTypeInfo().IsInterface)
{
foreach (
var interfaceType in
pluggedType.GetInterfaces()
.Where(type =>
type.GetTypeInfo().IsGenericType && (type.GetGenericTypeDefinition() == templateType)))
{
yield return interfaceType;
}
}
else if (pluggedType.GetTypeInfo().BaseType.GetTypeInfo().IsGenericType &&
(pluggedType.GetTypeInfo().BaseType?.GetGenericTypeDefinition() == templateType))
{
yield return pluggedType.GetTypeInfo().BaseType;
}
if (pluggedType.GetTypeInfo().BaseType == typeof(object)) yield break;
foreach (var interfaceType in FindInterfacesThatClosesCore(pluggedType.GetTypeInfo().BaseType, templateType)
)
{
yield return interfaceType;
}
}
private static bool IsConcrete(this Type type)
{
return !type.GetTypeInfo().IsAbstract && !type.GetTypeInfo().IsInterface;
}
private static bool IsOpenGeneric(this Type type)
{
return type.GetTypeInfo().IsGenericTypeDefinition || type.GetTypeInfo().ContainsGenericParameters;
}
private static void Fill<T>(this ICollection<T> list, T value)
{
if (list.Contains(value)) return;
list.Add(value);
}
}
}
================================================
FILE: src/Infrastructure/Persistence/AggregateNotFoundException.cs
================================================
using System;
namespace Infrastructure.Persistence
{
public class AggregateNotFoundException : Exception
{
}
}
================================================
FILE: src/Infrastructure/Persistence/ConcurrencyException.cs
================================================
using System;
namespace Infrastructure.Persistence
{
public class ConcurrencyException : Exception
{
public ConcurrencyException()
{
}
public ConcurrencyException(string message) : base(message)
{
}
}
}
================================================
FILE: src/Infrastructure/Persistence/EventStoreProductRepository.cs
================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using Domain.ProductContext;
namespace Infrastructure.Persistence
{
internal class EventStoreProductRepository : IProductRepository
{
private readonly IEventStore _eventStore;
public EventStoreProductRepository(IEventStore eventStore) => _eventStore = eventStore;
public async Task Save(Product aggregate, CancellationToken cancellationToken)
{
await _eventStore.SaveEvents(aggregate.Id, aggregate.GetUncommittedChanges(), aggregate.Version);
aggregate.MarkChangesAsCommitted();
}
public async Task<Product> Load(Guid id, CancellationToken cancellationToken)
{
var aggregate = new Product();//lots of ways to do this
var e = await _eventStore.GetEventsForAggregate(id);
aggregate.LoadsFromHistory(e);
return aggregate;
}
}
}
================================================
FILE: src/Infrastructure/Persistence/IEventStore.cs
================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Infrastructure.Persistence
{
public interface IEventStore
{
Task SaveEvents(Guid aggregateId, IEnumerable<object> events, int expectedVersion);
Task<List<object>> GetEventsForAggregate(Guid aggregateId);
}
}
================================================
FILE: src/Infrastructure/Persistence/InMemory/EventStore/InMemoryEventStore.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Infrastructure.Messaging.Mediator;
namespace Infrastructure.Persistence.InMemory.EventStore
{
public class InMemoryEventStore : IEventStore
{
private readonly IMediator _publisher;
private struct EventDescriptor
{
public readonly object EventData;
public readonly Guid Id;
public readonly int Version;
public EventDescriptor(Guid id, object eventData, int version)
{
EventData = eventData;
Version = version;
Id = id;
}
}
public InMemoryEventStore(IMediator publisher)
{
_publisher = publisher;
}
private readonly Dictionary<Guid, List<EventDescriptor>> _current =
new Dictionary<Guid, List<EventDescriptor>>();
public async Task SaveEvents(Guid aggregateId, IEnumerable<object> events, int expectedVersion)
{
List<EventDescriptor> eventDescriptors;
// try to get event descriptors list for given aggregate id
// otherwise -> create empty dictionary
if (!_current.TryGetValue(aggregateId, out eventDescriptors))
{
eventDescriptors = new List<EventDescriptor>();
_current.Add(aggregateId, eventDescriptors);
}
// check whether latest event version matches current aggregate version
// otherwise -> throw exception
else if (eventDescriptors[eventDescriptors.Count - 1].Version != expectedVersion && expectedVersion != -1)
{
throw new ConcurrencyException();
}
var i = expectedVersion;
// iterate through current aggregate events increasing version with each processed event
foreach (var @event in events)
{
i++;
// @event.Version = i;
// push event to the event descriptors list for current aggregate
eventDescriptors.Add(new EventDescriptor(aggregateId, @event, i));
// publish current event to the bus for further processing by subscribers
await _publisher.PublishAsync(@event);
}
}
// collect all processed events for given aggregate and return them as a list
// used to build up an aggregate from its history (Domain.LoadsFromHistory)
public Task<List<object>> GetEventsForAggregate(Guid aggregateId)
{
List<EventDescriptor> eventDescriptors;
if (!_current.TryGetValue(aggregateId, out eventDescriptors))
{
throw new AggregateNotFoundException();
}
return Task.FromResult(eventDescriptors.Select(desc => desc.EventData).ToList());
}
}
}
================================================
FILE: src/Infrastructure/Persistence/InMemory/ServiceCollectionExtensions.cs
================================================
using Domain.ProductContext;
using Infrastructure.Persistence.InMemory.EventStore;
using Microsoft.Extensions.DependencyInjection;
namespace Infrastructure.Persistence.InMemory
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddInMemoryEventStorePersistence(this IServiceCollection services)
{
return services
.AddSingleton<IEventStore, InMemoryEventStore>()
.AddScoped<IProductRepository, EventStoreProductRepository>();
}
}
}
================================================
FILE: src/Infrastructure/Persistence/Mongo/EventStore/MongoDbEventStore.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Infrastructure.Messaging.Mediator;
using MongoDB.Driver;
namespace Infrastructure.Persistence.Mongo.EventStore
{
public class MongoDbEventStore : IEventStore
{
private readonly IMediator _publisher;
private readonly IMongoCollection<EventDescriptor> _collection;
public MongoDbEventStore(IMediator publisher, IMongoDbContext _mongoDbContext)
{
_publisher = publisher;
_collection = _mongoDbContext.GetCollection<EventDescriptor>("events");
}
private class EventDescriptor
{
public EventDescriptor(Guid id, object eventData, int version)
{
EventData = eventData;
Version = version;
AggregateRootId = id;
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
public long Timestamp { get; }
public object EventData { get; }
public int Version { get; }
public Guid AggregateRootId { get; }
}
public async Task SaveEvents(Guid aggregateId, IEnumerable<object> events, int expectedVersion)
{
var last = _collection
.AsQueryable()
.Where(e => e.AggregateRootId == aggregateId)
.OrderBy(e => e.Version)
.ThenBy(e => e.Timestamp).LastOrDefault();
if (last != null && last.Version != expectedVersion && expectedVersion != -1)
{
throw new ConcurrencyException();
}
var i = expectedVersion;
var eventDescriptors = events.ToList().Select(@event =>
{
i++;
return new EventDescriptor(aggregateId, @event, i);
});
await _collection.InsertManyAsync(eventDescriptors, new InsertManyOptions
{
IsOrdered = true,
BypassDocumentValidation = false
});
await Task.WhenAll(events.Select(@event => _publisher.PublishAsync(@event)));
}
public Task<List<object>> GetEventsForAggregate(Guid aggregateId)
{
return Task.FromResult(_collection
.AsQueryable()
.Where(e => e.AggregateRootId == aggregateId)
.OrderBy(e => e.Version)
.ThenBy(e => e.Timestamp).ToList().Select(descriptor => descriptor.EventData).ToList());
}
}
}
================================================
FILE: src/Infrastructure/Persistence/Mongo/IMongoDbContext.cs
================================================
using MongoDB.Driver;
namespace Infrastructure.Persistence.Mongo
{
public interface IMongoDbContext
{
IMongoCollection<T> GetCollection<T>(string collectionName);
}
}
================================================
FILE: src/Infrastructure/Persistence/Mongo/MongoDbContext.cs
================================================
using MongoDB.Driver;
namespace Infrastructure.Persistence.Mongo
{
public sealed class MongoDbContext : IMongoDbContext
{
private readonly IMongoDatabase _database;
public MongoDbContext(IMongoDatabase database)
{
_database = database;
}
public IMongoCollection<T> GetCollection<T>(string collectionName)
{
return _database.GetCollection<T>(collectionName);
}
}
}
================================================
FILE: src/Infrastructure/Persistence/Mongo/MongoDbProductRepository.cs
================================================
using System;
using Domain.ProductContext;
using Infrastructure.Messaging.Mediator;
namespace Infrastructure.Persistence.Mongo
{
public class MongoDbProductRepository : MongoDbRepository<Product, Guid>, IProductRepository
{
public MongoDbProductRepository(IMongoDbContext context, IMediator mediator) : base(mediator, context)
{
}
protected override string CollectionName => "products";
}
}
================================================
FILE: src/Infrastructure/Persistence/Mongo/MongoDbProvider.cs
================================================
using MongoDB.Driver;
namespace Infrastructure.Persistence.Mongo
{
public class MongoDbProvider
{
public static IMongoDatabase Provide(MongoSettings settings)
{
//var client = new MongoClient(settings.Value.ConnectionString);
MongoClientSettings clientSettings = new MongoClientSettings();
clientSettings.Server = new MongoServerAddress(settings.Host, 10255);
clientSettings.UseTls = settings.UseSsl;
if (settings.UseSsl)
{
clientSettings.SslSettings = new SslSettings();
clientSettings.SslSettings.EnabledSslProtocols = settings.EnabledSslProtocols;
}
MongoIdentity identity = new MongoInternalIdentity(settings.Database, settings.UserName);
MongoIdentityEvidence evidence = new PasswordEvidence(settings.Password);
clientSettings.Credential = new MongoCredential("SCRAM-SHA-1", identity, evidence);
MongoClient client = new MongoClient(clientSettings);
return client.GetDatabase(settings.Database);
}
}
}
================================================
FILE: src/Infrastructure/Persistence/Mongo/MongoDbRepository.cs
================================================
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Domain;
using Domain.Abstraction;
using Infrastructure.Messaging.Mediator;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
namespace Infrastructure.Persistence.Mongo
{
// todo: https://github.com/NEventStore/NEventStore.Persistence.MongoDB review the impl
public abstract class MongoDbRepository<TAggregate, TKey> : IRepository<TAggregate, TKey>
where TAggregate : AggregateRoot<TKey>
{
protected abstract string CollectionName { get; }
private readonly IMediator _mediator;
private readonly IMongoCollection<TAggregate> _collection;
protected MongoDbRepository(IMediator mediator,IMongoDbContext context )
{
_mediator = mediator;
_collection = context.GetCollection<TAggregate>(CollectionName);
}
public virtual async Task Save(TAggregate aggregate, CancellationToken cancellationToken)
{
var existing = await _collection.AsQueryable().Where(p => p.Id.Equals(aggregate.Id)).FirstOrDefaultAsync(cancellationToken);
if (existing != null)
{
if (existing.Version > aggregate.Version)
{
throw new ConcurrencyException();
}
await _collection.ReplaceOneAsync(c => c.Id.Equals(aggregate.Id), aggregate, new UpdateOptions()
{
IsUpsert = true
}, cancellationToken);
}
else
{
await _collection.InsertOneAsync(aggregate, new InsertOneOptions
{
BypassDocumentValidation = false
}, cancellationToken);
}
// todo: find a way to prevent inconsistency between state and domain events
await DispatchDomainEventsAsync(aggregate, cancellationToken);
}
protected async Task DispatchDomainEventsAsync(TAggregate aggregateRoot, CancellationToken cancellationToken)
{
var domainEvents = aggregateRoot.GetUncommittedChanges();
aggregateRoot.MarkChangesAsCommitted();
await Task.WhenAll(domainEvents
.Select(async domainEvent => await _mediator.PublishAsync(domainEvent, cancellationToken)));
}
public virtual Task<TAggregate> Load(TKey id, CancellationToken cancellationToken)
{
return _collection.AsQueryable().Where(p => p.Id.Equals(id)).FirstOrDefaultAsync(cancellationToken);
}
}
}
================================================
FILE: src/Infrastructure/Persistence/Mongo/MongoSettings.cs
================================================
using System.Security.Authentication;
namespace Infrastructure.Persistence.Mongo
{
public class MongoSettings
{
//public string ConnectionString { get; set; }
public int Port { get; set; } = 10255;
public string Host { get; set; }
public bool UseSsl { get; set; } = true;
public string Database { get; set; }
public SslProtocols EnabledSslProtocols { get; set; } = SslProtocols.Tls12;
public string Password { get; internal set; }
public string UserName { get; internal set; }
}
}
================================================
FILE: src/Infrastructure/Persistence/Mongo/ServiceCollectionExtensions.cs
================================================
using Domain.ProductContext;
using Infrastructure.Persistence.Mongo.EventStore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Infrastructure.Persistence.Mongo
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMongoDbPersistence(this IServiceCollection services,
IConfiguration configuration)
{
return services
.Configure<MongoSettings>(configuration.GetSection("MongoDb"))
.AddSingleton(c => MongoDbProvider.Provide(c.GetRequiredService<IOptions<MongoSettings>>().Value))
.AddSingleton<IMongoDbContext, MongoDbContext>()
.AddScoped<IProductRepository, MongoDbProductRepository>();
}
public static IServiceCollection AddMongoDbEventStorePersistence(this IServiceCollection services,
IConfiguration configuration)
{
return services
.Configure<MongoSettings>(configuration.GetSection("MongoDb"))
.AddSingleton(c => MongoDbProvider.Provide(c.GetRequiredService<IOptions<MongoSettings>>().Value))
.AddSingleton<IMongoDbContext, MongoDbContext>()
.AddSingleton<IEventStore, MongoDbEventStore>()
.AddScoped<IProductRepository, EventStoreProductRepository>();
}
}
}
================================================
FILE: src/Infrastructure/ServiceCollectionExtensions.cs
================================================
using Infrastructure.Persistence.InMemory;
using Infrastructure.Persistence.Mongo;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Infrastructure
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services,
IConfiguration configuration)
{
return services
.AddInMemoryEventStorePersistence()
// .AddMongoDbPersistence(configuration)
// .AddMongoDbEventStorePersistence(configuration)
.AddInProcessMessageBus();
}
}
}
================================================
FILE: src/WebApi/Extensions/ApplicationBuilderExtensions.cs
================================================
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
namespace WebApi.Extensions
{
public static class ApplicationBuilderExtensions
{
public static IApplicationBuilder UseAspNetInfrastructure(this IApplicationBuilder app,
IWebHostEnvironment hostingEnvironment,
IConfiguration configuration)
{
return app.UseResponseCompression()
.UseApplicationHeaders(hostingEnvironment, configuration)
.UseSwaggerConf()
.UseMvc()
.UseHsts();
}
public static IApplicationBuilder UseSwaggerConf(this IApplicationBuilder app)
{
return app
.UseSwagger()
.UseSwaggerUI(c =>
{
c.RoutePrefix = "";
c.SwaggerEndpoint("/swagger/v1/swagger.json", "PimReadProjector API Swagger v1");
});
}
public static IApplicationBuilder UseApplicationHeaders(this IApplicationBuilder app,
IWebHostEnvironment hostingEnvironment,
IConfiguration configuration)
{
var appVersion = configuration["APP_VERSION"] ??
Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
return app.Use(async (context, func) =>
{
context.Response.OnStarting(state =>
{
var httpContext = (HttpContext) state;
httpContext.Response.Headers.Add("X-Environment", new[] {hostingEnvironment.EnvironmentName});
httpContext.Response.Headers.Add("X-Version", new[] {appVersion});
return Task.FromResult(0);
}, context);
await func.Invoke();
});
}
}
}
================================================
FILE: src/WebApi/Extensions/OriginalNameCamelCaseContractResolver.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace WebApi.Extensions
{
internal class OriginalNameCamelCaseContractResolver : CamelCasePropertyNamesContractResolver
{
private readonly string[] _namespaces;
public OriginalNameCamelCaseContractResolver(Type[] types)
: this(types.Select(t => t.Namespace).ToArray())
{
}
public OriginalNameCamelCaseContractResolver(string[] namespaces)
{
_namespaces = namespaces;
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var list = base.CreateProperties(type, memberSerialization);
if (!_namespaces.Any(x => type.Namespace.StartsWith(x)))
{
return list;
}
foreach (var prop in list)
{
prop.PropertyName = NamingStrategy.GetPropertyName(prop.UnderlyingName, false);
}
return list;
}
}
}
================================================
FILE: src/WebApi/Extensions/ServiceCollectionExtensions.cs
================================================
using System;
using System.IO;
using System.Reflection;
using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Swashbuckle.AspNetCore.Swagger;
namespace WebApi.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddAspNetInfrastructure(this IServiceCollection services)
{
services
.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Title = "Pim translation API Swagger",
Version = "v1"
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.XML";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
if (File.Exists(xmlPath))
{
c.IncludeXmlComments(xmlPath);
}
})
.AddResponseCompression()
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
.AddFluentValidation(fv => fv.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly()));
return services;
}
}
}
================================================
FILE: src/WebApi/Program.cs
================================================
using System.Threading.Tasks;
using Infrastructure;
using Infrastructure.Logging;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace WebApi
{
public class Program
{
public static Task Main(string[] args) => CreateHostBuilder(args).Build().RunAsync();
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.ConfigureLoggingPlumbing()
.CaptureStartupErrors(true);
});
}
}
================================================
FILE: src/WebApi/Properties/launchSettings.json
================================================
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:58681",
"sslPort": 44388
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"WebApi": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/values",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
================================================
FILE: src/WebApi/Startup.cs
================================================
using Infrastructure;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WebApi.Extensions;
namespace WebApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services
.AddInfrastructure(Configuration)
.AddAspNetInfrastructure();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAspNetInfrastructure(env, Configuration);
}
}
}
================================================
FILE: src/WebApi/UseCases/AddContentToProduct/AddContentToProductCommandValidator.cs
================================================
using Application.UseCases.AddContentToProduct;
using FluentValidation;
namespace WebApi.UseCases.AddContentToProduct
{
public class AddContentToProductCommandValidator: AbstractValidator<AddContentToProductCommand> {
public AddContentToProductCommandValidator() {
RuleFor(x => x.Description).NotNull();
RuleFor(x => x.Title).NotNull();
RuleFor(x => x.AttributeId).NotNull();
RuleFor(x => x.ProductId).NotNull();
RuleFor(x => x.AttributeValueId).NotNull();
}
}
}
================================================
FILE: src/WebApi/UseCases/AddContentToProduct/ProductController.cs
================================================
using System.Threading;
using System.Threading.Tasks;
using Application.UseCases.AddContentToProduct;
using Infrastructure.Messaging.Mediator;
using Microsoft.AspNetCore.Mvc;
namespace WebApi.UseCases.AddContentToProduct
{
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator) => _mediator = mediator;
// POST api/products
[HttpPost]
public async Task Post([FromBody] AddContentToProductCommand command, CancellationToken cancellationToken)
{
await _mediator.SendAsync(command, cancellationToken);
}
}
}
================================================
FILE: src/WebApi/UseCases/CreateProduct/CreateProductCommandValidator.cs
================================================
using Application.UseCases.CreateProduct;
using FluentValidation;
namespace WebApi.UseCases.CreateProduct
{
public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
public CreateProductCommandValidator()
{
RuleFor(x => x.BrandId).NotNull();
RuleFor(x => x.CategoryId).NotNull();
RuleFor(x => x.ProductCode).NotNull();
}
}
}
================================================
FILE: src/WebApi/UseCases/CreateProduct/ProductsController.cs
================================================
using System.Threading;
using System.Threading.Tasks;
using Application.UseCases.CreateProduct;
using Infrastructure.Messaging.Mediator;
using Microsoft.AspNetCore.Mvc;
namespace WebApi.UseCases.CreateProduct
{
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator) => _mediator = mediator;
// POST api/products
[HttpPost]
public async Task Post([FromBody] CreateProductCommand command, CancellationToken cancellationToken)
{
await _mediator.SendAsync(command, cancellationToken);
}
}
}
================================================
FILE: src/WebApi/WebApi.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="8.5.0" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.Development.json">
<DependentUpon>appsettings.json</DependentUpon>
</Content>
<Content Update="appsettings.Production.json">
<DependentUpon>appsettings.json</DependentUpon>
</Content>
<Content Update="appsettings.Staging.json">
<DependentUpon>appsettings.json</DependentUpon>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Application\Application.csproj" />
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
</ItemGroup>
</Project>
================================================
FILE: src/WebApi/app.config
================================================
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<gcServer enabled="true"/>
</runtime>
</configuration>
================================================
FILE: src/WebApi/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
================================================
FILE: src/WebApi/appsettings.Production.json
================================================
{
"Serilog": {
"Console": false,
"MinimumLevel": {
"Default": "Error"
}
}
}
================================================
FILE: src/WebApi/appsettings.Staging.json
================================================
{
"Serilog": {
"Console": false,
"MinimumLevel": {
"Default": "Error"
}
}
}
================================================
FILE: src/WebApi/appsettings.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"Serilog": {
"GraylogIp": "",
"GraylogPort": 0,
"Console": true,
"FacilityName": "service-name",
"MinimumLevel": {
"Default": "Verbose",
"Override": {
"Microsoft": "Information",
"System": "Warning",
"System.Net.Http.HttpClient": "Warning"
}
}
},
"AllowedHosts": "*"
}
================================================
FILE: test/Application.Tests/Application.Tests.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="8.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Application\Application.csproj" />
<ProjectReference Include="..\..\src\Infrastructure\Infrastructure.csproj" />
<ProjectReference Include="..\TestBase\TestBase.csproj" />
</ItemGroup>
</Project>
================================================
FILE: test/Application.Tests/CommandHandlerTestBase.cs
================================================
using Microsoft.Extensions.DependencyInjection;
using TestBase;
namespace Application.Tests
{
public abstract class CommandHandlerTestBase: TestBaseWithIoC
{
protected CommandHandlerTestBase()
{
ConfigureServices(services =>
{
services
.AddInProcessMessageBus();
}).
Build();
}
}
}
================================================
FILE: test/Application.Tests/CreateProductCommandHandlerTests.cs
================================================
using System.Threading;
using System.Threading.Tasks;
using Application.UseCases.CreateProduct;
using Domain.ProductContext;
using Xunit;
namespace Application.Tests
{
public class CreateProductCommandHandlerTests : CommandHandlerTestBase
{
private readonly CreateProductCommandHandler _sut;
private readonly IProductRepository _repository;
public CreateProductCommandHandlerTests()
{
_repository = FakeAndRegister<IProductRepository>();
_sut = new CreateProductCommandHandler(_repository);
}
[Fact]
public async Task Should_Persist_Created_Product()
{
// Given
var expectedCategoryId = Random<int>();
var expectedBrandId = Random<int>();
var expectedProductCode = Random<string>();
// When
await _sut.Handle(new CreateProductCommand(expectedCategoryId, expectedBrandId, expectedProductCode),
CancellationToken.None);
// Then
Verify(() => _repository.Save(Matches<Product>(c => c.Category.Id == expectedCategoryId
&& c.Brand.Id == expectedBrandId
&& c.Code == expectedProductCode),
Ignore<CancellationToken>()));
}
}
}
================================================
FILE: test/Domain.Specs/Domain.Specs.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="8.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TestBase\TestBase.csproj" />
</ItemGroup>
</Project>
================================================
FILE: test/Domain.Specs/ProductSpecs/WhenContentAddedToProduct.cs
================================================
using System;
using System.Linq;
using Domain.ProductContext;
using Domain.ProductContext.Events;
using TestBase;
using Xunit;
namespace Domain.Specs.ProductSpecs
{
public class WhenContentAddedToProduct : SpecBase<Product>
{
[Fact]
public void Content_Should_Be_Added()
{
var productId = Random<Guid>();
var title = Random<string>();
var description = Random<string>();
var attributeId = Random<int>();
var attributeValueId = Random<int>();
var @event = new ContentAddedToProduct(
productId,
title,
description, new AttributeRef(attributeId, attributeValueId));
ScenarioForExisting()
.Given(
() => Product.Create(productId, Random<int>(), Random<int>(), Random<string>()))
.When(
product => product.AddContent(title, description, new AttributeRef(attributeId, attributeValueId))
)
.Then(@event)
.AlsoAssert(
product => product.Contents
.Any(x => x.Title == title)
);
}
[Fact]
public void Given_Product_Has_DifferentType_Attribute_Then_Business_Exception_Should_Be_Thrown()
{
ScenarioForExisting()
.Given(
() =>
{
var aggregate = Product.Create(Random<Guid>(), Random<int>(), Random<int>(), Random<string>());
aggregate.AddContent(Random<string>(), Random<string>(),
new AttributeRef(Random<int>(), Random<int>()));
return aggregate;
})
.When(
product => product.AddContent(Random<string>(), Random<string>(),
new AttributeRef(Random<int>(), Random<int>()))
)
.ThenThrows<BusinessException>("Given attribute type should belong to any content of product as slicer");
}
[Fact]
public void Given_Product_Has_Same_Attribute_Then_Business_Exception_Should_Be_Thrown()
{
var anAttribute = new AttributeRef(Random<int>(), Random<int>());
ScenarioForExisting()
.Given(
() =>
{
var aggregate = Product.Create(Random<Guid>(), Random<int>(), Random<int>(), Random<string>());
aggregate.AddContent(Random<string>(), Random<string>(), anAttribute);
return aggregate;
})
.When(
product => product.AddContent(Random<string>(), Random<string>(), anAttribute)
)
.ThenThrows<BusinessException>("Same content already exists with given attribute");
}
}
}
================================================
FILE: test/Domain.Specs/ProductSpecs/WhenCreated.cs
================================================
using System;
using Domain.ProductContext;
using Domain.ProductContext.Events;
using TestBase;
using Xunit;
namespace Domain.Specs.ProductSpecs
{
public class WhenCreated : SpecBase<Product>
{
[Fact]
public void Aggregate_Should_Be_Created_And_Publish_ProductCreated_Event()
{
var productId = Random<Guid>();
var brandId = Random<int>();
var productCode = Random<string>();
var categoryId = Random<int>();
var productCreated = new ProductCreated(
productId,
productCode,
brandId, categoryId);
ScenarioFor(
() => Product.Create(productId, categoryId, brandId, productCode)
)
.When(product => { })
.Then(productCreated);
}
}
}
================================================
FILE: test/Domain.Specs/ProductSpecs/WhenProductApproved.cs
================================================
using System;
using Domain.ProductContext;
using Domain.ProductContext.Events;
using TestBase;
using Xunit;
namespace Domain.Specs.ProductSpecs
{
public class WhenProductApproved : SpecBase<Product>
{
[Fact]
public void Given_Product_Has_No_Content_Business_Exception_Should_Be_Thrown()
{
var productId = Random<Guid>();
ScenarioForExisting()
.Given(
() =>
{
var aggregate = Product.Create(productId, Random<int>(), Random<int>(), Random<string>());
return aggregate;
})
.When(
product => product.Approve())
.ThenThrows<BusinessException>("Product must have at least one content");
}
[Fact]
public void Given_Product_Has_No_Variant_Business_Exception_Should_Be_Thrown()
{
var productId = Random<Guid>();
ScenarioForExisting()
.Given(
() =>
{
var aggregate = Product.Create(productId, Random<int>(), Random<int>(), Random<string>());
aggregate.AddContent(Random<string>(), Random<string>(),
new AttributeRef(Random<int>(), Random<int>()));
return aggregate;
})
.When(
product => product.Approve())
.ThenThrows<BusinessException>("Product must have at least one variant");
}
[Fact]
public void Given_Product_Has_No_Image_Business_Exception_Should_Be_Thrown()
{
var productId = Random<Guid>();
ScenarioForExisting()
.Given(
() =>
{
var aggregate = Product.Create(productId, Random<int>(), Random<int>(), Random<string>());
var slicerAttribute = new AttributeRef(Random<int>(), Random<int>());
aggregate.AddContent(Random<string>(), Random<string>(), slicerAttribute);
aggregate.AddVariant(Random<string>(), slicerAttribute,
new AttributeRef(Random<int>(), Random<int>()));
return aggregate;
})
.When(
product => product.Approve())
.ThenThrows<BusinessException>("Product must have at least one image");
}
[Fact]
public void Product_Should_Be_Approved()
{
var productId = Random<Guid>();
var @event = new ProductApproved(productId);
ScenarioForExisting()
.Given(
() =>
{
var aggregate = Product.Create(productId, Random<int>(), Random<int>(), Random<string>());
var slicerAttribute = new AttributeRef(Random<int>(), Random<int>());
aggregate.AddContent(Random<string>(), Random<string>(), slicerAttribute);
var varianterAttribute = new AttributeRef(Random<int>(), Random<int>());
aggregate.AddVariant(Random<string>(), slicerAttribute, varianterAttribute);
aggregate.AssignImage(Random<ImageRef>(), varianterAttribute);
return aggregate;
})
.When(
product => product.Approve())
.Then(@event)
.AlsoAssert(product => product.IsApproved);
}
}
}
================================================
FILE: test/Domain.Specs/ProductSpecs/WhenVariantAddedToProduct.cs
================================================
using System;
using System.Linq;
using Domain.ProductContext;
using Domain.ProductContext.Events;
using TestBase;
using Xunit;
namespace Domain.Specs.ProductSpecs
{
public class WhenVariantAddedToProduct : SpecBase<Product>
{
[Fact]
public void Variant_Should_Be_Added()
{
var slicerAttributeId = Random<int>();
var slicerAttributeValueId = Random<int>();
var productId = Random<Guid>();
var barcode = Random<String>();
var varianterAttributeId = Random<int>();
var varianterAttributeValueId = Random<int>();
var @event = new VariantAddedToProduct(productId, barcode,
new AttributeRef(slicerAttributeId, slicerAttributeValueId),
new AttributeRef(varianterAttributeId, varianterAttributeValueId) );
ScenarioForExisting()
.Given(
() =>
{
var aggregate = Product.Create(productId, Random<int>(), Random<int>(), Random<string>());
aggregate.AddContent(Random<string>(), Random<string>(), new AttributeRef(slicerAttributeId, slicerAttributeValueId));
return aggregate;
})
.When
(
product => product.AddVariant(barcode,
new AttributeRef(slicerAttributeId, slicerAttributeValueId),
new AttributeRef(varianterAttributeId, varianterAttributeValueId))
)
.Then(@event)
.AlsoAssert(
product => product.Contents.SelectMany(c => c.Variants)
.Any(x => x.Barcode == barcode)
);
}
[Fact]
public void Given_Product_Has_No_Content_With_Given_Slicer_Should_Throw_Business_Exception()
{
var slicerAttributeId = Random<int>();
var slicerAttributeValueId = Random<int>();
var barcode = Random<String>();
var varianterAttributeId = Random<int>();
var varianterAttributeValueId = Random<int>();
ScenarioForExisting()
.Given(
() => Product.Create(Random<Guid>(), Random<int>(), Random<int>(), Random<string>()))
.When
(
product => product.AddVariant(barcode,
new AttributeRef(slicerAttributeId, slicerAttributeValueId),
new AttributeRef(varianterAttributeId, varianterAttributeValueId))
)
.ThenThrows<BusinessException>("No content found with given slicer attribute.");
}
}
}
================================================
FILE: test/Infrastructure.Tests/Infrastructure.Tests.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="8.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Infrastructure\Infrastructure.csproj" />
<ProjectReference Include="..\TestBase\TestBase.csproj" />
</ItemGroup>
</Project>
================================================
FILE: test/Infrastructure.Tests/MediatorTests.cs
================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using Application;
using Infrastructure.Messaging.Mediator;
using Microsoft.Extensions.DependencyInjection;
using TestBase;
using Xunit;
namespace Infrastructure.Tests
{
public class MediatorTests : TestBaseWithIoC
{
internal static Guid Command_Should_Be_Handled_Test_Assertion = Guid.Empty;
public MediatorTests()
{
ConfigureServices(services =>
{
services
.AddInProcessMessageBus();
}).
Build();
}
[Fact]
public async Task Command_Should_Be_Handled()
{
var bus = GetRequiredService<IMediator>();
var newGuid = Guid.NewGuid();
await bus.SendAsync(new TestCommand(newGuid));
Assert.Equal(newGuid, Command_Should_Be_Handled_Test_Assertion);
}
}
public class TestCommand
{
public TestCommand(Guid test)
{
Test = test;
}
public Guid Test { get; }
}
public class TestCommandHandler : ICommandHandler<TestCommand>
{
public Task Handle(TestCommand message, CancellationToken cancellationToken)
{
MediatorTests.Command_Should_Be_Handled_Test_Assertion = message.Test;
return Task.CompletedTask;
}
}
}
================================================
FILE: test/TestBase/InMemoryDatabaseFixture.cs
================================================
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq.Expressions;
using ServiceStack.OrmLite;
using ServiceStack.OrmLite.Legacy;
using ServiceStack.OrmLite.Sqlite;
namespace TestBase
{
public class InMemoryDatabaseFixture : IDisposable
{
private readonly OrmLiteConnectionFactory _dbFactory =
new OrmLiteConnectionFactory(":memory:", SqliteOrmLiteDialectProvider.Instance);
private IDbConnection _db;
public IDbConnection OpenConnection() => _dbFactory.OpenDbConnection();
public void Insert<T>(IEnumerable<T> items, string tableName)
{
_db = _db ?? OpenConnection();
_db.DropAndCreateTable<T>(tableName);
foreach (var item in items)
{
_db.Insert(tableName, item);
}
}
public void Dispose()
{
_db?.Dispose();
_db = null;
}
}
public static class GenericTableExtensions
{
static object ExecWithAlias<T>(string table, Func<object> fn)
{
var modelDef = typeof(T).GetModelMetadata();
lock (modelDef)
{
var hold = modelDef.Alias;
try
{
modelDef.Alias = table;
return fn();
}
finally
{
modelDef.Alias = hold;
}
}
}
public static void DropAndCreateTable<T>(this IDbConnection db, string table)
{
ExecWithAlias<T>(table, () =>
{
db.DropAndCreateTable<T>();
return null;
});
}
public static long Insert<T>(this IDbConnection db, string table, T obj, bool selectIdentity = false)
{
return (long) ExecWithAlias<T>(table, () => db.Insert(obj, selectIdentity));
}
public static List<T> Select<T>(this IDbConnection db, string table,
Func<SqlExpression<T>, SqlExpression<T>> expression)
{
return (List<T>) ExecWithAlias<T>(table, () => db.Select<T>(expression));
}
public static int Update<T>(this IDbConnection db, string table, T item, Expression<Func<T, bool>> where)
{
return (int) ExecWithAlias<T>(table, () => db.Update(item, where));
}
}
}
================================================
FILE: test/TestBase/ScenarioFor.cs
================================================
using FluentAssertions;
using KellermanSoftware.CompareNetObjects;
using System;
using System.Linq;
using Domain;
namespace TestBase
{
public class ScenarioFor<TAggregateRoot> where TAggregateRoot : IAggregateRoot
{
private TAggregateRoot _aggregateRoot;
private Action<TAggregateRoot>[] _whens;
public ScenarioFor(Func<TAggregateRoot> constructor) => _aggregateRoot = constructor();
public ScenarioFor<TAggregateRoot> When(params Action<TAggregateRoot>[] whens)
{
_whens = whens;
return this;
}
public ScenarioFor<TAggregateRoot> With(Action<TAggregateRoot> act)
{
act(_aggregateRoot);
return this;
}
public void Then(params object[] events)
{
var logic = new CompareLogic().Compare(_aggregateRoot.GetUncommittedChanges().ToArray(), events);
logic.AreEqual.Should().Be(true, "Expected domain event(s) should be fired");
}
public void ThenThrows<TException>(string message = "") where TException : Exception
{
Action act = () =>
{
foreach (var action in _whens)
{
action(_aggregateRoot);
}
};
act.Should().Throw<TException>(message);
}
}
}
================================================
FILE: test/TestBase/ScenarioForExisting.cs
================================================
using System;
using System.Linq;
using Domain;
using FluentAssertions;
using KellermanSoftware.CompareNetObjects;
namespace TestBase
{
public class ScenarioForExisting<TAggregateRoot> where TAggregateRoot : IAggregateRoot
{
private TAggregateRoot _aggregateRoot;
private Action<TAggregateRoot>[] _whens;
public ScenarioForExisting<TAggregateRoot> Given(Func<TAggregateRoot> aggregateRoot)
{
_aggregateRoot = aggregateRoot();
_aggregateRoot.MarkChangesAsCommitted();
return this;
}
public ScenarioForExisting<TAggregateRoot> When(params Action<TAggregateRoot>[] whens)
{
_whens = whens;
return this;
}
public ScenarioForExisting<TAggregateRoot> With(Action<TAggregateRoot> act)
{
act(_aggregateRoot);
return this;
}
public ScenarioForExisting<TAggregateRoot> Then(params object[] events)
{
foreach (var action in _whens)
{
action(_aggregateRoot);
}
var logic = new CompareLogic().Compare(_aggregateRoot.GetUncommittedChanges().ToArray(), events);
logic.AreEqual.Should().Be(true, "Expected domain event(s) should be fired");
return this;
}
public ScenarioForExisting<TAggregateRoot> AlsoAssert(Func<TAggregateRoot, bool> expression)
{
expression(_aggregateRoot).Should().Be(true, "AlsoAssert is not validated maybe event is not applied");
return this;
}
public void ThenNone()
{
foreach (var action in _whens)
{
action(_aggregateRoot);
}
_aggregateRoot.GetUncommittedChanges().ToArray().Should()
.BeEmpty("Aggregate should not have any events! But founded.");
}
public void ThenThrows<TException>(string message = null) where TException : Exception
{
Action act = () =>
{
foreach (var action in _whens)
{
action(_aggregateRoot);
}
};
if (message != null)
act.Should().Throw<TException>().WithMessage(message);
else
act.Should().Throw<TException>();
}
}
}
================================================
FILE: test/TestBase/SpecBase.cs
================================================
using System;
using System.Linq;
using AutoFixture;
using Domain;
namespace TestBase
{
public abstract class SpecBase<TAggregate> : SpecBase where TAggregate : IAggregateRoot
{
protected virtual ScenarioForExisting<TAggregate> ScenarioForExisting()
{
return base.ScenarioForExisting<TAggregate>();
}
protected ScenarioFor<TAggregate> ScenarioFor(Func<TAggregate> constructor)
{
return base.ScenarioFor<TAggregate>(constructor);
}
}
public abstract class SpecBase
{
protected virtual ScenarioForExisting<TAggregate> ScenarioForExisting<TAggregate>() where TAggregate : IAggregateRoot
{
return new ScenarioForExisting<TAggregate>();
}
protected virtual ScenarioFor<TAggregate> ScenarioFor<TAggregate>(Func<TAggregate> constructor) where TAggregate : IAggregateRoot
{
return new ScenarioFor<TAggregate>(constructor);
}
private static readonly Fixture fixture = new Fixture();
protected T Random<T>()
{
if (typeof(T) == typeof(long))
{
return (T) Convert.ChangeType(new Random().Next(),
typeof(T)); // workaround to avoid autoFixture's similer long values
}
return fixture.Create<Generator<T>>().First();
}
}
}
================================================
FILE: test/TestBase/TestBase.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.11.0" />
<PackageReference Include="CompareNETObjects" Version="4.63.0" />
<PackageReference Include="FakeItEasy" Version="5.2.0" />
<PackageReference Include="FluentAssertions" Version="5.9.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="8.5.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="ServiceStack.OrmLite.Sqlite" Version="5.7.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Domain\Domain.csproj" />
</ItemGroup>
</Project>
================================================
FILE: test/TestBase/TestBaseWithInMemoryDb.cs
================================================
using Xunit;
namespace TestBase
{
public abstract class TestBaseWithInMemoryDb : TestBaseWithIoC, IClassFixture<InMemoryDatabaseFixture>
{
protected static InMemoryDatabaseFixture InMemoryDb { get; private set; }
protected TestBaseWithInMemoryDb(InMemoryDatabaseFixture inMemoryDatabaseFixture)
{
InMemoryDb = inMemoryDatabaseFixture;
}
public override void Dispose()
{
base.Dispose();
InMemoryDb.Dispose();
}
}
}
================================================
FILE: test/TestBase/TestBaseWithIoC.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using AutoFixture;
using FakeItEasy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace TestBase
{
public abstract class TestBaseWithIoC : IDisposable
{
private IServiceProvider _localResolver;
private readonly IServiceCollection _services;
protected TestBaseWithIoC() => _services = new ServiceCollection();
protected IConfiguration Configuration => InMemoryConfiguration != null
? new ConfigurationBuilder().AddInMemoryCollection(InMemoryConfiguration).Build()
: new ConfigurationBuilder().AddJsonFile(ConfigJsonFilename).Build();
protected virtual IDictionary<string, string> InMemoryConfiguration => null;
protected virtual string ConfigJsonFilename => "appsettings.test.json";
public virtual void Dispose()
{
}
protected TestBaseWithIoC ConfigureServices(Action<IServiceCollection> builder)
{
builder(_services);
return this;
}
public void Build()
{
_localResolver = _services.BuildServiceProvider();
}
protected T GetRequiredService<T>() => _localResolver.GetRequiredService<T>();
protected virtual T Fake<T>(bool strict = false) where T : class
{
return A.Fake<T>(cfg =>
{
if (strict)
{
cfg.Strict();
}
});
}
protected virtual T FakeAndRegister<T>(bool strict = false)
where T : class
{
T a = Fake<T>();
_services.AddSingleton(c => a);
return a;
}
protected virtual T FakeAndRegisterWithType<T>(bool strict = false)
where T : class
{
T a = Fake<T>();
_services.AddSingleton(typeof(T), c => a);
return a;
}
protected virtual void Verify(Expression<Action> callSpecification, int numberOfTimes = 1)
{
A.CallTo(callSpecification).MustHaveHappened(numberOfTimes, Times.Exactly);
}
protected virtual void Stub<T>(Expression<Func<T>> callSpecification, T stubbingValue)
{
A.CallTo(callSpecification).Returns(stubbingValue);
}
protected virtual T Ignore<T>() => A<T>.Ignored;
protected T Matches<T>(Expression<Func<T, bool>> predicate) => A<T>.That.Matches(predicate);
private static readonly Fixture fixture = new Fixture();
protected T Random<T>()
{
if (typeof(T) == typeof(long))
{
return (T) Convert.ChangeType(new Random().Next(),
typeof(T)); // workaround to avoid autoFixture's similer long values
}
return fixture.Create<Generator<T>>().First();
}
}
}
gitextract_22c5k2bq/
├── .gitignore
├── Directory.build.props
├── LICENSE
├── README.md
├── aspnet-core-clean-arch.sln
├── src/
│ ├── Application/
│ │ ├── Application.csproj
│ │ ├── ICommandHandler.cs
│ │ ├── IEventHandler.cs
│ │ ├── IQueryHandler.cs
│ │ └── UseCases/
│ │ ├── AddContentToProduct/
│ │ │ ├── AddContentToProductCommand.cs
│ │ │ └── AddContentToProductCommandHandler.cs
│ │ └── CreateProduct/
│ │ ├── CreateProductCommand.cs
│ │ └── CreateProductCommandHandler.cs
│ ├── Domain/
│ │ ├── Abstraction/
│ │ │ ├── AggregateRoot.cs
│ │ │ ├── BaseDomainEvent.cs
│ │ │ ├── Entity.cs
│ │ │ ├── IRepository.cs
│ │ │ ├── InstanceEventRouter.cs
│ │ │ └── ValueObject.cs
│ │ ├── BusinessException.cs
│ │ ├── Domain.csproj
│ │ └── ProductContext/
│ │ ├── AttributeRef.cs
│ │ ├── BrandRef.cs
│ │ ├── CategoryRef.cs
│ │ ├── Content.cs
│ │ ├── Events/
│ │ │ ├── AttributeAddedToProduct.cs
│ │ │ ├── BaseProductEvent.cs
│ │ │ ├── ContentAddedToProduct.cs
│ │ │ ├── ImageAddedToProduct.cs
│ │ │ ├── ProductApproved.cs
│ │ │ ├── ProductCreated.cs
│ │ │ └── VariantAddedToProduct.cs
│ │ ├── IProductRepository.cs
│ │ ├── ImageRef.cs
│ │ ├── Product.cs
│ │ └── Variant.cs
│ ├── Infrastructure/
│ │ ├── Infrastructure.csproj
│ │ ├── Logging/
│ │ │ └── WebHostExtensions.cs
│ │ ├── Messaging/
│ │ │ └── Mediator/
│ │ │ ├── IMediator.cs
│ │ │ ├── Mediator.cs
│ │ │ └── ServiceCollectionExtensions.cs
│ │ ├── Persistence/
│ │ │ ├── AggregateNotFoundException.cs
│ │ │ ├── ConcurrencyException.cs
│ │ │ ├── EventStoreProductRepository.cs
│ │ │ ├── IEventStore.cs
│ │ │ ├── InMemory/
│ │ │ │ ├── EventStore/
│ │ │ │ │ └── InMemoryEventStore.cs
│ │ │ │ └── ServiceCollectionExtensions.cs
│ │ │ └── Mongo/
│ │ │ ├── EventStore/
│ │ │ │ └── MongoDbEventStore.cs
│ │ │ ├── IMongoDbContext.cs
│ │ │ ├── MongoDbContext.cs
│ │ │ ├── MongoDbProductRepository.cs
│ │ │ ├── MongoDbProvider.cs
│ │ │ ├── MongoDbRepository.cs
│ │ │ ├── MongoSettings.cs
│ │ │ └── ServiceCollectionExtensions.cs
│ │ └── ServiceCollectionExtensions.cs
│ └── WebApi/
│ ├── Extensions/
│ │ ├── ApplicationBuilderExtensions.cs
│ │ ├── OriginalNameCamelCaseContractResolver.cs
│ │ └── ServiceCollectionExtensions.cs
│ ├── Program.cs
│ ├── Properties/
│ │ └── launchSettings.json
│ ├── Startup.cs
│ ├── UseCases/
│ │ ├── AddContentToProduct/
│ │ │ ├── AddContentToProductCommandValidator.cs
│ │ │ └── ProductController.cs
│ │ └── CreateProduct/
│ │ ├── CreateProductCommandValidator.cs
│ │ └── ProductsController.cs
│ ├── WebApi.csproj
│ ├── app.config
│ ├── appsettings.Development.json
│ ├── appsettings.Production.json
│ ├── appsettings.Staging.json
│ └── appsettings.json
└── test/
├── Application.Tests/
│ ├── Application.Tests.csproj
│ ├── CommandHandlerTestBase.cs
│ └── CreateProductCommandHandlerTests.cs
├── Domain.Specs/
│ ├── Domain.Specs.csproj
│ └── ProductSpecs/
│ ├── WhenContentAddedToProduct.cs
│ ├── WhenCreated.cs
│ ├── WhenProductApproved.cs
│ └── WhenVariantAddedToProduct.cs
├── Infrastructure.Tests/
│ ├── Infrastructure.Tests.csproj
│ └── MediatorTests.cs
└── TestBase/
├── InMemoryDatabaseFixture.cs
├── ScenarioFor.cs
├── ScenarioForExisting.cs
├── SpecBase.cs
├── TestBase.csproj
├── TestBaseWithInMemoryDb.cs
└── TestBaseWithIoC.cs
SYMBOL INDEX (271 symbols across 70 files)
FILE: src/Application/ICommandHandler.cs
type ICommandHandler (line 6) | public interface ICommandHandler<in TMessage>
method Handle (line 8) | Task Handle(TMessage message, CancellationToken cancellationToken);
FILE: src/Application/IEventHandler.cs
type IEventHandler (line 6) | public interface IEventHandler <in TEvent>
method Handle (line 8) | Task Handle(TEvent @event, CancellationToken cancellationToken);
FILE: src/Application/IQueryHandler.cs
type IQueryHandler (line 6) | public interface IQueryHandler<in TMessage, TResult>
method Handle (line 8) | Task<TResult> Handle(TMessage message, CancellationToken cancellationT...
FILE: src/Application/UseCases/AddContentToProduct/AddContentToProductCommand.cs
class AddContentToProductCommand (line 5) | public class AddContentToProductCommand
method AddContentToProductCommand (line 7) | public AddContentToProductCommand(Guid productId, string title, string...
FILE: src/Application/UseCases/AddContentToProduct/AddContentToProductCommandHandler.cs
class AddContentToProductCommandHandler (line 7) | public class AddContentToProductCommandHandler : ICommandHandler<AddCont...
method AddContentToProductCommandHandler (line 11) | public AddContentToProductCommandHandler(IProductRepository productRep...
method Handle (line 16) | public async Task Handle(AddContentToProductCommand command, Cancellat...
FILE: src/Application/UseCases/CreateProduct/CreateProductCommand.cs
class CreateProductCommand (line 3) | public class CreateProductCommand
method CreateProductCommand (line 5) | public CreateProductCommand(int categoryId, int brandId, string produc...
FILE: src/Application/UseCases/CreateProduct/CreateProductCommandHandler.cs
class CreateProductCommandHandler (line 8) | public class CreateProductCommandHandler : ICommandHandler<CreateProduct...
method CreateProductCommandHandler (line 12) | public CreateProductCommandHandler(IProductRepository productRepository)
method Handle (line 17) | public async Task Handle(CreateProductCommand command, CancellationTok...
FILE: src/Domain/Abstraction/AggregateRoot.cs
type IAggregateRoot (line 8) | public interface IAggregateRoot
method HasChanges (line 11) | bool HasChanges();
method GetUncommittedChanges (line 12) | IEnumerable<object> GetUncommittedChanges();
method MarkChangesAsCommitted (line 13) | void MarkChangesAsCommitted();
method LoadsFromHistory (line 14) | void LoadsFromHistory(IEnumerable<object> history);
class AggregateRoot (line 17) | public abstract class AggregateRoot<TKey> : IAggregateRoot
method AggregateRoot (line 22) | public AggregateRoot()
method HasChanges (line 31) | public bool HasChanges() => _changes.Any();
method GetUncommittedChanges (line 33) | public IEnumerable<object> GetUncommittedChanges()
method MarkChangesAsCommitted (line 38) | public void MarkChangesAsCommitted()
method LoadsFromHistory (line 43) | public void LoadsFromHistory(IEnumerable<object> history)
method Register (line 48) | protected void Register<TEvent>(Action<TEvent> handler)
method ApplyChange (line 54) | protected void ApplyChange(object @event)
method ApplyChange (line 59) | protected void ApplyChange(object @event, bool isNew)
method IsTransient (line 78) | public virtual bool IsTransient()
method Equals (line 99) | public override bool Equals(object obj)
method GetHashCode (line 131) | public override int GetHashCode() => Id.GetHashCode();
method Should (line 150) | protected void Should(Func<bool> predicate)
method Should (line 155) | protected void Should(Func<bool> predicate, string otherwiseMessage)
method Should (line 160) | protected void Should(bool clause)
method Should (line 166) | protected void Should(bool clause, string otherwiseMessage)
FILE: src/Domain/Abstraction/BaseDomainEvent.cs
class BaseDomainEvent (line 3) | public class BaseDomainEvent
FILE: src/Domain/Abstraction/Entity.cs
class Entity (line 5) | public abstract class Entity
method Entity (line 9) | protected Entity() => _router = new InstanceEventRouter();
method Register (line 11) | protected void Register<TEvent>(Action<TEvent> handler)
method Route (line 21) | public virtual void Route(object @event)
FILE: src/Domain/Abstraction/IRepository.cs
type IRepository (line 11) | public interface IRepository<T, TKey> where T : AggregateRoot<TKey>
method Save (line 13) | Task Save(T aggregate, CancellationToken cancellationToken);
method Load (line 14) | Task<T> Load(TKey id, CancellationToken cancellationToken);
FILE: src/Domain/Abstraction/InstanceEventRouter.cs
class InstanceEventRouter (line 9) | internal class InstanceEventRouter
method InstanceEventRouter (line 16) | public InstanceEventRouter() => _handlers = new Dictionary<Type, Actio...
method ConfigureRoute (line 27) | public void ConfigureRoute(Type @event, Action<object> handler)
method ConfigureRoute (line 48) | public void ConfigureRoute<TEvent>(Action<TEvent> handler)
method Route (line 63) | public void Route(object @event)
FILE: src/Domain/Abstraction/ValueObject.cs
class ValueObject (line 11) | public abstract class ValueObject<TValueObject> : IEquatable<TValueObject>
method Equals (line 14) | public bool Equals(TValueObject other)
method Equals (line 31) | public override bool Equals(object obj)
method GetHashCode (line 42) | public override int GetHashCode()
FILE: src/Domain/BusinessException.cs
class BusinessException (line 5) | public class BusinessException : Exception
method BusinessException (line 7) | public BusinessException()
method BusinessException (line 11) | public BusinessException(string message) : base(message)
method BusinessException (line 15) | public BusinessException(string message, Exception innerException) : b...
FILE: src/Domain/ProductContext/AttributeRef.cs
class AttributeRef (line 3) | public class AttributeRef : ValueObject<AttributeRef>
method AttributeRef (line 5) | public AttributeRef(int attributeId, int attributeValueId)
FILE: src/Domain/ProductContext/BrandRef.cs
class BrandRef (line 3) | public class BrandRef : ValueObject<BrandRef>
method BrandRef (line 5) | public BrandRef(int id, string name)
FILE: src/Domain/ProductContext/CategoryRef.cs
class CategoryRef (line 3) | public class CategoryRef : ValueObject<CategoryRef>
method CategoryRef (line 5) | public CategoryRef(int id, string name)
FILE: src/Domain/ProductContext/Content.cs
class Content (line 10) | public class Content : Entity
method Content (line 14) | internal Content(string title, string description, AttributeRef slicer...
method Apply (line 23) | private void Apply(VariantAddedToProduct @event)
method HasSameTypeSlicerAttribute (line 36) | public bool HasSameTypeSlicerAttribute(AttributeRef attribute)
FILE: src/Domain/ProductContext/Events/AttributeAddedToProduct.cs
class AttributeAddedToProduct (line 5) | public class AttributeAddedToProduct : BaseProductEvent
method AttributeAddedToProduct (line 7) | public AttributeAddedToProduct(Guid productId, AttributeRef attribute)
FILE: src/Domain/ProductContext/Events/BaseProductEvent.cs
class BaseProductEvent (line 6) | public abstract class BaseProductEvent : BaseDomainEvent
FILE: src/Domain/ProductContext/Events/ContentAddedToProduct.cs
class ContentAddedToProduct (line 5) | public class ContentAddedToProduct : BaseProductEvent
method ContentAddedToProduct (line 7) | public ContentAddedToProduct(Guid productId, string title, string desc...
FILE: src/Domain/ProductContext/Events/ImageAddedToProduct.cs
class ImageAddedToProduct (line 5) | public class ImageAddedToProduct :BaseProductEvent
method ImageAddedToProduct (line 7) | public ImageAddedToProduct(Guid productId, AttributeRef varianterAttr,...
FILE: src/Domain/ProductContext/Events/ProductApproved.cs
class ProductApproved (line 5) | public class ProductApproved : BaseProductEvent
method ProductApproved (line 7) | public ProductApproved(Guid productId)
FILE: src/Domain/ProductContext/Events/ProductCreated.cs
class ProductCreated (line 5) | public class ProductCreated : BaseProductEvent
method ProductCreated (line 7) | public ProductCreated(Guid productId, string productCode, int brandId,...
FILE: src/Domain/ProductContext/Events/VariantAddedToProduct.cs
class VariantAddedToProduct (line 5) | public class VariantAddedToProduct: BaseProductEvent
method VariantAddedToProduct (line 7) | public VariantAddedToProduct(Guid productId, string barcode, Attribute...
FILE: src/Domain/ProductContext/IProductRepository.cs
type IProductRepository (line 6) | public interface IProductRepository : IRepository<Product, Guid>
FILE: src/Domain/ProductContext/ImageRef.cs
class ImageRef (line 3) | public class ImageRef : ValueObject<ImageRef>
method ImageRef (line 5) | public ImageRef(string relativeUrl, string relativeThumbUrl)
FILE: src/Domain/ProductContext/Product.cs
class Product (line 8) | public class Product : AggregateRoot<Guid>
method Product (line 14) | public Product()
method Create (line 24) | public static Product Create(Guid productId,
method Apply (line 54) | private void Apply(ProductApproved @event) => IsApproved = true;
method Apply (line 56) | private void Apply(ContentAddedToProduct @event) => _contents.Add(new ...
method Apply (line 58) | private void Apply(AttributeAddedToProduct @event) => _attributes.Add(...
method Apply (line 60) | private void Apply(VariantAddedToProduct @event) => _contents.First(c ...
method Apply (line 62) | private void Apply(ProductCreated @event)
method Apply (line 70) | private void Apply(ImageAddedToProduct @event)
method AddContent (line 79) | public void AddContent(string title, string description, AttributeRef ...
method AddVariant (line 93) | public void AddVariant(string barcode, AttributeRef slicerAttribute, A...
method AddAttributeToContent (line 113) | public void AddAttributeToContent(AttributeRef attribute)
method Approve (line 121) | public void Approve()
method AssignImage (line 130) | public void AssignImage(ImageRef image, AttributeRef varianterAttr)
FILE: src/Domain/ProductContext/Variant.cs
class Variant (line 7) | public class Variant : Entity
method Variant (line 11) | internal Variant(string barcode, AttributeRef varianterAttribute)
method Apply (line 19) | private void Apply(ImageAddedToProduct @event)
method HasSameTypeVarianterAttribute (line 30) | public bool HasSameTypeVarianterAttribute(AttributeRef attribute)
FILE: src/Infrastructure/Logging/WebHostExtensions.cs
class WebHostExtensions (line 13) | public static class WebHostExtensions
method ConfigureLoggingPlumbing (line 15) | public static IWebHostBuilder ConfigureLoggingPlumbing(this IWebHostBu...
FILE: src/Infrastructure/Messaging/Mediator/IMediator.cs
type IMediator (line 6) | public interface IMediator
method SendAsync (line 8) | Task SendAsync<TMessage>(TMessage message, CancellationToken cancellat...
method PublishAsync (line 9) | Task PublishAsync<TEvent>(TEvent message, CancellationToken cancellati...
method SendAsync (line 10) | Task<TResult> SendAsync<TMessage, TResult>(TMessage @event, Cancellati...
FILE: src/Infrastructure/Messaging/Mediator/Mediator.cs
class Mediator (line 9) | public class Mediator : IMediator
method Mediator (line 13) | public Mediator(IServiceProvider serviceProvider)
method SendAsync (line 18) | public Task SendAsync<TMessage>(TMessage message, CancellationToken ca...
method PublishAsync (line 26) | public Task PublishAsync<TEvent>(TEvent @event, CancellationToken canc...
method SendAsync (line 33) | public Task<TResult> SendAsync<TMessage, TResult>(TMessage message, Ca...
FILE: src/Infrastructure/Messaging/Mediator/ServiceCollectionExtensions.cs
class ServiceCollectionExtensions (line 11) | public static class ServiceCollectionExtensions
method AddInProcessMessageBus (line 13) | public static IServiceCollection AddInProcessMessageBus(this IServiceC...
method ConnectImplementationsToTypesClosing (line 56) | private static void ConnectImplementationsToTypesClosing(Type openRequ...
method IsMatchingWithInterface (line 110) | private static bool IsMatchingWithInterface(Type handlerType, Type han...
method AddConcretionsThatCouldBeClosed (line 132) | private static void AddConcretionsThatCouldBeClosed(Type @interface, L...
method CouldCloseTo (line 149) | private static bool CouldCloseTo(this Type openConcretion, Type closed...
method CanBeCastTo (line 158) | private static bool CanBeCastTo(this Type pluggedType, Type pluginType)
method FindInterfacesThatClose (line 167) | private static IEnumerable<Type> FindInterfacesThatClose(this Type plu...
method FindInterfacesThatClosesCore (line 173) | private static IEnumerable<Type> FindInterfacesThatClosesCore(Type plu...
method IsConcrete (line 205) | private static bool IsConcrete(this Type type)
method IsOpenGeneric (line 210) | private static bool IsOpenGeneric(this Type type)
method Fill (line 215) | private static void Fill<T>(this ICollection<T> list, T value)
FILE: src/Infrastructure/Persistence/AggregateNotFoundException.cs
class AggregateNotFoundException (line 5) | public class AggregateNotFoundException : Exception
FILE: src/Infrastructure/Persistence/ConcurrencyException.cs
class ConcurrencyException (line 5) | public class ConcurrencyException : Exception
method ConcurrencyException (line 7) | public ConcurrencyException()
method ConcurrencyException (line 11) | public ConcurrencyException(string message) : base(message)
FILE: src/Infrastructure/Persistence/EventStoreProductRepository.cs
class EventStoreProductRepository (line 8) | internal class EventStoreProductRepository : IProductRepository
method EventStoreProductRepository (line 12) | public EventStoreProductRepository(IEventStore eventStore) => _eventSt...
method Save (line 14) | public async Task Save(Product aggregate, CancellationToken cancellati...
method Load (line 20) | public async Task<Product> Load(Guid id, CancellationToken cancellatio...
FILE: src/Infrastructure/Persistence/IEventStore.cs
type IEventStore (line 7) | public interface IEventStore
method SaveEvents (line 9) | Task SaveEvents(Guid aggregateId, IEnumerable<object> events, int expe...
method GetEventsForAggregate (line 10) | Task<List<object>> GetEventsForAggregate(Guid aggregateId);
FILE: src/Infrastructure/Persistence/InMemory/EventStore/InMemoryEventStore.cs
class InMemoryEventStore (line 9) | public class InMemoryEventStore : IEventStore
type EventDescriptor (line 13) | private struct EventDescriptor
method EventDescriptor (line 19) | public EventDescriptor(Guid id, object eventData, int version)
method InMemoryEventStore (line 27) | public InMemoryEventStore(IMediator publisher)
method SaveEvents (line 35) | public async Task SaveEvents(Guid aggregateId, IEnumerable<object> eve...
method GetEventsForAggregate (line 71) | public Task<List<object>> GetEventsForAggregate(Guid aggregateId)
FILE: src/Infrastructure/Persistence/InMemory/ServiceCollectionExtensions.cs
class ServiceCollectionExtensions (line 7) | public static class ServiceCollectionExtensions
method AddInMemoryEventStorePersistence (line 9) | public static IServiceCollection AddInMemoryEventStorePersistence(this...
FILE: src/Infrastructure/Persistence/Mongo/EventStore/MongoDbEventStore.cs
class MongoDbEventStore (line 10) | public class MongoDbEventStore : IEventStore
method MongoDbEventStore (line 16) | public MongoDbEventStore(IMediator publisher, IMongoDbContext _mongoDb...
class EventDescriptor (line 22) | private class EventDescriptor
method EventDescriptor (line 24) | public EventDescriptor(Guid id, object eventData, int version)
method SaveEvents (line 40) | public async Task SaveEvents(Guid aggregateId, IEnumerable<object> eve...
method GetEventsForAggregate (line 69) | public Task<List<object>> GetEventsForAggregate(Guid aggregateId)
FILE: src/Infrastructure/Persistence/Mongo/IMongoDbContext.cs
type IMongoDbContext (line 5) | public interface IMongoDbContext
method GetCollection (line 7) | IMongoCollection<T> GetCollection<T>(string collectionName);
FILE: src/Infrastructure/Persistence/Mongo/MongoDbContext.cs
class MongoDbContext (line 6) | public sealed class MongoDbContext : IMongoDbContext
method MongoDbContext (line 10) | public MongoDbContext(IMongoDatabase database)
method GetCollection (line 15) | public IMongoCollection<T> GetCollection<T>(string collectionName)
FILE: src/Infrastructure/Persistence/Mongo/MongoDbProductRepository.cs
class MongoDbProductRepository (line 7) | public class MongoDbProductRepository : MongoDbRepository<Product, Guid>...
method MongoDbProductRepository (line 9) | public MongoDbProductRepository(IMongoDbContext context, IMediator med...
FILE: src/Infrastructure/Persistence/Mongo/MongoDbProvider.cs
class MongoDbProvider (line 5) | public class MongoDbProvider
method Provide (line 7) | public static IMongoDatabase Provide(MongoSettings settings)
FILE: src/Infrastructure/Persistence/Mongo/MongoDbRepository.cs
class MongoDbRepository (line 13) | public abstract class MongoDbRepository<TAggregate, TKey> : IRepository<...
method MongoDbRepository (line 21) | protected MongoDbRepository(IMediator mediator,IMongoDbContext context )
method Save (line 27) | public virtual async Task Save(TAggregate aggregate, CancellationToken...
method DispatchDomainEventsAsync (line 54) | protected async Task DispatchDomainEventsAsync(TAggregate aggregateRoo...
method Load (line 63) | public virtual Task<TAggregate> Load(TKey id, CancellationToken cancel...
FILE: src/Infrastructure/Persistence/Mongo/MongoSettings.cs
class MongoSettings (line 5) | public class MongoSettings
FILE: src/Infrastructure/Persistence/Mongo/ServiceCollectionExtensions.cs
class ServiceCollectionExtensions (line 9) | public static class ServiceCollectionExtensions
method AddMongoDbPersistence (line 11) | public static IServiceCollection AddMongoDbPersistence(this IServiceCo...
method AddMongoDbEventStorePersistence (line 21) | public static IServiceCollection AddMongoDbEventStorePersistence(this ...
FILE: src/Infrastructure/ServiceCollectionExtensions.cs
class ServiceCollectionExtensions (line 8) | public static class ServiceCollectionExtensions
method AddInfrastructure (line 10) | public static IServiceCollection AddInfrastructure(this IServiceCollec...
FILE: src/WebApi/Extensions/ApplicationBuilderExtensions.cs
class ApplicationBuilderExtensions (line 10) | public static class ApplicationBuilderExtensions
method UseAspNetInfrastructure (line 12) | public static IApplicationBuilder UseAspNetInfrastructure(this IApplic...
method UseSwaggerConf (line 23) | public static IApplicationBuilder UseSwaggerConf(this IApplicationBuil...
method UseApplicationHeaders (line 34) | public static IApplicationBuilder UseApplicationHeaders(this IApplicat...
FILE: src/WebApi/Extensions/OriginalNameCamelCaseContractResolver.cs
class OriginalNameCamelCaseContractResolver (line 9) | internal class OriginalNameCamelCaseContractResolver : CamelCaseProperty...
method OriginalNameCamelCaseContractResolver (line 13) | public OriginalNameCamelCaseContractResolver(Type[] types)
method OriginalNameCamelCaseContractResolver (line 18) | public OriginalNameCamelCaseContractResolver(string[] namespaces)
method CreateProperties (line 23) | protected override IList<JsonProperty> CreateProperties(Type type, Mem...
FILE: src/WebApi/Extensions/ServiceCollectionExtensions.cs
class ServiceCollectionExtensions (line 12) | public static class ServiceCollectionExtensions
method AddAspNetInfrastructure (line 14) | public static IServiceCollection AddAspNetInfrastructure(this IService...
FILE: src/WebApi/Program.cs
class Program (line 10) | public class Program
method Main (line 12) | public static Task Main(string[] args) => CreateHostBuilder(args).Buil...
method CreateHostBuilder (line 14) | public static IHostBuilder CreateHostBuilder(string[] args) =>
FILE: src/WebApi/Startup.cs
class Startup (line 10) | public class Startup
method Startup (line 12) | public Startup(IConfiguration configuration)
method ConfigureServices (line 19) | public void ConfigureServices(IServiceCollection services)
method Configure (line 26) | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
FILE: src/WebApi/UseCases/AddContentToProduct/AddContentToProductCommandValidator.cs
class AddContentToProductCommandValidator (line 6) | public class AddContentToProductCommandValidator: AbstractValidator<AddC...
method AddContentToProductCommandValidator (line 7) | public AddContentToProductCommandValidator() {
FILE: src/WebApi/UseCases/AddContentToProduct/ProductController.cs
class ProductsController (line 9) | [Route("api/[controller]")]
method ProductsController (line 15) | public ProductsController(IMediator mediator) => _mediator = mediator;
method Post (line 18) | [HttpPost]
FILE: src/WebApi/UseCases/CreateProduct/CreateProductCommandValidator.cs
class CreateProductCommandValidator (line 6) | public class CreateProductCommandValidator : AbstractValidator<CreatePro...
method CreateProductCommandValidator (line 8) | public CreateProductCommandValidator()
FILE: src/WebApi/UseCases/CreateProduct/ProductsController.cs
class ProductsController (line 9) | [Route("api/[controller]")]
method ProductsController (line 15) | public ProductsController(IMediator mediator) => _mediator = mediator;
method Post (line 18) | [HttpPost]
FILE: test/Application.Tests/CommandHandlerTestBase.cs
class CommandHandlerTestBase (line 6) | public abstract class CommandHandlerTestBase: TestBaseWithIoC
method CommandHandlerTestBase (line 8) | protected CommandHandlerTestBase()
FILE: test/Application.Tests/CreateProductCommandHandlerTests.cs
class CreateProductCommandHandlerTests (line 9) | public class CreateProductCommandHandlerTests : CommandHandlerTestBase
method CreateProductCommandHandlerTests (line 14) | public CreateProductCommandHandlerTests()
method Should_Persist_Created_Product (line 20) | [Fact]
FILE: test/Domain.Specs/ProductSpecs/WhenContentAddedToProduct.cs
class WhenContentAddedToProduct (line 10) | public class WhenContentAddedToProduct : SpecBase<Product>
method Content_Should_Be_Added (line 12) | [Fact]
method Given_Product_Has_DifferentType_Attribute_Then_Business_Exception_Should_Be_Thrown (line 39) | [Fact]
method Given_Product_Has_Same_Attribute_Then_Business_Exception_Should_Be_Thrown (line 58) | [Fact]
FILE: test/Domain.Specs/ProductSpecs/WhenCreated.cs
class WhenCreated (line 9) | public class WhenCreated : SpecBase<Product>
method Aggregate_Should_Be_Created_And_Publish_ProductCreated_Event (line 11) | [Fact]
FILE: test/Domain.Specs/ProductSpecs/WhenProductApproved.cs
class WhenProductApproved (line 9) | public class WhenProductApproved : SpecBase<Product>
method Given_Product_Has_No_Content_Business_Exception_Should_Be_Thrown (line 11) | [Fact]
method Given_Product_Has_No_Variant_Business_Exception_Should_Be_Thrown (line 28) | [Fact]
method Given_Product_Has_No_Image_Business_Exception_Should_Be_Thrown (line 47) | [Fact]
method Product_Should_Be_Approved (line 68) | [Fact]
FILE: test/Domain.Specs/ProductSpecs/WhenVariantAddedToProduct.cs
class WhenVariantAddedToProduct (line 10) | public class WhenVariantAddedToProduct : SpecBase<Product>
method Variant_Should_Be_Added (line 12) | [Fact]
method Given_Product_Has_No_Content_With_Given_Slicer_Should_Throw_Business_Exception (line 47) | [Fact]
FILE: test/Infrastructure.Tests/MediatorTests.cs
class MediatorTests (line 12) | public class MediatorTests : TestBaseWithIoC
method MediatorTests (line 15) | public MediatorTests()
method Command_Should_Be_Handled (line 25) | [Fact]
class TestCommand (line 38) | public class TestCommand
method TestCommand (line 40) | public TestCommand(Guid test)
class TestCommandHandler (line 48) | public class TestCommandHandler : ICommandHandler<TestCommand>
method Handle (line 50) | public Task Handle(TestCommand message, CancellationToken cancellation...
FILE: test/TestBase/InMemoryDatabaseFixture.cs
class InMemoryDatabaseFixture (line 11) | public class InMemoryDatabaseFixture : IDisposable
method OpenConnection (line 18) | public IDbConnection OpenConnection() => _dbFactory.OpenDbConnection();
method Insert (line 20) | public void Insert<T>(IEnumerable<T> items, string tableName)
method Dispose (line 30) | public void Dispose()
class GenericTableExtensions (line 37) | public static class GenericTableExtensions
method ExecWithAlias (line 39) | static object ExecWithAlias<T>(string table, Func<object> fn)
method DropAndCreateTable (line 57) | public static void DropAndCreateTable<T>(this IDbConnection db, string...
method Insert (line 66) | public static long Insert<T>(this IDbConnection db, string table, T ob...
method Select (line 71) | public static List<T> Select<T>(this IDbConnection db, string table,
method Update (line 77) | public static int Update<T>(this IDbConnection db, string table, T ite...
FILE: test/TestBase/ScenarioFor.cs
class ScenarioFor (line 9) | public class ScenarioFor<TAggregateRoot> where TAggregateRoot : IAggrega...
method ScenarioFor (line 14) | public ScenarioFor(Func<TAggregateRoot> constructor) => _aggregateRoot...
method When (line 16) | public ScenarioFor<TAggregateRoot> When(params Action<TAggregateRoot>[...
method With (line 22) | public ScenarioFor<TAggregateRoot> With(Action<TAggregateRoot> act)
method Then (line 28) | public void Then(params object[] events)
method ThenThrows (line 34) | public void ThenThrows<TException>(string message = "") where TExcepti...
FILE: test/TestBase/ScenarioForExisting.cs
class ScenarioForExisting (line 9) | public class ScenarioForExisting<TAggregateRoot> where TAggregateRoot : ...
method Given (line 14) | public ScenarioForExisting<TAggregateRoot> Given(Func<TAggregateRoot> ...
method When (line 21) | public ScenarioForExisting<TAggregateRoot> When(params Action<TAggrega...
method With (line 27) | public ScenarioForExisting<TAggregateRoot> With(Action<TAggregateRoot>...
method Then (line 33) | public ScenarioForExisting<TAggregateRoot> Then(params object[] events)
method AlsoAssert (line 45) | public ScenarioForExisting<TAggregateRoot> AlsoAssert(Func<TAggregateR...
method ThenNone (line 51) | public void ThenNone()
method ThenThrows (line 62) | public void ThenThrows<TException>(string message = null) where TExcep...
FILE: test/TestBase/SpecBase.cs
class SpecBase (line 8) | public abstract class SpecBase<TAggregate> : SpecBase where TAggregate :...
method ScenarioForExisting (line 10) | protected virtual ScenarioForExisting<TAggregate> ScenarioForExisting()
method ScenarioFor (line 15) | protected ScenarioFor<TAggregate> ScenarioFor(Func<TAggregate> constru...
method ScenarioForExisting (line 23) | protected virtual ScenarioForExisting<TAggregate> ScenarioForExisting<...
method ScenarioFor (line 28) | protected virtual ScenarioFor<TAggregate> ScenarioFor<TAggregate>(Func...
method Random (line 35) | protected T Random<T>()
class SpecBase (line 21) | public abstract class SpecBase
method ScenarioForExisting (line 10) | protected virtual ScenarioForExisting<TAggregate> ScenarioForExisting()
method ScenarioFor (line 15) | protected ScenarioFor<TAggregate> ScenarioFor(Func<TAggregate> constru...
method ScenarioForExisting (line 23) | protected virtual ScenarioForExisting<TAggregate> ScenarioForExisting<...
method ScenarioFor (line 28) | protected virtual ScenarioFor<TAggregate> ScenarioFor<TAggregate>(Func...
method Random (line 35) | protected T Random<T>()
FILE: test/TestBase/TestBaseWithInMemoryDb.cs
class TestBaseWithInMemoryDb (line 5) | public abstract class TestBaseWithInMemoryDb : TestBaseWithIoC, IClassFi...
method TestBaseWithInMemoryDb (line 10) | protected TestBaseWithInMemoryDb(InMemoryDatabaseFixture inMemoryDatab...
method Dispose (line 15) | public override void Dispose()
FILE: test/TestBase/TestBaseWithIoC.cs
class TestBaseWithIoC (line 12) | public abstract class TestBaseWithIoC : IDisposable
method TestBaseWithIoC (line 17) | protected TestBaseWithIoC() => _services = new ServiceCollection();
method Dispose (line 27) | public virtual void Dispose()
method ConfigureServices (line 31) | protected TestBaseWithIoC ConfigureServices(Action<IServiceCollection>...
method Build (line 37) | public void Build()
method GetRequiredService (line 42) | protected T GetRequiredService<T>() => _localResolver.GetRequiredServi...
method Fake (line 44) | protected virtual T Fake<T>(bool strict = false) where T : class
method FakeAndRegister (line 55) | protected virtual T FakeAndRegister<T>(bool strict = false)
method FakeAndRegisterWithType (line 65) | protected virtual T FakeAndRegisterWithType<T>(bool strict = false)
method Verify (line 75) | protected virtual void Verify(Expression<Action> callSpecification, in...
method Stub (line 80) | protected virtual void Stub<T>(Expression<Func<T>> callSpecification, ...
method Ignore (line 85) | protected virtual T Ignore<T>() => A<T>.Ignored;
method Matches (line 87) | protected T Matches<T>(Expression<Func<T, bool>> predicate) => A<T>.Th...
method Random (line 91) | protected T Random<T>()
Condensed preview — 89 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (126K chars).
[
{
"path": ".gitignore",
"chars": 589,
"preview": " Common IntelliJ Platform excludes\n\n# User specific\n\n.idea/\n\n**/.idea/**/workspace.xml\n**/.idea/**/tasks.xml\n**/.idea/sh"
},
{
"path": "Directory.build.props",
"chars": 131,
"preview": "<Project>\n <PropertyGroup>\n <Version>0.0.1</Version>\n\t\t<LangVersion>latest</LangVersion>\n </PropertyGroup>\n"
},
{
"path": "LICENSE",
"chars": 11327,
"preview": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/li"
},
{
"path": "README.md",
"chars": 1989,
"preview": "## Experimental Product Domain Based On Hexagonal Architecture Principles\nThis project is a sample application built usi"
},
{
"path": "aspnet-core-clean-arch.sln",
"chars": 5485,
"preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"src\","
},
{
"path": "src/Application/Application.csproj",
"chars": 464,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>netstandard2.1</TargetFramework>\n </"
},
{
"path": "src/Application/ICommandHandler.cs",
"chars": 218,
"preview": "using System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Application\n{\n public interface ICommandHandler<in T"
},
{
"path": "src/Application/IEventHandler.cs",
"chars": 212,
"preview": "using System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Application\n{\n public interface IEventHandler <in TE"
},
{
"path": "src/Application/IQueryHandler.cs",
"chars": 234,
"preview": "using System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Application\n{\n public interface IQueryHandler<in TMe"
},
{
"path": "src/Application/UseCases/AddContentToProduct/AddContentToProductCommand.cs",
"chars": 682,
"preview": "using System;\n\nnamespace Application.UseCases.AddContentToProduct\n{\n public class AddContentToProductCommand\n {\n "
},
{
"path": "src/Application/UseCases/AddContentToProduct/AddContentToProductCommandHandler.cs",
"chars": 921,
"preview": "using System.Threading;\nusing System.Threading.Tasks;\nusing Domain.ProductContext;\n\nnamespace Application.UseCases.AddCo"
},
{
"path": "src/Application/UseCases/CreateProduct/CreateProductCommand.cs",
"chars": 438,
"preview": "namespace Application.UseCases.CreateProduct\n{\n public class CreateProductCommand\n {\n public CreateProductC"
},
{
"path": "src/Application/UseCases/CreateProduct/CreateProductCommandHandler.cs",
"chars": 788,
"preview": "using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Domain.ProductContext;\n\nnamespace Application."
},
{
"path": "src/Domain/Abstraction/AggregateRoot.cs",
"chars": 4686,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Reflection;\n\nnamespace Domain\n{\n publ"
},
{
"path": "src/Domain/Abstraction/BaseDomainEvent.cs",
"chars": 118,
"preview": "namespace Domain.Abstraction\n{\n public class BaseDomainEvent\n {\n public int Version { get; set; }\n }\n}"
},
{
"path": "src/Domain/Abstraction/Entity.cs",
"chars": 693,
"preview": "using System;\n\nnamespace Domain\n{\n public abstract class Entity \n {\n private readonly InstanceEventRouter "
},
{
"path": "src/Domain/Abstraction/IRepository.cs",
"chars": 535,
"preview": "using System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Domain.Abstraction\n{\n /// <summary>\n /// Provides"
},
{
"path": "src/Domain/Abstraction/InstanceEventRouter.cs",
"chars": 2866,
"preview": "using System;\nusing System.Collections.Generic;\n\nnamespace Domain\n{\n /// <summary>\n /// Routes an event to a "
},
{
"path": "src/Domain/Abstraction/ValueObject.cs",
"chars": 2696,
"preview": "using System;\nusing System.Linq;\n\nnamespace Domain\n{\n //Inspired from https://blogs.msdn.microsoft.com/cesardelator"
},
{
"path": "src/Domain/BusinessException.cs",
"chars": 371,
"preview": "using System;\n\nnamespace Domain\n{\n public class BusinessException : Exception\n {\n public BusinessException("
},
{
"path": "src/Domain/Domain.csproj",
"chars": 147,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>netstandard2.1</TargetFramework>\n </"
},
{
"path": "src/Domain/ProductContext/AttributeRef.cs",
"chars": 611,
"preview": "namespace Domain.ProductContext\n{\n public class AttributeRef : ValueObject<AttributeRef>\n {\n public Attribu"
},
{
"path": "src/Domain/ProductContext/BrandRef.cs",
"chars": 277,
"preview": "namespace Domain.ProductContext\n{\n public class BrandRef : ValueObject<BrandRef>\n {\n public BrandRef(int id"
},
{
"path": "src/Domain/ProductContext/CategoryRef.cs",
"chars": 286,
"preview": "namespace Domain.ProductContext\n{\n public class CategoryRef : ValueObject<CategoryRef>\n {\n public CategoryR"
},
{
"path": "src/Domain/ProductContext/Content.cs",
"chars": 1248,
"preview": "using System.Collections.Generic;\nusing System.Linq;\nusing Domain.ProductContext.Events;\n\nnamespace Domain.ProductContex"
},
{
"path": "src/Domain/ProductContext/Events/AttributeAddedToProduct.cs",
"chars": 394,
"preview": "using System;\n\nnamespace Domain.ProductContext.Events\n{\n public class AttributeAddedToProduct : BaseProductEvent\n "
},
{
"path": "src/Domain/ProductContext/Events/BaseProductEvent.cs",
"chars": 204,
"preview": "using System;\nusing Domain.Abstraction;\n\nnamespace Domain.ProductContext.Events\n{\n public abstract class BaseProductE"
},
{
"path": "src/Domain/ProductContext/Events/ContentAddedToProduct.cs",
"chars": 594,
"preview": "using System;\n\nnamespace Domain.ProductContext.Events\n{\n public class ContentAddedToProduct : BaseProductEvent\n {\n"
},
{
"path": "src/Domain/ProductContext/Events/ImageAddedToProduct.cs",
"chars": 483,
"preview": "using System;\n\nnamespace Domain.ProductContext.Events\n{\n public class ImageAddedToProduct :BaseProductEvent\n {\n "
},
{
"path": "src/Domain/ProductContext/Events/ProductApproved.cs",
"chars": 272,
"preview": "using System;\n\nnamespace Domain.ProductContext.Events\n{\n public class ProductApproved : BaseProductEvent\n {\n "
},
{
"path": "src/Domain/ProductContext/Events/ProductCreated.cs",
"chars": 544,
"preview": "using System;\n\nnamespace Domain.ProductContext.Events\n{\n public class ProductCreated : BaseProductEvent\n {\n "
},
{
"path": "src/Domain/ProductContext/Events/VariantAddedToProduct.cs",
"chars": 641,
"preview": "using System;\n\nnamespace Domain.ProductContext.Events\n{\n public class VariantAddedToProduct: BaseProductEvent\n {\n "
},
{
"path": "src/Domain/ProductContext/IProductRepository.cs",
"chars": 157,
"preview": "using System;\nusing Domain.Abstraction;\n\nnamespace Domain.ProductContext\n{\n public interface IProductRepository : IRe"
},
{
"path": "src/Domain/ProductContext/ImageRef.cs",
"chars": 366,
"preview": "namespace Domain.ProductContext\n{\n public class ImageRef : ValueObject<ImageRef>\n {\n public ImageRef(string"
},
{
"path": "src/Domain/ProductContext/Product.cs",
"chars": 5555,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Domain.ProductContext.Events;\n\nnamespace Domain"
},
{
"path": "src/Domain/ProductContext/Variant.cs",
"chars": 968,
"preview": "using System.Collections.Generic;\nusing System.Linq;\nusing Domain.ProductContext.Events;\n\nnamespace Domain.ProductContex"
},
{
"path": "src/Infrastructure/Infrastructure.csproj",
"chars": 1123,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>netstandard2.1</TargetFramework>\n </"
},
{
"path": "src/Infrastructure/Logging/WebHostExtensions.cs",
"chars": 2100,
"preview": "using System.Reflection;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.E"
},
{
"path": "src/Infrastructure/Messaging/Mediator/IMediator.cs",
"chars": 458,
"preview": "using System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Infrastructure.Messaging.Mediator\n{\n public interfac"
},
{
"path": "src/Infrastructure/Messaging/Mediator/Mediator.cs",
"chars": 1614,
"preview": "using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Application;\nusing Microsoft.Extensions.Depend"
},
{
"path": "src/Infrastructure/Messaging/Mediator/ServiceCollectionExtensions.cs",
"chars": 7929,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Reflection;\nusing Application;\nusing Inf"
},
{
"path": "src/Infrastructure/Persistence/AggregateNotFoundException.cs",
"chars": 123,
"preview": "using System;\n\nnamespace Infrastructure.Persistence\n{\n public class AggregateNotFoundException : Exception\n {\n "
},
{
"path": "src/Infrastructure/Persistence/ConcurrencyException.cs",
"chars": 285,
"preview": "using System;\n\nnamespace Infrastructure.Persistence\n{\n public class ConcurrencyException : Exception\n {\n pu"
},
{
"path": "src/Infrastructure/Persistence/EventStoreProductRepository.cs",
"chars": 950,
"preview": "using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Domain.ProductContext;\n\nnamespace Infrastructu"
},
{
"path": "src/Infrastructure/Persistence/IEventStore.cs",
"chars": 324,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Infrastructure.Persistence\n{\n "
},
{
"path": "src/Infrastructure/Persistence/InMemory/EventStore/InMemoryEventStore.cs",
"chars": 2950,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Infrastructure.Me"
},
{
"path": "src/Infrastructure/Persistence/InMemory/ServiceCollectionExtensions.cs",
"chars": 545,
"preview": "using Domain.ProductContext;\nusing Infrastructure.Persistence.InMemory.EventStore;\nusing Microsoft.Extensions.Dependency"
},
{
"path": "src/Infrastructure/Persistence/Mongo/EventStore/MongoDbEventStore.cs",
"chars": 2596,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Infrastructure.Me"
},
{
"path": "src/Infrastructure/Persistence/Mongo/IMongoDbContext.cs",
"chars": 187,
"preview": "using MongoDB.Driver;\n\nnamespace Infrastructure.Persistence.Mongo\n{\n public interface IMongoDbContext\n {\n I"
},
{
"path": "src/Infrastructure/Persistence/Mongo/MongoDbContext.cs",
"chars": 460,
"preview": "using MongoDB.Driver;\n\nnamespace Infrastructure.Persistence.Mongo\n{\n\n public sealed class MongoDbContext : IMongoDbCo"
},
{
"path": "src/Infrastructure/Persistence/Mongo/MongoDbProductRepository.cs",
"chars": 436,
"preview": "using System;\nusing Domain.ProductContext;\nusing Infrastructure.Messaging.Mediator;\n\nnamespace Infrastructure.Persistenc"
},
{
"path": "src/Infrastructure/Persistence/Mongo/MongoDbProvider.cs",
"chars": 1124,
"preview": "using MongoDB.Driver;\n\nnamespace Infrastructure.Persistence.Mongo\n{\n public class MongoDbProvider\n {\n publi"
},
{
"path": "src/Infrastructure/Persistence/Mongo/MongoDbRepository.cs",
"chars": 2654,
"preview": "using System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Domain;\nusing Domain.Abstraction;\nusing I"
},
{
"path": "src/Infrastructure/Persistence/Mongo/MongoSettings.cs",
"chars": 557,
"preview": "using System.Security.Authentication;\n\nnamespace Infrastructure.Persistence.Mongo\n{\n public class MongoSettings\n {"
},
{
"path": "src/Infrastructure/Persistence/Mongo/ServiceCollectionExtensions.cs",
"chars": 1439,
"preview": "using Domain.ProductContext;\nusing Infrastructure.Persistence.Mongo.EventStore;\nusing Microsoft.Extensions.Configuration"
},
{
"path": "src/Infrastructure/ServiceCollectionExtensions.cs",
"chars": 666,
"preview": "using Infrastructure.Persistence.InMemory;\nusing Infrastructure.Persistence.Mongo;\nusing Microsoft.Extensions.Configurat"
},
{
"path": "src/WebApi/Extensions/ApplicationBuilderExtensions.cs",
"chars": 2025,
"preview": "using System.Reflection;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.H"
},
{
"path": "src/WebApi/Extensions/OriginalNameCamelCaseContractResolver.cs",
"chars": 1125,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Newtonsoft.Json;\nusing Newtonsoft.Json.Seriali"
},
{
"path": "src/WebApi/Extensions/ServiceCollectionExtensions.cs",
"chars": 1370,
"preview": "using System;\nusing System.IO;\nusing System.Reflection;\nusing FluentValidation.AspNetCore;\nusing Microsoft.AspNetCore.Bu"
},
{
"path": "src/WebApi/Program.cs",
"chars": 728,
"preview": "using System.Threading.Tasks;\nusing Infrastructure;\nusing Infrastructure.Logging;\nusing Microsoft.AspNetCore;\nusing Mic"
},
{
"path": "src/WebApi/Properties/launchSettings.json",
"chars": 775,
"preview": "{\n \"$schema\": \"http://json.schemastore.org/launchsettings.json\",\n \"iisSettings\": {\n \"windowsAuthentication\": false"
},
{
"path": "src/WebApi/Startup.cs",
"chars": 804,
"preview": "using Infrastructure; \nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensio"
},
{
"path": "src/WebApi/UseCases/AddContentToProduct/AddContentToProductCommandValidator.cs",
"chars": 549,
"preview": "using Application.UseCases.AddContentToProduct;\nusing FluentValidation;\n\nnamespace WebApi.UseCases.AddContentToProduct\n{"
},
{
"path": "src/WebApi/UseCases/AddContentToProduct/ProductController.cs",
"chars": 719,
"preview": "using System.Threading;\nusing System.Threading.Tasks;\nusing Application.UseCases.AddContentToProduct;\nusing Infrastructu"
},
{
"path": "src/WebApi/UseCases/CreateProduct/CreateProductCommandValidator.cs",
"chars": 426,
"preview": "using Application.UseCases.CreateProduct;\nusing FluentValidation;\n\nnamespace WebApi.UseCases.CreateProduct\n{\n public "
},
{
"path": "src/WebApi/UseCases/CreateProduct/ProductsController.cs",
"chars": 701,
"preview": "using System.Threading;\nusing System.Threading.Tasks;\nusing Application.UseCases.CreateProduct;\nusing Infrastructure.Mes"
},
{
"path": "src/WebApi/WebApi.csproj",
"chars": 901,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n <PropertyGroup>\n <TargetFramework>netcoreapp3.0</TargetFramework>\n "
},
{
"path": "src/WebApi/app.config",
"chars": 129,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<configuration>\n <runtime>\n <gcServer enabled=\"true\"/>\n </runtime>\n</confi"
},
{
"path": "src/WebApi/appsettings.Development.json",
"chars": 137,
"preview": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Debug\",\n \"System\": \"Information\",\n \"Microsoft\": \"Informat"
},
{
"path": "src/WebApi/appsettings.Production.json",
"chars": 97,
"preview": "{\n \"Serilog\": {\n \"Console\": false,\n \"MinimumLevel\": {\n \"Default\": \"Error\"\n }\n }\n}"
},
{
"path": "src/WebApi/appsettings.Staging.json",
"chars": 97,
"preview": "{\n \"Serilog\": {\n \"Console\": false,\n \"MinimumLevel\": {\n \"Default\": \"Error\"\n }\n }\n}"
},
{
"path": "src/WebApi/appsettings.json",
"chars": 414,
"preview": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Warning\"\n }\n },\n \"Serilog\": {\n \"GraylogIp\": \"\",\n \"Graylo"
},
{
"path": "test/Application.Tests/Application.Tests.csproj",
"chars": 785,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>netcoreapp3.0</TargetFramework>\n\n "
},
{
"path": "test/Application.Tests/CommandHandlerTestBase.cs",
"chars": 403,
"preview": "using Microsoft.Extensions.DependencyInjection;\nusing TestBase;\n\nnamespace Application.Tests\n{\n public abstract class"
},
{
"path": "test/Application.Tests/CreateProductCommandHandlerTests.cs",
"chars": 1387,
"preview": "using System.Threading;\nusing System.Threading.Tasks;\nusing Application.UseCases.CreateProduct;\nusing Domain.ProductCont"
},
{
"path": "test/Domain.Specs/Domain.Specs.csproj",
"chars": 624,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>netcoreapp3.0</TargetFramework>\n\n "
},
{
"path": "test/Domain.Specs/ProductSpecs/WhenContentAddedToProduct.cs",
"chars": 2979,
"preview": "using System;\nusing System.Linq;\nusing Domain.ProductContext;\nusing Domain.ProductContext.Events;\nusing TestBase;\nusing "
},
{
"path": "test/Domain.Specs/ProductSpecs/WhenCreated.cs",
"chars": 859,
"preview": "using System;\nusing Domain.ProductContext;\nusing Domain.ProductContext.Events;\nusing TestBase;\nusing Xunit;\n\nnamespace D"
},
{
"path": "test/Domain.Specs/ProductSpecs/WhenProductApproved.cs",
"chars": 3685,
"preview": "using System;\nusing Domain.ProductContext;\nusing Domain.ProductContext.Events;\nusing TestBase;\nusing Xunit;\n\nnamespace D"
},
{
"path": "test/Domain.Specs/ProductSpecs/WhenVariantAddedToProduct.cs",
"chars": 2792,
"preview": "using System;\nusing System.Linq;\nusing Domain.ProductContext;\nusing Domain.ProductContext.Events;\nusing TestBase;\nusing "
},
{
"path": "test/Infrastructure.Tests/Infrastructure.Tests.csproj",
"chars": 707,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>netcoreapp3.0</TargetFramework>\n\n "
},
{
"path": "test/Infrastructure.Tests/MediatorTests.cs",
"chars": 1450,
"preview": "using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Application;\nusing Infrastructure.Messaging.Me"
},
{
"path": "test/TestBase/InMemoryDatabaseFixture.cs",
"chars": 2437,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Data;\nusing System.Linq.Expressions;\nusing ServiceStack.Orm"
},
{
"path": "test/TestBase/ScenarioFor.cs",
"chars": 1367,
"preview": "using FluentAssertions;\nusing KellermanSoftware.CompareNetObjects;\nusing System;\nusing System.Linq;\nusing Domain;\n\nname"
},
{
"path": "test/TestBase/ScenarioForExisting.cs",
"chars": 2397,
"preview": "using System;\nusing System.Linq;\nusing Domain;\nusing FluentAssertions;\nusing KellermanSoftware.CompareNetObjects;\n\nname"
},
{
"path": "test/TestBase/SpecBase.cs",
"chars": 1424,
"preview": "using System;\nusing System.Linq;\nusing AutoFixture;\nusing Domain;\n\nnamespace TestBase\n{\n public abstract class SpecB"
},
{
"path": "test/TestBase/TestBase.csproj",
"chars": 1180,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>netcoreapp3.0</TargetFramework>\n\n "
},
{
"path": "test/TestBase/TestBaseWithInMemoryDb.cs",
"chars": 529,
"preview": "using Xunit;\n\nnamespace TestBase\n{\n public abstract class TestBaseWithInMemoryDb : TestBaseWithIoC, IClassFixture<InM"
},
{
"path": "test/TestBase/TestBaseWithIoC.cs",
"chars": 3006,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Linq.Expressions;\nusing AutoFixture;\nusi"
}
]
About this extraction
This page contains the full source code of the CanerPatir/aspnet-core-clean-arch GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 89 files (113.0 KB), approximately 26.6k tokens, and a symbol index with 271 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.