├── .editorconfig ├── .github └── workflows │ ├── build-and-push-nuget.yaml │ └── build-and-test.yml ├── .gitignore ├── Directory.Build.props ├── Directory.Packages.props ├── LICENSE ├── README.md ├── Resrcify.SharedKernel.sln ├── samples └── Resrcify.SharedKernel.WebApiExample │ ├── .dockerignore │ ├── .editorconfig │ ├── .gitignore │ ├── Directory.Build.props │ ├── Dockerfile │ ├── README.md │ ├── Resrcify.SharedKernel.WebApiExample.sln │ ├── docker-compose.override.yml │ ├── docker-compose.yml │ ├── images │ ├── ArchitecturalTests.PNG │ ├── CompanyDatastore.PNG │ ├── ContactDatastore.PNG │ ├── ManualCachingOfQueries.PNG │ ├── ManualCreateCompany.PNG │ ├── ManualCreateContact.PNG │ ├── ManualGetAllCompanies.PNG │ ├── ManualGetCompanyById.PNG │ ├── ManualRemoveContactByEmail.PNG │ ├── ManualSendEventOnCompanyCreation.PNG │ ├── ManualSendEventOnCompanyNameChange.PNG │ ├── ManualUpdateCompanyName.PNG │ ├── ManualUpdateContactByEmail.PNG │ └── UnitTests.PNG │ ├── src │ ├── Resrcify.SharedKernel.WebApiExample.Application │ │ ├── Abstractions │ │ │ └── Repositories │ │ │ │ └── ICompanyRepository.cs │ │ ├── ApplicationServiceRegistration.cs │ │ ├── Features │ │ │ └── Companies │ │ │ │ ├── AddContact │ │ │ │ ├── AddContactCommand.cs │ │ │ │ ├── AddContactCommandHandler.cs │ │ │ │ └── AddContactCommandRequest.cs │ │ │ │ ├── CompanyCreated │ │ │ │ └── CompanyCreatedEventHandler.cs │ │ │ │ ├── CompanyNameUpdated │ │ │ │ └── CompanyNameUpdatedEventHandler.cs │ │ │ │ ├── CreateCompany │ │ │ │ ├── CreateCompanyCommand.cs │ │ │ │ ├── CreateCompanyCommandHandler.cs │ │ │ │ └── CreateCompanyCommandRequest.cs │ │ │ │ ├── GetAllCompanies │ │ │ │ ├── GetAllCompaniesQuery.cs │ │ │ │ ├── GetAllCompaniesQueryHandler.cs │ │ │ │ └── GetAllCompaniesQueryResponse.cs │ │ │ │ ├── GetCompanyById │ │ │ │ ├── GetCompanyByIdQuery.cs │ │ │ │ ├── GetCompanyByIdQueryHandler.cs │ │ │ │ └── GetCompanyByIdQueryResponse.cs │ │ │ │ ├── RemoveContact │ │ │ │ ├── RemoveContactCommand.cs │ │ │ │ ├── RemoveContactCommandHandler.cs │ │ │ │ └── RemoveContactCommandRequest.cs │ │ │ │ ├── UpdateCompanyName │ │ │ │ ├── UpdateCompanyNameCommand.cs │ │ │ │ ├── UpdateCompanyNameCommandHandler.cs │ │ │ │ └── UpdateCompanyNameCommandRequest.cs │ │ │ │ └── UpdateContactByEmail │ │ │ │ ├── UpdateContactByEmailCommand.cs │ │ │ │ ├── UpdateContactByEmailCommandHandler.cs │ │ │ │ └── UpdateContactByEmailCommandRequest.cs │ │ └── Resrcify.SharedKernel.WebApiExample.Application.csproj │ ├── Resrcify.SharedKernel.WebApiExample.Domain │ │ ├── AssemblyFlag.cs │ │ ├── Errors │ │ │ └── DomainErrors.Company.cs │ │ ├── Features │ │ │ └── Companies │ │ │ │ ├── Company.cs │ │ │ │ ├── Entities │ │ │ │ └── Contact.cs │ │ │ │ ├── Enums │ │ │ │ └── CompanyType.cs │ │ │ │ ├── Events │ │ │ │ ├── CompanyCreatedEvent.cs │ │ │ │ └── CompanyNameUpdatedEvent.cs │ │ │ │ └── ValueObjects │ │ │ │ ├── CompanyId.cs │ │ │ │ ├── ContactId.cs │ │ │ │ ├── Email.cs │ │ │ │ ├── Name.cs │ │ │ │ └── OrganizationNumber.cs │ │ └── Resrcify.SharedKernel.WebApiExample.Domain.csproj │ ├── Resrcify.SharedKernel.WebApiExample.Infrastructure │ │ ├── InfrastructureServiceRegistration.cs │ │ └── Resrcify.SharedKernel.WebApiExample.Infrastructure.csproj │ ├── Resrcify.SharedKernel.WebApiExample.Persistence │ │ ├── AppDbContext.cs │ │ ├── AppDbContextFactory.cs │ │ ├── Configurations │ │ │ └── Companies │ │ │ │ ├── CompanyConfiguration.cs │ │ │ │ └── ContactConfiguration.cs │ │ ├── Migrations │ │ │ ├── 20241021160202_InitialMigration.Designer.cs │ │ │ ├── 20241021160202_InitialMigration.cs │ │ │ └── AppDbContextModelSnapshot.cs │ │ ├── PersistenceServiceRegistration.cs │ │ ├── Repositories │ │ │ └── CompanyRepository.cs │ │ └── Resrcify.SharedKernel.WebApiExample.Persistence.csproj │ ├── Resrcify.SharedKernel.WebApiExample.Presentation │ │ ├── Controllers │ │ │ └── CompanyController.cs │ │ ├── PresentationServiceRegistration.cs │ │ └── Resrcify.SharedKernel.WebApiExample.Presentation.csproj │ └── Resrcify.SharedKernel.WebApiExample.Web │ │ ├── Program.cs │ │ ├── Properties │ │ └── launchSettings.json │ │ ├── Resrcify.SharedKernel.WebApiExample.Web.csproj │ │ ├── Startup.cs │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ └── tests │ ├── Resrcify.SharedKernel.WebApiExample.ArchitectureTests │ ├── Extensions │ │ └── NetArchTestExtensions.cs │ ├── Helpers │ │ └── BaseTest.cs │ ├── Resrcify.SharedKernel.WebApiExample.ArchitectureTests.csproj │ └── Tests │ │ ├── ApplicationTests.cs │ │ ├── DomainTests.cs │ │ ├── LayerTests.cs │ │ └── PresentationTests.cs │ └── Resrcify.SharedKernel.WebApiExample.Domain.UnitTests │ ├── Companies │ ├── CompanyIdsTests.cs │ ├── CompanyTests.cs │ ├── ContactIdTests.cs │ ├── ContactTests.cs │ ├── EmailTests.cs │ ├── NameTests.cs │ └── OrganizationNumberTests.cs │ └── Resrcify.SharedKernel.WebApiExample.Domain.UnitTests.csproj ├── src ├── Resrcify.SharedKernel.Caching │ ├── Abstractions │ │ └── ICachingService.cs │ ├── LICENSE │ ├── Primitives │ │ └── InMemoryCachingService.cs │ ├── README.md │ └── Resrcify.SharedKernel.Caching.csproj ├── Resrcify.SharedKernel.DomainDrivenDesign │ ├── Abstractions │ │ ├── IAggregateRoot.cs │ │ ├── IAuditableEntity.cs │ │ ├── IDeletableEntity.cs │ │ └── IDomainEvent.cs │ ├── LICENSE │ ├── Primitives │ │ ├── AggregateRoot.cs │ │ ├── DomainEvent.cs │ │ ├── Entity.cs │ │ ├── Enumeration.cs │ │ └── ValueObject.cs │ ├── README.md │ └── Resrcify.SharedKernel.DomainDrivenDesign.csproj ├── Resrcify.SharedKernel.Messaging │ ├── Abstractions │ │ ├── ICachingQuery.cs │ │ ├── ICommand.cs │ │ ├── ICommandHandler.cs │ │ ├── IDomainEventHandler.cs │ │ ├── IQuery.cs │ │ ├── IQueryHandler.cs │ │ └── ITransactionCommand.cs │ ├── Behaviors │ │ ├── CachingPipelineBehavior.cs │ │ ├── LoggingPipelineBehavior.cs │ │ ├── TransactionPipelineBehavior.cs │ │ ├── UnitOfWorkPipelineBehavior.cs │ │ └── ValidationPipelineBehavior.cs │ ├── LICENSE │ ├── README.md │ └── Resrcify.SharedKernel.Messaging.csproj ├── Resrcify.SharedKernel.Repository │ ├── Abstractions │ │ └── IRepository.cs │ ├── Extensions │ │ └── QueryableExtensions.cs │ ├── LICENSE │ ├── Primitives │ │ ├── Repository.cs │ │ ├── ResultRepository.cs │ │ ├── Specification.cs │ │ └── SpecificationEvaluator.cs │ ├── README.md │ └── Resrcify.SharedKernel.Repository.csproj ├── Resrcify.SharedKernel.ResultFramework │ ├── LICENSE │ ├── Primitives │ │ ├── Error.cs │ │ ├── ErrorType.cs │ │ ├── Result.cs │ │ ├── ResultExtensions.cs │ │ └── ResultT.cs │ ├── README.md │ └── Resrcify.SharedKernel.ResultFramework.csproj ├── Resrcify.SharedKernel.UnitOfWork │ ├── Abstractions │ │ └── IUnitOfWork.cs │ ├── BackgroundJobs │ │ ├── ProcessOutboxMessagesJob.cs │ │ ├── ProcessOutboxMessagesJobSetup.cs │ │ ├── ProcessOutboxMessagesNewtonsoftJob.cs │ │ └── ProcessOutboxMessagesNewtonsoftJobSetup.cs │ ├── Converters │ │ └── DomainEventConverter.cs │ ├── Extensions │ │ └── MigrationExtensions.cs │ ├── Interceptors │ │ ├── InsertOutboxMessagesInterceptor.cs │ │ ├── InsertOutboxMessagesNewtonsoftInterceptor.cs │ │ ├── UpdateAuditableEntitiesInterceptor.cs │ │ └── UpdateDeletableEntitiesInterceptor.cs │ ├── LICENSE │ ├── Outbox │ │ └── OutboxMessage.cs │ ├── Primitives │ │ └── UnitOfWork.cs │ ├── README.md │ └── Resrcify.SharedKernel.UnitOfWork.csproj └── Resrcify.SharedKernel.Web │ ├── Extensions │ ├── InternalControllersExtensions.cs │ └── ResultExtensions.cs │ ├── LICENSE │ ├── Primitives │ └── ApiController.cs │ ├── README.md │ └── Resrcify.SharedKernel.Web.csproj └── tests ├── Resrcify.SharedKernel.Caching.UnitTests ├── Primitives │ └── DistributedCachingServiceTests.cs └── Resrcify.SharedKernel.Caching.UnitTests.csproj ├── Resrcify.SharedKernel.DomainDrivenDesign.UnitTests ├── Primitives │ ├── AggregateRootTests.cs │ ├── DomainEventTests.cs │ ├── EntityTests.cs │ ├── EnumerationTests.cs │ └── ValueObjectTests.cs └── Resrcify.SharedKernel.DomainDrivenDesign.UnitTests.csproj ├── Resrcify.SharedKernel.Messaging.UnitTests ├── Behaviors │ ├── CachingPipelineBehaviorTests.cs │ ├── LoggingPipelineBehaviorTests.cs │ ├── TransactionPipelineBehaviorTests.cs │ ├── UnitOfWorkPipelineBehaviorTests.cs │ └── ValidationPipelineBehaviorTests.cs └── Resrcify.SharedKernel.Messaging.UnitTests.csproj ├── Resrcify.SharedKernel.Repository.UnitTests ├── Extensions │ └── QueryableExtensionsTests.cs ├── Models │ ├── Child.cs │ ├── DbSetupBase.cs │ ├── Person.cs │ ├── PersonSpecification.cs │ ├── SocialSecurityNumber.cs │ ├── TestDbContext.cs │ └── TestRepository.cs ├── Primitives │ ├── RepositoryTests.cs │ ├── SpecificationEvaluatorTests.cs │ └── SpecificationTests.cs └── Resrcify.SharedKernel.Repository.UnitTests.csproj ├── Resrcify.SharedKernel.ResultFramework.UnitTests ├── Primitives │ ├── ErrorTests.cs │ ├── ResultExtensionsTests.cs │ ├── ResultTTests.cs │ └── ResultTests.cs └── Resrcify.SharedKernel.ResultFramework.UnitTests.csproj ├── Resrcify.SharedKernel.UnitOfWork.UnitTests ├── BackgroundJobs │ └── ProcessOutboxMessagesJobTests.cs ├── Interceptors │ ├── InsertOutboxMessagesInterceptorTests.cs │ ├── UpdateAuditableEntitiesInterceptorTests.cs │ └── UpdateDeletableEntitiesInterceptorTests.cs ├── Models │ ├── Child.cs │ ├── DbSetupBase.cs │ ├── Person.cs │ ├── SocialSecurityNumber.cs │ ├── TestDbContext.cs │ └── TestDomainEvent.cs ├── Primitives │ └── UnitOfWorkTests.cs └── Resrcify.SharedKernel.UnitOfWork.UnitTests.csproj └── Resrcify.SharedKernel.Web.UnitTests ├── Extensions ├── InternalControllersExtensionTests.cs └── ResultExtensionsTests.cs ├── Primitives └── ApiControllerTests.cs └── Resrcify.SharedKernel.Web.UnitTests.csproj /.github/workflows/build-and-push-nuget.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "[0-9]+.[0-9]+.[0-9]+" 5 | jobs: 6 | tests: 7 | uses: ./.github/workflows/build-and-test.yml 8 | docker: 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 15 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Verify commit exists in origin/master 16 | run: | 17 | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* 18 | git branch --remote --contains | grep origin/master 19 | 20 | - name: Set VERSION variable from tag 21 | run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 22 | - name: Setup .NET 23 | uses: actions/setup-dotnet@v3 24 | with: 25 | dotnet-version: | 26 | 8.x.x 27 | 9.x.x 28 | - name: Build 29 | run: dotnet build --configuration Release /p:Version=${VERSION} 30 | - name: Pack 31 | run: dotnet pack --configuration Release /p:Version=${VERSION} --no-build --output . 32 | - name: List generated packages 33 | run: ls *.nupkg 34 | - name: Push Packages to NuGet 35 | run: | 36 | for package in *.nupkg; do 37 | dotnet nuget push "$package" --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} 38 | done 39 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: build-and-test 5 | 6 | on: 7 | push: 8 | branches: ["master"] 9 | pull_request: 10 | branches: ["master"] 11 | workflow_call: 12 | 13 | jobs: 14 | build: 15 | name: test 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v3 22 | with: 23 | dotnet-version: | 24 | 8.x.x 25 | 9.x.x 26 | - name: Restore dependencies 27 | run: dotnet restore 28 | - name: Build 29 | run: dotnet build --no-restore 30 | - name: Test 31 | run: dotnet test --no-build --verbosity normal 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # From .NET Core 3.0 you can use the command: `dotnet new gitignore` to generate a customizable .gitignore file 2 | 3 | *.swp 4 | *.*~ 5 | project.lock.json 6 | .DS_Store 7 | *.pyc 8 | 9 | # Visual Studio Code 10 | .vscode 11 | 12 | # User-specific files 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | build/ 26 | bld/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | msbuild.log 30 | msbuild.err 31 | msbuild.wrn 32 | 33 | # Visual Studio 2015 34 | .vs/ -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | Rickard Marjanovic (@rmarjanovic) 5 | Resrcify AB 6 | enable 7 | disable 8 | latest 9 | all 10 | true 11 | true 12 | true 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Resrcify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resrcify.SharedKernel 2 | 3 | # Description 4 | Implemented a Clean Architecture Shared Kernel, including the building blocks for Domain Driven Design and the Result pattern, to be used in internal projects. 5 | The different modules are seperated into different NuGet packages using pipelines and published on the public package manager for .NET. 6 | 7 | # Table of Contents 8 | ## Modules 9 | - [Caching](src/Resrcify.SharedKernel.Caching/) 10 | - [Domain-Driven Design](src/Resrcify.SharedKernel.DomainDrivenDesign/) 11 | - [Messaging](src/Resrcify.SharedKernel.Messaging/) 12 | - [Result Framework](src/Resrcify.SharedKernel.ResultFramework/) 13 | - [Repository](src/Resrcify.SharedKernel.Repository/) 14 | - [Unit Of Work](src/Resrcify.SharedKernel.UnitOfWork/) 15 | - [Web](src/Resrcify.SharedKernel.Web/) 16 | ## Samples 17 | - [Web Api Example](samples/Resrcify.SharedKernel.WebApiExample/) 18 | 19 | ## Contributions 20 | First off, thank you for considering contributing to this project. We appreciate any contributions, from reporting issues to writing code, improving documentation, and suggesting new features. 21 | 22 | ### Contribution Process 23 | 24 | 1. **Fork the repository**: Click the "Fork" button at the top of the repository page. 25 | 2. **Clone your fork**: 26 | ```bash 27 | git clone https://github.com/Resrcify/Resrcify.SharedKernel.git 28 | ``` 29 | 3. **Create a branch** for your changes: 30 | ```bash 31 | git checkout -b feature/your-feature-name 32 | ``` 33 | 4. **Make your changes**: Ensure your code follows the project’s coding style by implementing / using the .editorconfig as provided in this repository. 34 | 5. **Write tests** (if applicable) to ensure the functionality works as expected. 35 | 6. **Commit your changes**: Write clear and concise commit messages. 36 | ```bash 37 | git commit -m "Add feature or fix bug" 38 | ``` 39 | 7. **Push your branch**: 40 | ```bash 41 | git push origin feature/your-feature-name 42 | ``` 43 | 8. **Create a pull request**: Submit a PR to the `master` branch on the original repository. In your PR description, explain the changes you’ve made and reference any related issues. 44 | 45 | # Credits 46 | Inspired by [Milan Jovanovic](https://www.youtube.com/@MilanJovanovicTech)'s Clean Architecture series to create this Shared Kernel. -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/bin 15 | **/charts 16 | **/docker-compose* 17 | **/compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | README.md 25 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/.gitignore: -------------------------------------------------------------------------------- 1 | # From .NET Core 3.0 you can use the command: `dotnet new gitignore` to generate a customizable .gitignore file 2 | 3 | *.swp 4 | *.*~ 5 | project.lock.json 6 | .DS_Store 7 | *.pyc 8 | *.db 9 | # Visual Studio Code 10 | .vscode 11 | 12 | # User-specific files 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | build/ 26 | bld/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | msbuild.log 30 | msbuild.err 31 | msbuild.wrn 32 | 33 | # Visual Studio 2015 34 | .vs/ -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | enable 5 | disable 6 | false 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS base 2 | WORKDIR /app 3 | EXPOSE 11000 4 | 5 | ENV ASPNETCORE_URLS=http://+:11000 6 | ENV ASPNETCORE_ENVIRONMENT ASPNETCORE_ENVIRONMENT 7 | 8 | # Creates a non-root user with an explicit UID and adds permission to access the /app folder 9 | # For more info, please refer to https://aka.ms/vscode-docker-dotnet-configure-containers 10 | RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app 11 | USER appuser 12 | 13 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 14 | 15 | # copy all the layers' csproj files into respective folders 16 | COPY ["src/Resrcify.SharedKernel.WebApiExample.Domain/Resrcify.SharedKernel.WebApiExample.Domain.csproj", "Resrcify.SharedKernel.WebApiExample.Domain/"] 17 | COPY ["src/Resrcify.SharedKernel.WebApiExample.Application/Resrcify.SharedKernel.WebApiExample.Application.csproj", "Resrcify.SharedKernel.WebApiExample.Application/"] 18 | COPY ["src/Resrcify.SharedKernel.WebApiExample.Infrastructure/Resrcify.SharedKernel.WebApiExample.Infrastructure.csproj", "Resrcify.SharedKernel.WebApiExample.Infrastructure/"] 19 | COPY ["src/Resrcify.SharedKernel.WebApiExample.Persistence/Resrcify.SharedKernel.WebApiExample.Persistence.csproj", "Resrcify.SharedKernel.WebApiExample.Persistence/"] 20 | COPY ["src/Resrcify.SharedKernel.WebApiExample.Presentation/Resrcify.SharedKernel.WebApiExample.Presentation.csproj", "Resrcify.SharedKernel.WebApiExample.Presentation/"] 21 | COPY ["src/Resrcify.SharedKernel.WebApiExample.Web/Resrcify.SharedKernel.WebApiExample.Web.csproj", "Resrcify.SharedKernel.WebApiExample.Web/"] 22 | 23 | RUN dotnet restore "Resrcify.SharedKernel.WebApiExample.Web/Resrcify.SharedKernel.WebApiExample.Web.csproj" 24 | 25 | # WORKDIR /app 26 | COPY . . 27 | RUN dotnet build -c Release --property:OutputPath=/app/build 28 | 29 | FROM build AS publish 30 | RUN dotnet publish -c Release --property:PublishDir=/app/publish 31 | 32 | FROM base AS final 33 | WORKDIR /app 34 | COPY --from=publish /app/publish . 35 | ENTRYPOINT ["dotnet", "Resrcify.SharedKernel.WebApiExample.Web.dll"] 36 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | services: 2 | webapiexample: 3 | environment: 4 | - ASPNETCORE_ENVIRONMENT=Development 5 | build: 6 | context: ./ 7 | dockerfile: Dockerfile 8 | ports: 9 | - 11000:11000 10 | 11 | webapiexampledb: 12 | ports: 13 | - 5440:5432 14 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | webapiexample: 3 | image: webapiexample:latest 4 | container_name: Resrcify.WebApiExample 5 | restart: always 6 | depends_on: 7 | - webapiexampledb 8 | 9 | webapiexampledb: 10 | image: postgres:latest 11 | container_name: WebApiExampleDb 12 | cap_add: 13 | - SYS_NICE # CAP_SYS_NICE 14 | environment: 15 | - POSTGRES_DB=AppDb 16 | - POSTGRES_USER=ExampleUser 17 | - POSTGRES_PASSWORD=testingStuffOut 18 | restart: always 19 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/ArchitecturalTests.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/ArchitecturalTests.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/CompanyDatastore.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/CompanyDatastore.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/ContactDatastore.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/ContactDatastore.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/ManualCachingOfQueries.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/ManualCachingOfQueries.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/ManualCreateCompany.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/ManualCreateCompany.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/ManualCreateContact.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/ManualCreateContact.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/ManualGetAllCompanies.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/ManualGetAllCompanies.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/ManualGetCompanyById.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/ManualGetCompanyById.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/ManualRemoveContactByEmail.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/ManualRemoveContactByEmail.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/ManualSendEventOnCompanyCreation.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/ManualSendEventOnCompanyCreation.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/ManualSendEventOnCompanyNameChange.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/ManualSendEventOnCompanyNameChange.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/ManualUpdateCompanyName.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/ManualUpdateCompanyName.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/ManualUpdateContactByEmail.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/ManualUpdateContactByEmail.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/images/UnitTests.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/images/UnitTests.PNG -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Abstractions/Repositories/ICompanyRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Resrcify.SharedKernel.Repository.Abstractions; 4 | using Resrcify.SharedKernel.ResultFramework.Primitives; 5 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies; 6 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 7 | 8 | namespace Resrcify.SharedKernel.WebApiExample.Application.Abstractions.Repositories; 9 | public interface ICompanyRepository 10 | : IRepository 11 | { 12 | Task> GetCompanyAggregateByIdAsync( 13 | CompanyId companyId, 14 | CancellationToken cancellationToken = default); 15 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/ApplicationServiceRegistration.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using FluentValidation; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Resrcify.SharedKernel.Messaging.Behaviors; 5 | 6 | namespace Resrcify.SharedKernel.WebApiExample.Application; 7 | 8 | public static class ApplicationServiceRegistration 9 | { 10 | public static IServiceCollection AddApplicationServices(this IServiceCollection services) 11 | { 12 | services.AddMediatR(config => 13 | { 14 | config.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly()); 15 | config.AddOpenBehavior(typeof(LoggingPipelineBehavior<,>)); 16 | config.AddOpenBehavior(typeof(UnitOfWorkPipelineBehavior<,>)); 17 | config.AddOpenBehavior(typeof(CachingPipelineBehavior<,>)); 18 | }); 19 | 20 | services.AddValidatorsFromAssembly( 21 | Assembly.GetExecutingAssembly(), 22 | includeInternalTypes: true); 23 | 24 | return services; 25 | } 26 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/AddContact/AddContactCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.Messaging.Abstractions; 3 | 4 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.AddContact; 5 | 6 | public sealed record AddContactCommand( 7 | Guid CompanyId, 8 | string FirstName, 9 | string LastName, 10 | string Email) 11 | : ICommand; -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/AddContact/AddContactCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Resrcify.SharedKernel.Messaging.Abstractions; 4 | using Resrcify.SharedKernel.ResultFramework.Primitives; 5 | using Resrcify.SharedKernel.WebApiExample.Application.Abstractions.Repositories; 6 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 7 | 8 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.AddContact; 9 | 10 | internal sealed class AddContactCommandHandler( 11 | ICompanyRepository _companyRepository) 12 | : ICommandHandler 13 | { 14 | public async Task Handle( 15 | AddContactCommand request, 16 | CancellationToken cancellationToken) 17 | => await CompanyId 18 | .Create(request.CompanyId) 19 | .Bind(companyId => _companyRepository.GetCompanyAggregateByIdAsync( 20 | companyId, 21 | cancellationToken)) 22 | .Tap(company => company.AddContact( 23 | request.FirstName, 24 | request.LastName, 25 | request.Email)); 26 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/AddContact/AddContactCommandRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.AddContact; 2 | 3 | public sealed record AddContactCommandRequest( 4 | string FirstName, 5 | string LastName, 6 | string Email); -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/CompanyCreated/CompanyCreatedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using Resrcify.SharedKernel.Messaging.Abstractions; 5 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.Events; 6 | 7 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.CompanyCreated; 8 | 9 | internal sealed class CompanyCreatedEventHandler( 10 | ILogger _logger) 11 | : IDomainEventHandler 12 | { 13 | public Task Handle( 14 | CompanyCreatedEvent notification, 15 | CancellationToken cancellationToken) 16 | { 17 | _logger.LogInformation("Company created: {CompanyId}", notification.CompanyId); 18 | 19 | return Task.CompletedTask; 20 | } 21 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/CompanyNameUpdated/CompanyNameUpdatedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using Resrcify.SharedKernel.Messaging.Abstractions; 5 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.Events; 6 | 7 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.CompanyNameUpdated; 8 | 9 | internal sealed class CompanyNameUpdatedEventHandler( 10 | ILogger _logger) 11 | : IDomainEventHandler 12 | { 13 | public Task Handle( 14 | CompanyNameUpdatedEvent notification, 15 | CancellationToken cancellationToken) 16 | { 17 | _logger.LogInformation("Company name updated: {CompanyId}", notification.CompanyId); 18 | 19 | return Task.CompletedTask; 20 | } 21 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/CreateCompany/CreateCompanyCommand.cs: -------------------------------------------------------------------------------- 1 | using Resrcify.SharedKernel.Messaging.Abstractions; 2 | 3 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.CreateCompany; 4 | 5 | public sealed record CreateCompanyCommand( 6 | string Name, 7 | string OrganizationNumber) 8 | : ICommand; -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/CreateCompany/CreateCompanyCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Resrcify.SharedKernel.Messaging.Abstractions; 5 | using Resrcify.SharedKernel.ResultFramework.Primitives; 6 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies; 7 | using Resrcify.SharedKernel.WebApiExample.Application.Abstractions.Repositories; 8 | using Resrcify.SharedKernel.WebApiExample.Domain.Errors; 9 | 10 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.CreateCompany; 11 | 12 | internal sealed class CreateCompanyCommandHandler( 13 | ICompanyRepository _companyRepository) 14 | : ICommandHandler 15 | { 16 | public async Task Handle( 17 | CreateCompanyCommand command, 18 | CancellationToken cancellationToken) 19 | { 20 | var newCompany = Company.Create( 21 | Guid.NewGuid(), 22 | command.Name, 23 | command.OrganizationNumber); 24 | 25 | if (newCompany.IsFailure) 26 | return newCompany; 27 | 28 | var oldCompany = await _companyRepository.FirstOrDefaultAsync( 29 | company => company.OrganizationNumber == newCompany.Value.OrganizationNumber, 30 | cancellationToken); 31 | 32 | if (oldCompany is not null) 33 | return DomainErrors.Company.OrganizationNumberAlreadyExist(command.OrganizationNumber); 34 | 35 | await _companyRepository.AddAsync(newCompany.Value, cancellationToken); 36 | 37 | return Result.Success(); 38 | } 39 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/CreateCompany/CreateCompanyCommandRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.CreateCompany; 2 | 3 | public sealed record CreateCompanyCommandRequest( 4 | string Name, 5 | string OrganizationNumber); -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/GetAllCompanies/GetAllCompaniesQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.Messaging.Abstractions; 3 | 4 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.GetAllCompanies; 5 | 6 | public sealed record GetAllCompaniesQuery : ICachingQuery 7 | { 8 | public string? CacheKey { get; set; } = "Companies"; 9 | public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(5); 10 | } 11 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/GetAllCompanies/GetAllCompaniesQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Resrcify.SharedKernel.Messaging.Abstractions; 5 | using Resrcify.SharedKernel.ResultFramework.Primitives; 6 | using Resrcify.SharedKernel.WebApiExample.Application.Abstractions.Repositories; 7 | 8 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.GetAllCompanies; 9 | 10 | internal sealed class GetAllCompaniesQueryHandler( 11 | ICompanyRepository _companyRepository) 12 | : IQueryHandler 13 | { 14 | public async Task> Handle( 15 | GetAllCompaniesQuery request, 16 | CancellationToken cancellationToken) 17 | { 18 | var allCompanies = _companyRepository.GetAllAsync(); 19 | var allCompaniesMaterialized = await allCompanies.ToListAsync(cancellationToken); 20 | var companyDtos = allCompaniesMaterialized.Select(company => new CompanyDto( 21 | company.Id.Value, 22 | company.Name.Value, 23 | company.OrganizationNumber.Value.ToString())); 24 | return new GetAllCompaniesQueryResponse(companyDtos); 25 | } 26 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/GetAllCompanies/GetAllCompaniesQueryResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.GetAllCompanies; 5 | 6 | public sealed record GetAllCompaniesQueryResponse(IEnumerable Companies); 7 | 8 | public sealed record CompanyDto( 9 | Guid Id, 10 | string Name, 11 | string OrganizationNumber); -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/GetCompanyById/GetCompanyByIdQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.Messaging.Abstractions; 3 | 4 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.GetCompanyById; 5 | 6 | public sealed record GetCompanyByIdQuery( 7 | Guid CompanyId) 8 | : ICachingQuery 9 | { 10 | public string? CacheKey { get; set; } = $"Company-{CompanyId}"; 11 | public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(5); 12 | } 13 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/GetCompanyById/GetCompanyByIdQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Resrcify.SharedKernel.Messaging.Abstractions; 4 | using Resrcify.SharedKernel.ResultFramework.Primitives; 5 | using Resrcify.SharedKernel.WebApiExample.Application.Abstractions.Repositories; 6 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 7 | using System.Linq; 8 | 9 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.GetCompanyById; 10 | 11 | internal sealed class GetAllCompaniesQueryHandler( 12 | ICompanyRepository _companyRepository) 13 | : IQueryHandler 14 | { 15 | public async Task> Handle( 16 | GetCompanyByIdQuery request, 17 | CancellationToken cancellationToken) 18 | => await CompanyId 19 | .Create(request.CompanyId) 20 | .Bind(companyId => _companyRepository.GetCompanyAggregateByIdAsync(companyId, cancellationToken)) 21 | .Map(company => new GetCompanyByIdQueryResponse( 22 | company!.Id.Value, 23 | company.Name.Value, 24 | company.OrganizationNumber.Value.ToString(), 25 | company.Contacts.Select(contact => 26 | new ContactDto( 27 | contact.FirstName.Value, 28 | contact.LastName.Value, 29 | contact.Email.Value)))); 30 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/GetCompanyById/GetCompanyByIdQueryResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.GetCompanyById; 5 | 6 | public record GetCompanyByIdQueryResponse( 7 | Guid Id, 8 | string Name, 9 | string OrganizationNumber, 10 | IEnumerable Contacts); 11 | 12 | public record ContactDto( 13 | string FirstName, 14 | string LastName, 15 | string Email); -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/RemoveContact/RemoveContactCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.Messaging.Abstractions; 3 | 4 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.RemoveContact; 5 | 6 | public sealed record RemoveContactCommand( 7 | Guid CompanyId, 8 | string Email) 9 | : ICommand; -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/RemoveContact/RemoveContactCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Resrcify.SharedKernel.Messaging.Abstractions; 4 | using Resrcify.SharedKernel.ResultFramework.Primitives; 5 | using Resrcify.SharedKernel.WebApiExample.Application.Abstractions.Repositories; 6 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 7 | 8 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.RemoveContact; 9 | 10 | internal sealed class RemoveContactCommandHandler( 11 | ICompanyRepository _companyRepository) 12 | : ICommandHandler 13 | { 14 | public async Task Handle( 15 | RemoveContactCommand request, 16 | CancellationToken cancellationToken) 17 | => await CompanyId 18 | .Create(request.CompanyId) 19 | .Bind(companyId => _companyRepository.GetCompanyAggregateByIdAsync( 20 | companyId, 21 | cancellationToken)) 22 | .Tap(company => company.RemoveContactByEmail(request.Email)); 23 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/RemoveContact/RemoveContactCommandRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.RemoveContact; 2 | 3 | public sealed record RemoveContactCommandRequest( 4 | string Email); -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/UpdateCompanyName/UpdateCompanyNameCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.Messaging.Abstractions; 3 | 4 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.UpdateCompanyName; 5 | 6 | public sealed record UpdateCompanyNameCommand( 7 | Guid CompanyId, 8 | string Name) 9 | : ICommand; -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/UpdateCompanyName/UpdateCompanyNameCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Resrcify.SharedKernel.Messaging.Abstractions; 4 | using Resrcify.SharedKernel.ResultFramework.Primitives; 5 | using Resrcify.SharedKernel.WebApiExample.Application.Abstractions.Repositories; 6 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 7 | 8 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.UpdateCompanyName; 9 | 10 | internal sealed class UpdateCompanyNameCommandHandler( 11 | ICompanyRepository _companyRepository) 12 | : ICommandHandler 13 | { 14 | public async Task Handle( 15 | UpdateCompanyNameCommand request, 16 | CancellationToken cancellationToken) 17 | => await CompanyId 18 | .Create(request.CompanyId) 19 | .Bind(companyId => _companyRepository.GetCompanyAggregateByIdAsync( 20 | companyId, 21 | cancellationToken)) 22 | .Tap(company => company.UpdateName(request.Name)); 23 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/UpdateCompanyName/UpdateCompanyNameCommandRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.UpdateCompanyName; 2 | 3 | public sealed record UpdateCompanyNameCommandRequest( 4 | string Name); -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/UpdateContactByEmail/UpdateContactByEmailCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.Messaging.Abstractions; 3 | 4 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.UpdateContactByEmail; 5 | 6 | public sealed record UpdateContactByEmailCommand( 7 | Guid CompanyId, 8 | string NewFirstName, 9 | string NewLastName, 10 | string Email) 11 | : ICommand; -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/UpdateContactByEmail/UpdateContactByEmailCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Resrcify.SharedKernel.Messaging.Abstractions; 4 | using Resrcify.SharedKernel.ResultFramework.Primitives; 5 | using Resrcify.SharedKernel.WebApiExample.Application.Abstractions.Repositories; 6 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 7 | 8 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.UpdateContactByEmail; 9 | 10 | internal sealed class UpdateContactByEmailCommandHandler( 11 | ICompanyRepository _companyRepository) 12 | : ICommandHandler 13 | { 14 | public async Task Handle( 15 | UpdateContactByEmailCommand request, 16 | CancellationToken cancellationToken) 17 | => await CompanyId 18 | .Create(request.CompanyId) 19 | .Bind(companyId => _companyRepository.GetCompanyAggregateByIdAsync( 20 | companyId, 21 | cancellationToken)) 22 | .Tap(company => company.UpdateContactByEmail( 23 | request.Email, 24 | request.NewFirstName, 25 | request.NewLastName)); 26 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Features/Companies/UpdateContactByEmail/UpdateContactByEmailCommandRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Resrcify.SharedKernel.WebApiExample.Application.Features.Companies.UpdateContactByEmail; 2 | 3 | public sealed record UpdateContactByEmailCommandRequest( 4 | string NewFirstName, 5 | string NewLastName, 6 | string Email); -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Application/Resrcify.SharedKernel.WebApiExample.Application.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | false 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Domain/AssemblyFlag.cs: -------------------------------------------------------------------------------- 1 | namespace Resrcify.SharedKernel.WebApiExample.Domain; 2 | 3 | public class AssemblyFlag 4 | { 5 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Domain/Features/Companies/Entities/Contact.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 3 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 4 | using Resrcify.SharedKernel.ResultFramework.Primitives; 5 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 6 | 7 | namespace Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.Entities; 8 | 9 | public sealed class Contact 10 | : Entity, 11 | IAuditableEntity 12 | { 13 | private Contact( 14 | ContactId id, 15 | CompanyId companyId, 16 | Name firstName, 17 | Name lastName, 18 | Email email) 19 | : base(id) 20 | { 21 | CompanyId = companyId; 22 | FirstName = firstName; 23 | LastName = lastName; 24 | Email = email; 25 | } 26 | public CompanyId CompanyId { get; private set; } 27 | public Name FirstName { get; private set; } 28 | public Name LastName { get; private set; } 29 | public Email Email { get; private set; } 30 | public DateTime CreatedOnUtc { get; } 31 | public DateTime ModifiedOnUtc { get; } 32 | 33 | public static Result Create( 34 | CompanyId companyId, 35 | string firstName, 36 | string lastName, 37 | string email) 38 | => Result 39 | .Combine( 40 | ContactId.Create(Guid.NewGuid()), 41 | Name.Create(firstName), 42 | Name.Create(lastName), 43 | Email.Create(email)) 44 | .Map(c => new Contact( 45 | c.Item1, 46 | companyId, 47 | c.Item2, 48 | c.Item3, 49 | c.Item4)); 50 | public Result Update( 51 | string firstName, 52 | string lastName) 53 | => Result 54 | .Combine( 55 | Name.Create(firstName), 56 | Name.Create(lastName)) 57 | .Tap(c => FirstName = c.Item1) 58 | .Tap(c => LastName = c.Item2); 59 | 60 | 61 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Domain/Features/Companies/Enums/CompanyType.cs: -------------------------------------------------------------------------------- 1 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 2 | 3 | namespace Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.Enums; 4 | 5 | public sealed class CompanyType : Enumeration 6 | { 7 | public static readonly CompanyType Unknown = new(0, "Unknown"); 8 | public static readonly CompanyType DeceasedEstate = new(1, "Deceased estate"); 9 | public static readonly CompanyType GovernmentEntity = new(2, "Government entities (State, region, municipalitie, parish)"); 10 | public static readonly CompanyType ForeignCompany = new(3, "Foreign company"); 11 | public static readonly CompanyType LimitedCompany = new(5, "Limited company"); 12 | public static readonly CompanyType EconomicAssociation = new(7, "Economic association, housing cooperative, and community association"); 13 | public static readonly CompanyType NonProfitOrganization = new(8, "Non-profit organization"); 14 | public static readonly CompanyType Foundations = new(9, "Foundation"); 15 | 16 | private CompanyType(int value, string name) 17 | : base(value, name) 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Domain/Features/Companies/Events/CompanyCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 3 | 4 | namespace Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.Events; 5 | 6 | public sealed record CompanyCreatedEvent( 7 | Guid Id, 8 | Guid CompanyId) 9 | : DomainEvent(Id); -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Domain/Features/Companies/Events/CompanyNameUpdatedEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 3 | 4 | namespace Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.Events; 5 | 6 | public sealed record CompanyNameUpdatedEvent( 7 | Guid Id, 8 | Guid CompanyId, 9 | string OldName, 10 | string NewName) 11 | : DomainEvent(Id); -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Domain/Features/Companies/ValueObjects/CompanyId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 4 | using Resrcify.SharedKernel.ResultFramework.Primitives; 5 | using Resrcify.SharedKernel.WebApiExample.Domain.Errors; 6 | 7 | namespace Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 8 | 9 | public sealed class CompanyId : ValueObject 10 | { 11 | public Guid Value { get; } 12 | private CompanyId(Guid value) 13 | => Value = value; 14 | 15 | public static Result Create(Guid value) 16 | => Result 17 | .Ensure( 18 | value, 19 | value => !value.Equals(Guid.Empty), 20 | DomainErrors.CompanyId.Empty) 21 | .Map(value => new CompanyId(value)); 22 | 23 | public override IEnumerable GetAtomicValues() 24 | { 25 | yield return Value; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Domain/Features/Companies/ValueObjects/ContactId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 4 | using Resrcify.SharedKernel.ResultFramework.Primitives; 5 | using Resrcify.SharedKernel.WebApiExample.Domain.Errors; 6 | 7 | namespace Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 8 | 9 | public sealed class ContactId : ValueObject 10 | { 11 | public Guid Value { get; } 12 | private ContactId(Guid value) 13 | => Value = value; 14 | 15 | public static Result Create(Guid value) 16 | => Result 17 | .Ensure( 18 | value, 19 | value => !value.Equals(Guid.Empty), 20 | DomainErrors.CompanyId.Empty) 21 | .Map(value => new ContactId(value)); 22 | 23 | public override IEnumerable GetAtomicValues() 24 | { 25 | yield return Value; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Domain/Features/Companies/ValueObjects/Email.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.RegularExpressions; 3 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 4 | using Resrcify.SharedKernel.ResultFramework.Primitives; 5 | using Resrcify.SharedKernel.WebApiExample.Domain.Errors; 6 | 7 | namespace Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 8 | 9 | public sealed class Email : ValueObject 10 | { 11 | public const int MaxLength = 256; 12 | public const int MinLength = 6; 13 | private static readonly Regex _validEmail = new(@"^[^@\s]+@[^@\s]+\.[^@\s]+$"); 14 | private Email(string value) 15 | { 16 | Value = value; 17 | } 18 | 19 | public string Value { get; } 20 | 21 | public static Result Create(string value) 22 | => Result 23 | .Ensure( 24 | value, 25 | (value => !string.IsNullOrEmpty(value), DomainErrors.Email.Empty), 26 | (value => value.Length >= MinLength, DomainErrors.Email.TooShort(value, MinLength)), 27 | (value => value.Length <= MaxLength, DomainErrors.Email.TooLong(value, MaxLength)), 28 | (_validEmail.IsMatch, DomainErrors.Email.Invalid)) 29 | .Map(e => new Email(e)); 30 | 31 | public override IEnumerable GetAtomicValues() 32 | { 33 | yield return Value; 34 | } 35 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Domain/Features/Companies/ValueObjects/Name.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.RegularExpressions; 3 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 4 | using Resrcify.SharedKernel.ResultFramework.Primitives; 5 | using Resrcify.SharedKernel.WebApiExample.Domain.Errors; 6 | 7 | namespace Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 8 | 9 | public sealed class Name : ValueObject 10 | { 11 | public const int MaxLength = 100; 12 | public const int MinLength = 1; 13 | private static readonly Regex _allowedCharacters = new(@"^[a-zA-ZåäöÅÄÖ0-9]+( [a-zA-ZåäöÅÄÖ0-9]+)*$"); 14 | public string Value { get; } 15 | private Name(string value) 16 | => Value = value; 17 | public static Result Create(string value) 18 | => Result 19 | .Ensure( 20 | value, 21 | (value => !string.IsNullOrEmpty(value), DomainErrors.Name.Empty), 22 | (value => value.Length >= MinLength, DomainErrors.Name.TooShort(value, MinLength)), 23 | (value => value.Length <= MaxLength, DomainErrors.Name.TooLong(value, MaxLength)), 24 | (_allowedCharacters.IsMatch, DomainErrors.Name.Invalid)) 25 | .Map(value => new Name(value)); 26 | 27 | public override IEnumerable GetAtomicValues() 28 | { 29 | yield return Value; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Domain/Features/Companies/ValueObjects/OrganizationNumber.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 3 | using Resrcify.SharedKernel.ResultFramework.Primitives; 4 | using Resrcify.SharedKernel.WebApiExample.Domain.Errors; 5 | 6 | namespace Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 7 | 8 | public sealed class OrganizationNumber : ValueObject 9 | { 10 | private const int Length = 10; 11 | public long Value { get; } 12 | private OrganizationNumber(long value) 13 | => Value = value; 14 | public static Result Create(string value) 15 | => Result 16 | .Ensure( 17 | value, 18 | (value => !string.IsNullOrEmpty(value), DomainErrors.OrganizationNumber.Empty), 19 | (value => value.ToString().Length == Length, DomainErrors.OrganizationNumber.InvalidLength), 20 | (value => value.Length > 0 && IsAllowedStartingDigit(value[0]), DomainErrors.OrganizationNumber.InvalidStartingDigit), 21 | (IsValidChecksum, DomainErrors.OrganizationNumber.InvalidChecksum)) 22 | .Map(value => new OrganizationNumber(long.Parse(value))); 23 | private static bool IsAllowedStartingDigit(char firstDigit) 24 | => firstDigit is '1' || 25 | firstDigit is '2' || 26 | firstDigit is '3' || 27 | firstDigit is '5' || 28 | firstDigit is '7' || 29 | firstDigit is '8' || 30 | firstDigit is '9'; 31 | private static bool IsValidChecksum(string number) 32 | { 33 | int sum = 0; 34 | bool isEvenDigitPlacement = false; 35 | 36 | for (int i = number.Length - 1; i >= 0; i--) 37 | { 38 | int n = int.Parse(number[i].ToString()); 39 | 40 | if (isEvenDigitPlacement) 41 | { 42 | n *= 2; 43 | if (n > 9) 44 | n -= 9; 45 | } 46 | 47 | sum += n; 48 | isEvenDigitPlacement = !isEvenDigitPlacement; 49 | } 50 | 51 | return sum % 10 == 0; 52 | } 53 | public override IEnumerable GetAtomicValues() 54 | { 55 | yield return Value; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Domain/Resrcify.SharedKernel.WebApiExample.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | false 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Infrastructure/InfrastructureServiceRegistration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Quartz; 3 | using Resrcify.SharedKernel.Caching.Abstractions; 4 | using Resrcify.SharedKernel.Caching.Primitives; 5 | using Resrcify.SharedKernel.UnitOfWork.BackgroundJobs; 6 | using Resrcify.SharedKernel.WebApiExample.Persistence; 7 | 8 | namespace Resrcify.SharedKernel.WebApiExample.Infrastructure; 9 | 10 | public static class InfrastructureServiceRegistration 11 | { 12 | public static IServiceCollection AddInfrastructureServices(this IServiceCollection services) 13 | { 14 | services.AddDistributedMemoryCache(); 15 | services.AddSingleton(); 16 | services.AddSwaggerGen(options => 17 | options.CustomSchemaIds(type => type.ToString())); 18 | services.AddQuartz(); 19 | services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true); 20 | services.ConfigureOptions>(); 21 | return services; 22 | } 23 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Infrastructure/Resrcify.SharedKernel.WebApiExample.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | false 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Persistence/AppDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Resrcify.SharedKernel.UnitOfWork.Outbox; 3 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies; 4 | 5 | namespace Resrcify.SharedKernel.WebApiExample.Persistence; 6 | 7 | public partial class AppDbContext( 8 | DbContextOptions options) 9 | : DbContext(options) 10 | { 11 | public DbSet OutboxMessages { get; set; } = default!; 12 | public DbSet Companies { get; set; } = default!; 13 | protected override void OnModelCreating(ModelBuilder modelBuilder) 14 | { 15 | modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); 16 | } 17 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Persistence/AppDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Design; 3 | 4 | namespace Resrcify.SharedKernel.WebApiExample.Persistence; 5 | 6 | public class AppDbContextFactory : IDesignTimeDbContextFactory 7 | { 8 | public AppDbContext CreateDbContext(string[] args) 9 | { 10 | var connectionString = "host=webapiexampledb;database=AppDb;username=ExampleUser;password=testingStuffOut;"; 11 | var optionsBuilder = new DbContextOptionsBuilder() 12 | .UseNpgsql(connectionString); 13 | 14 | return new AppDbContext(optionsBuilder.Options); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Persistence/Configurations/Companies/CompanyConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies; 4 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.Enums; 5 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 6 | 7 | namespace Resrcify.SharedKernel.WebApiExample.Persistence.Configurations.Companies; 8 | 9 | public class CompanyConfiguration : IEntityTypeConfiguration 10 | { 11 | public void Configure(EntityTypeBuilder builder) 12 | { 13 | builder 14 | .ToTable("Companies"); 15 | 16 | builder 17 | .HasKey(x => x.Id); 18 | 19 | builder 20 | .Property(x => x.Id) 21 | .HasConversion(x => x.Value, v => CompanyId.Create(v).Value) 22 | .ValueGeneratedNever(); 23 | 24 | builder 25 | .Property(x => x.Name) 26 | .HasConversion(x => x.Value, v => Name.Create(v).Value) 27 | .HasMaxLength(Name.MaxLength) 28 | .IsRequired(); 29 | 30 | builder 31 | .Property(x => x.OrganizationNumber) 32 | .HasConversion(x => x.Value, v => OrganizationNumber.Create(v.ToString()).Value) 33 | .IsRequired(); 34 | 35 | builder 36 | .Property(x => x.CompanyType) 37 | .HasConversion( 38 | x => x.Value, 39 | v => CompanyType.FromValue(v)!) 40 | .IsRequired(); 41 | 42 | builder 43 | .Property(x => x.CreatedOnUtc) 44 | .IsRequired(); 45 | 46 | builder 47 | .Property(x => x.ModifiedOnUtc) 48 | .IsRequired(); 49 | 50 | builder 51 | .Property(x => x.DeletedOnUtc); 52 | 53 | builder 54 | .Property(x => x.IsDeleted) 55 | .IsRequired(); 56 | 57 | builder 58 | .HasMany(x => x.Contacts) 59 | .WithOne() 60 | .HasForeignKey(x => x.CompanyId); 61 | 62 | builder 63 | .Metadata 64 | .FindNavigation(nameof(Company.Contacts))! 65 | .SetPropertyAccessMode(PropertyAccessMode.Field); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Persistence/Configurations/Companies/ContactConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.Entities; 4 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 5 | 6 | namespace Resrcify.SharedKernel.WebApiExample.Persistence.Configurations.Companies; 7 | 8 | public class ContactConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder 13 | .ToTable("Contacts"); 14 | 15 | builder 16 | .HasKey(x => x.Id); 17 | 18 | builder 19 | .Property(x => x.Id) 20 | .HasConversion(x => x.Value, v => ContactId.Create(v).Value) 21 | .ValueGeneratedNever(); 22 | 23 | builder 24 | .Property(x => x.CompanyId) 25 | .HasConversion(x => x.Value, v => CompanyId.Create(v).Value) 26 | .ValueGeneratedNever(); 27 | 28 | builder 29 | .Property(x => x.FirstName) 30 | .HasConversion(x => x.Value, v => Name.Create(v).Value) 31 | .HasMaxLength(Name.MaxLength) 32 | .IsRequired(); 33 | 34 | builder 35 | .Property(x => x.LastName) 36 | .HasConversion(x => x.Value, v => Name.Create(v).Value) 37 | .HasMaxLength(Name.MaxLength) 38 | .IsRequired(); 39 | 40 | builder 41 | .Property(x => x.Email) 42 | .HasConversion(x => x.Value, v => Email.Create(v).Value) 43 | .HasMaxLength(Name.MaxLength) 44 | .IsRequired(); 45 | 46 | builder 47 | .Property(x => x.CreatedOnUtc) 48 | .IsRequired(); 49 | 50 | builder 51 | .Property(x => x.ModifiedOnUtc) 52 | .IsRequired(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Persistence/PersistenceServiceRegistration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Resrcify.SharedKernel.UnitOfWork.Abstractions; 5 | using Resrcify.SharedKernel.UnitOfWork.Extensions; 6 | using Resrcify.SharedKernel.UnitOfWork.Interceptors; 7 | using Resrcify.SharedKernel.UnitOfWork.Primitives; 8 | using Resrcify.SharedKernel.WebApiExample.Application.Abstractions.Repositories; 9 | using Resrcify.SharedKernel.WebApiExample.Persistence.Repositories; 10 | 11 | namespace Resrcify.SharedKernel.WebApiExample.Persistence; 12 | 13 | public static class PersistenceServiceRegistration 14 | { 15 | public static IServiceCollection AddPersistanceServices(this IServiceCollection services, IConfiguration configuration) 16 | { 17 | services.AddDbContext(option => 18 | { 19 | option.UseNpgsql(configuration.GetConnectionString("Database")); 20 | option.AddInterceptors( 21 | new InsertOutboxMessagesInterceptor(), 22 | new UpdateAuditableEntitiesInterceptor(), 23 | new UpdateDeletableEntitiesInterceptor()); 24 | }); 25 | 26 | services.AddScoped>(); 27 | services.AddScoped(); 28 | 29 | services.ApplyMigrations(); 30 | 31 | return services; 32 | } 33 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Persistence/Repositories/CompanyRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.EntityFrameworkCore; 5 | using Resrcify.SharedKernel.Repository.Primitives; 6 | using Resrcify.SharedKernel.ResultFramework.Primitives; 7 | using Resrcify.SharedKernel.WebApiExample.Application.Abstractions.Repositories; 8 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies; 9 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 10 | using Resrcify.SharedKernel.WebApiExample.Domain.Errors; 11 | 12 | namespace Resrcify.SharedKernel.WebApiExample.Persistence.Repositories; 13 | 14 | internal sealed class CompanyRepository(AppDbContext context) 15 | : Repository(context), 16 | ICompanyRepository 17 | { 18 | public async Task> GetCompanyAggregateByIdAsync( 19 | CompanyId companyId, 20 | CancellationToken cancellationToken = default) 21 | => Result 22 | .Create( 23 | await Context.Companies 24 | .Include(x => x.Contacts) 25 | .FirstOrDefaultAsync(x => x.Id == companyId, cancellationToken)) 26 | .Match( 27 | company => company, 28 | DomainErrors.Company.NotFound(companyId.Value)); 29 | } 30 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Persistence/Resrcify.SharedKernel.WebApiExample.Persistence.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | false 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Presentation/PresentationServiceRegistration.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using Microsoft.AspNetCore.Http.Json; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Resrcify.SharedKernel.WebApiExample.Presentation; 6 | 7 | public static class PresentationServiceRegistration 8 | { 9 | public static IServiceCollection AddPresentationServices(this IServiceCollection services) 10 | { 11 | services.Configure(options => 12 | { 13 | options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); 14 | options.SerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals; 15 | }); 16 | 17 | services 18 | .AddControllers() 19 | .AddJsonOptions(options => 20 | { 21 | options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); 22 | options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals; 23 | }); 24 | services.AddEndpointsApiExplorer(); 25 | 26 | services.AddCors( 27 | options => options.AddPolicy("WebApiExampleCors", 28 | builder => builder 29 | .AllowAnyOrigin() 30 | .AllowAnyHeader() 31 | .AllowAnyMethod())); 32 | 33 | services.AddRouting(); 34 | return services; 35 | } 36 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Presentation/Resrcify.SharedKernel.WebApiExample.Presentation.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | false 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using Serilog; 4 | 5 | namespace Resrcify.SharedKernel.WebApiExample.Web; 6 | 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | => CreateHostBuilder(args) 11 | .UseSerilog((context, configuration) => 12 | configuration.ReadFrom.Configuration(context.Configuration)) 13 | .Build() 14 | .Run(); 15 | 16 | public static IHostBuilder CreateHostBuilder(string[] args) 17 | => Host.CreateDefaultBuilder(args) 18 | .ConfigureWebHostDefaults(webBuilder => 19 | { 20 | webBuilder.UseUrls("http://*:11000"); 21 | webBuilder.UseStartup(); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:62040", 8 | "sslPort": 44306 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5167", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7259;http://localhost:5167", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Web/Resrcify.SharedKernel.WebApiExample.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Web/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Resrcify.SharedKernel.WebApiExample.Infrastructure; 7 | using Resrcify.SharedKernel.WebApiExample.Persistence; 8 | using Resrcify.SharedKernel.WebApiExample.Application; 9 | using Resrcify.SharedKernel.WebApiExample.Presentation; 10 | using Serilog; 11 | 12 | namespace Resrcify.SharedKernel.WebApiExample.Web; 13 | 14 | public class Startup(IConfiguration configuration) 15 | { 16 | public IConfiguration Configuration { get; } = configuration; 17 | 18 | public void ConfigureServices(IServiceCollection services) 19 | { 20 | services.AddPresentationServices(); 21 | services.AddApplicationServices(); 22 | services.AddInfrastructureServices(); 23 | services.AddPersistanceServices(Configuration); 24 | } 25 | 26 | public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) 27 | { 28 | if (env.IsDevelopment()) 29 | { 30 | app.UseDeveloperExceptionPage(); 31 | app.UseSwagger(); 32 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Resrcify.WebApiExample v1")); 33 | } 34 | 35 | app.UseSerilogRequestLogging(); 36 | app.UseRouting(); 37 | app.UseCors("WebApiExampleCors"); 38 | app.UseAuthentication(); 39 | app.UseAuthorization(); 40 | app.UseEndpoints(endpoints => 41 | endpoints.MapControllers()); 42 | } 43 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Web/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/src/Resrcify.SharedKernel.WebApiExample.Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "ConnectionStrings": { 4 | "Database": "host=webapiexampledb;database=AppDb;username=ExampleUser;password=testingStuffOut;" 5 | }, 6 | "Serilog": { 7 | "Using": [ 8 | "Serilog.Sinks.Console" 9 | ], 10 | "MinimumLevel": { 11 | "Default": "Information", 12 | "Override": { 13 | "Microsoft": "Warning", 14 | "System": "Warning" 15 | } 16 | }, 17 | "WriteTo": [ 18 | { 19 | "Name": "Console" 20 | } 21 | ], 22 | "Enrich": [ 23 | "FromLogContext", 24 | "WithMachineName", 25 | "WithThreadId" 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/tests/Resrcify.SharedKernel.WebApiExample.ArchitectureTests/Extensions/NetArchTestExtensions.cs: -------------------------------------------------------------------------------- 1 | using NetArchTest.Rules; 2 | using Shouldly; 3 | 4 | namespace Resrcify.SharedKernel.WebApiExample.ArchitectureTests.Extensions; 5 | 6 | public static class NetArchTestExtensions 7 | { 8 | public static void Evaluate(this ConditionList conditionList) 9 | { 10 | var failingTypeNames = conditionList 11 | .GetResult().FailingTypeNames 12 | ?? []; 13 | failingTypeNames 14 | .ShouldBeEmpty(); 15 | } 16 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/tests/Resrcify.SharedKernel.WebApiExample.ArchitectureTests/Helpers/BaseTest.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Resrcify.SharedKernel.WebApiExample.Application; 3 | using Resrcify.SharedKernel.WebApiExample.Domain; 4 | using Resrcify.SharedKernel.WebApiExample.Infrastructure; 5 | using Resrcify.SharedKernel.WebApiExample.Persistence; 6 | using Resrcify.SharedKernel.WebApiExample.Presentation; 7 | using Resrcify.SharedKernel.WebApiExample.Web; 8 | 9 | namespace Resrcify.SharedKernel.WebApiExample.ArchitectureTests.Helpers; 10 | 11 | public abstract class BaseTest 12 | { 13 | protected static readonly Assembly DomainAssembly = typeof(AssemblyFlag).Assembly; 14 | protected static readonly Assembly ApplicationAssembly = typeof(ApplicationServiceRegistration).Assembly; 15 | protected static readonly Assembly InfrastructureAssembly = typeof(InfrastructureServiceRegistration).Assembly; 16 | protected static readonly Assembly PersistenceAssembly = typeof(PersistenceServiceRegistration).Assembly; 17 | protected static readonly Assembly PresentationAssembly = typeof(PresentationServiceRegistration).Assembly; 18 | protected static readonly Assembly WebAssembly = typeof(Program).Assembly; 19 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/tests/Resrcify.SharedKernel.WebApiExample.ArchitectureTests/Resrcify.SharedKernel.WebApiExample.ArchitectureTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/tests/Resrcify.SharedKernel.WebApiExample.ArchitectureTests/Tests/DomainTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using NetArchTest.Rules; 6 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 7 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 8 | using Resrcify.SharedKernel.WebApiExample.ArchitectureTests.Extensions; 9 | using Resrcify.SharedKernel.WebApiExample.ArchitectureTests.Helpers; 10 | using Shouldly; 11 | using Xunit.Abstractions; 12 | 13 | namespace Resrcify.SharedKernel.WebApiExample.ArchitectureTests.Tests; 14 | 15 | public class DomainTests : BaseTest 16 | { 17 | private readonly ITestOutputHelper _output; 18 | 19 | public DomainTests(ITestOutputHelper output) 20 | { 21 | _output = output; 22 | } 23 | 24 | [Fact] 25 | public void DomainEvents_Should_BeSealed() 26 | => Types 27 | .InAssembly(DomainAssembly) 28 | .That() 29 | .ImplementInterface(typeof(IDomainEvent)) 30 | .And() 31 | .AreNotAbstract() 32 | .Should() 33 | .BeSealed() 34 | .Evaluate(); 35 | 36 | [Fact] 37 | public void DomainEvents_Should_HaveEventPostFix() 38 | => Types 39 | .InAssembly(DomainAssembly) 40 | .That() 41 | .ImplementInterface(typeof(IDomainEvent)) 42 | .Should() 43 | .HaveNameEndingWith("Event") 44 | .Evaluate(); 45 | 46 | [Fact] 47 | public void Entities_Should_HavePrivateConstructor() 48 | { 49 | var entityTypes = Types 50 | .InAssembly(DomainAssembly) 51 | .That() 52 | .Inherit(typeof(Entity<>)) 53 | .And() 54 | .AreNotAbstract() 55 | .GetTypes(); 56 | 57 | var failingTypes = new List(); 58 | 59 | foreach (var type in entityTypes) 60 | { 61 | var constructors = type.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance); 62 | if (!constructors.Any(c => c.IsPrivate)) 63 | failingTypes.Add(type); 64 | } 65 | 66 | failingTypes 67 | .ShouldBeEmpty(); 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/tests/Resrcify.SharedKernel.WebApiExample.ArchitectureTests/Tests/PresentationTests.cs: -------------------------------------------------------------------------------- 1 | using NetArchTest.Rules; 2 | using Resrcify.SharedKernel.Web.Primitives; 3 | using Resrcify.SharedKernel.WebApiExample.ArchitectureTests.Extensions; 4 | using Resrcify.SharedKernel.WebApiExample.ArchitectureTests.Helpers; 5 | 6 | namespace Resrcify.SharedKernel.WebApiExample.ArchitectureTests.Tests; 7 | 8 | public class PresentationTests : BaseTest 9 | { 10 | [Fact] 11 | public void Controllers_Should_HaveDependecyOnMediatR() 12 | => Types 13 | .InAssembly(PresentationAssembly) 14 | .That() 15 | .HaveNameEndingWith("Controller") 16 | .Should() 17 | .HaveDependencyOn("MediatR") 18 | .Evaluate(); 19 | 20 | 21 | [Fact] 22 | public void Controllers_Should_ImplementApiController() 23 | => Types 24 | .InAssembly(PresentationAssembly) 25 | .That() 26 | .HaveNameEndingWith("Controller") 27 | .And() 28 | .AreNotAbstract() 29 | .Should().Inherit(typeof(ApiController)) 30 | .Evaluate(); 31 | } 32 | -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/tests/Resrcify.SharedKernel.WebApiExample.Domain.UnitTests/Companies/CompanyIdsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 4 | using Resrcify.SharedKernel.WebApiExample.Domain.Errors; 5 | using Shouldly; 6 | 7 | namespace Resrcify.SharedKernel.WebApiExample.Domain.UnitTests.Companies; 8 | 9 | public class CompanyIdTests 10 | { 11 | [Fact] 12 | public void Create_ShouldCreateValidGuid_WhenValidInputIsUsed() 13 | { 14 | // Arrange 15 | var validGuid = Guid.NewGuid(); 16 | 17 | // Act 18 | var result = CompanyId.Create(validGuid); 19 | 20 | // Assert 21 | result.IsSuccess 22 | .ShouldBeTrue(); 23 | result.Value 24 | .ShouldNotBeNull(); 25 | result.Value.Value 26 | .ShouldBe(validGuid); 27 | } 28 | 29 | [Fact] 30 | public void Create_ShouldReturnFailureResult_WhenValueIsEmpty() 31 | { 32 | // Arrange 33 | var emptyGuid = Guid.Empty; 34 | 35 | // Act 36 | var result = CompanyId.Create(emptyGuid); 37 | 38 | // Assert 39 | result.IsFailure 40 | .ShouldBeTrue(); 41 | result.Errors.ShouldHaveSingleItem(); 42 | 43 | // Assert that the error is equal to DomainErrors.CompanyId.Empty 44 | result.Errors[0].ShouldBe(DomainErrors.CompanyId.Empty); 45 | } 46 | 47 | [Fact] 48 | public void GetAtomicValues_ShouldReturnCorrectValues() 49 | { 50 | // Arrange 51 | var validGuid = Guid.NewGuid(); 52 | var companyId = CompanyId.Create(validGuid).Value; 53 | 54 | // Act 55 | var atomicValues = companyId.GetAtomicValues().ToArray(); 56 | 57 | // Assert 58 | atomicValues.ShouldHaveSingleItem(); 59 | atomicValues[0].ShouldBe(validGuid); 60 | } 61 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/tests/Resrcify.SharedKernel.WebApiExample.Domain.UnitTests/Companies/ContactIdTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 4 | using Resrcify.SharedKernel.WebApiExample.Domain.Errors; 5 | using Shouldly; 6 | 7 | namespace Resrcify.SharedKernel.WebApiExample.Domain.UnitTests.Companies; 8 | 9 | public class ContactIdTests 10 | { 11 | [Fact] 12 | public void Create_ShouldCreateValidContactId_WhenValidInputIsUsed() 13 | { 14 | // Arrange 15 | var validGuid = Guid.NewGuid(); 16 | 17 | // Act 18 | var result = ContactId.Create(validGuid); 19 | 20 | // Assert 21 | result.IsSuccess 22 | .ShouldBeTrue(); 23 | result.Value 24 | .ShouldNotBeNull(); 25 | result.Value.Value 26 | .ShouldBe(validGuid); 27 | } 28 | 29 | [Fact] 30 | public void Create_ShouldReturnFailureResult_WhenValueIsEmpty() 31 | { 32 | // Arrange 33 | var emptyGuid = Guid.Empty; 34 | 35 | // Act 36 | var result = ContactId.Create(emptyGuid); 37 | 38 | // Assert 39 | result.Errors.ShouldHaveSingleItem(); 40 | result.Errors[0].ShouldBe(DomainErrors.CompanyId.Empty); 41 | } 42 | 43 | [Fact] 44 | public void GetAtomicValues_ShouldReturnCorrectValues() 45 | { 46 | // Arrange 47 | var validGuid = Guid.NewGuid(); 48 | var contactId = ContactId.Create(validGuid).Value; 49 | 50 | // Act 51 | var atomicValues = contactId.GetAtomicValues().ToArray(); 52 | 53 | // Assert 54 | atomicValues.ShouldHaveSingleItem(); 55 | atomicValues[0].ShouldBe(validGuid); 56 | } 57 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/tests/Resrcify.SharedKernel.WebApiExample.Domain.UnitTests/Companies/ContactTests.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Resrcify/Resrcify.SharedKernel/ab28626ec81563e0cc759a9ed4e4d5d9320bec47/samples/Resrcify.SharedKernel.WebApiExample/tests/Resrcify.SharedKernel.WebApiExample.Domain.UnitTests/Companies/ContactTests.cs -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/tests/Resrcify.SharedKernel.WebApiExample.Domain.UnitTests/Companies/NameTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Resrcify.SharedKernel.WebApiExample.Domain.Features.Companies.ValueObjects; 3 | using Resrcify.SharedKernel.WebApiExample.Domain.Errors; 4 | using Shouldly; 5 | 6 | namespace Resrcify.SharedKernel.WebApiExample.Domain.UnitTests.Companies; 7 | 8 | public class NameTests 9 | { 10 | [Fact] 11 | public void Create_ShouldCreateValidName_WhenValidInputIsUsed() 12 | { 13 | // Arrange 14 | var validName = "JohnDoe"; 15 | 16 | // Act 17 | var result = Name.Create(validName); 18 | 19 | // Assert 20 | result.IsSuccess 21 | .ShouldBeTrue(); 22 | result.Value 23 | .ShouldNotBeNull(); 24 | result.Value.Value 25 | .ShouldBe(validName); 26 | } 27 | 28 | [Fact] 29 | public void Create_ShouldReturnFailureResult_WhenValueIsEmpty() 30 | { 31 | // Arrange 32 | var emptyName = string.Empty; 33 | 34 | // Act 35 | var result = Name.Create(emptyName); 36 | 37 | // Assert 38 | result.IsFailure 39 | .ShouldBeTrue(); 40 | result.Errors 41 | .ShouldContain(DomainErrors.Name.Empty); 42 | } 43 | 44 | [Fact] 45 | public void Create_ShouldReturnFailureResult_WhenValueIsTooShort() 46 | { 47 | // Arrange 48 | var shortName = string.Empty; 49 | 50 | // Act 51 | var result = Name.Create(shortName); 52 | 53 | // Assert 54 | result.IsFailure 55 | .ShouldBeTrue(); 56 | result.Errors 57 | .ShouldContain(DomainErrors.Name.TooShort(shortName, Name.MinLength)); 58 | } 59 | 60 | [Fact] 61 | public void Create_ShouldReturnFailureResult_WhenValueIsTooLong() 62 | { 63 | // Arrange 64 | var longName = new string('a', 101); 65 | 66 | // Act 67 | var result = Name.Create(longName); 68 | 69 | // Assert 70 | result.IsFailure 71 | .ShouldBeTrue(); 72 | result.Errors 73 | .ShouldContain(DomainErrors.Name.TooLong(longName, Name.MaxLength)); 74 | } 75 | 76 | [Fact] 77 | public void Create_ShouldReturnFailureResult_WhenValueContainsInvalidCharacters() 78 | { 79 | // Arrange 80 | var invalidName = "John!Doe"; 81 | 82 | // Act 83 | var result = Name.Create(invalidName); 84 | 85 | // Assert 86 | result.IsFailure 87 | .ShouldBeTrue(); 88 | result.Errors 89 | .ShouldContain(DomainErrors.Name.Invalid); 90 | } 91 | 92 | [Fact] 93 | public void GetAtomicValues_ShouldReturnCorrectValues() 94 | { 95 | // Arrange 96 | var validName = "JohnDoe"; 97 | var name = Name.Create(validName).Value; 98 | 99 | // Act 100 | var atomicValues = name.GetAtomicValues().ToArray(); 101 | 102 | // Assert 103 | atomicValues.ShouldHaveSingleItem(); 104 | atomicValues[0].ShouldBe(validName); 105 | } 106 | } -------------------------------------------------------------------------------- /samples/Resrcify.SharedKernel.WebApiExample/tests/Resrcify.SharedKernel.WebApiExample.Domain.UnitTests/Resrcify.SharedKernel.WebApiExample.Domain.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | true 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Caching/Abstractions/ICachingService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Caching.Distributed; 7 | 8 | namespace Resrcify.SharedKernel.Caching.Abstractions; 9 | 10 | public interface ICachingService 11 | { 12 | Task GetAsync( 13 | string key, 14 | JsonSerializerOptions? serializerOptions, 15 | CancellationToken cancellationToken = default) 16 | where T : class; 17 | 18 | 19 | Task GetAsync( 20 | string key, 21 | CancellationToken cancellationToken = default) 22 | where T : class; 23 | 24 | Task SetAsync( 25 | string key, 26 | T value, 27 | TimeSpan slidingExpiration, 28 | CancellationToken cancellationToken = default) 29 | where T : class; 30 | 31 | Task SetAsync( 32 | string key, 33 | T value, 34 | TimeSpan slidingExpiration, 35 | JsonSerializerOptions? serializerOptions, 36 | CancellationToken cancellationToken = default) 37 | where T : class; 38 | 39 | Task SetAsync( 40 | string key, 41 | T value, 42 | DistributedCacheEntryOptions cacheOptions, 43 | JsonSerializerOptions? serializerOptions, 44 | CancellationToken cancellationToken = default) 45 | where T : class; 46 | 47 | Task SetAsync( 48 | string key, 49 | T value, 50 | DistributedCacheEntryOptions cacheOptions, 51 | CancellationToken cancellationToken = default) 52 | where T : class; 53 | 54 | Task RemoveAsync( 55 | string key, 56 | CancellationToken cancellationToken = default); 57 | 58 | Task> GetBulkAsync( 59 | IEnumerable keys, 60 | JsonSerializerOptions? serializerOptions, 61 | CancellationToken cancellationToken = default); 62 | 63 | Task> GetBulkAsync( 64 | IEnumerable keys, 65 | CancellationToken cancellationToken = default); 66 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Caching/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Resrcify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Caching/Resrcify.SharedKernel.Caching.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0;net9.0 4 | Resrcify.SharedKernel.Caching 5 | Resrcify.SharedKernel.Caching 6 | Adds interfaces and implements different caching options using the IDistributedCache interfaces. 7 | Adds interfaces and implements different caching options using the IDistributedCache interfaces. 8 | 9 | 10 | 11 | 12 | all 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.DomainDrivenDesign/Abstractions/IAggregateRoot.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 4 | 5 | public interface IAggregateRoot 6 | { 7 | public IReadOnlyList GetDomainEvents(); 8 | public void ClearDomainEvents(); 9 | } 10 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.DomainDrivenDesign/Abstractions/IAuditableEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 4 | 5 | public interface IAuditableEntity 6 | { 7 | public DateTime CreatedOnUtc { get; } 8 | public DateTime ModifiedOnUtc { get; } 9 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.DomainDrivenDesign/Abstractions/IDeletableEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 4 | 5 | public interface IDeletableEntity 6 | { 7 | public bool IsDeleted { get; } 8 | public DateTime DeletedOnUtc { get; } 9 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.DomainDrivenDesign/Abstractions/IDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MediatR; 3 | 4 | namespace Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 5 | 6 | public interface IDomainEvent : INotification 7 | { 8 | public Guid Id { get; init; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.DomainDrivenDesign/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Resrcify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.DomainDrivenDesign/Primitives/AggregateRoot.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 3 | 4 | namespace Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 5 | 6 | public abstract class AggregateRoot(TId id) : Entity(id), IAggregateRoot 7 | where TId : notnull 8 | { 9 | private readonly List _domainEvents = []; 10 | 11 | public IReadOnlyList GetDomainEvents() => [.. _domainEvents]; 12 | 13 | public void ClearDomainEvents() => _domainEvents.Clear(); 14 | 15 | protected void RaiseDomainEvent(IDomainEvent domainEvent) => 16 | _domainEvents.Add(domainEvent); 17 | } 18 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.DomainDrivenDesign/Primitives/DomainEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 3 | 4 | namespace Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 5 | 6 | public abstract record DomainEvent(Guid Id) 7 | : IDomainEvent; 8 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.DomainDrivenDesign/Primitives/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 4 | public abstract class Entity : IEquatable> 5 | where TId : notnull 6 | { 7 | public TId Id { get; private init; } 8 | 9 | protected Entity(TId id) 10 | { 11 | Id = id; 12 | } 13 | 14 | public static bool operator ==(Entity first, Entity second) 15 | => first is not null && 16 | second is not null && 17 | first.Equals(second); 18 | 19 | public static bool operator !=(Entity first, Entity second) 20 | => !(first == second); 21 | 22 | public override bool Equals(object? obj) 23 | { 24 | if (obj is null) 25 | return false; 26 | if (obj.GetType() != GetType()) 27 | return false; 28 | if (obj is not Entity entity) 29 | return false; 30 | return Id.Equals(entity.Id); 31 | } 32 | 33 | public bool Equals(Entity? other) 34 | { 35 | if (other is null) 36 | return false; 37 | if (other.GetType() != GetType()) 38 | return false; 39 | return Id.Equals(other.Id); 40 | } 41 | 42 | public override string ToString() 43 | => Id?.ToString() ?? string.Empty; 44 | 45 | public override int GetHashCode() 46 | => Id.GetHashCode() * 41; 47 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.DomainDrivenDesign/Primitives/Enumeration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 7 | 8 | public abstract class Enumeration : IEquatable> 9 | where TEnum : Enumeration 10 | { 11 | private static readonly Dictionary Enumerations = CreateEnumeration(); 12 | 13 | public int Value { get; protected init; } 14 | public string Name { get; protected init; } 15 | protected Enumeration(int value, string name) 16 | { 17 | Value = value; 18 | Name = name; 19 | } 20 | public static TEnum? FromValue(int value) 21 | => Enumerations.TryGetValue( 22 | value, 23 | out TEnum? enumeration) ? 24 | enumeration : 25 | null; 26 | 27 | public static TEnum? FromName(string name) 28 | => Enumerations.Values 29 | .SingleOrDefault(e => e.Name == name); 30 | 31 | public bool Equals(Enumeration? other) 32 | { 33 | if (other is null) 34 | return false; 35 | 36 | return 37 | GetType() == other.GetType() && 38 | Value == other.Value; 39 | } 40 | public override bool Equals(object? obj) 41 | => obj is Enumeration other && 42 | Equals(other); 43 | 44 | public override int GetHashCode() 45 | => Value.GetHashCode(); 46 | 47 | public override string ToString() 48 | => Name; 49 | 50 | private static Dictionary CreateEnumeration() 51 | { 52 | var enumerationType = typeof(TEnum); 53 | 54 | var fieldForType = enumerationType 55 | .GetFields( 56 | BindingFlags.Public | 57 | BindingFlags.Static | 58 | BindingFlags.FlattenHierarchy) 59 | .Where(fieldInfo => 60 | enumerationType.IsAssignableFrom(fieldInfo.FieldType)) 61 | .Select(fieldInfo => 62 | (TEnum)fieldInfo.GetValue(default)!); 63 | 64 | return fieldForType.ToDictionary(x => x.Value); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.DomainDrivenDesign/Primitives/ValueObject.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 6 | public abstract class ValueObject : IEquatable 7 | { 8 | public abstract IEnumerable GetAtomicValues(); 9 | 10 | private bool ValuesAreEqual(ValueObject other) 11 | { 12 | return GetAtomicValues() 13 | .SequenceEqual(other.GetAtomicValues()); 14 | } 15 | 16 | public override string ToString() 17 | => string.Join(", ", GetAtomicValues()); 18 | 19 | public override bool Equals(object? obj) 20 | { 21 | return obj is ValueObject other && 22 | ValuesAreEqual(other); 23 | } 24 | 25 | public bool Equals(ValueObject? other) 26 | { 27 | return other is not null && 28 | ValuesAreEqual(other); 29 | } 30 | 31 | public override int GetHashCode() 32 | { 33 | return GetAtomicValues() 34 | .Aggregate(default(int), HashCode.Combine); 35 | } 36 | 37 | public static bool operator ==(ValueObject? left, ValueObject? right) 38 | { 39 | if (left is null || right is null) 40 | return false; 41 | 42 | if (ReferenceEquals(left, right)) 43 | return true; 44 | 45 | return left.Equals(right); 46 | } 47 | 48 | public static bool operator !=(ValueObject? left, ValueObject? right) 49 | => !(left == right); 50 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.DomainDrivenDesign/Resrcify.SharedKernel.DomainDrivenDesign.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0;net9.0 5 | Resrcify.SharedKernel.DomainDrivenDesign 6 | Resrcify.SharedKernel.DomainDrivenDesign 7 | Implements the different building blocks used in Domain Driven Desing. 8 | Implements the different building blocks used in Domain Driven Desing. 9 | 10 | 11 | 12 | all 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Abstractions/ICachingQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Resrcify.SharedKernel.Messaging.Abstractions; 4 | 5 | public interface ICachingQuery 6 | : IQuery, ICachingQuery; 7 | 8 | public interface ICachingQuery 9 | { 10 | string? CacheKey { get; set; } 11 | TimeSpan Expiration { get; set; } 12 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Abstractions/ICommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Resrcify.SharedKernel.ResultFramework.Primitives; 3 | 4 | namespace Resrcify.SharedKernel.Messaging.Abstractions; 5 | 6 | public interface ICommand 7 | : IRequest, IBaseCommand; 8 | 9 | public interface ICommand 10 | : IRequest>, IBaseCommand; 11 | 12 | public interface IBaseCommand; -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Abstractions/ICommandHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Resrcify.SharedKernel.ResultFramework.Primitives; 3 | 4 | namespace Resrcify.SharedKernel.Messaging.Abstractions; 5 | 6 | public interface ICommandHandler 7 | : IRequestHandler 8 | where TCommand : ICommand; 9 | 10 | public interface ICommandHandler 11 | : IRequestHandler> 12 | where TCommand : ICommand; 13 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Abstractions/IDomainEventHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 3 | 4 | namespace Resrcify.SharedKernel.Messaging.Abstractions; 5 | 6 | public interface IDomainEventHandler 7 | : INotificationHandler 8 | where TEvent : IDomainEvent; 9 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Abstractions/IQuery.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Resrcify.SharedKernel.ResultFramework.Primitives; 3 | 4 | namespace Resrcify.SharedKernel.Messaging.Abstractions; 5 | 6 | public interface IQuery 7 | : IRequest>; 8 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Abstractions/IQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Resrcify.SharedKernel.ResultFramework.Primitives; 3 | 4 | namespace Resrcify.SharedKernel.Messaging.Abstractions; 5 | 6 | public interface IQueryHandler 7 | : IRequestHandler> 8 | where TQuery : IQuery; 9 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Abstractions/ITransactionCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | 4 | namespace Resrcify.SharedKernel.Messaging.Abstractions; 5 | 6 | public interface ITransactionCommand 7 | : ICommand, ITransactionalCommand; 8 | 9 | public interface ITransactionCommand 10 | : ICommand, ITransactionalCommand; 11 | 12 | public interface ITransactionalCommand 13 | { 14 | TimeSpan? CommandTimeout { get; } 15 | IsolationLevel? IsolationLevel { get; } 16 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Behaviors/CachingPipelineBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using MediatR; 4 | using Microsoft.Extensions.Logging; 5 | using Resrcify.SharedKernel.Caching.Abstractions; 6 | using Resrcify.SharedKernel.Messaging.Abstractions; 7 | using Resrcify.SharedKernel.ResultFramework.Primitives; 8 | 9 | namespace Resrcify.SharedKernel.Messaging.Behaviors; 10 | 11 | public class CachingPipelineBehavior 12 | : IPipelineBehavior 13 | where TRequest : ICachingQuery 14 | where TResponse : Result 15 | { 16 | private readonly ICachingService _cachingService; 17 | private readonly ILogger _logger; 18 | public CachingPipelineBehavior( 19 | ICachingService cachingService, 20 | ILogger> logger) 21 | { 22 | _cachingService = cachingService; 23 | _logger = logger; 24 | } 25 | 26 | public async Task Handle( 27 | TRequest request, 28 | RequestHandlerDelegate next, 29 | CancellationToken cancellationToken) 30 | { 31 | string requestName = typeof(TRequest).Name; 32 | 33 | if (string.IsNullOrEmpty(request.CacheKey)) 34 | { 35 | _logger.LogInformation("{RequestName}: Key property not set", requestName); 36 | return await next(cancellationToken); 37 | } 38 | 39 | TResponse? cacheResult = await _cachingService.GetAsync( 40 | request.CacheKey, 41 | cancellationToken); 42 | 43 | if (cacheResult is not null) 44 | { 45 | _logger.LogInformation("{RequestName}: Cache hit", requestName); 46 | return cacheResult; 47 | } 48 | 49 | _logger.LogInformation("{RequestName}: Cache miss", requestName); 50 | var result = await next(cancellationToken); 51 | 52 | if (result.IsSuccess) 53 | { 54 | await _cachingService.SetAsync( 55 | request.CacheKey, 56 | result, 57 | request.Expiration, 58 | cancellationToken); 59 | } 60 | 61 | return result; 62 | } 63 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Behaviors/LoggingPipelineBehavior.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using MediatR; 5 | using Microsoft.Extensions.Logging; 6 | using Resrcify.SharedKernel.ResultFramework.Primitives; 7 | 8 | namespace Resrcify.SharedKernel.Messaging.Behaviors; 9 | 10 | public class LoggingPipelineBehavior 11 | : IPipelineBehavior 12 | where TRequest : IRequest 13 | where TResponse : Result 14 | { 15 | private readonly ILogger> _logger; 16 | public LoggingPipelineBehavior(ILogger> logger) 17 | => _logger = logger; 18 | 19 | public async Task Handle( 20 | TRequest request, 21 | RequestHandlerDelegate next, 22 | CancellationToken cancellationToken) 23 | { 24 | var start = DateTime.UtcNow; 25 | _logger.LogInformation("Starting request {@RequestName}, {@DateTimeUtc}", 26 | typeof(TRequest).Name, 27 | start); 28 | 29 | var result = await next(cancellationToken); 30 | 31 | var end = DateTime.UtcNow; 32 | var differenceMs = (end - start).TotalMilliseconds; 33 | if (result.IsFailure) 34 | _logger.LogInformation("Request failure {@RequestName}, {@Error}, {@DateTimeUtc} ({@DifferenceMs} ms)", 35 | typeof(TRequest).Name, 36 | result.Errors, 37 | end, 38 | differenceMs); 39 | 40 | _logger.LogInformation("Completed request {@RequestName}, {@DateTimeUtc} ({@DifferenceMs} ms)", 41 | typeof(TRequest).Name, 42 | end, 43 | differenceMs); 44 | return result; 45 | } 46 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Behaviors/TransactionPipelineBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using MediatR; 4 | using System; 5 | using Microsoft.Extensions.Logging; 6 | using Resrcify.SharedKernel.Messaging.Abstractions; 7 | using Resrcify.SharedKernel.UnitOfWork.Abstractions; 8 | using Resrcify.SharedKernel.ResultFramework.Primitives; 9 | 10 | namespace Resrcify.SharedKernel.Messaging.Behaviors; 11 | 12 | public class TransactionPipelineBehavior 13 | : IPipelineBehavior 14 | where TRequest : ITransactionalCommand 15 | where TResponse : Result 16 | { 17 | private readonly IUnitOfWork _unitOfWork; 18 | private readonly ILogger _logger; 19 | public TransactionPipelineBehavior( 20 | IUnitOfWork unitOfWork, 21 | ILogger> logger) 22 | { 23 | _unitOfWork = unitOfWork; 24 | _logger = logger; 25 | } 26 | 27 | public async Task Handle( 28 | TRequest request, 29 | RequestHandlerDelegate next, 30 | CancellationToken cancellationToken) 31 | { 32 | try 33 | { 34 | var commandTimeout = request.CommandTimeout 35 | ?? TimeSpan.FromSeconds(30); 36 | 37 | var isolationLevel = request.IsolationLevel 38 | ?? System.Data.IsolationLevel.ReadCommitted; 39 | 40 | await _unitOfWork.BeginTransactionAsync( 41 | isolationLevel, 42 | commandTimeout, 43 | cancellationToken); 44 | 45 | var response = await next(cancellationToken); 46 | 47 | if (response is Result { IsSuccess: true }) 48 | { 49 | await _unitOfWork.CommitTransactionAsync(cancellationToken); 50 | return response; 51 | } 52 | 53 | await _unitOfWork.RollbackTransactionAsync(cancellationToken); 54 | return response; 55 | } 56 | catch (Exception ex) 57 | { 58 | _logger.LogError(ex, "Exception caught in TransactionPipelineBehavior"); 59 | await _unitOfWork.RollbackTransactionAsync(cancellationToken); 60 | throw new InvalidOperationException("An error occurred while processing the TransactionPipelineBehavior.", ex); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Behaviors/UnitOfWorkPipelineBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using MediatR; 4 | using System; 5 | using Microsoft.Extensions.Logging; 6 | using Resrcify.SharedKernel.Messaging.Abstractions; 7 | using Resrcify.SharedKernel.UnitOfWork.Abstractions; 8 | using Resrcify.SharedKernel.ResultFramework.Primitives; 9 | 10 | namespace Resrcify.SharedKernel.Messaging.Behaviors; 11 | 12 | public class UnitOfWorkPipelineBehavior 13 | : IPipelineBehavior 14 | where TRequest : IBaseCommand 15 | where TResponse : Result 16 | { 17 | private readonly IUnitOfWork _unitOfWork; 18 | private readonly ILogger _logger; 19 | public UnitOfWorkPipelineBehavior( 20 | IUnitOfWork unitOfWork, 21 | ILogger> logger) 22 | { 23 | _unitOfWork = unitOfWork; 24 | _logger = logger; 25 | } 26 | 27 | public async Task Handle( 28 | TRequest request, 29 | RequestHandlerDelegate next, 30 | CancellationToken cancellationToken) 31 | { 32 | try 33 | { 34 | var response = await next(cancellationToken); 35 | if (response is Result { IsSuccess: true }) 36 | await _unitOfWork.CompleteAsync(cancellationToken); 37 | 38 | return response; 39 | } 40 | catch (Exception ex) 41 | { 42 | _logger.LogError(ex, "Exception caught in UnitOfWorkPipelineBehavior"); 43 | throw new InvalidOperationException("An error occurred while processing the UnitOfWorkPipelineBehavior.", ex); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Behaviors/ValidationPipelineBehavior.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using MediatR; 3 | using Resrcify.SharedKernel.ResultFramework.Primitives; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using ValidationResult = FluentValidation.Results.ValidationResult; 9 | 10 | namespace Resrcify.SharedKernel.Messaging.Behaviors; 11 | 12 | public sealed class ValidationPipelineBehavior 13 | : IPipelineBehavior 14 | where TRequest : IRequest 15 | where TResponse : Result 16 | { 17 | private readonly IEnumerable> _validators; 18 | 19 | public ValidationPipelineBehavior(IEnumerable> validators) => 20 | _validators = validators; 21 | 22 | public async Task Handle( 23 | TRequest request, 24 | RequestHandlerDelegate next, 25 | CancellationToken cancellationToken) 26 | { 27 | if (!_validators.Any()) 28 | return await next(cancellationToken); 29 | 30 | Error[] errors = await GetValidationErrorsAsync(request); 31 | 32 | if (errors.Length != 0) 33 | return CreateValidationResult(errors); 34 | 35 | return await next(cancellationToken); 36 | } 37 | 38 | public async Task GetValidationErrorsAsync(TRequest request) 39 | { 40 | var validationTasks = _validators 41 | .Select(validator => validator.ValidateAsync(request)); 42 | 43 | var validationResults = await Task.WhenAll(validationTasks); 44 | 45 | Error[] errors = ExtractErrors(validationResults); 46 | 47 | return errors; 48 | } 49 | 50 | private static Error[] ExtractErrors(ValidationResult[] validationResults) 51 | => validationResults 52 | .SelectMany(result => result.Errors) 53 | .Where(failure => failure is not null) 54 | .Select(failure => new Error( 55 | failure.PropertyName, 56 | failure.ErrorMessage, 57 | ErrorType.Validation 58 | )) 59 | .Distinct() 60 | .ToArray(); 61 | 62 | private static TResult CreateValidationResult(Error[] errors) 63 | where TResult : Result 64 | { 65 | if (!typeof(TResult).IsGenericType) 66 | return (TResult)Result.Failure(errors); 67 | 68 | object result = typeof(Result) 69 | .GetMethods() 70 | .First(m => 71 | m is { IsGenericMethod: true, Name: nameof(Result.Failure) } && 72 | m.GetParameters()[0].ParameterType == typeof(Error[]))! 73 | .MakeGenericMethod(typeof(TResult).GenericTypeArguments[0]) 74 | .Invoke(null, [errors])!; 75 | 76 | return (TResult)result; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Resrcify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Messaging/Resrcify.SharedKernel.Messaging.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0;net9.0 4 | Resrcify.SharedKernel.Messaging 5 | Resrcify.SharedKernel.Messaging 6 | Implements custom messaging abstractions based on MediatR and several different pipeline behaviors. 7 | Implements custom messaging abstractions based on MediatR and several different pipeline behaviors. 8 | 9 | 10 | 11 | all 12 | 13 | 14 | all 15 | 16 | 17 | all 18 | 19 | 20 | all 21 | 22 | 23 | all 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Repository/Abstractions/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System; 3 | using System.Threading.Tasks; 4 | using System.Collections.Generic; 5 | using System.Linq.Expressions; 6 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 7 | using Resrcify.SharedKernel.Repository.Primitives; 8 | 9 | namespace Resrcify.SharedKernel.Repository.Abstractions; 10 | 11 | public interface IRepository 12 | where TEntity : AggregateRoot 13 | where TId : notnull 14 | { 15 | Task GetByIdAsync( 16 | TId id, 17 | CancellationToken cancellationToken = default); 18 | 19 | Task FirstOrDefaultAsync( 20 | Expression> predicate, 21 | CancellationToken cancellationToken = default); 22 | 23 | Task FirstOrDefaultAsync( 24 | Specification specification, 25 | CancellationToken cancellationToken = default); 26 | 27 | IAsyncEnumerable GetAllAsync(); 28 | IAsyncEnumerable FindAsync(Expression> predicate); 29 | IAsyncEnumerable FindAsync(Specification specification); 30 | 31 | Task ExistsAsync( 32 | TId id, 33 | CancellationToken cancellationToken = default); 34 | 35 | Task ExistsAsync( 36 | Expression> predicate, 37 | CancellationToken cancellationToken = default); 38 | 39 | Task AddAsync( 40 | TEntity entity, 41 | CancellationToken cancellationToken = default); 42 | 43 | Task AddRangeAsync( 44 | IEnumerable entities, 45 | CancellationToken cancellationToken = default); 46 | 47 | void Remove(TEntity entity); 48 | void RemoveRange(IEnumerable entities); 49 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Repository/Extensions/QueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Resrcify.SharedKernel.Repository.Extensions; 7 | 8 | public static class QueryableExtensions 9 | { 10 | public static IQueryable WhereIf( 11 | this IQueryable queryable, 12 | bool condition, 13 | Expression> predicate) 14 | => condition 15 | ? queryable.Where(predicate) 16 | : queryable; 17 | 18 | public static IQueryable IncludeIf( 19 | this IQueryable queryable, 20 | bool condition, 21 | Expression> keySelector) 22 | where T : class 23 | => condition 24 | ? queryable.Include(keySelector) 25 | : queryable; 26 | 27 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Repository/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Resrcify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Repository/Primitives/ResultRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System; 3 | using System.Linq.Expressions; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 7 | using Resrcify.SharedKernel.ResultFramework.Primitives; 8 | 9 | namespace Resrcify.SharedKernel.Repository.Primitives; 10 | 11 | public abstract class ResultRepository(TDbContext context) 12 | : Repository(context) 13 | where TDbContext : DbContext 14 | where TEntity : AggregateRoot 15 | where TId : notnull 16 | { 17 | public new virtual async Task> GetByIdAsync( 18 | TId id, 19 | CancellationToken cancellationToken = default) 20 | => Result 21 | .Create(await base.GetByIdAsync(id, cancellationToken)) 22 | .Match( 23 | entity => entity, 24 | new Error( 25 | $"{typeof(TEntity).Name}.NotFound", 26 | $"{typeof(TEntity).Name} with Id '{id}' was not found.", 27 | ErrorType.NotFound)); 28 | 29 | public new virtual async Task> FirstOrDefaultAsync( 30 | Expression> predicate, 31 | CancellationToken cancellationToken = default) 32 | => Result 33 | .Create(await base.FirstOrDefaultAsync(predicate, cancellationToken)) 34 | .Match( 35 | entity => entity, 36 | new Error( 37 | $"{typeof(TEntity).Name}.NotFound", 38 | $"{typeof(TEntity).Name} matching the specified criteria was not found.", 39 | ErrorType.NotFound)); 40 | public new async Task> FirstOrDefaultAsync( 41 | Specification specification, 42 | CancellationToken cancellationToken = default) 43 | => Result 44 | .Create(await base.FirstOrDefaultAsync( 45 | specification, 46 | cancellationToken)) 47 | .Match( 48 | entity => entity, 49 | new Error( 50 | $"{typeof(TEntity).Name}.NotFound", 51 | $"{typeof(TEntity).Name} matching the specified criteria was not found.", 52 | ErrorType.NotFound)); 53 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Repository/Primitives/Specification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 5 | 6 | namespace Resrcify.SharedKernel.Repository.Primitives; 7 | 8 | public abstract class Specification 9 | where TEntity : AggregateRoot 10 | where TId : notnull 11 | { 12 | protected Specification(Expression>? criteria) 13 | => Criteria = criteria; 14 | public Expression>? Criteria { get; } 15 | public bool IsSplitQuery { get; protected set; } 16 | public bool IsNoTrackingQuery { get; protected set; } 17 | public ICollection>> IncludeExpressions { get; } = []; 18 | public Expression>? OrderByExpression { get; private set; } 19 | public Expression>? OrderByDescendingExpression { get; private set; } 20 | protected void AddInclude(Expression> includeExpression) 21 | => IncludeExpressions.Add(includeExpression); 22 | protected void AddOrderBy(Expression> ordertByExpression) 23 | => OrderByExpression = ordertByExpression; 24 | protected void AddOrderByDescending(Expression> ordertByDescendingExpression) 25 | => OrderByDescendingExpression = ordertByDescendingExpression; 26 | 27 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Repository/Primitives/SpecificationEvaluator.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.EntityFrameworkCore; 3 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 4 | 5 | namespace Resrcify.SharedKernel.Repository.Primitives; 6 | 7 | public static class SpecificationEvaluator 8 | { 9 | public static IQueryable GetQuery( 10 | IQueryable inputQueryable, Specification specification) 11 | where TEntity : AggregateRoot 12 | where TId : notnull 13 | { 14 | IQueryable queryable = inputQueryable; 15 | 16 | if (specification.Criteria is not null) 17 | queryable = queryable.Where(specification.Criteria); 18 | 19 | queryable = specification.IncludeExpressions.Aggregate( 20 | queryable, 21 | (current, includeExpressions) => 22 | current.Include(includeExpressions)); 23 | 24 | if (specification.OrderByExpression is not null) 25 | queryable = queryable.OrderBy(specification.OrderByExpression); 26 | else if (specification.OrderByDescendingExpression is not null) 27 | queryable = queryable.OrderByDescending(specification.OrderByDescendingExpression); 28 | 29 | if (specification.IsSplitQuery) 30 | queryable = queryable.AsSplitQuery(); 31 | 32 | if (specification.IsNoTrackingQuery) 33 | queryable = queryable.AsNoTracking(); 34 | 35 | return queryable; 36 | } 37 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Repository/Resrcify.SharedKernel.Repository.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0;net9.0 4 | Resrcify.SharedKernel.Repository 5 | Resrcify.SharedKernel.Repository 6 | Implements a generic repository pattern using EntityFrameworkCore. 7 | Implements a generic repository pattern using EntityFrameworkCore. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | 17 | 18 | all 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.ResultFramework/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Resrcify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.ResultFramework/Primitives/Error.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Resrcify.SharedKernel.ResultFramework.Primitives; 4 | 5 | public class Error : IEquatable 6 | { 7 | public static readonly Error None = new( 8 | string.Empty, 9 | string.Empty, 10 | ErrorType.Failure); 11 | public static readonly Error NullValue = new( 12 | "Error.NullValue", 13 | "The specified result value is null.", 14 | ErrorType.Failure); 15 | public static Error NotFound(string code, string message) 16 | => new( 17 | code, 18 | message, 19 | ErrorType.NotFound); 20 | public static Error Validation(string code, string message) 21 | => new( 22 | code, 23 | message, 24 | ErrorType.Validation); 25 | public static Error Conflict(string code, string message) 26 | => new( 27 | code, 28 | message, 29 | ErrorType.Conflict); 30 | public static Error Failure(string code, string message) 31 | => new( 32 | code, 33 | message, 34 | ErrorType.Failure); 35 | public Error(string code, string message, ErrorType type) 36 | { 37 | Code = code; 38 | Message = message; 39 | Type = type; 40 | } 41 | 42 | public string Code { get; init; } 43 | public string Message { get; init; } 44 | public ErrorType Type { get; init; } 45 | 46 | public static implicit operator string(Error error) 47 | => error.Code; 48 | public static implicit operator Result(Error error) 49 | => Result.Failure(error); 50 | 51 | public static bool operator ==(Error? a, Error? b) 52 | { 53 | if (a is null && b is null) 54 | return true; 55 | 56 | if (a is null || b is null) 57 | return false; 58 | 59 | return a.Equals(b); 60 | } 61 | 62 | public static bool operator !=(Error? a, Error? b) 63 | => !(a == b); 64 | 65 | public virtual bool Equals(Error? other) 66 | => other is not null && 67 | Code == other.Code && 68 | Message == other.Message && 69 | Type == other.Type; 70 | 71 | public override bool Equals(object? obj) 72 | => obj is Error error && Equals(error); 73 | 74 | public override int GetHashCode() 75 | => HashCode.Combine(Code, Message, Type); 76 | 77 | public override string ToString() 78 | => Code; 79 | } 80 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.ResultFramework/Primitives/ErrorType.cs: -------------------------------------------------------------------------------- 1 | namespace Resrcify.SharedKernel.ResultFramework.Primitives; 2 | 3 | public enum ErrorType 4 | { 5 | Failure = 0, 6 | Validation = 1, 7 | NotFound = 2, 8 | Conflict = 3 9 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.ResultFramework/Primitives/ResultT.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Resrcify.SharedKernel.ResultFramework.Primitives; 5 | 6 | public class Result : Result 7 | { 8 | private readonly TValue? _value; 9 | 10 | protected internal Result(TValue? value, bool isSuccess, Error error) 11 | : base(isSuccess, error) => 12 | _value = value; 13 | 14 | [JsonConstructor] 15 | protected internal Result(TValue? value, bool isSuccess, Error[] errors) 16 | : base(isSuccess, errors) => 17 | _value = value; 18 | 19 | public TValue Value => IsSuccess 20 | ? _value! 21 | : throw new InvalidOperationException("The value of a failure result can not be accessed."); 22 | 23 | public static implicit operator Result(TValue? value) 24 | => Create(value); 25 | public static implicit operator Result(Error error) 26 | => Failure(error); 27 | public static implicit operator Result(Error[] errors) 28 | => Failure(errors); 29 | } 30 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.ResultFramework/Resrcify.SharedKernel.ResultFramework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0;net9.0 4 | Resrcify.SharedKernel.ResultFramework 5 | Resrcify.SharedKernel.ResultFramework 6 | Implements the result pattern. 7 | Implements the result pattern. 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/Abstractions/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Resrcify.SharedKernel.UnitOfWork.Abstractions; 7 | 8 | public interface IUnitOfWork : IDisposable 9 | { 10 | Task CompleteAsync(CancellationToken cancellationToken = default); 11 | Task BeginTransactionAsync( 12 | IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, 13 | TimeSpan? commandLifetime = null, 14 | CancellationToken cancellationToken = default); 15 | Task CommitTransactionAsync(CancellationToken cancellationToken = default); 16 | Task RollbackTransactionAsync(CancellationToken cancellationToken = default); 17 | } 18 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/BackgroundJobs/ProcessOutboxMessagesJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Quartz; 5 | using Microsoft.EntityFrameworkCore; 6 | using MediatR; 7 | using Resrcify.SharedKernel.UnitOfWork.Outbox; 8 | using System.Text.Json; 9 | using Resrcify.SharedKernel.UnitOfWork.Converters; 10 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 11 | using System.Reflection; 12 | 13 | namespace Resrcify.SharedKernel.UnitOfWork.BackgroundJobs; 14 | 15 | [DisallowConcurrentExecution] 16 | public sealed class ProcessOutboxMessagesJob 17 | : IJob 18 | where TDbContext : DbContext 19 | { 20 | private readonly JsonSerializerOptions _jsonOptions = new() 21 | { 22 | Converters = { new DomainEventConverter() } 23 | }; 24 | 25 | private readonly TDbContext _context; 26 | private readonly IPublisher _publisher; 27 | 28 | public ProcessOutboxMessagesJob( 29 | TDbContext context, 30 | IPublisher publisher) 31 | { 32 | _context = context; 33 | _publisher = publisher; 34 | } 35 | 36 | public async Task Execute(IJobExecutionContext context) 37 | { 38 | if (!context.MergedJobDataMap.TryGetInt("ProcessBatchSize", out var batchSize)) 39 | batchSize = 20; 40 | if (!context.MergedJobDataMap.TryGetString("EventsAssemblyFullName", out var eventAssemblyFullName)) 41 | eventAssemblyFullName = string.Empty; 42 | 43 | var eventAssembly = Assembly.Load( 44 | new AssemblyName( 45 | eventAssemblyFullName 46 | ?? string.Empty)); 47 | 48 | var messages = await _context 49 | .Set() 50 | .Where(m => m.ProcessedOnUtc == null) 51 | .OrderBy(x => x.OccurredOnUtc) 52 | .Take(batchSize) 53 | .ToListAsync(context.CancellationToken); 54 | 55 | foreach (OutboxMessage outboxMessage in messages) 56 | { 57 | var messageType = eventAssembly.GetType(outboxMessage.Type); 58 | if (messageType is null) 59 | continue; 60 | 61 | var domainEvent = JsonSerializer.Deserialize( 62 | outboxMessage.Content, 63 | messageType, 64 | _jsonOptions); 65 | 66 | if (domainEvent is IDomainEvent specificDomainEvent) 67 | await _publisher.Publish(specificDomainEvent, context.CancellationToken); 68 | 69 | outboxMessage.ProcessedOnUtc = DateTime.UtcNow; 70 | } 71 | 72 | await _context.SaveChangesAsync(); 73 | } 74 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/BackgroundJobs/ProcessOutboxMessagesJobSetup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.Extensions.Options; 5 | using Quartz; 6 | 7 | namespace Resrcify.SharedKernel.UnitOfWork.BackgroundJobs; 8 | 9 | public sealed class ProcessOutboxMessagesJobSetup( 10 | Assembly eventsAssembly, 11 | int processBatchSize = 20, 12 | int processIntervalInSeconds = 60, 13 | int delayInSecondsBeforeStart = 60) 14 | : IConfigureOptions 15 | where TDbContext : DbContext 16 | { 17 | public void Configure(QuartzOptions options) 18 | { 19 | var jobKey = new JobKey(nameof(ProcessOutboxMessagesJob)); 20 | 21 | options 22 | .AddJob>(jobBuilder => 23 | jobBuilder 24 | .WithIdentity(jobKey) 25 | .UsingJobData("ProcessBatchSize", processBatchSize) 26 | .UsingJobData("EventsAssemblyFullName", eventsAssembly.FullName)) 27 | .AddTrigger( 28 | trigger => 29 | trigger.ForJob(jobKey) 30 | .StartAt(DateTime.UtcNow.AddSeconds(delayInSecondsBeforeStart)) 31 | .WithSimpleSchedule( 32 | schedule => 33 | schedule.WithIntervalInSeconds(processIntervalInSeconds) 34 | .RepeatForever())); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/BackgroundJobs/ProcessOutboxMessagesNewtonsoftJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | using Quartz; 7 | using Microsoft.EntityFrameworkCore; 8 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 9 | using MediatR; 10 | using Resrcify.SharedKernel.UnitOfWork.Outbox; 11 | 12 | namespace Resrcify.SharedKernel.UnitOfWork.BackgroundJobs; 13 | 14 | [DisallowConcurrentExecution] 15 | public sealed class ProcessOutboxMessagesNewtonsoftJob 16 | : IJob 17 | where TDbContext : DbContext 18 | { 19 | private readonly TDbContext _context; 20 | private readonly IPublisher _publisher; 21 | 22 | public ProcessOutboxMessagesNewtonsoftJob( 23 | TDbContext context, 24 | IPublisher publisher) 25 | { 26 | _context = context; 27 | _publisher = publisher; 28 | } 29 | 30 | public async Task Execute(IJobExecutionContext context) 31 | { 32 | if (!context.MergedJobDataMap.TryGetInt("ProcessBatchSize", out var batchSize)) 33 | batchSize = 20; 34 | 35 | List messages = await _context 36 | .Set() 37 | .Where(m => m.ProcessedOnUtc == null) 38 | .OrderBy(x => x.OccurredOnUtc) 39 | .Take(batchSize) 40 | .ToListAsync(context.CancellationToken); 41 | 42 | foreach (OutboxMessage outboxMessage in messages) 43 | { 44 | IDomainEvent? domainEvent = JsonConvert 45 | .DeserializeObject( 46 | outboxMessage.Content, 47 | new JsonSerializerSettings 48 | { 49 | TypeNameHandling = TypeNameHandling.All 50 | }); 51 | 52 | if (domainEvent is null) 53 | continue; 54 | 55 | await _publisher.Publish(domainEvent, context.CancellationToken); 56 | 57 | outboxMessage.ProcessedOnUtc = DateTime.UtcNow; 58 | } 59 | 60 | await _context.SaveChangesAsync(); 61 | } 62 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/BackgroundJobs/ProcessOutboxMessagesNewtonsoftJobSetup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Options; 4 | using Quartz; 5 | 6 | namespace Resrcify.SharedKernel.UnitOfWork.BackgroundJobs; 7 | 8 | public sealed class ProcessOutboxMessagesNewtonsoftJobSetup( 9 | int processBatchSize = 20, 10 | int processIntervalInSeconds = 60, 11 | int delayInSecondsBeforeStart = 60) 12 | : IConfigureOptions 13 | where TDbContext : DbContext 14 | { 15 | public void Configure(QuartzOptions options) 16 | { 17 | var jobKey = new JobKey(nameof(ProcessOutboxMessagesNewtonsoftJob)); 18 | 19 | options 20 | .AddJob>(jobBuilder => 21 | jobBuilder 22 | .WithIdentity(jobKey) 23 | .UsingJobData("ProcessBatchSize", processBatchSize)) 24 | .AddTrigger( 25 | trigger => 26 | trigger.ForJob(jobKey) 27 | .StartAt(DateTime.UtcNow.AddSeconds(delayInSecondsBeforeStart)) 28 | .WithSimpleSchedule( 29 | schedule => 30 | schedule.WithIntervalInSeconds(processIntervalInSeconds) 31 | .RepeatForever())); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/Converters/DomainEventConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 5 | namespace Resrcify.SharedKernel.UnitOfWork.Converters; 6 | 7 | public class DomainEventConverter : JsonConverter 8 | { 9 | public override IDomainEvent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | // Parse the JSON document 12 | using var jsonDoc = JsonDocument.ParseValue(ref reader); 13 | var root = jsonDoc.RootElement; 14 | 15 | // Extract the `$type` metadata 16 | if (!root.TryGetProperty("$type", out var typeProperty)) 17 | { 18 | throw new JsonException("The JSON does not contain a $type property."); 19 | } 20 | 21 | var typeName = typeProperty.GetString(); 22 | if (string.IsNullOrEmpty(typeName)) 23 | { 24 | throw new JsonException("$type property is empty or null."); 25 | } 26 | 27 | // Resolve the type 28 | var eventType = Type.GetType(typeName) 29 | ?? throw new InvalidOperationException($"Type '{typeName}' could not be resolved."); 30 | 31 | // Deserialize to the resolved type 32 | return (IDomainEvent?)JsonSerializer.Deserialize(root.GetRawText(), eventType, options); 33 | } 34 | 35 | public override void Write(Utf8JsonWriter writer, IDomainEvent value, JsonSerializerOptions options) 36 | { 37 | // Serialize the object with type metadata 38 | var typeName = value.GetType().AssemblyQualifiedName; 39 | 40 | // Convert object to JSON 41 | using var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value, value.GetType(), options)); 42 | var jsonObj = jsonDoc.RootElement.Clone(); 43 | 44 | // Create a new JSON object with `$type` included 45 | writer.WriteStartObject(); 46 | writer.WriteString("$type", typeName); 47 | 48 | foreach (var property in jsonObj.EnumerateObject()) 49 | { 50 | property.WriteTo(writer); 51 | } 52 | 53 | writer.WriteEndObject(); 54 | } 55 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/Extensions/MigrationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Resrcify.SharedKernel.UnitOfWork.Extensions; 5 | 6 | public static class MigrationsExtensions 7 | { 8 | public static void ApplyMigrations(this IServiceCollection services) 9 | where T : DbContext 10 | { 11 | using IServiceScope scope = services 12 | .BuildServiceProvider() 13 | .CreateScope(); 14 | 15 | using T? dbContext = scope.ServiceProvider.GetService(); 16 | if (dbContext is null) 17 | return; 18 | dbContext.Database.Migrate(); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/Interceptors/InsertOutboxMessagesInterceptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text.Json; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.Diagnostics; 8 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 9 | using Resrcify.SharedKernel.UnitOfWork.Converters; 10 | using Resrcify.SharedKernel.UnitOfWork.Outbox; 11 | 12 | namespace Resrcify.SharedKernel.UnitOfWork.Interceptors; 13 | 14 | public sealed class InsertOutboxMessagesInterceptor 15 | : SaveChangesInterceptor 16 | { 17 | private static readonly JsonSerializerOptions _jsonOptions = new() 18 | { 19 | Converters = { new DomainEventConverter() } 20 | }; 21 | 22 | public override async ValueTask> SavingChangesAsync( 23 | DbContextEventData eventData, 24 | InterceptionResult result, 25 | CancellationToken cancellationToken = default) 26 | { 27 | if (eventData.Context is not null) 28 | await ConvertDomainEventsToOutboxMessages(eventData.Context); 29 | return await base.SavingChangesAsync(eventData, result, cancellationToken); 30 | } 31 | 32 | private static async Task ConvertDomainEventsToOutboxMessages(DbContext context) 33 | { 34 | var outboxMessages = context.ChangeTracker 35 | .Entries() 36 | .Select(x => x.Entity) 37 | .SelectMany(aggregateRoot => 38 | { 39 | var domainEvents = aggregateRoot.GetDomainEvents(); 40 | 41 | aggregateRoot.ClearDomainEvents(); 42 | 43 | return domainEvents; 44 | }) 45 | .Select(domainEvent => new OutboxMessage 46 | { 47 | Id = Guid.NewGuid(), 48 | OccurredOnUtc = DateTime.UtcNow, 49 | Type = domainEvent.GetType().FullName!, 50 | Content = JsonSerializer.Serialize(domainEvent, _jsonOptions) 51 | }) 52 | .ToList(); 53 | 54 | await context.Set().AddRangeAsync(outboxMessages); 55 | } 56 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/Interceptors/InsertOutboxMessagesNewtonsoftInterceptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Diagnostics; 7 | using Newtonsoft.Json; 8 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 9 | using Resrcify.SharedKernel.UnitOfWork.Outbox; 10 | 11 | namespace Resrcify.SharedKernel.UnitOfWork.Interceptors; 12 | 13 | public sealed class InsertOutboxMessagesNewtonsoftInterceptor 14 | : SaveChangesInterceptor 15 | { 16 | public override async ValueTask> SavingChangesAsync( 17 | DbContextEventData eventData, 18 | InterceptionResult result, 19 | CancellationToken cancellationToken = default) 20 | { 21 | if (eventData.Context is not null) 22 | await ConvertDomainEventsToOutboxMessages(eventData.Context); 23 | return await base.SavingChangesAsync(eventData, result, cancellationToken); 24 | } 25 | 26 | private static async Task ConvertDomainEventsToOutboxMessages(DbContext context) 27 | { 28 | var outboxMessages = context.ChangeTracker 29 | .Entries() 30 | .Select(x => x.Entity) 31 | .SelectMany(aggregateRoot => 32 | { 33 | var domainEvents = aggregateRoot.GetDomainEvents(); 34 | 35 | aggregateRoot.ClearDomainEvents(); 36 | 37 | return domainEvents; 38 | }) 39 | .Select(domainEvent => new OutboxMessage 40 | { 41 | Id = Guid.NewGuid(), 42 | OccurredOnUtc = DateTime.UtcNow, 43 | Type = domainEvent.GetType().Name, 44 | Content = JsonConvert.SerializeObject( 45 | domainEvent, 46 | new JsonSerializerSettings 47 | { 48 | TypeNameHandling = TypeNameHandling.All 49 | }) 50 | }) 51 | .ToList(); 52 | 53 | await context.Set().AddRangeAsync(outboxMessages); 54 | } 55 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/Interceptors/UpdateAuditableEntitiesInterceptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.ChangeTracking; 7 | using Microsoft.EntityFrameworkCore.Diagnostics; 8 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 9 | 10 | namespace Resrcify.SharedKernel.UnitOfWork.Interceptors; 11 | 12 | public sealed class UpdateAuditableEntitiesInterceptor 13 | : SaveChangesInterceptor 14 | { 15 | public override async ValueTask> SavingChangesAsync( 16 | DbContextEventData eventData, 17 | InterceptionResult result, 18 | CancellationToken cancellationToken = default) 19 | { 20 | if (eventData.Context is not null) 21 | UpdateAuditableEntities(eventData.Context); 22 | return await base.SavingChangesAsync(eventData, result, cancellationToken); 23 | } 24 | private static void UpdateAuditableEntities(DbContext context) 25 | { 26 | DateTime utcNow = DateTime.UtcNow; 27 | IEnumerable> entries = 28 | context 29 | .ChangeTracker 30 | .Entries(); 31 | 32 | foreach (EntityEntry entityEntry in entries) 33 | { 34 | if (entityEntry.State == EntityState.Added) 35 | { 36 | SetCurrentPropertyValue( 37 | entityEntry, nameof(IAuditableEntity.CreatedOnUtc), utcNow); 38 | SetCurrentPropertyValue( 39 | entityEntry, nameof(IAuditableEntity.ModifiedOnUtc), utcNow); 40 | } 41 | 42 | if (entityEntry.State == EntityState.Modified) 43 | { 44 | SetCurrentPropertyValue( 45 | entityEntry, nameof(IAuditableEntity.ModifiedOnUtc), utcNow); 46 | } 47 | } 48 | } 49 | private static void SetCurrentPropertyValue( 50 | EntityEntry entry, 51 | string propertyName, 52 | DateTime utcNow) 53 | => entry.Property(propertyName).CurrentValue = utcNow; 54 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/Interceptors/UpdateDeletableEntitiesInterceptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.ChangeTracking; 8 | using Microsoft.EntityFrameworkCore.Diagnostics; 9 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 10 | 11 | namespace Resrcify.SharedKernel.UnitOfWork.Interceptors; 12 | 13 | public sealed class UpdateDeletableEntitiesInterceptor 14 | : SaveChangesInterceptor 15 | { 16 | public override async ValueTask> SavingChangesAsync( 17 | DbContextEventData eventData, 18 | InterceptionResult result, 19 | CancellationToken cancellationToken = default) 20 | { 21 | if (eventData.Context is not null) 22 | UpdateDeletableEntities(eventData.Context); 23 | return await base.SavingChangesAsync(eventData, result, cancellationToken); 24 | } 25 | private static void UpdateDeletableEntities(DbContext context) 26 | { 27 | IEnumerable> entries = 28 | context 29 | .ChangeTracker 30 | .Entries() 31 | .Where(e => e.State == EntityState.Deleted); 32 | 33 | foreach (EntityEntry entityEntry in entries) 34 | { 35 | entityEntry.State = EntityState.Modified; 36 | entityEntry.Property(a => a.DeletedOnUtc) 37 | .CurrentValue = DateTime.UtcNow; 38 | entityEntry.Property(a => a.IsDeleted) 39 | .CurrentValue = true; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Resrcify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/Outbox/OutboxMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Resrcify.SharedKernel.UnitOfWork.Outbox; 4 | 5 | public sealed class OutboxMessage 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string Type { get; set; } = string.Empty; 10 | 11 | public string Content { get; set; } = string.Empty; 12 | 13 | public DateTime OccurredOnUtc { get; set; } 14 | 15 | public DateTime? ProcessedOnUtc { get; set; } 16 | 17 | public string? Error { get; set; } 18 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/Primitives/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Resrcify.SharedKernel.UnitOfWork.Abstractions; 7 | 8 | namespace Resrcify.SharedKernel.UnitOfWork.Primitives; 9 | 10 | public sealed class UnitOfWork : IUnitOfWork 11 | where TDbContext : DbContext 12 | { 13 | 14 | private readonly TDbContext _context; 15 | 16 | public UnitOfWork(TDbContext context) 17 | => _context = context; 18 | 19 | public async Task CompleteAsync(CancellationToken cancellationToken = default) 20 | { 21 | await _context.SaveChangesAsync(cancellationToken); 22 | } 23 | 24 | public void Dispose() 25 | { 26 | _context.Dispose(); 27 | GC.SuppressFinalize(this); 28 | } 29 | 30 | public async Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, TimeSpan? commandLifetime = null, CancellationToken cancellationToken = default) 31 | { 32 | if (commandLifetime is not null) 33 | _context.Database.SetCommandTimeout((int)commandLifetime.Value.TotalSeconds); 34 | 35 | await _context.Database.BeginTransactionAsync(isolationLevel, cancellationToken); 36 | } 37 | 38 | public async Task CommitTransactionAsync(CancellationToken cancellationToken = default) 39 | { 40 | var currentTransaction = _context.Database.CurrentTransaction; 41 | if (currentTransaction == null) 42 | return; 43 | 44 | await currentTransaction.CommitAsync(cancellationToken); 45 | } 46 | 47 | public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) 48 | { 49 | var currentTransaction = _context.Database.CurrentTransaction; 50 | if (currentTransaction == null) 51 | return; 52 | 53 | await currentTransaction.RollbackAsync(cancellationToken); 54 | } 55 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.UnitOfWork/Resrcify.SharedKernel.UnitOfWork.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0;net9.0 4 | Resrcify.SharedKernel.UnitOfWork 5 | Resrcify.SharedKernel.UnitOfWork 6 | Implements a unit of work pattern, including interceptors using EntityFrameworkCore. 7 | Implements a unit of work pattern, includin interceptors using EntityFrameworkCore. 8 | 9 | 10 | 11 | all 12 | 13 | 14 | all 15 | 16 | 17 | all 18 | 19 | 20 | all 21 | 22 | 23 | all 24 | 25 | 26 | all 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Web/Extensions/InternalControllersExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Microsoft.AspNetCore.Mvc.Controllers; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Resrcify.SharedKernel.Web.Primitives; 6 | 7 | namespace Resrcify.SharedKernel.Web.Extensions; 8 | 9 | public static class InternalControllersExtensions 10 | { 11 | public static IMvcBuilder EnableInternalControllers( 12 | this IMvcBuilder builder, 13 | Type? controllerType = null) 14 | => builder.ConfigureApplicationPartManager( 15 | manager => manager.FeatureProviders.Add( 16 | new CustomControllerFeatureProvider(controllerType))); 17 | 18 | } 19 | internal class CustomControllerFeatureProvider(Type? controllerType) : ControllerFeatureProvider 20 | { 21 | public Type? ControllerType { get; } = controllerType; 22 | protected override bool IsController(TypeInfo typeInfo) 23 | { 24 | var selectedControllerType = ControllerType ?? typeof(ApiController); 25 | var isCustomController = !typeInfo.IsAbstract && selectedControllerType.IsAssignableFrom(typeInfo); 26 | return isCustomController || base.IsController(typeInfo); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Web/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Resrcify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Web/Primitives/ApiController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Linq; 5 | using MediatR; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Resrcify.SharedKernel.ResultFramework.Primitives; 9 | using ResultExtensions = Resrcify.SharedKernel.Web.Extensions.ResultExtensions; 10 | 11 | namespace Resrcify.SharedKernel.Web.Primitives; 12 | 13 | [ApiController] 14 | public abstract class ApiController : ControllerBase 15 | { 16 | protected ISender Sender { get; } 17 | 18 | protected ApiController(ISender sender) 19 | => Sender = sender; 20 | 21 | [SuppressMessage( 22 | "Globalization", 23 | "CA1308:Normalize strings to uppercase", 24 | Justification = "Lowercase needed for consistent JSON keys")] 25 | public static IResult ToProblemDetails(Result result) 26 | { 27 | if (result.IsSuccess) 28 | throw new InvalidOperationException("Successful result should not be converted to problem details."); 29 | 30 | var firstError = result.Errors.FirstOrDefault(); 31 | var errorType = firstError?.Type ?? ErrorType.Failure; 32 | 33 | return Results.Problem( 34 | statusCode: ResultExtensions.GetStatusCode(errorType), 35 | title: ResultExtensions.GetTitle(errorType), 36 | type: ResultExtensions.GetType(errorType), 37 | extensions: new Dictionary 38 | { 39 | { nameof(result.Errors).ToLowerInvariant(), result.Errors } 40 | }); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Resrcify.SharedKernel.Web/Resrcify.SharedKernel.Web.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0;net9.0 4 | Resrcify.SharedKernel.Web 5 | Resrcify.SharedKernel.Web 6 | Maps errors from the result patterns to a problem details response. 7 | Maps errors from the result patterns to a problem details response. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Caching.UnitTests/Resrcify.SharedKernel.Caching.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.DomainDrivenDesign.UnitTests/Primitives/AggregateRootTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 3 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 4 | using Shouldly; 5 | using Xunit; 6 | 7 | namespace Resrcify.SharedKernel.DomainDrivenDesign.UnitTests.Primitives; 8 | 9 | public class AggregateRootTests 10 | { 11 | private sealed class TestAggregateRoot(int id) : AggregateRoot(id) 12 | { 13 | public void PublicRaiseDomainEvent(IDomainEvent domainEvent) => RaiseDomainEvent(domainEvent); 14 | } 15 | 16 | private sealed record TestDomainEvent(Guid Id) : DomainEvent(Id); 17 | 18 | [Fact] 19 | public void GetDomainEvents_ShouldBeEmpty_WhenNoEventsAreRaised() 20 | { 21 | // Arrange 22 | var aggregateRoot = new TestAggregateRoot(1); 23 | 24 | // Act 25 | var events = aggregateRoot.GetDomainEvents(); 26 | 27 | // Assert 28 | events.ShouldBeEmpty(); 29 | } 30 | 31 | [Fact] 32 | public void RaiseDomainEvent_ShouldAddEventToDomainEvents() 33 | { 34 | // Arrange 35 | var aggregateRoot = new TestAggregateRoot(1); 36 | var domainEvent = new TestDomainEvent(Guid.NewGuid()); 37 | 38 | // Act 39 | aggregateRoot.PublicRaiseDomainEvent(domainEvent); 40 | var events = aggregateRoot.GetDomainEvents(); 41 | 42 | // Assert 43 | events.ShouldHaveSingleItem(); 44 | events[0].ShouldBe(domainEvent); 45 | } 46 | 47 | [Fact] 48 | public void ClearDomainEvents_ShouldRemoveAllDomainEvents() 49 | { 50 | // Arrange 51 | var aggregateRoot = new TestAggregateRoot(1); 52 | var domainEvent = new TestDomainEvent(Guid.NewGuid()); 53 | aggregateRoot.PublicRaiseDomainEvent(domainEvent); 54 | 55 | // Act 56 | aggregateRoot.ClearDomainEvents(); 57 | var events = aggregateRoot.GetDomainEvents(); 58 | 59 | // Assert 60 | events.ShouldBeEmpty(); 61 | } 62 | } -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.DomainDrivenDesign.UnitTests/Primitives/DomainEventTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | namespace Resrcify.SharedKernel.DomainDrivenDesign.UnitTests.Primitives; 7 | 8 | public class DomainEventTests 9 | { 10 | private sealed record TestDomainEvent(Guid Id) : DomainEvent(Id); 11 | [Fact] 12 | public void DomainEvent_ShouldInitialize_WithGivenId() 13 | { 14 | // Arrange 15 | var expectedGuid = Guid.NewGuid(); 16 | 17 | // Act 18 | var domainEvent = new TestDomainEvent(expectedGuid); 19 | 20 | // Assert 21 | domainEvent.Id.ShouldBe(expectedGuid); 22 | } 23 | 24 | [Fact] 25 | public void DomainEvents_WithSameId_ShouldBeEqual() 26 | { 27 | // Arrange 28 | var guid = Guid.NewGuid(); 29 | var event1 = new TestDomainEvent(guid); 30 | var event2 = new TestDomainEvent(guid); 31 | 32 | // Act & Assert 33 | event1.ShouldBe(event2); 34 | (event1 == event2).ShouldBeTrue(); 35 | } 36 | 37 | [Fact] 38 | public void DomainEvents_WithDifferentIds_ShouldNotBeEqual() 39 | { 40 | // Arrange 41 | var event1 = new TestDomainEvent(Guid.NewGuid()); 42 | var event2 = new TestDomainEvent(Guid.NewGuid()); 43 | 44 | // Act & Assert 45 | event1.ShouldNotBe(event2); 46 | (event1 != event2).ShouldBeTrue(); 47 | } 48 | 49 | [Fact] 50 | public void DomainEvents_WhenDuplicated_ShouldHaveSameHashCode() 51 | { 52 | // Arrange 53 | var guid = Guid.NewGuid(); 54 | var event1 = new TestDomainEvent(guid); 55 | var event2 = new TestDomainEvent(guid); 56 | 57 | // Act & Assert 58 | event1.GetHashCode().ShouldBe(event2.GetHashCode()); 59 | } 60 | } -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.DomainDrivenDesign.UnitTests/Primitives/EnumerationTests.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using Xunit; 4 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 5 | using Shouldly; 6 | 7 | namespace Resrcify.SharedKernel.DomainDrivenDesign.UnitTests.Primitives; 8 | public class EnumerationTests 9 | { 10 | [Fact] 11 | public void FromValue_GivenValidValue_ReturnsCorrectEnumeration() 12 | { 13 | var result = ExampleEnumeration.FromValue(1); 14 | 15 | result.ShouldNotBeNull(); 16 | result.ShouldBeEquivalentTo(ExampleEnumeration.Example1); 17 | } 18 | 19 | [Fact] 20 | public void FromValue_GivenInvalidValue_ReturnsNull() 21 | { 22 | var result = ExampleEnumeration.FromValue(999); 23 | 24 | result.ShouldBeNull(); 25 | } 26 | 27 | [Fact] 28 | public void FromName_GivenValidName_ReturnsCorrectEnumeration() 29 | { 30 | var result = ExampleEnumeration.FromName("Example1"); 31 | 32 | result.ShouldNotBeNull(); 33 | result.ShouldBeEquivalentTo(ExampleEnumeration.Example1); 34 | } 35 | 36 | [Fact] 37 | public void FromName_GivenInvalidName_ReturnsNull() 38 | { 39 | var result = ExampleEnumeration.FromName("NonExistent"); 40 | 41 | result.ShouldBeNull(); 42 | } 43 | 44 | [Fact] 45 | public void Equals_GivenSameInstance_ReturnsTrue() 46 | { 47 | var instance = ExampleEnumeration.Example1; 48 | 49 | instance.Equals(instance).ShouldBeTrue(); 50 | } 51 | 52 | [Fact] 53 | public void Equals_GivenSameValueDifferentInstance_ReturnsTrue() 54 | { 55 | var instance1 = ExampleEnumeration.Example1; 56 | var instance2 = ExampleEnumeration.Example1; 57 | 58 | instance1.Equals(instance2).ShouldBeTrue(); 59 | } 60 | 61 | [Fact] 62 | public void Equals_GivenDifferentValue_ReturnsFalse() 63 | { 64 | var instance1 = ExampleEnumeration.Example1; 65 | var instance2 = ExampleEnumeration.Example2; 66 | 67 | instance1.Equals(instance2).ShouldBeFalse(); 68 | } 69 | 70 | [Fact] 71 | public void GetHashCode_ReturnsConsistentResult() 72 | { 73 | var instance = ExampleEnumeration.Example1; 74 | var expectedHashCode = instance.Value.GetHashCode(); 75 | 76 | instance.GetHashCode().ShouldBe(expectedHashCode); 77 | } 78 | 79 | [Fact] 80 | public void ToString_ReturnsCorrectName() 81 | { 82 | var instance = ExampleEnumeration.Example1; 83 | 84 | instance.ToString().ShouldBe("Example1"); 85 | } 86 | 87 | internal sealed class ExampleEnumeration : Enumeration 88 | { 89 | public static ExampleEnumeration Example1 = new(1, "Example1"); 90 | public static ExampleEnumeration Example2 = new(2, "Example2"); 91 | 92 | private ExampleEnumeration(int value, string name) : base(value, name) 93 | { 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.DomainDrivenDesign.UnitTests/Resrcify.SharedKernel.DomainDrivenDesign.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Messaging.UnitTests/Behaviors/LoggingPipelineBehaviorTests.cs: -------------------------------------------------------------------------------- 1 | 2 | using Xunit; 3 | using NSubstitute; 4 | using Microsoft.Extensions.Logging; 5 | using System.Threading.Tasks; 6 | using System.Threading; 7 | using MediatR; 8 | using System; 9 | using Resrcify.SharedKernel.Messaging.Behaviors; 10 | using System.Linq; 11 | using Resrcify.SharedKernel.ResultFramework.Primitives; 12 | using Shouldly; 13 | 14 | namespace Resrcify.SharedKernel.Messaging.UnitTests.Behaviors; 15 | 16 | public class LoggingPipelineBehaviorTests 17 | { 18 | private readonly ILogger> _logger; 19 | private readonly LoggingPipelineBehavior _behavior; 20 | 21 | public LoggingPipelineBehaviorTests() 22 | { 23 | _logger = Substitute.For>>(); 24 | _behavior = new LoggingPipelineBehavior(_logger); 25 | } 26 | 27 | [Fact] 28 | public async Task Handle_ShouldLogInformationAtStartAndCompletion() 29 | { 30 | // Arrange 31 | var request = new MockRequest(); 32 | var response = Result.Success(); 33 | var cancellationToken = CancellationToken.None; 34 | Task next(CancellationToken cancellationToken = default) => Task.FromResult(response); 35 | 36 | // Act 37 | await _behavior.Handle(request, next, cancellationToken); 38 | 39 | // Assert 40 | _logger 41 | .ReceivedCalls() 42 | .Select(call => call.GetArguments()) 43 | .Count(callArguments => callArguments[0]!.Equals(LogLevel.Information)) 44 | .ShouldBe(2); 45 | } 46 | 47 | [Fact] 48 | public async Task Handle_WhenResponseIndicatesFailure_ShouldLogErrorInformation() 49 | { 50 | // Arrange 51 | var request = new MockRequest(); 52 | var response = Result.Failure(Error.NullValue); 53 | var cancellationToken = CancellationToken.None; 54 | Task next(CancellationToken cancellationToken = default) => Task.FromResult(response); 55 | 56 | // Act 57 | await _behavior.Handle(request, next, cancellationToken); 58 | 59 | // Assert 60 | _logger 61 | .ReceivedCalls() 62 | .Select(call => call.GetArguments()) 63 | .Count(callArguments => callArguments[0]!.Equals(LogLevel.Information)) 64 | .ShouldBe(3); 65 | } 66 | 67 | [System.Diagnostics.CodeAnalysis.SuppressMessage( 68 | "Maintainability", 69 | "CA1515:Consider making public types internal", 70 | Justification = "NSubstitute (which uses Castle DynamicProxy) cannot generate a mock of a type containing inaccessible generic parameters")] 71 | public sealed class MockRequest : IRequest { } 72 | } -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Messaging.UnitTests/Resrcify.SharedKernel.Messaging.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Repository.UnitTests/Models/Child.cs: -------------------------------------------------------------------------------- 1 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 2 | 3 | namespace Resrcify.SharedKernel.Repository.UnitTests.Models; 4 | 5 | internal sealed class Child(SocialSecurityNumber id, SocialSecurityNumber personId, string name = "Test") : Entity(id) 6 | { 7 | public string Name { get; private set; } = name; 8 | public SocialSecurityNumber PersonId { get; private set; } = personId; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Repository.UnitTests/Models/DbSetupBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Threading.Tasks; 4 | using Microsoft.EntityFrameworkCore; 5 | using Xunit; 6 | 7 | namespace Resrcify.SharedKernel.Repository.UnitTests.Models; 8 | [SuppressMessage( 9 | "Maintainability", 10 | "CA1515:Because an application's API isn't typically referenced from outside the assembly, types can be made internal", 11 | Justification = "Needed to be public due to being abstract and test classes needs to be public.")] 12 | public abstract class DbSetupBase : IAsyncLifetime, IDisposable 13 | { 14 | internal TestDbContext DbContext { get; } 15 | private bool _disposed; 16 | protected DbSetupBase() 17 | { 18 | var builder = new DbContextOptionsBuilder() 19 | .UseSqlite("Data Source=:memory:;Cache=Shared"); 20 | 21 | DbContext = new TestDbContext(builder.Options); 22 | } 23 | 24 | public async Task InitializeAsync() 25 | { 26 | await DbContext.Database.OpenConnectionAsync(); 27 | await DbContext.Database.EnsureCreatedAsync(); 28 | } 29 | 30 | public async Task DisposeAsync() 31 | { 32 | if (!_disposed) 33 | { 34 | await DbContext.Database.CloseConnectionAsync(); 35 | await DbContext.DisposeAsync(); 36 | _disposed = true; 37 | } 38 | } 39 | 40 | public void Dispose() 41 | { 42 | Dispose(true); 43 | GC.SuppressFinalize(this); 44 | } 45 | 46 | protected virtual void Dispose(bool disposing) 47 | { 48 | if (_disposed) 49 | return; 50 | 51 | if (disposing) 52 | { 53 | DbContext?.Dispose(); 54 | } 55 | 56 | _disposed = true; 57 | } 58 | } -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Repository.UnitTests/Models/Person.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 3 | 4 | namespace Resrcify.SharedKernel.Repository.UnitTests.Models; 5 | 6 | internal sealed class Person( 7 | SocialSecurityNumber id, 8 | string name = "Test") 9 | : AggregateRoot(id) 10 | { 11 | public string Name { get; private set; } = name; 12 | public List Children = []; 13 | }; 14 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Repository.UnitTests/Models/PersonSpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using Resrcify.SharedKernel.Repository.Primitives; 4 | 5 | namespace Resrcify.SharedKernel.Repository.UnitTests.Models; 6 | 7 | internal sealed class PersonSpecification : Specification 8 | { 9 | public PersonSpecification( 10 | Expression>? criteria = null, 11 | Expression>? orderBy = null, 12 | Expression>? orderByDecending = null, 13 | Expression>? addInclude = null, 14 | bool isSplitQuery = false, 15 | bool isNoTrackingQuery = false 16 | ) : base(criteria) 17 | { 18 | if (orderBy is not null) 19 | AddOrderBy(orderBy); 20 | if (orderByDecending is not null) 21 | AddOrderByDescending(orderByDecending); 22 | if (addInclude is not null) 23 | AddInclude(addInclude); 24 | IsSplitQuery = isSplitQuery; 25 | IsNoTrackingQuery = isNoTrackingQuery; 26 | } 27 | } -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Repository.UnitTests/Models/SocialSecurityNumber.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 3 | 4 | namespace Resrcify.SharedKernel.Repository.UnitTests.Models; 5 | 6 | internal sealed class SocialSecurityNumber : ValueObject 7 | { 8 | public int Value { get; private set; } 9 | private SocialSecurityNumber(int value) 10 | { 11 | Value = value; 12 | } 13 | public static SocialSecurityNumber Create(int value) 14 | => new(value); 15 | public override IEnumerable GetAtomicValues() 16 | { 17 | yield return Value; 18 | } 19 | public static explicit operator int(SocialSecurityNumber socialSecurityNumber) 20 | => socialSecurityNumber.Value; 21 | } 22 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Repository.UnitTests/Models/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Resrcify.SharedKernel.Repository.UnitTests.Models; 4 | 5 | internal sealed class TestDbContext(DbContextOptions options) : DbContext(options) 6 | { 7 | protected override void OnModelCreating(ModelBuilder modelBuilder) 8 | { 9 | modelBuilder.Entity().HasKey(x => x.Id); 10 | modelBuilder.Entity().Property(x => x.Name).HasMaxLength(10); 11 | modelBuilder.Entity().Property(x => x.Id).HasConversion(x => x.Value, v => SocialSecurityNumber.Create(v)); 12 | modelBuilder.Entity().HasMany(x => x.Children).WithOne().HasForeignKey(x => x.PersonId); 13 | modelBuilder.Entity().HasKey(x => x.Id); 14 | modelBuilder.Entity().Property(x => x.Name).HasMaxLength(10); 15 | modelBuilder.Entity().Property(x => x.Id).HasConversion(x => x.Value, v => SocialSecurityNumber.Create(v)); 16 | modelBuilder.Entity().Property(x => x.PersonId).HasConversion(x => x.Value, v => SocialSecurityNumber.Create(v)); 17 | } 18 | 19 | public DbSet Persons { get; set; } = default!; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Repository.UnitTests/Models/TestRepository.cs: -------------------------------------------------------------------------------- 1 | using Resrcify.SharedKernel.Repository.Primitives; 2 | 3 | namespace Resrcify.SharedKernel.Repository.UnitTests.Models; 4 | 5 | internal sealed class TestRepository(TestDbContext context) : Repository(context); 6 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Repository.UnitTests/Resrcify.SharedKernel.Repository.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.ResultFramework.UnitTests/Primitives/ResultTTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.ResultFramework.Primitives; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | 7 | namespace Resrcify.SharedKernel.ResultFramework.UnitTests.Primitives; 8 | 9 | public class ResultTTests 10 | { 11 | [Fact] 12 | public void ConstructorT_WithSuccessAndSingleError_ShouldCreateSuccessResultWithSingleError() 13 | { 14 | // Arrange 15 | int value = 42; 16 | Error error = Error.None; 17 | 18 | // Act 19 | var result = new TestResult(value, true, error); 20 | 21 | // Assert 22 | result.IsSuccess.ShouldBeTrue(); 23 | result.IsFailure.ShouldBeFalse(); 24 | result.Errors.ShouldBeEmpty(); 25 | result.Value.ShouldBe(value); 26 | } 27 | 28 | [Fact] 29 | public void ConstructorT_WithSuccessAndMultipleErrors_ShouldCreateSuccessResultWithMultipleErrors() 30 | { 31 | // Arrange 32 | int value = 42; 33 | Error[] errors = [Error.None, Error.NullValue]; 34 | 35 | // Act 36 | var result = new TestResult(value, true, errors); 37 | 38 | // Assert 39 | result.IsSuccess.ShouldBeTrue(); 40 | result.IsFailure.ShouldBeFalse(); 41 | result.Errors.ShouldBeEquivalentTo(errors); 42 | result.Value.ShouldBe(value); 43 | } 44 | 45 | [Fact] 46 | public void ConstructorT_WithFailureAndSingleError_ShouldCreateFailureResultWithSingleError() 47 | { 48 | // Arrange 49 | Error error = Error.NullValue; 50 | 51 | // Act 52 | var result = new TestResult(default, false, error); 53 | 54 | // Assert 55 | result.IsSuccess.ShouldBeFalse(); 56 | result.IsFailure.ShouldBeTrue(); 57 | result.Errors.ShouldBe([error]); 58 | Should.Throw(() => _ = result.Value); 59 | } 60 | 61 | [Fact] 62 | public void ConstructorT_WithFailureAndMultipleErrors_ShouldCreateFailureResultWithMultipleErrors() 63 | { 64 | // Arrange 65 | Error[] errors = [Error.None, Error.NullValue]; 66 | 67 | // Act 68 | var result = new TestResult(default, false, errors); 69 | 70 | // Assert 71 | result.IsSuccess.ShouldBeFalse(); 72 | result.IsFailure.ShouldBeTrue(); 73 | result.Errors.ShouldBeEquivalentTo(errors); 74 | Should.Throw(() => _ = result.Value); 75 | } 76 | 77 | private sealed class TestResult : Result 78 | { 79 | public TestResult(TValue? value, bool isSuccess, Error error) : base(value, isSuccess, error) 80 | { 81 | } 82 | 83 | public TestResult(TValue? value, bool isSuccess, Error[] errors) : base(value, isSuccess, errors) 84 | { 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.ResultFramework.UnitTests/Resrcify.SharedKernel.ResultFramework.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.UnitOfWork.UnitTests/BackgroundJobs/ProcessOutboxMessagesJobTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.EntityFrameworkCore; 5 | using Quartz; 6 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 7 | using MediatR; 8 | using NSubstitute; 9 | using Xunit; 10 | using System.Linq; 11 | using Resrcify.SharedKernel.UnitOfWork.UnitTests.Models; 12 | using Resrcify.SharedKernel.UnitOfWork.BackgroundJobs; 13 | using Resrcify.SharedKernel.UnitOfWork.Outbox; 14 | using System.Text.Json; 15 | using Resrcify.SharedKernel.UnitOfWork.Converters; 16 | using System.Reflection; 17 | using Shouldly; 18 | 19 | namespace Resrcify.SharedKernel.UnitOfWork.UnitTests.BackgroundJobs; 20 | 21 | public class ProcessOutboxMessagesJobTests 22 | { 23 | private static readonly JsonSerializerOptions _jsonOptions = new() 24 | { 25 | Converters = { new DomainEventConverter() } 26 | }; 27 | 28 | [Fact] 29 | public async Task Execute_ShouldProcessOutboxMessagesAndPublishDomainEvents() 30 | { 31 | // Arrange 32 | var options = new DbContextOptionsBuilder() 33 | .UseInMemoryDatabase(databaseName: "TestDatabase") 34 | .Options; 35 | 36 | using var dbContext = new TestDbContext(options); 37 | var publisherMock = Substitute.For(); 38 | var jobContextMock = Substitute.For(); 39 | 40 | var dataMap = new JobDataMap 41 | { 42 | { "EventsAssemblyFullName", Assembly.GetExecutingAssembly().FullName! }, 43 | { "ProcessBatchSize", 2 } 44 | }; 45 | jobContextMock.MergedJobDataMap.Returns(dataMap); 46 | 47 | var job = new ProcessOutboxMessagesJob(dbContext, publisherMock); 48 | 49 | var outboxMessages = Enumerable 50 | .Repeat(new TestDomainEvent(Guid.NewGuid(), "Test message"), 2) 51 | .Select(domainEvent => new OutboxMessage 52 | { 53 | Id = Guid.NewGuid(), 54 | OccurredOnUtc = DateTime.UtcNow, 55 | Type = domainEvent.GetType().FullName!, 56 | Content = JsonSerializer.Serialize(domainEvent, _jsonOptions) 57 | }) 58 | .ToList(); 59 | 60 | await dbContext.OutboxMessages.AddRangeAsync(outboxMessages); 61 | await dbContext.SaveChangesAsync(); 62 | 63 | // Act 64 | await job.Execute(jobContextMock); 65 | 66 | // Assert 67 | await publisherMock.Received(outboxMessages.Count).Publish(Arg.Any(), Arg.Any()); 68 | await dbContext.SaveChangesAsync(); 69 | outboxMessages.ForEach(m => m.ProcessedOnUtc.ShouldNotBeNull()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.UnitOfWork.UnitTests/Interceptors/InsertOutboxMessagesInterceptorTests.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 7 | using Resrcify.SharedKernel.UnitOfWork.Converters; 8 | using Resrcify.SharedKernel.UnitOfWork.Interceptors; 9 | using Resrcify.SharedKernel.UnitOfWork.UnitTests.Models; 10 | using Shouldly; 11 | using Xunit; 12 | 13 | namespace Resrcify.SharedKernel.UnitOfWork.UnitTests.Interceptors; 14 | 15 | public sealed class InsertOutboxMessagesInterceptorTests : DbSetupBase 16 | { 17 | public InsertOutboxMessagesInterceptorTests() : base(new InsertOutboxMessagesInterceptor()) 18 | { 19 | } 20 | private static readonly JsonSerializerOptions _jsonOptions = new() 21 | { 22 | Converters = { new DomainEventConverter() } 23 | }; 24 | [Fact] 25 | public async Task SaveChangesAsync_ConvertsDomainEventsToOutboxMessages() 26 | { 27 | // Arrange 28 | var entity = new TestAggregateRoot(SocialSecurityNumber.Create(123456789), "John Doe"); 29 | entity.PublicRaiseDomainEvent(new TestDomainEvent(Guid.NewGuid(), "Hello, World!")); 30 | await DbContext.Persons.AddAsync(entity); 31 | 32 | // Act 33 | await DbContext.SaveChangesAsync(); 34 | 35 | // Assert 36 | var outboxMessages = await DbContext.OutboxMessages.ToListAsync(); 37 | var testDomainEventName = typeof(TestDomainEvent).FullName; 38 | outboxMessages.Count.ShouldBe(1); 39 | outboxMessages[0].Type.ShouldBe(testDomainEventName); 40 | } 41 | 42 | [Fact] 43 | public async Task SaveChangesAsync_SavesAllAvailableProperties() 44 | { 45 | // Arrange 46 | var entity = new TestAggregateRoot(SocialSecurityNumber.Create(123456789), "John Doe"); 47 | entity.PublicRaiseDomainEvent(new TestDomainEvent(Guid.NewGuid(), "Test message")); 48 | await DbContext.Persons.AddAsync(entity); 49 | 50 | // Act 51 | await DbContext.SaveChangesAsync(); 52 | 53 | // Assert 54 | var outboxMessages = await DbContext.OutboxMessages.ToListAsync(); 55 | outboxMessages.ShouldHaveSingleItem(); 56 | var message = outboxMessages[0]; 57 | var deserializedMessage = (TestDomainEvent?)JsonSerializer.Deserialize(message.Content, _jsonOptions); 58 | deserializedMessage.ShouldNotBeNull(); 59 | deserializedMessage!.Id.ShouldNotBe(Guid.Empty); 60 | deserializedMessage!.Message.ShouldBe("Test message"); 61 | } 62 | 63 | private sealed class TestAggregateRoot : Person 64 | { 65 | public TestAggregateRoot(SocialSecurityNumber id, string name) : base(id, name) 66 | { 67 | } 68 | public void PublicRaiseDomainEvent(IDomainEvent domainEvent) => RaiseDomainEvent(domainEvent); 69 | } 70 | } -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.UnitOfWork.UnitTests/Interceptors/UpdateDeletableEntitiesInterceptorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore; 4 | using Resrcify.SharedKernel.UnitOfWork.Interceptors; 5 | using Resrcify.SharedKernel.UnitOfWork.UnitTests.Models; 6 | using Shouldly; 7 | using Xunit; 8 | 9 | namespace Resrcify.SharedKernel.UnitOfWork.UnitTests.Interceptors; 10 | 11 | public sealed class UpdateDeletableEntitiesInterceptorTests : DbSetupBase 12 | { 13 | public UpdateDeletableEntitiesInterceptorTests() : base(new UpdateDeletableEntitiesInterceptor()) 14 | { 15 | } 16 | 17 | [Fact] 18 | public async Task SaveChangesAsync_ShouldUpdateDeletableEntities() 19 | { 20 | // Arrange 21 | var now = DateTime.UtcNow; 22 | var entity = new Person(SocialSecurityNumber.Create(123456789), "John Doe"); 23 | await DbContext.Persons.AddAsync(entity); 24 | await DbContext.SaveChangesAsync(); 25 | // Act 26 | DbContext.Persons.Remove(entity); 27 | await DbContext.SaveChangesAsync(); 28 | 29 | //Assert 30 | entity.DeletedOnUtc 31 | .ShouldBe(now, TimeSpan.FromSeconds(1)); 32 | 33 | entity.IsDeleted 34 | .ShouldBeTrue(); 35 | } 36 | 37 | [Fact] 38 | public async Task SaveChangesAsync_ShouldNotDeleteTheEntity_WhenUpdateDeletableEntities() 39 | { 40 | // Arrange 41 | var now = DateTime.UtcNow; 42 | var entity = new Person(SocialSecurityNumber.Create(123456789), "John Doe"); 43 | await DbContext.Persons.AddAsync(entity); 44 | await DbContext.SaveChangesAsync(); 45 | 46 | // Act 47 | DbContext.Persons.Remove(entity); 48 | await DbContext.SaveChangesAsync(); 49 | 50 | //Assert 51 | var foundEntity = await DbContext.Persons 52 | .FirstOrDefaultAsync(x => x.Id == entity.Id); 53 | 54 | foundEntity 55 | .ShouldNotBeNull(); 56 | 57 | foundEntity!.DeletedOnUtc 58 | .ShouldBe(now, TimeSpan.FromSeconds(1)); 59 | 60 | foundEntity!.IsDeleted 61 | .ShouldBeTrue(); 62 | 63 | } 64 | } -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.UnitOfWork.UnitTests/Models/Child.cs: -------------------------------------------------------------------------------- 1 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 2 | 3 | namespace Resrcify.SharedKernel.UnitOfWork.UnitTests.Models; 4 | 5 | internal sealed class Child( 6 | SocialSecurityNumber id, 7 | string name = "Test") 8 | : Entity(id) 9 | { 10 | public string Name { get; private set; } = name; 11 | } 12 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.UnitOfWork.UnitTests/Models/DbSetupBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Threading.Tasks; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Diagnostics; 6 | using Resrcify.SharedKernel.UnitOfWork.Primitives; 7 | using Xunit; 8 | 9 | namespace Resrcify.SharedKernel.UnitOfWork.UnitTests.Models; 10 | [SuppressMessage( 11 | "Maintainability", 12 | "CA1515:Because an application's API isn't typically referenced from outside the assembly, types can be made internal", 13 | Justification = "Needed to be public due to being abstract and test classes needs to be public.")] 14 | public abstract class DbSetupBase : IAsyncLifetime, IDisposable 15 | { 16 | internal TestDbContext DbContext { get; } 17 | internal UnitOfWork UnitOfWork { get; } 18 | private bool _disposed; 19 | protected DbSetupBase(params IInterceptor[] interceptors) 20 | { 21 | var builder = new DbContextOptionsBuilder() 22 | .UseSqlite("Data Source=:memory:;Cache=Shared"); 23 | 24 | if (interceptors is not null) 25 | { 26 | builder.AddInterceptors(interceptors); 27 | } 28 | 29 | DbContext = new TestDbContext(builder.Options); 30 | 31 | UnitOfWork = new UnitOfWork(DbContext); 32 | } 33 | 34 | 35 | public async Task InitializeAsync() 36 | { 37 | await DbContext.Database.OpenConnectionAsync(); 38 | await DbContext.Database.EnsureCreatedAsync(); 39 | } 40 | 41 | public async Task DisposeAsync() 42 | { 43 | if (!_disposed) 44 | { 45 | await DbContext.Database.CloseConnectionAsync(); 46 | await DbContext.DisposeAsync(); 47 | _disposed = true; 48 | } 49 | } 50 | 51 | public void Dispose() 52 | { 53 | Dispose(true); 54 | GC.SuppressFinalize(this); 55 | } 56 | 57 | protected virtual void Dispose(bool disposing) 58 | { 59 | if (_disposed) 60 | return; 61 | 62 | if (disposing) 63 | { 64 | UnitOfWork.Dispose(); 65 | // Note: DbContext should only be disposed here if DisposeAsync hasn't already done it 66 | DbContext?.Dispose(); 67 | } 68 | 69 | _disposed = true; 70 | } 71 | } -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.UnitOfWork.UnitTests/Models/Person.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Resrcify.SharedKernel.DomainDrivenDesign.Abstractions; 4 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 5 | 6 | namespace Resrcify.SharedKernel.UnitOfWork.UnitTests.Models; 7 | 8 | internal class Person 9 | : AggregateRoot, IDeletableEntity, IAuditableEntity 10 | { 11 | public Person( 12 | SocialSecurityNumber id, 13 | string name = "Test") 14 | : base(id) 15 | { 16 | Name = name; 17 | } 18 | 19 | public string Name { get; set; } 20 | public bool IsDeleted { get; set; } 21 | public DateTime DeletedOnUtc { get; set; } 22 | public DateTime CreatedOnUtc { get; set; } 23 | public DateTime ModifiedOnUtc { get; set; } 24 | public List Children { get; } = []; 25 | } 26 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.UnitOfWork.UnitTests/Models/SocialSecurityNumber.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 3 | 4 | namespace Resrcify.SharedKernel.UnitOfWork.UnitTests.Models; 5 | 6 | internal sealed class SocialSecurityNumber : ValueObject 7 | { 8 | public int Value { get; private set; } 9 | private SocialSecurityNumber(int value) 10 | { 11 | Value = value; 12 | } 13 | public static SocialSecurityNumber Create(int value) 14 | => new(value); 15 | public override IEnumerable GetAtomicValues() 16 | { 17 | yield return Value; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.UnitOfWork.UnitTests/Models/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Resrcify.SharedKernel.UnitOfWork.Outbox; 3 | 4 | namespace Resrcify.SharedKernel.UnitOfWork.UnitTests.Models; 5 | 6 | internal sealed class TestDbContext(DbContextOptions options) : DbContext(options) 7 | { 8 | protected override void OnModelCreating(ModelBuilder modelBuilder) 9 | { 10 | modelBuilder.Entity().HasKey(x => x.Id); 11 | modelBuilder.Entity().Property(x => x.Name).HasMaxLength(10); 12 | modelBuilder.Entity().Property(x => x.Id).HasConversion(x => x.Value, v => SocialSecurityNumber.Create(v)); 13 | 14 | modelBuilder.Entity().Property(x => x.IsDeleted).IsRequired(); 15 | modelBuilder.Entity().Property(x => x.DeletedOnUtc); 16 | 17 | modelBuilder.Entity().Property(x => x.CreatedOnUtc); 18 | modelBuilder.Entity().Property(x => x.ModifiedOnUtc); 19 | 20 | modelBuilder.Entity().HasMany(x => x.Children).WithOne(); 21 | 22 | modelBuilder.Entity().HasKey(x => x.Id); 23 | modelBuilder.Entity().Property(x => x.Name).HasMaxLength(10); 24 | modelBuilder.Entity().Property(x => x.Id).HasConversion(x => x.Value, v => SocialSecurityNumber.Create(v)); 25 | 26 | } 27 | 28 | internal DbSet Persons { get; set; } = default!; 29 | internal DbSet OutboxMessages { get; set; } = default!; 30 | } 31 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.UnitOfWork.UnitTests/Models/TestDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Resrcify.SharedKernel.DomainDrivenDesign.Primitives; 3 | namespace Resrcify.SharedKernel.UnitOfWork.UnitTests.Models; 4 | 5 | internal sealed record TestDomainEvent( 6 | Guid Id, 7 | string Message) : DomainEvent(Id); -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.UnitOfWork.UnitTests/Primitives/UnitOfWorkTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.EntityFrameworkCore; 3 | using Xunit; 4 | using Resrcify.SharedKernel.UnitOfWork.UnitTests.Models; 5 | using Shouldly; 6 | 7 | namespace Resrcify.SharedKernel.UnitOfWork.UnitTests.Primitives; 8 | 9 | public class UnitOfWorkTests : DbSetupBase 10 | { 11 | [Fact] 12 | public async Task CompleteAsync_ShouldPersistChanges() 13 | { 14 | // Arrange 15 | var person = new Person(SocialSecurityNumber.Create(123456789), "John Doe"); 16 | DbContext.Persons.Add(person); 17 | 18 | // Act 19 | await UnitOfWork.CompleteAsync(); 20 | 21 | // Assert 22 | var fetchedPerson = await DbContext.Persons.SingleOrDefaultAsync(); 23 | fetchedPerson!.ShouldNotBeNull(); 24 | fetchedPerson!.Name.ShouldBe("John Doe"); 25 | } 26 | 27 | 28 | [Fact] 29 | public async Task CommitTransactionAsync_ShouldPersistChanges() 30 | { 31 | // Arrange 32 | using var transaction = await DbContext.Database.BeginTransactionAsync(); 33 | var person = new Person(SocialSecurityNumber.Create(987654321), "Jane Doe"); 34 | DbContext.Persons.Add(person); 35 | 36 | // Act 37 | await UnitOfWork.CompleteAsync(); 38 | await UnitOfWork.CommitTransactionAsync(); 39 | 40 | 41 | // Assert 42 | var fetchedPerson = await DbContext.Persons.SingleOrDefaultAsync(); 43 | fetchedPerson.ShouldNotBeNull(); 44 | fetchedPerson!.Name.ShouldBe("Jane Doe"); 45 | } 46 | 47 | [Fact] 48 | public async Task RollbackTransactionAsync_ShouldNotPersistChanges() 49 | { 50 | // Arrange 51 | using var transaction = await DbContext.Database.BeginTransactionAsync(); 52 | var person = new Person(SocialSecurityNumber.Create(112233445), "Alice"); 53 | DbContext.Persons.Add(person); 54 | 55 | // Act 56 | await UnitOfWork.CompleteAsync(); 57 | await UnitOfWork.RollbackTransactionAsync(); 58 | 59 | // Assert 60 | var fetchedPerson = await DbContext.Persons.SingleOrDefaultAsync(); 61 | fetchedPerson.ShouldBeNull(); 62 | } 63 | } -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.UnitOfWork.UnitTests/Resrcify.SharedKernel.UnitOfWork.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Web.UnitTests/Primitives/ApiControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Http.HttpResults; 5 | using Resrcify.SharedKernel.ResultFramework.Primitives; 6 | using Resrcify.SharedKernel.Web.Primitives; 7 | using Shouldly; 8 | using Xunit; 9 | 10 | namespace Resrcify.SharedKernel.Web.UnitTests.Primitives; 11 | public class ApiControllerTests 12 | { 13 | [Fact] 14 | public void ToProblemDetails_WithSuccessResult_ShouldThrowInvalidOperationException() 15 | { 16 | // Arrange 17 | var result = Result.Success(); 18 | 19 | // Act & Assert 20 | var act = () => ApiController.ToProblemDetails(result); 21 | 22 | // Verify that an exception is thrown 23 | var exception = act.ShouldThrow(); 24 | exception.Message.ShouldBe("Successful result should not be converted to problem details."); 25 | } 26 | 27 | [Theory] 28 | [InlineData(ErrorType.Validation, StatusCodes.Status400BadRequest, "Bad Request", "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1")] 29 | [InlineData(ErrorType.NotFound, StatusCodes.Status404NotFound, "Not Found", "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4")] 30 | [InlineData(ErrorType.Conflict, StatusCodes.Status409Conflict, "Conflict", "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8")] 31 | [InlineData(ErrorType.Failure, StatusCodes.Status500InternalServerError, "Internal Server Error", "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1")] 32 | public void ToProblemDetails_WithFailureResult_ShouldReturnProblemDetails(ErrorType errorType, int expectedStatusCode, string expectedTitle, string expectedType) 33 | { 34 | // Arrange 35 | var error = new Error("TestCode", "Test message", errorType); 36 | var result = Result.Failure(error); 37 | 38 | // Act 39 | var problemDetails = ApiController.ToProblemDetails(result) as ProblemHttpResult; 40 | 41 | // Assert 42 | problemDetails.ShouldNotBeNull(); 43 | problemDetails?.ProblemDetails.Type.ShouldBe(expectedType); 44 | problemDetails?.ProblemDetails.Title.ShouldBe(expectedTitle); 45 | problemDetails?.ProblemDetails.Status.ShouldBe(expectedStatusCode); 46 | problemDetails?.StatusCode.ShouldBe(expectedStatusCode); 47 | problemDetails?.ProblemDetails.Extensions.ShouldContainKey("errors"); 48 | problemDetails?.ProblemDetails.Extensions["errors"].ShouldBeAssignableTo>(); 49 | ((IEnumerable)problemDetails?.ProblemDetails.Extensions["errors"]!).ShouldContain(error); 50 | } 51 | } -------------------------------------------------------------------------------- /tests/Resrcify.SharedKernel.Web.UnitTests/Resrcify.SharedKernel.Web.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | all 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | --------------------------------------------------------------------------------