├── .editorconfig
├── .github
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ ├── build-ramdisk.yml
│ ├── codeql-analysis.yml
│ ├── dotnetcore.yml
│ └── publish.yml
├── .gitignore
├── .template.config
└── template.json
├── .vscode
├── launch.json
└── tasks.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Clean.Architecture.sln
├── Clean.Architecture.slnx
├── CleanArchitecture.nuspec
├── Directory.Build.props
├── Directory.Packages.props
├── LICENSE
├── README.md
├── docs
└── architecture-decisions
│ ├── README.md
│ └── adr-001-dotnet-di-adoption.md
├── global.json
├── icon.png
├── nuget.config
├── sample
├── .editorconfig
├── Directory.Build.props
├── Directory.Packages.props
├── NimblePros.SampleToDo.sln
├── src
│ ├── NimblePros.SampleToDo.AspireHost
│ │ ├── NimblePros.SampleToDo.AspireHost.csproj
│ │ ├── Program.cs
│ │ ├── Properties
│ │ │ └── launchSettings.json
│ │ ├── appsettings.Development.json
│ │ └── appsettings.json
│ ├── NimblePros.SampleToDo.Core
│ │ ├── ContributorAggregate
│ │ │ ├── Contributor.cs
│ │ │ ├── ContributorName.cs
│ │ │ ├── Events
│ │ │ │ ├── ContributorDeletedEvent.cs
│ │ │ │ └── ContributorNameUpdatedEvent.cs
│ │ │ ├── Handlers
│ │ │ │ ├── ContributorDeletedHandler.cs
│ │ │ │ └── ContributorNameUpdatedEventLoggingHandler.cs
│ │ │ └── Specifications
│ │ │ │ └── ContributorByIdSpec.cs
│ │ ├── CoreServiceExtensions.cs
│ │ ├── GlobalUsings.cs
│ │ ├── Interfaces
│ │ │ ├── IDeleteContributorService.cs
│ │ │ ├── IEmailSender.cs
│ │ │ └── IToDoItemSearchService.cs
│ │ ├── NimblePros.SampleToDo.Core.csproj
│ │ ├── ProjectAggregate
│ │ │ ├── Events
│ │ │ │ ├── ContributorAddedToItemEvent.cs
│ │ │ │ ├── NewItemAddedEvent.cs
│ │ │ │ └── ToDoItemCompletedEvent.cs
│ │ │ ├── Handlers
│ │ │ │ └── ItemCompletedEmailNotificationHandler.cs
│ │ │ ├── Priority.cs
│ │ │ ├── Project.cs
│ │ │ ├── ProjectId.cs
│ │ │ ├── ProjectName.cs
│ │ │ ├── ProjectStatus.cs
│ │ │ ├── Specifications
│ │ │ │ ├── IncompleteItemsSearchSpec.cs
│ │ │ │ ├── IncompleteItemsSpec.cs
│ │ │ │ ├── ProjectByIdWithItemsSpec.cs
│ │ │ │ └── ProjectsWithItemsByContributorId.cs
│ │ │ ├── ToDoItem.cs
│ │ │ └── ToDoItemId.cs
│ │ └── Services
│ │ │ ├── DeleteContributorService.cs
│ │ │ └── ToDoItemSearchService.cs
│ ├── NimblePros.SampleToDo.Infrastructure
│ │ ├── Data
│ │ │ ├── AppDbContext.cs
│ │ │ ├── Config
│ │ │ │ ├── ContributorConfiguration.cs
│ │ │ │ ├── DataSchemaConstants.cs
│ │ │ │ ├── ProjectConfiguration.cs
│ │ │ │ ├── ToDoItemConfiguration.cs
│ │ │ │ ├── VogenEfCoreConverters.cs
│ │ │ │ └── VogenIdValueGenerator.cs
│ │ │ ├── EfRepository.cs
│ │ │ └── Queries
│ │ │ │ ├── FakeListContributorsQueryService.cs
│ │ │ │ ├── FakeListIncompleteItemsQueryService.cs
│ │ │ │ ├── FakeListProjectsShallowQueryService.cs
│ │ │ │ ├── ListContributorsQueryService.cs
│ │ │ │ ├── ListIncompleteItemsQueryService.cs
│ │ │ │ └── ListProjectsShallowQueryService.cs
│ │ ├── Email
│ │ │ ├── FakeEmailSender.cs
│ │ │ ├── MailserverConfiguration.cs
│ │ │ ├── MimeKitEmailSender.cs
│ │ │ └── SmtpEmailSender.cs
│ │ ├── GlobalUsings.cs
│ │ ├── InfrastructureServiceExtensions.cs
│ │ └── NimblePros.SampleToDo.Infrastructure.csproj
│ ├── NimblePros.SampleToDo.ServiceDefaults
│ │ ├── Extensions.cs
│ │ └── NimblePros.SampleToDo.ServiceDefaults.csproj
│ ├── NimblePros.SampleToDo.UseCases
│ │ ├── Contributors
│ │ │ ├── Commands
│ │ │ │ ├── Create
│ │ │ │ │ ├── CreateContributorCommand.cs
│ │ │ │ │ └── CreateContributorHandler.cs
│ │ │ │ ├── Delete
│ │ │ │ │ ├── DeleteContributorCommand.cs
│ │ │ │ │ └── DeleteContributorHandler.cs
│ │ │ │ └── Update
│ │ │ │ │ ├── UpdateContributorCommand.cs
│ │ │ │ │ └── UpdateContributorHandler.cs
│ │ │ ├── ContributorDTO.cs
│ │ │ └── Queries
│ │ │ │ ├── Get
│ │ │ │ ├── GetContributorHandler.cs
│ │ │ │ └── GetContributorQuery.cs
│ │ │ │ └── List
│ │ │ │ ├── IListContributorsQueryService.cs
│ │ │ │ ├── ListContributorsHandler.cs
│ │ │ │ └── ListContributorsQuery.cs
│ │ ├── GlobalUsings.cs
│ │ ├── NimblePros.SampleToDo.UseCases.csproj
│ │ ├── Projects
│ │ │ ├── AddToDoItem
│ │ │ │ ├── AddToDoItemCommand.cs
│ │ │ │ └── AddToDoItemHandler.cs
│ │ │ ├── Create
│ │ │ │ ├── CreateProjectCommand.cs
│ │ │ │ └── CreateProjectHandler.cs
│ │ │ ├── Delete
│ │ │ │ ├── DeleteProjectCommand.cs
│ │ │ │ └── DeleteProjectHandler.cs
│ │ │ ├── GetWithAllItems
│ │ │ │ ├── GetProjectWithAllItemsHandler.cs
│ │ │ │ └── GetProjectWithAllItemsQuery.cs
│ │ │ ├── ListIncompleteItems
│ │ │ │ ├── IListIncompleteItemsQueryService.cs
│ │ │ │ ├── ListIncompleteItemsByProjectHandler.cs
│ │ │ │ └── ListIncompleteItemsByProjectQuery.cs
│ │ │ ├── ListShallow
│ │ │ │ ├── IListProjectsShallowQueryService.cs
│ │ │ │ ├── ListProjectsShallowHandler.cs
│ │ │ │ └── ListProjectsShallowQuery.cs
│ │ │ ├── MarkToDoItemComplete
│ │ │ │ ├── MarkToDoItemCompleteCommand.cs
│ │ │ │ └── MarkToDoItemCompleteHandler.cs
│ │ │ ├── ProjectDTO.cs
│ │ │ ├── ProjectWithAllItemsDTO.cs
│ │ │ ├── ToDoItemDTO.cs
│ │ │ └── Update
│ │ │ │ ├── UpdateProjectCommand.cs
│ │ │ │ └── UpdateProjectHandler.cs
│ │ └── README.md
│ └── NimblePros.SampleToDo.Web
│ │ ├── Configurations
│ │ ├── LoggerConfig.cs
│ │ ├── MediatrConfig.cs
│ │ ├── MiddlewareConfig.cs
│ │ ├── OptionConfigs.cs
│ │ └── ServiceConfigs.cs
│ │ ├── Contributors
│ │ ├── ContributorRecord.cs
│ │ ├── Create.CreateContributorRequest.cs
│ │ ├── Create.CreateContributorResponse.cs
│ │ ├── Create.CreateContributorValidator.cs
│ │ ├── Create.cs
│ │ ├── Delete.DeleteContributorRequest.cs
│ │ ├── Delete.DeleteContributorValidator.cs
│ │ ├── Delete.cs
│ │ ├── GetById.GetContributorByIdRequest.cs
│ │ ├── GetById.GetContributorValidator.cs
│ │ ├── GetById.cs
│ │ ├── List.ContributorListResponse.cs
│ │ ├── List.cs
│ │ ├── Update.UpdateContributorRequest.cs
│ │ ├── Update.UpdateContributorResponse.cs
│ │ ├── Update.UpdateContributorValidator.cs
│ │ └── Update.cs
│ │ ├── GlobalUsings.cs
│ │ ├── NimblePros.SampleToDo.Web.csproj
│ │ ├── Program.cs
│ │ ├── Projects
│ │ ├── Create.CreateProjectRequest.cs
│ │ ├── Create.CreateProjectResponse.cs
│ │ ├── Create.CreateProjectValidator.cs
│ │ ├── Create.cs
│ │ ├── CreateToDoItem.CreateToDoItemRequest.cs
│ │ ├── CreateToDoItem.CreateToDoItemValidator.cs
│ │ ├── CreateToDoItem.cs
│ │ ├── Delete.DeleteProjectRequest.cs
│ │ ├── Delete.cs
│ │ ├── GetById.GetProjectByIdRequest.cs
│ │ ├── GetById.GetProjectByIdResponse.cs
│ │ ├── GetById.cs
│ │ ├── List.ProjectListResponse.cs
│ │ ├── List.cs
│ │ ├── ListIncompleteItems.ListIncompleteItemsRequest.cs
│ │ ├── ListIncompleteItems.ListIncompleteItemsResponse.cs
│ │ ├── ListIncompleteItems.cs
│ │ ├── MarkItemComplete.MarkItemCompleteRequest.cs
│ │ ├── MarkItemComplete.cs
│ │ ├── ProjectRecord.cs
│ │ ├── ToDoItemRecord.cs
│ │ ├── Update.UpdateProjectRequest.cs
│ │ ├── Update.UpdateProjectRequestValidator.cs
│ │ ├── Update.UpdateProjectResponse.cs
│ │ └── Update.cs
│ │ ├── Properties
│ │ └── launchSettings.json
│ │ ├── SeedData.cs
│ │ ├── api.http
│ │ ├── appsettings.json
│ │ └── wwwroot
│ │ ├── css
│ │ └── site.css
│ │ ├── favicon.ico
│ │ ├── js
│ │ └── site.js
│ │ └── lib
│ │ ├── bootstrap
│ │ ├── LICENSE
│ │ └── dist
│ │ │ ├── css
│ │ │ ├── bootstrap-grid.css
│ │ │ ├── bootstrap-grid.css.map
│ │ │ ├── bootstrap-grid.min.css
│ │ │ ├── bootstrap-grid.min.css.map
│ │ │ ├── bootstrap-grid.rtl.css
│ │ │ ├── bootstrap-grid.rtl.css.map
│ │ │ ├── bootstrap-grid.rtl.min.css
│ │ │ ├── bootstrap-grid.rtl.min.css.map
│ │ │ ├── bootstrap-reboot.css
│ │ │ ├── bootstrap-reboot.css.map
│ │ │ ├── bootstrap-reboot.min.css
│ │ │ ├── bootstrap-reboot.min.css.map
│ │ │ ├── bootstrap-reboot.rtl.css
│ │ │ ├── bootstrap-reboot.rtl.css.map
│ │ │ ├── bootstrap-reboot.rtl.min.css
│ │ │ ├── bootstrap-reboot.rtl.min.css.map
│ │ │ ├── bootstrap-utilities.css
│ │ │ ├── bootstrap-utilities.css.map
│ │ │ ├── bootstrap-utilities.min.css
│ │ │ ├── bootstrap-utilities.min.css.map
│ │ │ ├── bootstrap-utilities.rtl.css
│ │ │ ├── bootstrap-utilities.rtl.css.map
│ │ │ ├── bootstrap-utilities.rtl.min.css
│ │ │ ├── bootstrap-utilities.rtl.min.css.map
│ │ │ ├── bootstrap.css
│ │ │ ├── bootstrap.css.map
│ │ │ ├── bootstrap.min.css
│ │ │ ├── bootstrap.min.css.map
│ │ │ ├── bootstrap.rtl.css
│ │ │ ├── bootstrap.rtl.css.map
│ │ │ ├── bootstrap.rtl.min.css
│ │ │ └── bootstrap.rtl.min.css.map
│ │ │ └── js
│ │ │ ├── bootstrap.bundle.js
│ │ │ ├── bootstrap.bundle.js.map
│ │ │ ├── bootstrap.bundle.min.js
│ │ │ ├── bootstrap.bundle.min.js.map
│ │ │ ├── bootstrap.esm.js
│ │ │ ├── bootstrap.esm.js.map
│ │ │ ├── bootstrap.esm.min.js
│ │ │ ├── bootstrap.esm.min.js.map
│ │ │ ├── bootstrap.js
│ │ │ ├── bootstrap.js.map
│ │ │ ├── bootstrap.min.js
│ │ │ └── bootstrap.min.js.map
│ │ ├── jquery-validation-unobtrusive
│ │ ├── LICENSE.txt
│ │ ├── jquery.validate.unobtrusive.js
│ │ └── jquery.validate.unobtrusive.min.js
│ │ ├── jquery-validation
│ │ ├── LICENSE.md
│ │ └── dist
│ │ │ ├── additional-methods.js
│ │ │ ├── additional-methods.min.js
│ │ │ ├── jquery.validate.js
│ │ │ └── jquery.validate.min.js
│ │ └── jquery
│ │ ├── LICENSE.txt
│ │ └── dist
│ │ ├── jquery.js
│ │ ├── jquery.min.js
│ │ └── jquery.min.map
└── tests
│ ├── NimblePros.SampleToDo.FunctionalTests
│ ├── Contributors
│ │ ├── ContributorCreate.cs
│ │ ├── ContributorDelete.cs
│ │ ├── ContributorGetById.cs
│ │ ├── ContributorList.cs
│ │ └── ContributorUpdate.cs
│ ├── CustomWebApplicationFactory.cs
│ ├── Fixtures
│ │ └── SmtpServerFixture.cs
│ ├── GlobalUsings.cs
│ ├── NimblePros.SampleToDo.FunctionalTests.csproj
│ ├── Projects
│ │ ├── ProjectAddToDoItem.cs
│ │ ├── ProjectCreate.cs
│ │ ├── ProjectGetById.cs
│ │ ├── ProjectItemMarkComplete.cs
│ │ └── ProjectList.cs
│ └── xunit.runner.json
│ ├── NimblePros.SampleToDo.IntegrationTests
│ ├── Data
│ │ ├── BaseEfRepoTestFixture.cs
│ │ ├── EfRepositoryAdd.cs
│ │ ├── EfRepositoryDelete.cs
│ │ └── EfRepositoryUpdate.cs
│ ├── GlobalUsings.cs
│ └── NimblePros.SampleToDo.IntegrationTests.csproj
│ └── NimblePros.SampleToDo.UnitTests
│ ├── Core
│ ├── ContributorAggregate
│ │ └── ContributorConstructor.cs
│ ├── Handlers
│ │ └── ItemCompletedEmailNotificationHandlerHandle.cs
│ ├── ProjectAggregate
│ │ ├── ProjectConstructor.cs
│ │ ├── ProjectNameFrom.cs
│ │ ├── Project_AddItem.cs
│ │ ├── ToDoItemConstructor.cs
│ │ └── ToDoItemMarkComplete.cs
│ ├── Services
│ │ ├── DeleteContributorSevice_DeleteContributor.cs
│ │ ├── ToDoItemSearchServiceTests.cs
│ │ ├── ToDoItemSearchService_GetAllIncompleteItems.cs
│ │ └── ToDoItemSearchService_GetNextIncompleteItem.cs
│ └── Specifications
│ │ └── IncompleteItemSpecificationsConstructor.cs
│ ├── GlobalUsings.cs
│ ├── NimblePros.SampleToDo.UnitTests.csproj
│ ├── NoOpMediator.cs
│ ├── ToDoItemBuilder.cs
│ ├── UseCases
│ └── Contributors
│ │ ├── CreateContributorHandlerHandle.cs
│ │ ├── GetContributorHandlerHandle.cs
│ │ └── UpdateContributorHandlerHandle.cs
│ └── xunit.runner.json
├── src
├── Clean.Architecture.AspireHost
│ ├── Clean.Architecture.AspireHost.csproj
│ ├── Program.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── appsettings.Development.json
│ └── appsettings.json
├── Clean.Architecture.Core
│ ├── Clean.Architecture.Core.csproj
│ ├── ContributorAggregate
│ │ ├── Contributor.cs
│ │ ├── ContributorStatus.cs
│ │ ├── Events
│ │ │ └── ContributorDeletedEvent.cs
│ │ ├── Handlers
│ │ │ └── ContributorDeletedHandler.cs
│ │ └── Specifications
│ │ │ └── ContributorByIdSpec.cs
│ ├── GlobalUsings.cs
│ ├── Interfaces
│ │ ├── IDeleteContributorService.cs
│ │ └── IEmailSender.cs
│ ├── README.md
│ └── Services
│ │ └── DeleteContributorService.cs
├── Clean.Architecture.Infrastructure
│ ├── Clean.Architecture.Infrastructure.csproj
│ ├── Data
│ │ ├── AppDbContext.cs
│ │ ├── AppDbContextExtensions.cs
│ │ ├── Config
│ │ │ ├── ContributorConfiguration.cs
│ │ │ └── DataSchemaConstants.cs
│ │ ├── EfRepository.cs
│ │ ├── Migrations
│ │ │ ├── 20231218143922_PhoneNumber.Designer.cs
│ │ │ ├── 20231218143922_PhoneNumber.cs
│ │ │ └── AppDbContextModelSnapshot.cs
│ │ ├── Queries
│ │ │ ├── FakeListContributorsQueryService.cs
│ │ │ └── ListContributorsQueryService.cs
│ │ └── SeedData.cs
│ ├── Email
│ │ ├── FakeEmailSender.cs
│ │ ├── MailserverConfiguration.cs
│ │ ├── MimeKitEmailSender.cs
│ │ └── SmtpEmailSender.cs
│ ├── GlobalUsings.cs
│ ├── InfrastructureServiceExtensions.cs
│ └── README.md
├── Clean.Architecture.ServiceDefaults
│ ├── Clean.Architecture.ServiceDefaults.csproj
│ └── Extensions.cs
├── Clean.Architecture.UseCases
│ ├── Clean.Architecture.UseCases.csproj
│ ├── Contributors
│ │ ├── ContributorDTO.cs
│ │ ├── Create
│ │ │ ├── CreateContributorCommand.cs
│ │ │ └── CreateContributorHandler.cs
│ │ ├── Delete
│ │ │ ├── DeleteContributorCommand.cs
│ │ │ └── DeleteContributorHandler.cs
│ │ ├── Get
│ │ │ ├── GetContributorHandler.cs
│ │ │ └── GetContributorQuery.cs
│ │ ├── List
│ │ │ ├── IListContributorsQueryService.cs
│ │ │ ├── ListContributorsHandler.cs
│ │ │ └── ListContributorsQuery.cs
│ │ └── Update
│ │ │ ├── UpdateContributorCommand.cs
│ │ │ └── UpdateContributorHandler.cs
│ ├── GlobalUsings.cs
│ └── README.md
└── Clean.Architecture.Web
│ ├── Clean.Architecture.Web.csproj
│ ├── Configurations
│ ├── LoggerConfigs.cs
│ ├── MediatrConfigs.cs
│ ├── MiddlewareConfig.cs
│ ├── OptionConfigs.cs
│ └── ServiceConfigs.cs
│ ├── Contributors
│ ├── ContributorRecord.cs
│ ├── Create.CreateContributorRequest.cs
│ ├── Create.CreateContributorResponse.cs
│ ├── Create.CreateContributorValidator.cs
│ ├── Create.cs
│ ├── Delete.DeleteContributorRequest.cs
│ ├── Delete.DeleteContributorValidator.cs
│ ├── Delete.cs
│ ├── GetById.GetContributorByIdRequest.cs
│ ├── GetById.GetContributorValidator.cs
│ ├── GetById.cs
│ ├── List.ContributorListResponse.cs
│ ├── List.cs
│ ├── Update.UpdateContributorRequest.cs
│ ├── Update.UpdateContributorResponse.cs
│ ├── Update.UpdateContributorValidator.cs
│ └── Update.cs
│ ├── GlobalUsings.cs
│ ├── Program.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── api.http
│ ├── appsettings.json
│ └── wwwroot
│ └── .gitkeep
└── tests
├── Clean.Architecture.AspireTests
├── AspireIntegrationTests.cs
└── Clean.Architecture.AspireTests.csproj
├── Clean.Architecture.FunctionalTests
├── ApiEndpoints
│ ├── ContributorGetById.cs
│ └── ContributorList.cs
├── Clean.Architecture.FunctionalTests.csproj
├── CustomWebApplicationFactory.cs
├── GlobalUsings.cs
└── xunit.runner.json
├── Clean.Architecture.IntegrationTests
├── Clean.Architecture.IntegrationTests.csproj
├── Data
│ ├── BaseEfRepoTestFixture.cs
│ ├── EfRepositoryAdd.cs
│ ├── EfRepositoryDelete.cs
│ └── EfRepositoryUpdate.cs
└── GlobalUsings.cs
└── Clean.Architecture.UnitTests
├── Clean.Architecture.UnitTests.csproj
├── Core
├── ContributorAggregate
│ └── ContributorConstructor.cs
└── Services
│ ├── DeleteContributorSevice_DeleteContributor.cs
│ ├── ToDoItemSearchService_GetAllIncompleteItems.cs
│ └── ToDoItemSearchService_GetNextIncompleteItem.cs
├── GlobalUsings.cs
├── NoOpMediator.cs
├── UseCases
└── Contributors
│ └── CreateContributorHandlerHandle.cs
└── xunit.runner.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [ardalis]
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | ---
5 |
6 |
7 |
8 | - .NET SDK Version:
9 |
10 | Steps to Reproduce:
11 |
12 | 1.
13 | 2.
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Question
4 | url: https://stackoverflow.com/questions/tagged/ardalis-cleanarchitecture
5 | about: Please ask and answer questions on Stack Overflow.
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: nuget
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/.github/workflows/build-ramdisk.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test with Ramdisk
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | build-and-test:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | # Step 1: Checkout the repository
15 | - name: Checkout Code
16 | uses: actions/checkout@v3
17 |
18 | # Step 2: Set up .NET environment
19 | - name: Setup .NET
20 | uses: actions/setup-dotnet@v3
21 | with:
22 | dotnet-version: 9.0.100
23 |
24 | # Step 3: Prepare a directory in /dev/shm
25 | - name: Set up Ramdisk Directory
26 | run: |
27 | mkdir -p /dev/shm/ramdisk/project
28 |
29 | # Step 4: Copy source code to the ramdisk
30 | - name: Copy Code to Ramdisk
31 | run: |
32 | cp -r $GITHUB_WORKSPACE/* /dev/shm/ramdisk/project
33 |
34 | # Step 5: Debug Directory Contents
35 | - name: Debug Directory
36 | run: |
37 | ls -R /dev/shm/ramdisk/project
38 |
39 | # Step 6: Build and Test from the ramdisk
40 | - name: Build and Test
41 | run: |
42 | cd /dev/shm/ramdisk/project
43 | dotnet build Clean.Architecture.sln --configuration Debug
44 | dotnet test Clean.Architecture.sln --
45 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: CodeQL Analysis
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | schedule:
8 | - cron: '0 8 * * *'
9 |
10 | jobs:
11 | analyze:
12 | name: CodeQL Analysis
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout repository
16 | id: checkout_repo
17 | uses: actions/checkout@v2
18 |
19 | - name: Initialize CodeQL
20 | id: init_codeql
21 | uses: github/codeql-action/init@v2
22 | with:
23 | queries: security-and-quality
24 |
25 | - name: Autobuild
26 | uses: github/codeql-action/autobuild@v2
27 |
28 | - name: Perform CodeQL Analysis
29 | id: analyze_codeql
30 | uses: github/codeql-action/analyze@v2
31 |
32 | # Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)
33 |
--------------------------------------------------------------------------------
/.github/workflows/dotnetcore.yml:
--------------------------------------------------------------------------------
1 | name: .NET Core
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Setup .NET Core
17 | uses: actions/setup-dotnet@v3
18 | with:
19 | dotnet-version: '9.0.100-rc.2.24474.11'
20 | - name: Build with dotnet
21 | run: dotnet build --configuration Release
22 | - name: Test with dotnet
23 | run: dotnet test ./Clean.Architecture.sln --configuration Release
24 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish Ardalis.CleanArchitecture Template to nuget
2 | on:
3 | push:
4 | branches:
5 | - main # Your default release branch
6 | paths:
7 | - 'CleanArchitecture.nuspec'
8 | jobs:
9 | publish:
10 | name: list on nuget
11 | runs-on: windows-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - uses: nuget/setup-nuget@v1
16 | with:
17 | nuget-version: '5.x'
18 |
19 | - name: Package the template
20 | run: nuget pack CleanArchitecture.nuspec -NoDefaultExcludes
21 |
22 | - name: Publish to nuget.org
23 | run: nuget push Ardalis.CleanArchitecture.Template.*.nupkg -src https://api.nuget.org/v3/index.json ${{secrets.NUGET_API_KEY}}
24 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "${workspaceFolder}/Clean.Architecture.sln",
11 | "/property:GenerateFullPaths=true",
12 | "/consoleloggerparameters:NoSummary;ForceNoAlign"
13 | ],
14 | "problemMatcher": "$msCompile"
15 | },
16 | {
17 | "label": "publish",
18 | "command": "dotnet",
19 | "type": "process",
20 | "args": [
21 | "publish",
22 | "${workspaceFolder}/Clean.Architecture.sln",
23 | "/property:GenerateFullPaths=true",
24 | "/consoleloggerparameters:NoSummary;ForceNoAlign"
25 | ],
26 | "problemMatcher": "$msCompile"
27 | },
28 | {
29 | "label": "watch",
30 | "command": "dotnet",
31 | "type": "process",
32 | "args": [
33 | "watch",
34 | "run",
35 | "--project",
36 | "${workspaceFolder}/Clean.Architecture.sln"
37 | ],
38 | "problemMatcher": "$msCompile"
39 | }
40 | ]
41 | }
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | This project has adopted the code of conduct defined by the Contributor Covenant
4 | to clarify expected behavior in our community.
5 | For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
6 |
--------------------------------------------------------------------------------
/CleanArchitecture.nuspec:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Ardalis.CleanArchitecture.Template
5 | ASP.NET Core Clean Architecture Solution
6 | 10.0.2
7 | Steve Smith
8 |
9 | The Clean Architecture Solution Template popularized by Steve @ardalis Smith. Provides a great starting point for modern and/or DDD solutions built with .NET 8 and C# 12.
10 | Features zero tight coupling to database or data access technology.
11 |
12 | en-US
13 | MIT
14 | https://github.com/ardalis/CleanArchitecture
15 |
16 | * Fixes project references in the SLN file for Aspire
17 |
18 |
19 |
20 |
21 | Web ASP.NET "Clean Architecture" ddd domain-driven-design clean-architecture clean architecture ardalis SOLID
22 | ./content/icon.png
23 | README.md
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | true
5 | net9.0
6 | enable
7 | enable
8 |
9 |
10 | 1591
11 |
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Steve Smith
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 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "9.0.100-rc.2.24474.11",
4 | "rollForward": "latestMajor"
5 | }
6 | }
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ardalis/CleanArchitecture/7c031c77e6b8db695f3266a5f5873a522bbb238a/icon.png
--------------------------------------------------------------------------------
/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
29 |
--------------------------------------------------------------------------------
/sample/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | false
5 | net9.0
6 |
7 |
8 | 1591
9 |
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.AspireHost/NimblePros.SampleToDo.AspireHost.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Exe
7 | net9.0
8 | enable
9 | enable
10 | true
11 | c540eeb6-e06b-4456-a539-be58dd8b88c7
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
25 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.AspireHost/Program.cs:
--------------------------------------------------------------------------------
1 | var builder = DistributedApplication.CreateBuilder(args);
2 |
3 | builder.AddProject("web");
4 |
5 | builder.Build().Run();
6 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.AspireHost/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "https": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": true,
8 | "applicationUrl": "https://localhost:17143;http://localhost:15258",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development",
11 | "DOTNET_ENVIRONMENT": "Development",
12 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21007",
13 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22245"
14 | }
15 | },
16 | "http": {
17 | "commandName": "Project",
18 | "dotnetRunMessages": true,
19 | "launchBrowser": true,
20 | "applicationUrl": "http://localhost:15258",
21 | "environmentVariables": {
22 | "ASPNETCORE_ENVIRONMENT": "Development",
23 | "DOTNET_ENVIRONMENT": "Development",
24 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19187",
25 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20134"
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.AspireHost/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.AspireHost/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning",
6 | "Aspire.Hosting.Dcp": "Warning"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ContributorAggregate/Contributor.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ContributorAggregate.Events;
2 |
3 | namespace NimblePros.SampleToDo.Core.ContributorAggregate;
4 |
5 | public class Contributor : EntityBase, IAggregateRoot
6 | {
7 | public ContributorName Name { get; private set; }
8 |
9 | public Contributor(ContributorName name)
10 | {
11 | Name = name;
12 | }
13 |
14 | public void UpdateName(ContributorName newName)
15 | {
16 | if (Name.Equals(newName)) return;
17 | Name = newName;
18 | this.RegisterDomainEvent(new ContributorNameUpdatedEvent(this));
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ContributorAggregate/ContributorName.cs:
--------------------------------------------------------------------------------
1 | using Vogen;
2 |
3 | namespace NimblePros.SampleToDo.Core.ContributorAggregate;
4 |
5 | [ValueObject(conversions: Conversions.SystemTextJson)]
6 | public partial struct ContributorName
7 | {
8 | private static Validation Validate(in string name) => String.IsNullOrEmpty(name) ?
9 | Validation.Invalid("Name cannot be empty") :
10 | Validation.Ok;
11 | }
12 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ContributorAggregate/Events/ContributorDeletedEvent.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.ContributorAggregate.Events;
2 |
3 | ///
4 | /// A domain event that is dispatched whenever a contributor is deleted.
5 | /// The DeleteContributorService is used to dispatch this event.
6 | ///
7 | internal class ContributorDeletedEvent(int contributorId) : DomainEventBase
8 | {
9 | public int ContributorId { get; set; } = contributorId;
10 | }
11 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ContributorAggregate/Events/ContributorNameUpdatedEvent.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.ContributorAggregate.Events;
2 |
3 | internal class ContributorNameUpdatedEvent(Contributor contributor) : DomainEventBase
4 | {
5 | public Contributor Contributor { get; private set; } = contributor;
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ContributorAggregate/Handlers/ContributorNameUpdatedEventLoggingHandler.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ContributorAggregate.Events;
2 |
3 | namespace NimblePros.SampleToDo.Core.ContributorAggregate.Handlers;
4 |
5 | internal class ContributorNameUpdatedEventLoggingHandler(ILogger logger) : INotificationHandler
6 | {
7 | private readonly ILogger _logger = logger;
8 |
9 | public Task Handle(ContributorNameUpdatedEvent notification, CancellationToken cancellationToken)
10 | {
11 | int contributorId = notification.Contributor.Id;
12 | string newName = notification.Contributor.Name.Value;
13 | _logger.LogInformation("Contributor {contributorId}'s name was updated to {newName}", contributorId, newName);
14 | return Task.CompletedTask;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ContributorAggregate/Specifications/ContributorByIdSpec.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.ContributorAggregate.Specifications;
2 |
3 | public class ContributorByIdSpec : Specification
4 | {
5 | public ContributorByIdSpec(int contributorId)
6 | {
7 | Query
8 | .Where(contributor => contributor.Id == contributorId);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/CoreServiceExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using NimblePros.SampleToDo.Core.Interfaces;
3 | using NimblePros.SampleToDo.Core.Services;
4 |
5 | namespace NimblePros.SampleToDo.Core;
6 |
7 | public static class CoreServiceExtensions
8 | {
9 | public static IServiceCollection AddCoreServices(this IServiceCollection services, ILogger logger)
10 | {
11 | services.AddScoped();
12 | services.AddScoped();
13 |
14 | logger.LogInformation("{Project} services registered", "Core");
15 |
16 | return services;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Ardalis.GuardClauses;
2 | global using Ardalis.Result;
3 | global using Ardalis.SharedKernel;
4 | global using Ardalis.SmartEnum;
5 | global using Ardalis.Specification;
6 | global using MediatR;
7 | global using Microsoft.Extensions.Logging;
8 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/Interfaces/IDeleteContributorService.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.Interfaces;
2 |
3 | public interface IDeleteContributorService
4 | {
5 | // This service and method exist to provide a place in which to fire domain events
6 | // when deleting this aggregate root entity
7 | public Task DeleteContributor(int contributorId);
8 | }
9 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/Interfaces/IEmailSender.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.Interfaces;
2 |
3 | public interface IEmailSender
4 | {
5 | Task SendEmailAsync(string to, string from, string subject, string body);
6 | }
7 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/Interfaces/IToDoItemSearchService.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.Core.Interfaces;
4 |
5 | public interface IToDoItemSearchService
6 | {
7 | Task> GetNextIncompleteItemAsync(ProjectId projectId);
8 | Task>> GetAllIncompleteItemsAsync(ProjectId projectId, string searchString);
9 | }
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/NimblePros.SampleToDo.Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Events/ContributorAddedToItemEvent.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.ProjectAggregate.Events;
2 |
3 | public class ContributorAddedToItemEvent : DomainEventBase
4 | {
5 | public int ContributorId { get; set; }
6 | public ToDoItem Item { get; set; }
7 |
8 | public ContributorAddedToItemEvent(ToDoItem item, int contributorId)
9 | {
10 | Item = item;
11 | ContributorId = contributorId;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Events/NewItemAddedEvent.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.ProjectAggregate.Events;
2 |
3 | public class NewItemAddedEvent : DomainEventBase
4 | {
5 | public ToDoItem NewItem { get; set; }
6 | public Project Project { get; set; }
7 |
8 | public NewItemAddedEvent(Project project,
9 | ToDoItem newItem)
10 | {
11 | Project = project;
12 | NewItem = newItem;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Events/ToDoItemCompletedEvent.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.ProjectAggregate.Events;
2 |
3 | public class ToDoItemCompletedEvent : DomainEventBase
4 | {
5 | public ToDoItem CompletedItem { get; set; }
6 |
7 | public ToDoItemCompletedEvent(ToDoItem completedItem)
8 | {
9 | CompletedItem = completedItem;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Handlers/ItemCompletedEmailNotificationHandler.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.Interfaces;
2 | using NimblePros.SampleToDo.Core.ProjectAggregate.Events;
3 |
4 | namespace NimblePros.SampleToDo.Core.ProjectAggregate.Handlers;
5 |
6 | public class ItemCompletedEmailNotificationHandler : INotificationHandler
7 | {
8 | private readonly IEmailSender _emailSender;
9 |
10 | // In a REAL app you might want to use the Outbox pattern and a command/queue here...
11 | public ItemCompletedEmailNotificationHandler(IEmailSender emailSender)
12 | {
13 | _emailSender = emailSender;
14 | }
15 |
16 | // configure a test email server to demo this works
17 | // https://ardalis.com/configuring-a-local-test-email-server
18 | public Task Handle(ToDoItemCompletedEvent domainEvent, CancellationToken cancellationToken)
19 | {
20 | Guard.Against.Null(domainEvent, nameof(domainEvent));
21 |
22 | return _emailSender.SendEmailAsync("test@test.com", "test@test.com", $"{domainEvent.CompletedItem.Title} was completed.", domainEvent.CompletedItem.ToString());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Priority.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | public class Priority : SmartEnum
4 | {
5 | public static readonly Priority Backlog = new(nameof(Backlog), 0);
6 | public static readonly Priority Critical = new(nameof(Critical), 1);
7 |
8 | protected Priority(string name, int value) : base(name, value) { }
9 | }
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Project.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate.Events;
2 |
3 | namespace NimblePros.SampleToDo.Core.ProjectAggregate;
4 |
5 | public class Project : EntityBase, IAggregateRoot
6 | {
7 | public ProjectName Name { get; private set; }
8 |
9 | private readonly List _items = new();
10 | public IEnumerable Items => _items.AsReadOnly();
11 | public ProjectStatus Status => _items.All(i => i.IsDone) ? ProjectStatus.Complete : ProjectStatus.InProgress;
12 |
13 | public Project(ProjectName name)
14 | {
15 | Name = name;
16 | }
17 |
18 | public void AddItem(ToDoItem newItem)
19 | {
20 | Guard.Against.Null(newItem);
21 | _items.Add(newItem);
22 |
23 | var newItemAddedEvent = new NewItemAddedEvent(this, newItem);
24 | base.RegisterDomainEvent(newItemAddedEvent);
25 | }
26 |
27 | public void UpdateName(ProjectName newName)
28 | {
29 | Name = newName;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ProjectId.cs:
--------------------------------------------------------------------------------
1 | using Vogen;
2 | namespace NimblePros.SampleToDo.Core.ProjectAggregate;
3 |
4 | [ValueObject]
5 | public partial struct ProjectId;
6 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ProjectName.cs:
--------------------------------------------------------------------------------
1 | using Vogen;
2 |
3 | [assembly: VogenDefaults(
4 | staticAbstractsGeneration: StaticAbstractsGeneration.MostCommon | StaticAbstractsGeneration.InstanceMethodsAndProperties)]
5 |
6 |
7 | namespace NimblePros.SampleToDo.Core.ProjectAggregate;
8 |
9 | // NOTE: Structs do not require conversion to work with EF Core
10 | [ValueObject(conversions: Conversions.SystemTextJson)]
11 | public partial struct ProjectName
12 | {
13 | private static Validation Validate(in string name) => String.IsNullOrEmpty(name) ?
14 | Validation.Invalid("Name cannot be empty") :
15 | Validation.Ok;
16 | }
17 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ProjectStatus.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | public enum ProjectStatus
4 | {
5 | InProgress, // NOTE: Better to use a SmartEnum if you want spaces in your strings e.g. "In Progress"
6 | Complete
7 | }
8 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Specifications/IncompleteItemsSearchSpec.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.ProjectAggregate.Specifications;
2 |
3 | public class IncompleteItemsSearchSpec : Specification
4 | {
5 | public IncompleteItemsSearchSpec(string searchString)
6 | {
7 | Query
8 | .Where(item => !item.IsDone &&
9 | (item.Title.Contains(searchString) ||
10 | item.Description.Contains(searchString)));
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Specifications/IncompleteItemsSpec.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.ProjectAggregate.Specifications;
2 |
3 | public class IncompleteItemsSpec : Specification
4 | {
5 | public IncompleteItemsSpec()
6 | {
7 | Query.Where(item => !item.IsDone);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Specifications/ProjectByIdWithItemsSpec.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.ProjectAggregate.Specifications;
2 |
3 | public class ProjectByIdWithItemsSpec : Specification
4 | {
5 | public ProjectByIdWithItemsSpec(ProjectId projectId)
6 | {
7 | Query
8 | .Where(project => project.Id == projectId)
9 | .Include(project => project.Items);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Specifications/ProjectsWithItemsByContributorId.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Core.ProjectAggregate.Specifications;
2 |
3 | public class ProjectsWithItemsByContributorIdSpec : Specification
4 | {
5 | public ProjectsWithItemsByContributorIdSpec(int contributorId)
6 | {
7 | Query
8 | .Where(project => project.Items.Any(item => item.ContributorId == contributorId))
9 | .Include(project => project.Items);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItem.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate.Events;
2 |
3 | namespace NimblePros.SampleToDo.Core.ProjectAggregate;
4 |
5 | public class ToDoItem : EntityBase
6 | {
7 | public ToDoItem() : this(Priority.Backlog)
8 | {
9 | }
10 |
11 | public ToDoItem(Priority priority)
12 | {
13 | Priority = priority;
14 | }
15 |
16 | public string Title { get; set; } = string.Empty;
17 | public string Description { get; set; } = string.Empty;
18 | public int? ContributorId { get; private set; } // tasks don't have anyone assigned when first created
19 | public bool IsDone { get; private set; }
20 |
21 | public Priority Priority { get; private set; }
22 |
23 |
24 | public void MarkComplete()
25 | {
26 | if (!IsDone)
27 | {
28 | IsDone = true;
29 |
30 | RegisterDomainEvent(new ToDoItemCompletedEvent(this));
31 | }
32 | }
33 |
34 | public void AddContributor(int contributorId)
35 | {
36 | Guard.Against.Null(contributorId);
37 | ContributorId = contributorId;
38 |
39 | var contributorAddedToItem = new ContributorAddedToItemEvent(this, contributorId);
40 | base.RegisterDomainEvent(contributorAddedToItem);
41 | }
42 |
43 | public void RemoveContributor()
44 | {
45 | ContributorId = null;
46 | }
47 |
48 | public override string ToString()
49 | {
50 | string status = IsDone ? "Done!" : "Not done.";
51 | return $"{Id}: Status: {status} - {Title} - {Description}";
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemId.cs:
--------------------------------------------------------------------------------
1 | using Vogen;
2 | namespace NimblePros.SampleToDo.Core.ProjectAggregate;
3 |
4 | [ValueObject]
5 | public partial struct ToDoItemId;
6 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Core/Services/DeleteContributorService.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ContributorAggregate;
2 | using NimblePros.SampleToDo.Core.ContributorAggregate.Events;
3 | using NimblePros.SampleToDo.Core.Interfaces;
4 |
5 | namespace NimblePros.SampleToDo.Core.Services;
6 |
7 | public class DeleteContributorService : IDeleteContributorService
8 | {
9 | private readonly IRepository _repository;
10 | private readonly IMediator _mediator;
11 | private readonly ILogger _logger;
12 |
13 | public DeleteContributorService(IRepository repository,
14 | IMediator mediator,
15 | ILogger logger)
16 | {
17 | _repository = repository;
18 | _mediator = mediator;
19 | _logger = logger;
20 | }
21 |
22 | public async Task DeleteContributor(int contributorId)
23 | {
24 | _logger.LogInformation("Deleting Contributor {contributorId}", contributorId);
25 | var aggregateToDelete = await _repository.GetByIdAsync(contributorId);
26 | if (aggregateToDelete == null) return Result.NotFound();
27 |
28 | await _repository.DeleteAsync(aggregateToDelete);
29 | var domainEvent = new ContributorDeletedEvent(contributorId);
30 | await _mediator.Publish(domainEvent);
31 | return Result.Success();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/ContributorConfiguration.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ContributorAggregate;
2 |
3 | namespace NimblePros.SampleToDo.Infrastructure.Data.Config;
4 |
5 | public class ContributorConfiguration : IEntityTypeConfiguration
6 | {
7 | public void Configure(EntityTypeBuilder builder)
8 | {
9 | builder.Property(p => p.Name)
10 | .HasVogenConversion()
11 | .HasMaxLength(DataSchemaConstants.DEFAULT_NAME_LENGTH)
12 | .IsRequired();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/DataSchemaConstants.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Infrastructure.Data.Config;
2 |
3 | public static class DataSchemaConstants
4 | {
5 | public const int DEFAULT_NAME_LENGTH = 100;
6 | }
7 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/ProjectConfiguration.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.Infrastructure.Data.Config;
4 |
5 | public class ProjectConfiguration : IEntityTypeConfiguration
6 | {
7 | public void Configure(EntityTypeBuilder builder)
8 | {
9 | builder.Property(p => p.Id)
10 | .HasValueGenerator>()
11 | .HasVogenConversion()
12 | .IsRequired();
13 | builder.Property(p => p.Name)
14 | .HasVogenConversion()
15 | .HasMaxLength(DataSchemaConstants.DEFAULT_NAME_LENGTH)
16 | .IsRequired();
17 | }
18 | }
19 |
20 |
21 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/ToDoItemConfiguration.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.Infrastructure.Data.Config;
4 |
5 | public class ToDoItemConfiguration : IEntityTypeConfiguration
6 | {
7 | public void Configure(EntityTypeBuilder builder)
8 | {
9 | builder.Property(p => p.Id)
10 | .HasValueGenerator>()
11 | .HasVogenConversion()
12 | .IsRequired();
13 | builder.Property(t => t.Title)
14 | .IsRequired();
15 | builder.Property(t => t.ContributorId)
16 | .IsRequired(false);
17 | builder.Property(t => t.Priority)
18 | .HasConversion(
19 | p => p.Value,
20 | p => Priority.FromValue(p));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/VogenEfCoreConverters.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ContributorAggregate;
2 | using NimblePros.SampleToDo.Core.ProjectAggregate;
3 | using Vogen;
4 |
5 | namespace NimblePros.SampleToDo.Infrastructure.Data.Config;
6 |
7 | [EfCoreConverter]
8 | [EfCoreConverter]
9 | [EfCoreConverter]
10 | [EfCoreConverter]
11 | internal partial class VogenEfCoreConverters;
12 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Data/EfRepository.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Infrastructure.Data;
2 |
3 | // inherit from Ardalis.Specification type
4 | public class EfRepository : RepositoryBase, IReadRepository, IRepository where T : class, IAggregateRoot
5 | {
6 | public EfRepository(AppDbContext dbContext) : base(dbContext)
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/FakeListContributorsQueryService.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.UseCases.Contributors;
2 | using NimblePros.SampleToDo.UseCases.Contributors.Queries.List;
3 |
4 | namespace NimblePros.SampleToDo.Infrastructure.Data.Queries;
5 |
6 | public class FakeListContributorsQueryService : IListContributorsQueryService
7 | {
8 | public Task> ListAsync()
9 | {
10 | var result = new List() { new ContributorDTO(1, "Ardalis"), new ContributorDTO(2, "Snowfrog") };
11 | return Task.FromResult(result.AsEnumerable());
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/FakeListIncompleteItemsQueryService.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.UseCases.Projects;
2 | using NimblePros.SampleToDo.UseCases.Projects.ListIncompleteItems;
3 |
4 | namespace NimblePros.SampleToDo.Infrastructure.Data.Queries;
5 |
6 | public class FakeListIncompleteItemsQueryService : IListIncompleteItemsQueryService
7 | {
8 | public async Task> ListAsync(int projectId)
9 | {
10 | var testItem = new ToDoItemDTO(Id: 1000, Title: "test", Description: "test description", IsComplete: false, null);
11 | return await Task.FromResult(new List() { testItem});
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/FakeListProjectsShallowQueryService.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.UseCases.Projects.ListShallow;
2 | using NimblePros.SampleToDo.UseCases.Projects;
3 |
4 | namespace NimblePros.SampleToDo.Infrastructure.Data.Queries;
5 |
6 | public class FakeListProjectsShallowQueryService : IListProjectsShallowQueryService
7 | {
8 | public async Task> ListAsync()
9 | {
10 | var testProject = new ProjectDTO(1000, "Test Project", "InProgress");
11 | return await Task.FromResult(new List { testProject });
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/ListContributorsQueryService.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.UseCases.Contributors;
2 | using NimblePros.SampleToDo.UseCases.Contributors.Queries.List;
3 |
4 | namespace NimblePros.SampleToDo.Infrastructure.Data.Queries;
5 |
6 | public class ListContributorsQueryService : IListContributorsQueryService
7 | {
8 | // You can use EF, Dapper, SqlClient, etc. for queries
9 | private readonly AppDbContext _db;
10 |
11 | public ListContributorsQueryService(AppDbContext db)
12 | {
13 | _db = db;
14 | }
15 |
16 | public async Task> ListAsync()
17 | {
18 | var result = await _db.Contributors.FromSqlRaw("SELECT Id, Name FROM Contributors") // don't fetch other big columns
19 | .Select(c => new ContributorDTO(c.Id, c.Name.Value))
20 | .ToListAsync();
21 |
22 | return result;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/ListIncompleteItemsQueryService.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.UseCases.Projects;
2 | using NimblePros.SampleToDo.UseCases.Projects.ListIncompleteItems;
3 |
4 | namespace NimblePros.SampleToDo.Infrastructure.Data.Queries;
5 |
6 | public class ListIncompleteItemsQueryService : IListIncompleteItemsQueryService
7 | {
8 | private readonly AppDbContext _db;
9 |
10 | public ListIncompleteItemsQueryService(AppDbContext db)
11 | {
12 | _db = db;
13 | }
14 |
15 | public async Task> ListAsync(int projectId)
16 | {
17 | var projectParameter = new SqlParameter("@projectId", System.Data.SqlDbType.Int);
18 | var result = await _db.ToDoItems.FromSqlRaw("SELECT Id, Title, Description, IsDone, ContributorId FROM ToDoItems WHERE ProjectId = @ProjectId",
19 | projectParameter) // don't fetch other big columns
20 | .Select(x => new ToDoItemDTO(x.Id.Value, x.Title, x.Description, x.IsDone, x.ContributorId))
21 | .ToListAsync();
22 |
23 | return result;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/ListProjectsShallowQueryService.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.UseCases.Projects;
2 | using NimblePros.SampleToDo.UseCases.Projects.ListShallow;
3 |
4 | namespace NimblePros.SampleToDo.Infrastructure.Data.Queries;
5 |
6 | public class ListProjectsShallowQueryService(AppDbContext db) :
7 | IListProjectsShallowQueryService
8 | {
9 | private readonly AppDbContext _db = db;
10 |
11 | public async Task> ListAsync()
12 | {
13 | var result = await _db.Projects.FromSqlRaw("SELECT Id, Name FROM Projects") // don't fetch other big columns
14 | .Select(x => new ProjectDTO(x.Id.Value, x.Name.Value, x.Status.ToString()))
15 | .ToListAsync();
16 |
17 | return result;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Email/FakeEmailSender.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.Interfaces;
2 |
3 | namespace NimblePros.SampleToDo.Infrastructure.Email;
4 |
5 | public class FakeEmailSender : IEmailSender
6 | {
7 | private readonly ILogger _logger;
8 |
9 | public FakeEmailSender(ILogger logger)
10 | {
11 | _logger = logger;
12 | }
13 | public Task SendEmailAsync(string to, string from, string subject, string body)
14 | {
15 | _logger.LogInformation("Not actually sending an email to {to} from {from} with subject {subject}", to, from, subject);
16 | return Task.CompletedTask;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Email/MailserverConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Infrastructure.Email;
2 |
3 | public class MailserverConfiguration()
4 | {
5 | public string Hostname { get; set; } = "localhost";
6 | public int Port { get; set; } = 25;
7 | }
8 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Email/MimeKitEmailSender.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Options;
2 | using MimeKit;
3 | using NimblePros.SampleToDo.Core.Interfaces;
4 |
5 | namespace NimblePros.SampleToDo.Infrastructure.Email;
6 |
7 | public class MimeKitEmailSender(ILogger logger,
8 | IOptions mailserverOptions) : IEmailSender
9 | {
10 | private readonly ILogger _logger = logger;
11 | private readonly MailserverConfiguration _mailserverConfiguration = mailserverOptions.Value!;
12 |
13 | public async Task SendEmailAsync(string to, string from, string subject, string body)
14 | {
15 | _logger.LogWarning("Sending email to {to} from {from} with subject {subject} using {type}.", to, from, subject, ToString());
16 |
17 | using var client = new MailKit.Net.Smtp.SmtpClient();
18 | await client.ConnectAsync(_mailserverConfiguration.Hostname,
19 | _mailserverConfiguration.Port, false);
20 | var message = new MimeMessage();
21 | message.From.Add(new MailboxAddress(from, from));
22 | message.To.Add(new MailboxAddress(to, to));
23 | message.Subject = subject;
24 | message.Body = new TextPart("plain") { Text = body };
25 |
26 | await client.SendAsync(message);
27 |
28 | await client.DisconnectAsync(true,
29 | new CancellationToken(canceled: true));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/Email/SmtpEmailSender.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.Interfaces;
2 |
3 | namespace NimblePros.SampleToDo.Infrastructure.Email;
4 |
5 | public class SmtpEmailSender : IEmailSender
6 | {
7 | private readonly ILogger _logger;
8 |
9 | public SmtpEmailSender(ILogger logger)
10 | {
11 | _logger = logger;
12 | }
13 |
14 | public async Task SendEmailAsync(string to, string from, string subject, string body)
15 | {
16 | var emailClient = new SmtpClient("localhost");
17 | var message = new MailMessage
18 | {
19 | From = new MailAddress(from),
20 | Subject = subject,
21 | Body = body
22 | };
23 | message.To.Add(new MailAddress(to));
24 | await emailClient.SendMailAsync(message);
25 | _logger.LogWarning("Sending email to {to} from {from} with subject {subject}.", to, from, subject);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using System.Net.Mail;
2 | global using System.Reflection;
3 | global using Ardalis.SharedKernel;
4 | global using Ardalis.Specification.EntityFrameworkCore;
5 | global using Microsoft.Data.SqlClient;
6 | global using Microsoft.EntityFrameworkCore;
7 | global using Microsoft.EntityFrameworkCore.Metadata.Builders;
8 | global using Microsoft.Extensions.DependencyInjection;
9 | global using Microsoft.Extensions.Logging;
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Infrastructure/NimblePros.SampleToDo.Infrastructure.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.ServiceDefaults/NimblePros.SampleToDo.ServiceDefaults.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | enable
5 | enable
6 | true
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Contributors/Commands/Create/CreateContributorCommand.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ContributorAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Contributors.Commands.Create;
4 |
5 | ///
6 | /// Create a new Contributor.
7 | ///
8 | ///
9 | public record CreateContributorCommand(ContributorName Name) : Ardalis.SharedKernel.ICommand>;
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Contributors/Commands/Create/CreateContributorHandler.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ContributorAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Contributors.Commands.Create;
4 |
5 | public class CreateContributorHandler : ICommandHandler>
6 | {
7 | private readonly IRepository _repository;
8 |
9 | public CreateContributorHandler(IRepository repository)
10 | {
11 | _repository = repository;
12 | }
13 |
14 | public async Task> Handle(CreateContributorCommand request,
15 | CancellationToken cancellationToken)
16 | {
17 | var newContributor = new Contributor(request.Name);
18 | var createdItem = await _repository.AddAsync(newContributor, cancellationToken);
19 |
20 | return createdItem.Id;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Contributors/Commands/Delete/DeleteContributorCommand.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Contributors.Commands.Delete;
2 |
3 | public record DeleteContributorCommand(int ContributorId) : ICommand;
4 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Contributors/Commands/Delete/DeleteContributorHandler.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.Interfaces;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Contributors.Commands.Delete;
4 |
5 | public class DeleteContributorHandler : ICommandHandler
6 | {
7 | private readonly IDeleteContributorService _deleteContributorService;
8 |
9 | public DeleteContributorHandler(IDeleteContributorService deleteContributorService)
10 | {
11 | _deleteContributorService = deleteContributorService;
12 | }
13 |
14 | public async Task Handle(DeleteContributorCommand request, CancellationToken cancellationToken)
15 | {
16 | // This Approach: Keep Domain Events in the Domain Model / Core project; this becomes a pass-through
17 | return await _deleteContributorService.DeleteContributor(request.ContributorId);
18 |
19 | // Another Approach: Do the real work here including dispatching domain events - change the event from internal to public
20 | // Ardalis prefers using the service so that "domain event" behavior remains in the domain model / core project
21 | // var aggregateToDelete = await _repository.GetByIdAsync(request.ContributorId);
22 | // if (aggregateToDelete == null) return Result.NotFound();
23 |
24 | // await _repository.DeleteAsync(aggregateToDelete);
25 | // var domainEvent = new ContributorDeletedEvent(request.ContributorId);
26 | // await _mediator.Publish(domainEvent);
27 | // return Result.Success();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Contributors/Commands/Update/UpdateContributorCommand.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ContributorAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Contributors.Commands.Update;
4 |
5 | public record UpdateContributorCommand(int ContributorId, ContributorName NewName) : ICommand>;
6 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Contributors/Commands/Update/UpdateContributorHandler.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ContributorAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Contributors.Commands.Update;
4 |
5 | public class UpdateContributorHandler : ICommandHandler>
6 | {
7 | private readonly IRepository _repository;
8 |
9 | public UpdateContributorHandler(IRepository repository)
10 | {
11 | _repository = repository;
12 | }
13 |
14 | public async Task> Handle(UpdateContributorCommand request, CancellationToken cancellationToken)
15 | {
16 | var existingContributor = await _repository.GetByIdAsync(request.ContributorId, cancellationToken);
17 | if (existingContributor == null)
18 | {
19 | return Result.NotFound();
20 | }
21 |
22 | existingContributor.UpdateName(request.NewName!);
23 |
24 | await _repository.UpdateAsync(existingContributor, cancellationToken);
25 |
26 | return Result.Success(new ContributorDTO(existingContributor.Id, existingContributor.Name.Value));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Contributors/ContributorDTO.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Contributors;
2 | public record ContributorDTO(int Id, string Name);
3 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Contributors/Queries/Get/GetContributorHandler.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ContributorAggregate;
2 | using NimblePros.SampleToDo.Core.ContributorAggregate.Specifications;
3 |
4 | namespace NimblePros.SampleToDo.UseCases.Contributors.Queries.Get;
5 |
6 | ///
7 | /// Queries don't necessarily need to use repository methods, but they can if it's convenient
8 | ///
9 | public class GetContributorHandler : IQueryHandler>
10 | {
11 | private readonly IReadRepository _repository;
12 |
13 | public GetContributorHandler(IReadRepository repository)
14 | {
15 | _repository = repository;
16 | }
17 |
18 | public async Task> Handle(GetContributorQuery request, CancellationToken cancellationToken)
19 | {
20 | var spec = new ContributorByIdSpec(request.ContributorId);
21 | var entity = await _repository.FirstOrDefaultAsync(spec, cancellationToken);
22 | if (entity == null) return Result.NotFound();
23 |
24 | return new ContributorDTO(entity.Id, entity.Name.Value);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Contributors/Queries/Get/GetContributorQuery.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Contributors.Queries.Get;
2 |
3 | public record GetContributorQuery(int ContributorId) : IQuery>;
4 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Contributors/Queries/List/IListContributorsQueryService.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Contributors.Queries.List;
2 |
3 | ///
4 | /// Represents a service that will actually fetch the necessary data
5 | /// Typically implemented in Infrastructure
6 | ///
7 | public interface IListContributorsQueryService
8 | {
9 | Task> ListAsync();
10 | }
11 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Contributors/Queries/List/ListContributorsHandler.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Contributors.Queries.List;
2 |
3 | public class ListContributorsHandler : IQueryHandler>>
4 | {
5 | private readonly IListContributorsQueryService _query;
6 |
7 | public ListContributorsHandler(IListContributorsQueryService query)
8 | {
9 | _query = query;
10 | }
11 |
12 | public async Task>> Handle(ListContributorsQuery request, CancellationToken cancellationToken)
13 | {
14 | var result = await _query.ListAsync();
15 |
16 | return Result.Success(result);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Contributors/Queries/List/ListContributorsQuery.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Contributors.Queries.List;
2 |
3 | public record ListContributorsQuery(int? Skip, int? Take) : IQuery>>;
4 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Ardalis.Result;
2 | global using Ardalis.SharedKernel;
3 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/NimblePros.SampleToDo.UseCases.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | enable
5 | enable
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemCommand.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Projects.AddToDoItem;
4 |
5 | ///
6 | /// Creates a new ToDoItem and adds it to a Project
7 | ///
8 | ///
9 | ///
10 | ///
11 | ///
12 | public record AddToDoItemCommand(ProjectId ProjectId,
13 | int? ContributorId,
14 | string Title,
15 | string Description) : ICommand>;
16 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemHandler.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 | using NimblePros.SampleToDo.Core.ProjectAggregate.Specifications;
3 |
4 | namespace NimblePros.SampleToDo.UseCases.Projects.AddToDoItem;
5 |
6 | public class AddToDoItemHandler : ICommandHandler>
7 | {
8 | private readonly IRepository _repository;
9 |
10 | public AddToDoItemHandler(IRepository repository)
11 | {
12 | _repository = repository;
13 | }
14 |
15 | public async Task> Handle(AddToDoItemCommand request,
16 | CancellationToken cancellationToken)
17 | {
18 | var spec = new ProjectByIdWithItemsSpec(request.ProjectId);
19 | var entity = await _repository.FirstOrDefaultAsync(spec, cancellationToken);
20 | if (entity == null)
21 | {
22 | return Result.NotFound();
23 | }
24 |
25 | var newItem = new ToDoItem()
26 | {
27 | Title = request.Title!,
28 | Description = request.Description!
29 | };
30 |
31 | if(request.ContributorId.HasValue)
32 | {
33 | newItem.AddContributor(request.ContributorId.Value);
34 | }
35 | entity.AddItem(newItem);
36 | await _repository.UpdateAsync(entity);
37 |
38 | return Result.Success(newItem.Id);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/Create/CreateProjectCommand.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Projects.Create;
4 |
5 | ///
6 | /// Create a new Project.
7 | ///
8 | ///
9 | public record CreateProjectCommand(string Name) : ICommand>;
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/Create/CreateProjectHandler.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Projects.Create;
4 |
5 | public class CreateProjectHandler(IRepository repository) : ICommandHandler>
6 | {
7 | private readonly IRepository _repository = repository;
8 |
9 | public async Task> Handle(CreateProjectCommand request,
10 | CancellationToken cancellationToken)
11 | {
12 | var newProject = new Project(ProjectName.From(request.Name));
13 | // NOTE: This implementation issues 3 queries due to Vogen implementation
14 | var createdItem = await _repository.AddAsync(newProject, cancellationToken);
15 |
16 | return createdItem.Id;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/Delete/DeleteProjectCommand.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Projects.Delete;
4 |
5 | public record DeleteProjectCommand(ProjectId ProjectId) : ICommand;
6 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/Delete/DeleteProjectHandler.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Projects.Delete;
4 |
5 | public class DeleteProjectHandler : ICommandHandler
6 | {
7 | private readonly IRepository _repository;
8 |
9 | public DeleteProjectHandler(IRepository repository)
10 | {
11 | _repository = repository;
12 | }
13 |
14 | public async Task Handle(DeleteProjectCommand request, CancellationToken cancellationToken)
15 | {
16 | var aggregateToDelete = await _repository.GetByIdAsync(request.ProjectId, cancellationToken);
17 | if (aggregateToDelete == null)
18 | {
19 | return Result.NotFound();
20 | }
21 |
22 | await _repository.DeleteAsync(aggregateToDelete, cancellationToken);
23 |
24 | return Result.Success();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/GetWithAllItems/GetProjectWithAllItemsHandler.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 | using NimblePros.SampleToDo.Core.ProjectAggregate.Specifications;
3 |
4 | namespace NimblePros.SampleToDo.UseCases.Projects.GetWithAllItems;
5 |
6 | ///
7 | /// Queries don't necessarily need to use repository methods, but they can if it's convenient
8 | ///
9 | public class GetProjectWithAllItemsHandler : IQueryHandler>
10 | {
11 | private readonly IReadRepository _repository;
12 |
13 | public GetProjectWithAllItemsHandler(IReadRepository repository)
14 | {
15 | _repository = repository;
16 | }
17 |
18 | public async Task> Handle(GetProjectWithAllItemsQuery request, CancellationToken cancellationToken)
19 | {
20 | var spec = new ProjectByIdWithItemsSpec(request.ProjectId);
21 | var entity = await _repository.FirstOrDefaultAsync(spec, cancellationToken);
22 | if (entity == null) return Result.NotFound();
23 |
24 | var items = entity.Items
25 | .Select(i => new ToDoItemDTO(i.Id.Value, i.Title, i.Description, i.IsDone, i.ContributorId)).ToList();
26 | return new ProjectWithAllItemsDTO(entity.Id.Value, entity.Name.Value, items, entity.Status.ToString())
27 | ;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/GetWithAllItems/GetProjectWithAllItemsQuery.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Projects.GetWithAllItems;
4 |
5 | public record GetProjectWithAllItemsQuery(ProjectId ProjectId) : IQuery>;
6 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/ListIncompleteItems/IListIncompleteItemsQueryService.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Projects.ListIncompleteItems;
2 |
3 | ///
4 | /// Represents a service that will actually fetch the necessary data
5 | /// Typically implemented in Infrastructure
6 | ///
7 | public interface IListIncompleteItemsQueryService
8 | {
9 | Task> ListAsync(int projectId);
10 | }
11 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/ListIncompleteItems/ListIncompleteItemsByProjectHandler.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Projects.ListIncompleteItems;
2 |
3 | public class ListIncompleteItemsByProjectHandler : IQueryHandler>>
4 | {
5 | private readonly IListIncompleteItemsQueryService _query;
6 |
7 | public ListIncompleteItemsByProjectHandler(IListIncompleteItemsQueryService query)
8 | {
9 | _query = query;
10 | }
11 |
12 | public async Task>> Handle(ListIncompleteItemsByProjectQuery request,
13 | CancellationToken cancellationToken)
14 | {
15 | var result = await _query.ListAsync(request.ProjectId);
16 |
17 | return Result.Success(result);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/ListIncompleteItems/ListIncompleteItemsByProjectQuery.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Projects.ListIncompleteItems;
2 |
3 | public record ListIncompleteItemsByProjectQuery(int ProjectId) : IQuery>>;
4 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/ListShallow/IListProjectsShallowQueryService.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Projects.ListShallow;
2 |
3 | ///
4 | /// Represents a service that will actually fetch the necessary data
5 | /// Typically implemented in Infrastructure
6 | ///
7 | public interface IListProjectsShallowQueryService
8 | {
9 | Task> ListAsync();
10 | }
11 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/ListShallow/ListProjectsShallowHandler.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Projects.ListShallow;
2 |
3 | public class ListProjectsShallowHandler(IListProjectsShallowQueryService query)
4 | : IQueryHandler>>
5 | {
6 | private readonly IListProjectsShallowQueryService _query = query;
7 |
8 | public async Task>> Handle(ListProjectsShallowQuery request, CancellationToken cancellationToken)
9 | {
10 | var result = await _query.ListAsync();
11 |
12 | return Result.Success(result);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/ListShallow/ListProjectsShallowQuery.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Projects.ListShallow;
2 |
3 | public record ListProjectsShallowQuery(int? Skip, int? Take) : IQuery>>;
4 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/MarkToDoItemComplete/MarkToDoItemCompleteCommand.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Projects.MarkToDoItemComplete;
4 |
5 | ///
6 | /// Create a new Project.
7 | ///
8 | ///
9 | public record MarkToDoItemCompleteCommand(ProjectId ProjectId, int ToDoItemId) : Ardalis.SharedKernel.ICommand;
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/MarkToDoItemComplete/MarkToDoItemCompleteHandler.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 | using NimblePros.SampleToDo.Core.ProjectAggregate.Specifications;
3 |
4 | namespace NimblePros.SampleToDo.UseCases.Projects.MarkToDoItemComplete;
5 |
6 | public class MarkToDoItemCompleteHandler : ICommandHandler
7 | {
8 | private readonly IRepository _repository;
9 |
10 | public MarkToDoItemCompleteHandler(IRepository repository)
11 | {
12 | _repository = repository;
13 | }
14 |
15 | public async Task Handle(MarkToDoItemCompleteCommand request,
16 | CancellationToken cancellationToken)
17 | {
18 | var spec = new ProjectByIdWithItemsSpec(request.ProjectId);
19 | var entity = await _repository.FirstOrDefaultAsync(spec, cancellationToken);
20 | if (entity == null) return Result.NotFound("Project not found.");
21 |
22 | var item = entity.Items.FirstOrDefault(i => i.Id == request.ToDoItemId);
23 | if (item == null) return Result.NotFound("Item not found.");
24 |
25 | item.MarkComplete();
26 | await _repository.UpdateAsync(entity);
27 |
28 | return Result.Success();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/ProjectDTO.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Projects;
2 |
3 | public record ProjectDTO(int Id, string Name, string Status);
4 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/ProjectWithAllItemsDTO.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Projects;
2 | public record ProjectWithAllItemsDTO(int Id, string Name, List Items, string Status);
3 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/ToDoItemDTO.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UseCases.Projects;
2 |
3 | public record ToDoItemDTO(int Id, string Title, string Description, bool IsComplete, int? ContributorId);
4 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/Update/UpdateProjectCommand.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Projects.Update;
4 |
5 | public record UpdateProjectCommand(ProjectId ProjectId, ProjectName NewName) : ICommand>;
6 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/Projects/Update/UpdateProjectHandler.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UseCases.Projects.Update;
4 |
5 | public class UpdateProjectHandler : ICommandHandler>
6 | {
7 | private readonly IRepository _repository;
8 |
9 | public UpdateProjectHandler(IRepository repository)
10 | {
11 | _repository = repository;
12 | }
13 |
14 | public async Task> Handle(UpdateProjectCommand request, CancellationToken cancellationToken)
15 | {
16 | var existingEntity = await _repository.GetByIdAsync(request.ProjectId, cancellationToken);
17 | if (existingEntity == null)
18 | {
19 | return Result.NotFound();
20 | }
21 |
22 | existingEntity.UpdateName(request.NewName!);
23 |
24 | await _repository.UpdateAsync(existingEntity, cancellationToken);
25 |
26 | return Result.Success(new ProjectDTO(existingEntity.Id.Value, existingEntity.Name.Value, existingEntity.Status.ToString()));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.UseCases/README.md:
--------------------------------------------------------------------------------
1 | ## Use Cases Project
2 |
3 | In Clean Architecture, the Use Cases (or Application Services) project is a relatively thin layer that wraps the domain model.
4 |
5 | Use Cases are typically organized by feature. These may be simple CRUD operations or much more complex activities.
6 |
7 | Use Cases should not depend directly on infrastructure concerns, making them simple to unit test in most cases.
8 |
9 | Use Cases are often grouped into Commands and Queries, following CQRS.
10 |
11 | Having Use Cases as a separate project can reduce the amount of logic in UI and Infrastructure projects.
12 |
13 | For simpler projects, the Use Cases project can be omitted, and its behavior moved into the UI project, either as using separate services or MediatR handlers, or by simply putting the logic into the API endpoints.
14 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Configurations/LoggerConfig.cs:
--------------------------------------------------------------------------------
1 | using Serilog;
2 |
3 | namespace NimblePros.SampleToDo.Web.Configurations;
4 |
5 | public static class LoggerConfig
6 | {
7 | public static WebApplicationBuilder AddLoggerConfigs(this WebApplicationBuilder builder)
8 | {
9 |
10 | builder.Host.UseSerilog((_, config) => config.ReadFrom.Configuration(builder.Configuration));
11 |
12 | return builder;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Configurations/MediatrConfig.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ContributorAggregate;
2 | using NimblePros.SampleToDo.Infrastructure;
3 | using NimblePros.SampleToDo.UseCases.Contributors.Commands.Create;
4 |
5 | namespace NimblePros.SampleToDo.Web.Configurations;
6 |
7 | public static class MediatrConfig
8 | {
9 | public static IServiceCollection AddMediatrConfigs(this IServiceCollection services)
10 | {
11 | var mediatRAssemblies = new[]
12 | {
13 | Assembly.GetAssembly(typeof(Contributor)), // Core
14 | Assembly.GetAssembly(typeof(CreateContributorCommand)), // UseCases
15 | Assembly.GetAssembly(typeof(InfrastructureServiceExtensions)) // Infrastructure
16 | };
17 |
18 | services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(mediatRAssemblies!))
19 | .AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>))
20 | .AddScoped();
21 |
22 | return services;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Configurations/ServiceConfigs.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Infrastructure;
2 | using NimblePros.SampleToDo.Core;
3 | using NimblePros.Metronome;
4 |
5 | namespace NimblePros.SampleToDo.Web.Configurations;
6 |
7 | public static class ServiceConfig
8 | {
9 | public static IServiceCollection AddServiceConfigs(this IServiceCollection services,
10 | Microsoft.Extensions.Logging.ILogger logger,
11 | WebApplicationBuilder builder)
12 | {
13 | services.AddCoreServices(logger)
14 | .AddInfrastructureServices(builder.Configuration, logger, builder.Environment.EnvironmentName)
15 | .AddMediatrConfigs();
16 |
17 | // add a default http client
18 | services.AddHttpClient("Default")
19 | .AddMetronomeHandler();
20 |
21 | logger.LogInformation("{Project} services registered", "Core and Infrastructure services registered");
22 |
23 | return services;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/ContributorRecord.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Contributors;
2 |
3 | public record ContributorRecord(int Id, string Name);
4 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/Create.CreateContributorRequest.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace NimblePros.SampleToDo.Web.Contributors;
4 |
5 | public class CreateContributorRequest
6 | {
7 | public const string Route = "/Contributors";
8 |
9 | [Required]
10 | public string Name { get; set; } = String.Empty;
11 | }
12 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/Create.CreateContributorResponse.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Contributors;
2 |
3 | public class CreateContributorResponse
4 | {
5 | public CreateContributorResponse(int id, string name)
6 | {
7 | Id = id;
8 | Name = name;
9 | }
10 | public int Id { get; set; }
11 | public string Name { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/Create.CreateContributorValidator.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Infrastructure.Data.Config;
2 | using FastEndpoints;
3 | using FluentValidation;
4 |
5 | namespace NimblePros.SampleToDo.Web.Contributors;
6 |
7 | ///
8 | /// See: https://fast-endpoints.com/docs/validation
9 | ///
10 | public class CreateContributorValidator : Validator
11 | {
12 | public CreateContributorValidator()
13 | {
14 | RuleFor(x => x.Name)
15 | .NotEmpty()
16 | .WithMessage("Name is required.")
17 | .MinimumLength(2)
18 | .MaximumLength(DataSchemaConstants.DEFAULT_NAME_LENGTH);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/Delete.DeleteContributorRequest.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Contributors;
2 |
3 | public record DeleteContributorRequest
4 | {
5 | public const string Route = "/Contributors/{ContributorId:int}";
6 | public static string BuildRoute(int contributorId) => Route.Replace("{ContributorId:int}", contributorId.ToString());
7 |
8 | public int ContributorId { get; set; }
9 | }
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/Delete.DeleteContributorValidator.cs:
--------------------------------------------------------------------------------
1 | using FastEndpoints;
2 | using FluentValidation;
3 |
4 | namespace NimblePros.SampleToDo.Web.Contributors;
5 |
6 | ///
7 | /// See: https://fast-endpoints.com/docs/validation
8 | ///
9 | public class DeleteContributorValidator : Validator
10 | {
11 | public DeleteContributorValidator()
12 | {
13 | RuleFor(x => x.ContributorId)
14 | .GreaterThan(0);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/Delete.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.UseCases.Contributors.Commands.Delete;
2 |
3 | namespace NimblePros.SampleToDo.Web.Contributors;
4 |
5 | ///
6 | /// Delete a Contributor.
7 | ///
8 | ///
9 | /// Delete a Contributor by providing a valid integer id.
10 | ///
11 | public class Delete : Endpoint
12 | {
13 | private readonly IMediator _mediator;
14 |
15 | public Delete(IMediator mediator)
16 | {
17 | _mediator = mediator;
18 | }
19 |
20 | public override void Configure()
21 | {
22 | Delete(DeleteContributorRequest.Route);
23 | AllowAnonymous();
24 | }
25 |
26 | public override async Task HandleAsync(
27 | DeleteContributorRequest request,
28 | CancellationToken cancellationToken)
29 | {
30 | var command = new DeleteContributorCommand(request.ContributorId);
31 |
32 | var result = await _mediator.Send(command);
33 |
34 | if (result.Status == ResultStatus.NotFound)
35 | {
36 | await SendNotFoundAsync(cancellationToken);
37 | return;
38 | }
39 |
40 | if (result.IsSuccess)
41 | {
42 | await SendNoContentAsync(cancellationToken);
43 | };
44 | // TODO: Handle other issues as needed
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/GetById.GetContributorByIdRequest.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Contributors;
2 |
3 | public class GetContributorByIdRequest
4 | {
5 | public const string Route = "/Contributors/{ContributorId:int}";
6 | public static string BuildRoute(int contributorId) => Route.Replace("{ContributorId:int}", contributorId.ToString());
7 |
8 | public int ContributorId { get; set; }
9 | }
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/GetById.GetContributorValidator.cs:
--------------------------------------------------------------------------------
1 | using FastEndpoints;
2 | using FluentValidation;
3 |
4 | namespace NimblePros.SampleToDo.Web.Contributors;
5 |
6 | ///
7 | /// See: https://fast-endpoints.com/docs/validation
8 | ///
9 | public class GetContributorValidator : Validator
10 | {
11 | public GetContributorValidator()
12 | {
13 | RuleFor(x => x.ContributorId)
14 | .GreaterThan(0);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/GetById.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.UseCases.Contributors.Queries.Get;
2 |
3 | namespace NimblePros.SampleToDo.Web.Contributors;
4 |
5 | ///
6 | /// Get a Contributor by integer ID.
7 | ///
8 | ///
9 | /// Takes a positive integer ID and returns a matching Contributor record.
10 | ///
11 | public class GetById : Endpoint
12 | {
13 | private readonly IMediator _mediator;
14 |
15 | public GetById(IMediator mediator)
16 | {
17 | _mediator = mediator;
18 | }
19 |
20 | public override void Configure()
21 | {
22 | Get(GetContributorByIdRequest.Route);
23 | AllowAnonymous();
24 | }
25 |
26 | public override async Task HandleAsync(GetContributorByIdRequest request,
27 | CancellationToken cancellationToken)
28 | {
29 | var command = new GetContributorQuery(request.ContributorId);
30 |
31 | var result = await _mediator.Send(command);
32 |
33 | if (result.Status == ResultStatus.NotFound)
34 | {
35 | await SendNotFoundAsync(cancellationToken);
36 | return;
37 | }
38 |
39 | if (result.IsSuccess)
40 | {
41 | Response = new ContributorRecord(result.Value.Id, result.Value.Name);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/List.ContributorListResponse.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Web.Contributors;
2 |
3 | namespace NimblePros.SampleToDo.Web.Contributors;
4 |
5 | public class ContributorListResponse
6 | {
7 | public List Contributors { get; set; } = new();
8 | }
9 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/List.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.UseCases.Contributors.Queries.List;
2 |
3 | namespace NimblePros.SampleToDo.Web.Contributors;
4 |
5 | ///
6 | /// List all Contributors
7 | ///
8 | ///
9 | /// List all contributors - returns a ContributorListResponse containing the Contributors.
10 | /// NOTE: In DEV always returns a FAKE set of 2 contributors
11 | ///
12 | public class List : EndpointWithoutRequest
13 | {
14 | private readonly IMediator _mediator;
15 |
16 | public List(IMediator mediator)
17 | {
18 | _mediator = mediator;
19 | }
20 |
21 | public override void Configure()
22 | {
23 | Get("/Contributors");
24 | AllowAnonymous();
25 | }
26 |
27 | public override async Task HandleAsync(CancellationToken cancellationToken)
28 | {
29 | var result = await _mediator.Send(new ListContributorsQuery(null, null));
30 |
31 | if (result.IsSuccess)
32 | {
33 | Response = new ContributorListResponse
34 | {
35 | Contributors = result.Value.Select(c => new ContributorRecord(c.Id, c.Name)).ToList()
36 | };
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/Update.UpdateContributorRequest.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace NimblePros.SampleToDo.Web.Contributors;
4 |
5 | public class UpdateContributorRequest
6 | {
7 | public const string Route = "/Contributors/{ContributorId:int}";
8 | public static string BuildRoute(int contributorId) => Route.Replace("{ContributorId:int}", contributorId.ToString());
9 |
10 | public int ContributorId { get; set; }
11 |
12 | [Required]
13 | public int Id { get; set; }
14 | [Required]
15 | public string? Name { get; set; }
16 | }
17 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/Update.UpdateContributorResponse.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Contributors;
2 |
3 | public class UpdateContributorResponse
4 | {
5 | public UpdateContributorResponse(ContributorRecord contributor)
6 | {
7 | Contributor = contributor;
8 | }
9 | public ContributorRecord Contributor { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Contributors/Update.UpdateContributorValidator.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Infrastructure.Data.Config;
2 | using FastEndpoints;
3 | using FluentValidation;
4 |
5 | namespace NimblePros.SampleToDo.Web.Contributors;
6 |
7 | ///
8 | /// See: https://fast-endpoints.com/docs/validation
9 | ///
10 | public class UpdateContributorValidator : Validator
11 | {
12 | public UpdateContributorValidator()
13 | {
14 | RuleFor(x => x.Name)
15 | .NotEmpty()
16 | .WithMessage("Name is required.")
17 | .MinimumLength(2)
18 | .MaximumLength(DataSchemaConstants.DEFAULT_NAME_LENGTH);
19 | RuleFor(x => x.ContributorId)
20 | .Must((args, contributorId) => args.Id == contributorId)
21 | .WithMessage("Route and body Ids must match; cannot update Id of an existing resource.");
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using System.Reflection;
2 | global using Ardalis.ListStartupServices;
3 | global using Ardalis.Result;
4 | global using Ardalis.SharedKernel;
5 | global using FastEndpoints;
6 | global using FastEndpoints.Swagger;
7 | global using MediatR;
8 | global using Microsoft.EntityFrameworkCore;
9 | global using Serilog;
10 | global using Serilog.Extensions.Logging;
11 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/Create.CreateProjectRequest.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace NimblePros.SampleToDo.Web.Projects;
4 |
5 | public class CreateProjectRequest
6 | {
7 | public const string Route = "/Projects";
8 |
9 | [Required]
10 | public string? Name { get; set; }
11 | }
12 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/Create.CreateProjectResponse.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Projects;
2 |
3 | public class CreateProjectResponse
4 | {
5 | public CreateProjectResponse(int id, string name)
6 | {
7 | Id = id;
8 | Name = name;
9 | }
10 |
11 | public int Id { get; set; }
12 | public string Name { get; set; }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/Create.CreateProjectValidator.cs:
--------------------------------------------------------------------------------
1 | using FastEndpoints;
2 | using FluentValidation;
3 | using NimblePros.SampleToDo.Infrastructure.Data.Config;
4 |
5 | namespace NimblePros.SampleToDo.Web.Projects;
6 |
7 | ///
8 | /// See: https://fast-endpoints.com/docs/validation
9 | ///
10 | public class CreateProjectValidator : Validator
11 | {
12 | public CreateProjectValidator()
13 | {
14 | RuleFor(x => x.Name)
15 | .NotEmpty()
16 | .WithMessage("Name is required.")
17 | .MinimumLength(2)
18 | .MaximumLength(DataSchemaConstants.DEFAULT_NAME_LENGTH);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/Create.cs:
--------------------------------------------------------------------------------
1 | using Ardalis.Result.AspNetCore;
2 | using NimblePros.SampleToDo.UseCases.Projects.Create;
3 |
4 | namespace NimblePros.SampleToDo.Web.Projects;
5 |
6 | ///
7 | /// Creates a new Project
8 | ///
9 | ///
10 | /// Creates a new project given a name.
11 | ///
12 | public class Create(IMediator mediator) : Endpoint
13 | {
14 | private readonly IMediator _mediator = mediator;
15 |
16 | public override void Configure()
17 | {
18 | Post(CreateProjectRequest.Route);
19 | AllowAnonymous();
20 | Summary(s =>
21 | {
22 | s.ExampleRequest = new CreateProjectRequest { Name = "Project Name" };
23 | });
24 | }
25 |
26 | public override async Task HandleAsync(
27 | CreateProjectRequest request,
28 | CancellationToken cancellationToken)
29 | {
30 | var result = await _mediator.Send(new CreateProjectCommand(request.Name!));
31 |
32 | if (result.IsSuccess)
33 | {
34 | Response = new CreateProjectResponse(result.Value.Value, request.Name!);
35 | return;
36 | }
37 | await SendResultAsync(result.ToMinimalApiResult());
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/CreateToDoItem.CreateToDoItemRequest.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using Microsoft.AspNetCore.Mvc;
3 |
4 | namespace NimblePros.SampleToDo.Web.Projects;
5 |
6 | public class CreateToDoItemRequest
7 | {
8 | public const string Route = "/Projects/{ProjectId:int}/ToDoItems";
9 | public static string BuildRoute(int projectId) => Route.Replace("{ProjectId:int}", projectId.ToString());
10 |
11 | [Required]
12 | [FromRoute]
13 | public int ProjectId { get; set; } = 0;
14 |
15 | [Required]
16 | public string Title { get; set; } = string.Empty;
17 | [Required]
18 | public string Description { get; set; } = string.Empty;
19 |
20 | public int? ContributorId { get; set; }
21 | }
22 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/CreateToDoItem.CreateToDoItemValidator.cs:
--------------------------------------------------------------------------------
1 | using FastEndpoints;
2 | using FluentValidation;
3 | using NimblePros.SampleToDo.Infrastructure.Data.Config;
4 |
5 | namespace NimblePros.SampleToDo.Web.Projects;
6 |
7 | ///
8 | /// See: https://fast-endpoints.com/docs/validation
9 | ///
10 | public class CreateToDoItemValidator : Validator
11 | {
12 | public CreateToDoItemValidator()
13 | {
14 | RuleFor(x => x.ProjectId)
15 | .GreaterThan(0);
16 | RuleFor(x => x.Title)
17 | .NotEmpty()
18 | .MinimumLength(2)
19 | .MaximumLength(DataSchemaConstants.DEFAULT_NAME_LENGTH);
20 | RuleFor(x => x.Description)
21 | .NotEmpty();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/Delete.DeleteProjectRequest.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Projects;
2 |
3 | public class DeleteProjectRequest
4 | {
5 | public const string Route = "/Projects/{ProjectId:int}";
6 | public static string BuildRoute(int projectId) => Route.Replace("{ProjectId:int}", projectId.ToString());
7 |
8 | public int ProjectId { get; set; }
9 | }
10 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/Delete.cs:
--------------------------------------------------------------------------------
1 | using Ardalis.Result.AspNetCore;
2 | using NimblePros.SampleToDo.Core.ProjectAggregate;
3 | using NimblePros.SampleToDo.UseCases.Projects.Delete;
4 |
5 | namespace NimblePros.SampleToDo.Web.Projects;
6 |
7 | ///
8 | /// Deletes a project
9 | ///
10 | public class Delete(IMediator mediator) : Endpoint
11 | {
12 | private readonly IMediator _mediator = mediator;
13 |
14 | public override void Configure()
15 | {
16 | Delete(DeleteProjectRequest.Route);
17 | AllowAnonymous();
18 | }
19 |
20 | public override async Task HandleAsync(
21 | DeleteProjectRequest request,
22 | CancellationToken cancellationToken)
23 | {
24 | var command = new DeleteProjectCommand(ProjectId.From(request.ProjectId));
25 |
26 | var result = await _mediator.Send(command);
27 |
28 | await SendResultAsync(result.ToMinimalApiResult());
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/GetById.GetProjectByIdRequest.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace NimblePros.SampleToDo.Web.Endpoints.Projects;
3 |
4 | public class GetProjectByIdRequest
5 | {
6 | public const string Route = "/Projects/{ProjectId:int}";
7 | public static string BuildRoute(int projectId) => Route.Replace("{ProjectId:int}", projectId.ToString());
8 |
9 | public int ProjectId { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/GetById.GetProjectByIdResponse.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Projects;
2 |
3 | public class GetProjectByIdResponse
4 | {
5 | public GetProjectByIdResponse(int id, string name, List items)
6 | {
7 | Id = id;
8 | Name = name;
9 | Items = items;
10 | }
11 |
12 | public int Id { get; set; }
13 | public string Name { get; set; }
14 | public List Items { get; set; } = new();
15 | }
16 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/GetById.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 | using NimblePros.SampleToDo.UseCases.Projects.GetWithAllItems;
3 | using NimblePros.SampleToDo.Web.Endpoints.Projects;
4 |
5 | namespace NimblePros.SampleToDo.Web.Projects;
6 |
7 | public class GetById(IMediator mediator) : Endpoint
8 | {
9 | private readonly IMediator _mediator = mediator;
10 |
11 | public override void Configure()
12 | {
13 | Get(GetProjectByIdRequest.Route);
14 | AllowAnonymous();
15 | }
16 |
17 | public override async Task HandleAsync(GetProjectByIdRequest request,
18 | CancellationToken cancellationToken)
19 | {
20 | var command = new GetProjectWithAllItemsQuery(ProjectId.From(request.ProjectId));
21 |
22 | var result = await _mediator.Send(command);
23 |
24 | if (result.Status == ResultStatus.NotFound)
25 | {
26 | await SendNotFoundAsync(cancellationToken);
27 | return;
28 | }
29 |
30 | if (result.IsSuccess)
31 | {
32 | Response = new GetProjectByIdResponse(result.Value.Id,
33 | result.Value.Name,
34 | items:
35 | result.Value.Items
36 | .Select(item => new ToDoItemRecord(
37 | item.Id,
38 | item.Title,
39 | item.Description,
40 | item.IsComplete,
41 | item.ContributorId
42 | ))
43 | .ToList()
44 | );
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/List.ProjectListResponse.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Projects;
2 |
3 | public class ProjectListResponse
4 | {
5 | public List Projects { get; set; } = new();
6 | }
7 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/List.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 | using NimblePros.SampleToDo.UseCases.Projects.ListShallow;
3 |
4 | namespace NimblePros.SampleToDo.Web.Projects;
5 |
6 | ///
7 | /// Lists all projects without their sub-properties.
8 | ///
9 | ///
10 | /// Lists all projects without their sub-properties.
11 | /// NOTE: In DEV will always show a FAKE ID 1000 project, not real data
12 | ///
13 | public class List(IMediator mediator) : EndpointWithoutRequest
14 | {
15 | private readonly IMediator _mediator = mediator;
16 |
17 | public override void Configure()
18 | {
19 | Get($"/{nameof(Project)}s");
20 | AllowAnonymous();
21 | }
22 |
23 | public override async Task HandleAsync(CancellationToken cancellationToken)
24 | {
25 | var result = await _mediator.Send(new ListProjectsShallowQuery(null, null));
26 |
27 | if (result.IsSuccess)
28 | {
29 | Response = new ProjectListResponse
30 | {
31 | Projects = result.Value.Select(c => new ProjectRecord(c.Id, c.Name)).ToList()
32 | };
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/ListIncompleteItems.ListIncompleteItemsRequest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace NimblePros.SampleToDo.Web.Endpoints.ProjectEndpoints;
4 |
5 | public class ListIncompleteItemsRequest
6 | {
7 | public const string Route = "/Projects/{ProjectId}/IncompleteItems";
8 | public static string BuildRoute(int projectId) => Route.Replace("{ProjectId:int}", projectId.ToString());
9 |
10 |
11 | [FromRoute]
12 | public int ProjectId { get; set; }
13 | //[FromQuery]
14 | //public string? SearchString { get; set; }
15 | }
16 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/ListIncompleteItems.ListIncompleteItemsResponse.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Projects;
2 |
3 | public class ListIncompleteItemsResponse
4 | {
5 | public ListIncompleteItemsResponse(int projectId, List incompleteItems)
6 | {
7 | ProjectId = projectId;
8 | IncompleteItems = incompleteItems;
9 | }
10 | public int ProjectId { get; set; }
11 | public List IncompleteItems { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/MarkItemComplete.MarkItemCompleteRequest.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using Microsoft.AspNetCore.Mvc;
3 |
4 | namespace NimblePros.SampleToDo.Web.ProjectEndpoints;
5 |
6 | public class MarkItemCompleteRequest
7 | {
8 | public const string Route = "/Projects/{ProjectId:int}/ToDoItems/{ToDoItemId:int}";
9 | public static string BuildRoute(int projectId, int toDoItemId) => Route.Replace("{ProjectId:int}", projectId.ToString())
10 | .Replace("{ToDoItemId:int}", toDoItemId.ToString());
11 |
12 | [Required]
13 | [FromRoute]
14 | public int ProjectId { get; set; } = 0;
15 | [Required]
16 | [FromRoute]
17 | public int ToDoItemId { get; set; } = 0;
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/MarkItemComplete.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 | using NimblePros.SampleToDo.UseCases.Projects.MarkToDoItemComplete;
3 |
4 | namespace NimblePros.SampleToDo.Web.ProjectEndpoints;
5 |
6 | ///
7 | /// Mark an item as complete
8 | ///
9 | public class MarkItemComplete(IMediator mediator) : Endpoint
10 | {
11 | private readonly IMediator _mediator = mediator;
12 |
13 | public override void Configure()
14 | {
15 | Post(MarkItemCompleteRequest.Route);
16 | AllowAnonymous();
17 | Summary(s =>
18 | {
19 | s.ExampleRequest = new MarkItemCompleteRequest
20 | {
21 | ProjectId = 1,
22 | ToDoItemId = 1
23 | };
24 | });
25 | }
26 |
27 | public override async Task HandleAsync(
28 | MarkItemCompleteRequest request,
29 | CancellationToken cancellationToken)
30 | {
31 | var command = new MarkToDoItemCompleteCommand(ProjectId.From(request.ProjectId), request.ToDoItemId);
32 | var result = await _mediator.Send(command);
33 |
34 | if (result.Status == Ardalis.Result.ResultStatus.NotFound)
35 | {
36 | await SendNotFoundAsync(cancellationToken);
37 | return;
38 | }
39 |
40 | if (result.IsSuccess)
41 | {
42 | await SendNoContentAsync(cancellationToken);
43 | };
44 | // TODO: Handle other issues as needed
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/ProjectRecord.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Projects;
2 |
3 | public record ProjectRecord(int Id, string Name);
4 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/ToDoItemRecord.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Projects;
2 | public record ToDoItemRecord(int Id, string Title, string Description, bool IsDone, int? ContributorId);
3 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/Update.UpdateProjectRequest.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace NimblePros.SampleToDo.Web.Projects;
4 |
5 | public class UpdateProjectRequest
6 | {
7 | public const string Route = "/Projects";
8 | public int Id { get; set; }
9 | public string? Name { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/Update.UpdateProjectRequestValidator.cs:
--------------------------------------------------------------------------------
1 | using FluentValidation;
2 |
3 | namespace NimblePros.SampleToDo.Web.Projects;
4 |
5 | public class UpdateProjectRequestValidator : Validator
6 | {
7 | public UpdateProjectRequestValidator()
8 | {
9 | RuleFor(x => x.Id)
10 | .GreaterThan(0).WithMessage("Id must be a positive integer.");
11 |
12 | RuleFor(x => x.Name)
13 | .NotEmpty().WithMessage("Name is required.")
14 | .Must(name => !string.IsNullOrWhiteSpace(name))
15 | .WithMessage("Name cannot be empty or whitespace.");
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/Update.UpdateProjectResponse.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.Web.Projects;
2 |
3 | public class UpdateProjectResponse
4 | {
5 | public UpdateProjectResponse(ProjectRecord project)
6 | {
7 | Project = project;
8 | }
9 | public ProjectRecord Project { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Projects/Update.cs:
--------------------------------------------------------------------------------
1 | using Ardalis.Result.AspNetCore;
2 | using NimblePros.SampleToDo.Core.ProjectAggregate;
3 | using NimblePros.SampleToDo.UseCases.Projects.Update;
4 |
5 | namespace NimblePros.SampleToDo.Web.Projects;
6 |
7 | public class Update(IMediator mediator) : Endpoint
8 | {
9 | private readonly IMediator _mediator = mediator;
10 |
11 | public override void Configure()
12 | {
13 | Put(UpdateProjectRequest.Route);
14 | AllowAnonymous();
15 | }
16 |
17 | public override async Task HandleAsync(
18 | UpdateProjectRequest request,
19 | CancellationToken cancellationToken)
20 | {
21 | var result = await _mediator.Send(new UpdateProjectCommand(ProjectId.From(request.Id), ProjectName.From(request.Name!)));
22 |
23 | await SendResultAsync(result.ToMinimalApiResult());
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:57677/",
7 | "sslPort": 0
8 | }
9 | },
10 | "profiles": {
11 | "https": {
12 | "commandName": "Project",
13 | "dotnetRunMessages": true,
14 | "launchUrl": "swagger",
15 | "environmentVariables": {
16 | "ASPNETCORE_ENVIRONMENT": "Development"
17 | },
18 | "applicationUrl": "https://localhost:57678/"
19 | },
20 | "IIS Express": {
21 | "commandName": "IISExpress",
22 | "environmentVariables": {
23 | "ASPNETCORE_ENVIRONMENT": "Development"
24 | }
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "DefaultConnection": "Server=(localdb)\\v11.0;Database=cleanarchitecture;Trusted_Connection=True;MultipleActiveResultSets=true",
4 | "SqliteConnection": "Data Source=database.sqlite"
5 | },
6 | "Serilog": {
7 | "MinimumLevel": {
8 | "Default": "Information"
9 | },
10 | "WriteTo": [
11 | {
12 | "Name": "Console"
13 | },
14 | {
15 | "Name": "File",
16 | "Args": {
17 | "path": "log.txt",
18 | "rollingInterval": "Day"
19 | }
20 | }
21 | //Uncomment this section if you'd like to push your logs to Azure Application Insights
22 | //Full list of Serilog Sinks can be found here: https://github.com/serilog/serilog/wiki/Provided-Sinks
23 | //{
24 | // "Name": "ApplicationInsights",
25 | // "Args": {
26 | // "instrumentationKey": "", //Fill in with your ApplicationInsights InstrumentationKey
27 | // "telemetryConverter": "Serilog.Sinks.ApplicationInsights.Sinks.ApplicationInsights.TelemetryConverters.TraceTelemetryConverter, Serilog.Sinks.ApplicationInsights"
28 | // }
29 | //}
30 | ]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/wwwroot/css/site.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-size: 14px;
3 | }
4 |
5 | @media (min-width: 768px) {
6 | html {
7 | font-size: 16px;
8 | }
9 | }
10 |
11 | html {
12 | position: relative;
13 | min-height: 100%;
14 | }
15 |
16 | body {
17 | margin-bottom: 60px;
18 | }
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ardalis/CleanArchitecture/7c031c77e6b8db695f3266a5f5873a522bbb238a/sample/src/NimblePros.SampleToDo.Web/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/wwwroot/js/site.js:
--------------------------------------------------------------------------------
1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
2 | // for details on configuring this project to bundle and minify static web assets.
3 |
4 | // Write your JavaScript code.
5 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/wwwroot/lib/bootstrap/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2011-2021 Twitter, Inc.
4 | Copyright (c) 2011-2021 The Bootstrap Authors
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) .NET Foundation. All rights reserved.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use
4 | these files except in compliance with the License. You may obtain a copy of the
5 | License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software distributed
10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the
12 | specific language governing permissions and limitations under the License.
13 |
--------------------------------------------------------------------------------
/sample/src/NimblePros.SampleToDo.Web/wwwroot/lib/jquery-validation/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | =====================
3 |
4 | Copyright Jörn Zaefferer
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.FunctionalTests/Contributors/ContributorCreate.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Web.Contributors;
2 | using Shouldly;
3 |
4 | namespace NimblePros.SampleToDo.FunctionalTests.Contributors;
5 |
6 | [Collection("Sequential")]
7 | public class ContributorCreate : IClassFixture>
8 | {
9 | private readonly HttpClient _client;
10 |
11 | public ContributorCreate(CustomWebApplicationFactory factory)
12 | {
13 | _client = factory.CreateClient();
14 | }
15 |
16 | [Fact]
17 | public async Task ReturnsOneContributor()
18 | {
19 | var testName = Guid.NewGuid().ToString();
20 | var request = new CreateContributorRequest() { Name = testName };
21 | var content = StringContentHelpers.FromModelAsJson(request);
22 |
23 | var result = await _client.PostAndDeserializeAsync(
24 | CreateContributorRequest.Route, content);
25 |
26 | result.Name.ShouldBe(testName);
27 | result.Id.ShouldBeGreaterThan(0);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.FunctionalTests/Contributors/ContributorDelete.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Web;
2 | using NimblePros.SampleToDo.Web.Contributors;
3 |
4 | namespace NimblePros.SampleToDo.FunctionalTests.Contributors;
5 |
6 | [Collection("Sequential")]
7 | public class ContributorDelete : IClassFixture>
8 | {
9 | private readonly HttpClient _client;
10 |
11 | public ContributorDelete(CustomWebApplicationFactory factory)
12 | {
13 | _client = factory.CreateClient();
14 | }
15 |
16 | [Fact]
17 | public async Task DeletesExistingContributor()
18 | {
19 | var deleteRoute = DeleteContributorRequest.BuildRoute(SeedData.Contributor1.Id);
20 | _ = await _client.DeleteAndEnsureNoContentAsync(deleteRoute);
21 |
22 | string getRoute = GetContributorByIdRequest.BuildRoute(SeedData.Contributor1.Id);
23 | _ = await _client.GetAndEnsureNotFoundAsync(getRoute);
24 | }
25 |
26 | [Fact]
27 | public async Task ReturnsNotFoundGivenMissingContributorId()
28 | {
29 | int invalidId = 1000;
30 | var deleteRoute = DeleteContributorRequest.BuildRoute(invalidId);
31 | _ = await _client.DeleteAndEnsureNotFoundAsync(deleteRoute);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.FunctionalTests/Contributors/ContributorGetById.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Web;
2 | using NimblePros.SampleToDo.Web.Contributors;
3 |
4 | namespace NimblePros.SampleToDo.FunctionalTests.Contributors;
5 |
6 | [Collection("Sequential")]
7 | public class ContributorGetById : IClassFixture>
8 | {
9 | private readonly HttpClient _client;
10 |
11 | public ContributorGetById(CustomWebApplicationFactory factory)
12 | {
13 | _client = factory.CreateClient();
14 | }
15 |
16 | [Fact]
17 | public async Task ReturnsSeedContributorGivenId1()
18 | {
19 | var result = await _client.GetAndDeserializeAsync(GetContributorByIdRequest.BuildRoute(1));
20 |
21 | Assert.Equal(1, result.Id);
22 | Assert.Equal(SeedData.Contributor1.Name.Value, result.Name);
23 | }
24 |
25 | [Fact]
26 | public async Task ReturnsNotFoundGivenInvalidId1000()
27 | {
28 | string route = GetContributorByIdRequest.BuildRoute(1000);
29 | _ = await _client.GetAndEnsureNotFoundAsync(route);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.FunctionalTests/Contributors/ContributorList.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Web;
2 | using NimblePros.SampleToDo.Web.Contributors;
3 |
4 | namespace NimblePros.SampleToDo.FunctionalTests.Contributors;
5 |
6 | [Collection("Sequential")]
7 | public class ContributorList : IClassFixture>
8 | {
9 | private readonly HttpClient _client;
10 |
11 | public ContributorList(CustomWebApplicationFactory factory)
12 | {
13 | _client = factory.CreateClient();
14 | }
15 |
16 | [Fact]
17 | public async Task ReturnsTwoContributors()
18 | {
19 | var result = await _client.GetAndDeserializeAsync("/Contributors");
20 |
21 | Assert.Equal(2, result.Contributors.Count);
22 | Assert.Contains(result.Contributors, i => i.Name == SeedData.Contributor1.Name);
23 | Assert.Contains(result.Contributors, i => i.Name == SeedData.Contributor2.Name);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.FunctionalTests/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using System.Text;
2 | global using Ardalis.HttpClientTestExtensions;
3 | global using DotNet.Testcontainers.Builders;
4 | global using DotNet.Testcontainers.Containers;
5 | global using Microsoft.AspNetCore.Hosting;
6 | global using Microsoft.AspNetCore.Mvc.Testing;
7 | global using Microsoft.EntityFrameworkCore;
8 | global using Microsoft.Extensions.DependencyInjection;
9 | global using Microsoft.Extensions.Hosting;
10 | global using Microsoft.Extensions.Logging;
11 | global using Newtonsoft.Json;
12 | global using Xunit;
13 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.FunctionalTests/Projects/ProjectCreate.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Web.Projects;
2 | using Shouldly;
3 |
4 | namespace NimblePros.SampleToDo.FunctionalTests.Projects;
5 |
6 | [Collection("Sequential")]
7 | public class ProjectCreate : IClassFixture>
8 | {
9 | private readonly HttpClient _client;
10 |
11 | public ProjectCreate(CustomWebApplicationFactory factory)
12 | {
13 | _client = factory.CreateClient();
14 | }
15 |
16 | [Fact]
17 | public async Task ReturnsOneProject()
18 | {
19 | var testName = Guid.NewGuid().ToString();
20 | var request = new CreateProjectRequest() { Name = testName };
21 | var content = StringContentHelpers.FromModelAsJson(request);
22 |
23 | var result = await _client.PostAndDeserializeAsync(
24 | CreateProjectRequest.Route, content);
25 |
26 | result.Name.ShouldBe(testName);
27 | result.Id.ShouldBeGreaterThan(0);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.FunctionalTests/Projects/ProjectGetById.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Web;
2 | using NimblePros.SampleToDo.Web.Endpoints.Projects;
3 | using NimblePros.SampleToDo.Web.Projects;
4 | using Shouldly;
5 |
6 | namespace NimblePros.SampleToDo.FunctionalTests.Projects;
7 |
8 | [Collection("Sequential")]
9 | public class ProjectGetById : IClassFixture>
10 | {
11 | private readonly HttpClient _client;
12 |
13 | public ProjectGetById(CustomWebApplicationFactory factory)
14 | {
15 | _client = factory.CreateClient();
16 | }
17 |
18 | [Fact]
19 | public async Task ReturnsSeedProjectGivenId1()
20 | {
21 | var result = await _client.GetAndDeserializeAsync(GetProjectByIdRequest.BuildRoute(1));
22 |
23 | result.Id.ShouldBe(1);
24 | result.Name.ShouldBe(SeedData.TestProject1.Name.Value);
25 | result.Items.Count.ShouldBe(3);
26 | }
27 |
28 | [Fact]
29 | public async Task ReturnsNotFoundGivenId0()
30 | {
31 | var route = GetProjectByIdRequest.BuildRoute(0);
32 | _ = await _client.GetAndEnsureNotFoundAsync(route);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.FunctionalTests/Projects/ProjectList.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Web;
2 | using NimblePros.SampleToDo.Web.Projects;
3 |
4 | namespace NimblePros.SampleToDo.FunctionalTests.Projects;
5 |
6 | [Collection("Sequential")]
7 | public class ProjectList : IClassFixture>
8 | {
9 | private readonly HttpClient _client;
10 |
11 | public ProjectList(CustomWebApplicationFactory factory)
12 | {
13 | _client = factory.CreateClient();
14 | }
15 |
16 | [Fact]
17 | public async Task ReturnsOneProject()
18 | {
19 | var result = await _client.GetAndDeserializeAsync("/Projects");
20 |
21 | Assert.Single(result.Projects);
22 | Assert.Contains(result.Projects, i => i.Name == SeedData.TestProject1.Name);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.FunctionalTests/xunit.runner.json:
--------------------------------------------------------------------------------
1 | {
2 | "shadowCopy": false,
3 | "parallelizeAssembly": false,
4 | "parallelizeTestCollections": false
5 | }
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.IntegrationTests/Data/BaseEfRepoTestFixture.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 | using NimblePros.SampleToDo.Infrastructure.Data;
3 |
4 | namespace NimblePros.SampleToDo.IntegrationTests.Data;
5 |
6 | public abstract class BaseEfRepoTestFixture
7 | {
8 | protected AppDbContext _dbContext;
9 |
10 | protected BaseEfRepoTestFixture()
11 | {
12 | var options = CreateNewContextOptions();
13 | var _fakeEventDispatcher = Substitute.For();
14 |
15 | _dbContext = new AppDbContext(options, _fakeEventDispatcher);
16 | }
17 |
18 | protected static DbContextOptions CreateNewContextOptions()
19 | {
20 | // Create a fresh service provider, and therefore a fresh
21 | // InMemory database instance.
22 | var serviceProvider = new ServiceCollection()
23 | .AddEntityFrameworkInMemoryDatabase()
24 | .BuildServiceProvider();
25 |
26 | // Create a new options instance telling the context to use an
27 | // InMemory database and the new service provider.
28 | var builder = new DbContextOptionsBuilder();
29 | builder.UseInMemoryDatabase("cleanarchitecture")
30 | .UseInternalServiceProvider(serviceProvider);
31 |
32 | return builder.Options;
33 | }
34 |
35 | protected EfRepository GetRepository()
36 | {
37 | return new EfRepository(_dbContext);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.IntegrationTests/Data/EfRepositoryAdd.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.IntegrationTests.Data;
4 |
5 | public class EfRepositoryAdd : BaseEfRepoTestFixture
6 | {
7 | [Fact]
8 | public async Task AddsProjectAndSetsId()
9 | {
10 | var testProjectName = ProjectName.From("testProject");
11 | var repository = GetRepository();
12 | var project = new Project(testProjectName);
13 |
14 | var item = new ToDoItem();
15 | item.Title = "test item title";
16 | project.AddItem(item);
17 |
18 | await repository.AddAsync(project);
19 |
20 | var newProject = (await repository.ListAsync())
21 | .FirstOrDefault();
22 |
23 | Assert.Equal(testProjectName, newProject?.Name);
24 | Assert.True(newProject?.Id.Value > 0);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.IntegrationTests/Data/EfRepositoryDelete.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.IntegrationTests.Data;
4 |
5 | public class EfRepositoryDelete : BaseEfRepoTestFixture
6 | {
7 | [Fact]
8 | public async Task DeletesItemAfterAddingIt()
9 | {
10 | // add a project
11 | var repository = GetRepository();
12 | var initialName = Guid.NewGuid().ToString();
13 | var project = new Project(ProjectName.From(initialName));
14 | await repository.AddAsync(project);
15 |
16 | // delete the item
17 | await repository.DeleteAsync(project);
18 |
19 | // verify it's no longer there
20 | Assert.DoesNotContain(await repository.ListAsync(),
21 | project => project.Name == initialName);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.IntegrationTests/Data/EfRepositoryUpdate.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.IntegrationTests.Data;
4 |
5 | public class EfRepositoryUpdate : BaseEfRepoTestFixture
6 | {
7 | [Fact]
8 | public async Task UpdatesItemAfterAddingIt()
9 | {
10 | // add a project
11 | var repository = GetRepository();
12 | var initialName = Guid.NewGuid().ToString();
13 | var project = new Project(ProjectName.From(initialName));
14 |
15 | await repository.AddAsync(project);
16 |
17 | // detach the item so we get a different instance
18 | _dbContext.Entry(project).State = EntityState.Detached;
19 |
20 | // fetch the item and update its title
21 | var newProject = (await repository.ListAsync())
22 | .FirstOrDefault(project => project.Name == initialName);
23 | if (newProject == null)
24 | {
25 | Assert.NotNull(newProject);
26 | return;
27 | }
28 | Assert.NotSame(project, newProject);
29 | var newName = Guid.NewGuid().ToString();
30 | newProject.UpdateName(ProjectName.From(newName));
31 |
32 | // Update the item
33 | await repository.UpdateAsync(newProject);
34 |
35 | // Fetch the updated item
36 | var updatedItem = (await repository.ListAsync())
37 | .FirstOrDefault(project => project.Name == newName);
38 |
39 | Assert.NotNull(updatedItem);
40 | Assert.NotEqual(project.Name, updatedItem?.Name);
41 | Assert.Equal(newProject.Id, updatedItem?.Id);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.IntegrationTests/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Ardalis.SharedKernel;
2 | global using Microsoft.EntityFrameworkCore;
3 | global using NSubstitute;
4 | global using Xunit;
5 | global using Microsoft.Extensions.DependencyInjection;
6 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.UnitTests/Core/Handlers/ItemCompletedEmailNotificationHandlerHandle.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.Interfaces;
2 | using NimblePros.SampleToDo.Core.ProjectAggregate;
3 | using NimblePros.SampleToDo.Core.ProjectAggregate.Events;
4 | using NimblePros.SampleToDo.Core.ProjectAggregate.Handlers;
5 |
6 | namespace NimblePros.SampleToDo.UnitTests.Core.Handlers;
7 |
8 | public class ItemCompletedEmailNotificationHandlerHandle
9 | {
10 | private ItemCompletedEmailNotificationHandler _handler;
11 | private IEmailSender _emailSender = Substitute.For();
12 |
13 | public ItemCompletedEmailNotificationHandlerHandle()
14 | {
15 | _handler = new ItemCompletedEmailNotificationHandler(_emailSender);
16 | }
17 |
18 | [Fact]
19 | public async Task ThrowsExceptionGivenNullEventArgument()
20 | {
21 | #nullable disable
22 | Exception ex = await Assert.ThrowsAsync(() => _handler.Handle(null, CancellationToken.None));
23 | #nullable enable
24 | }
25 |
26 | [Fact]
27 | public async Task SendsEmailGivenEventInstance()
28 | {
29 | await _handler.Handle(new ToDoItemCompletedEvent(new ToDoItem()), CancellationToken.None);
30 |
31 | await _emailSender.Received().SendEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any());
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/ProjectConstructor.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UnitTests.Core.ProjectAggregate;
4 |
5 | public class ProjectConstructor
6 | {
7 | private string _testName = "test name";
8 | private Priority _testPriority = Priority.Backlog;
9 | private Project? _testProject;
10 |
11 | private Project CreateProject()
12 | {
13 | return new Project(ProjectName.From(_testName));
14 | }
15 |
16 | [Fact]
17 | public void InitializesName()
18 | {
19 | _testProject = CreateProject();
20 |
21 | Assert.Equal(_testName, _testProject.Name.Value);
22 | }
23 |
24 | [Fact]
25 | public void InitializesTaskListToEmptyList()
26 | {
27 | _testProject = CreateProject();
28 |
29 | Assert.NotNull(_testProject.Items);
30 | }
31 |
32 | [Fact]
33 | public void InitializesStatusToInProgress()
34 | {
35 | _testProject = CreateProject();
36 |
37 | Assert.Equal(ProjectStatus.Complete, _testProject.Status);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/ProjectNameFrom.cs:
--------------------------------------------------------------------------------
1 | using Shouldly;
2 | using NimblePros.SampleToDo.Core.ProjectAggregate;
3 | using Vogen;
4 |
5 | namespace NimblePros.SampleToDo.UnitTests.Core.ProjectAggregate;
6 |
7 | public class ProjectNameFrom
8 | {
9 | [Theory]
10 | [InlineData("")]
11 | [InlineData(null!)]
12 | public void ThrowsGivenNullOrEmpty(string name)
13 | {
14 | Should.Throw(() => ProjectName.From(name));
15 | }
16 |
17 | [Fact]
18 | public void DoesNotThrowGivenValidData()
19 | {
20 | string validName = "valid name";
21 | var name = ProjectName.From(validName);
22 | name.Value.ShouldBe(validName);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/Project_AddItem.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 |
3 | namespace NimblePros.SampleToDo.UnitTests.Core.ProjectAggregate;
4 |
5 | public class Project_AddItem
6 | {
7 | private Project _testProject = new Project(ProjectName.From("some name"));
8 |
9 | [Fact]
10 | public void AddsItemToItems()
11 | {
12 | var _testItem = new ToDoItem
13 | {
14 | Title = "title",
15 | Description = "description"
16 | };
17 |
18 | _testProject.AddItem(_testItem);
19 |
20 | Assert.Contains(_testItem, _testProject.Items);
21 | }
22 |
23 | [Fact]
24 | public void ThrowsExceptionGivenNullItem()
25 | {
26 | #nullable disable
27 | Action action = () => _testProject.AddItem(null);
28 | #nullable enable
29 |
30 | var ex = Assert.Throws(action);
31 | Assert.Equal("newItem", ex.ParamName);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/ToDoItemConstructor.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 | using Xunit;
3 |
4 | namespace NimblePros.SampleToDo.UnitTests.Core.ProjectAggregate;
5 |
6 | public class ToDoItemConstructor
7 | {
8 | [Fact]
9 | public void InitializesPriority()
10 | {
11 | var item = new ToDoItemBuilder()
12 | .WithDefaultValues()
13 | .Build();
14 |
15 | Assert.Equal(item.Priority, Priority.Backlog);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/ToDoItemMarkComplete.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate.Events;
2 |
3 | namespace NimblePros.SampleToDo.UnitTests.Core.ProjectAggregate;
4 |
5 | public class ToDoItemMarkComplete
6 | {
7 | [Fact]
8 | public void SetsIsDoneToTrue()
9 | {
10 | var item = new ToDoItemBuilder()
11 | .WithDefaultValues()
12 | .Description("")
13 | .Build();
14 |
15 | item.MarkComplete();
16 |
17 | Assert.True(item.IsDone);
18 | }
19 |
20 | [Fact]
21 | public void RaisesToDoItemCompletedEvent()
22 | {
23 | var item = new ToDoItemBuilder().Build();
24 |
25 | item.MarkComplete();
26 |
27 | Assert.Single(item.DomainEvents);
28 | Assert.IsType(item.DomainEvents.First());
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.UnitTests/Core/Services/DeleteContributorSevice_DeleteContributor.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ContributorAggregate;
2 | using NimblePros.SampleToDo.Core.Services;
3 |
4 | namespace NimblePros.SampleToDo.UnitTests.Core.Services;
5 |
6 | public class DeleteContributorService_DeleteContributor
7 | {
8 | private readonly IRepository _repository = Substitute.For>();
9 | private readonly IMediator _mediator = Substitute.For();
10 | private readonly ILogger _logger = Substitute.For>();
11 |
12 | private readonly DeleteContributorService _service;
13 |
14 | public DeleteContributorService_DeleteContributor()
15 | {
16 | _service = new DeleteContributorService(_repository, _mediator, _logger);
17 | }
18 |
19 | [Fact]
20 | public async Task ReturnsNotFoundGivenCantFindContributor()
21 | {
22 | var result = await _service.DeleteContributor(0);
23 |
24 | Assert.Equal(Ardalis.Result.ResultStatus.NotFound, result.Status);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.UnitTests/Core/Specifications/IncompleteItemSpecificationsConstructor.cs:
--------------------------------------------------------------------------------
1 | using NimblePros.SampleToDo.Core.ProjectAggregate;
2 | using NimblePros.SampleToDo.Core.ProjectAggregate.Specifications;
3 |
4 | namespace NimblePros.SampleToDo.UnitTests.Core.Specifications;
5 |
6 | public class IncompleteItemsSpecificationConstructor
7 | {
8 | [Fact]
9 | public void FilterCollectionToOnlyReturnItemsWithIsDoneFalse()
10 | {
11 | var item1 = new ToDoItem();
12 | var item2 = new ToDoItem();
13 | var item3 = new ToDoItem();
14 | item3.MarkComplete();
15 |
16 | var items = new List() { item1, item2, item3 };
17 |
18 | var spec = new IncompleteItemsSpec();
19 |
20 | var filteredList = spec.Evaluate(items);
21 |
22 | Assert.Contains(item1, filteredList);
23 | Assert.Contains(item2, filteredList);
24 | Assert.DoesNotContain(item3, filteredList);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.UnitTests/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using System.Runtime.CompilerServices;
2 | global using Ardalis.Result;
3 | global using Ardalis.SharedKernel;
4 | global using Ardalis.Specification;
5 | global using MediatR;
6 | global using Microsoft.Extensions.Logging;
7 | global using NSubstitute;
8 | global using NSubstitute.ReturnsExtensions;
9 | global using Xunit;
10 |
--------------------------------------------------------------------------------
/sample/tests/NimblePros.SampleToDo.UnitTests/NoOpMediator.cs:
--------------------------------------------------------------------------------
1 | namespace NimblePros.SampleToDo.UnitTests;
2 |
3 | public class NoOpMediator : IMediator
4 | {
5 | public Task Publish(object notification, CancellationToken cancellationToken = default)
6 | {
7 | return Task.CompletedTask;
8 | }
9 |
10 | public Task Publish(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification
11 | {
12 | return Task.CompletedTask;
13 | }
14 |
15 | public Task Send(IRequest request, CancellationToken cancellationToken = default)
16 | {
17 | return Task.FromResult(default!);
18 | }
19 |
20 | public Task