├── Src ├── DDD.Domain │ ├── Services │ │ └── .gitkeep │ ├── Common │ │ ├── Constants │ │ │ └── GlobalConstant.cs │ │ ├── Exceptions │ │ │ ├── EntityNotFoundException.cs │ │ │ └── EntityAlreadyExistsException.cs │ │ ├── Extensions │ │ │ └── StringExtension.cs │ │ └── Utils │ │ │ └── ThrowIf.cs │ ├── Interfaces │ │ ├── IUnitOfWork.cs │ │ ├── ICustomerRepository.cs │ │ ├── IUser.cs │ │ ├── IRepository.cs │ │ └── ISpecification.cs │ ├── Providers │ │ ├── Hubs │ │ │ ├── HubRoutes.cs │ │ │ └── Notification │ │ │ │ ├── NotificationItem.cs │ │ │ │ ├── INotificationHub.cs │ │ │ │ ├── INotificationProvider.cs │ │ │ │ ├── NotificationHub.cs │ │ │ │ └── NotificationProvider.cs │ │ ├── Mail │ │ │ ├── IMailProvider.cs │ │ │ ├── MailAddress.cs │ │ │ ├── MailAttachment.cs │ │ │ ├── MailMessage.cs │ │ │ └── MailProvider.cs │ │ ├── Webhooks │ │ │ ├── IWebhookProvider.cs │ │ │ └── WebhookProvider.cs │ │ ├── Hash │ │ │ ├── HashingOptions.cs │ │ │ ├── IPasswordHasher.cs │ │ │ └── PasswordHasher.cs │ │ ├── Http │ │ │ ├── IFooClient.cs │ │ │ ├── Response.cs │ │ │ ├── IHttpProvider.cs │ │ │ └── HttpProvider.cs │ │ ├── Office │ │ │ ├── IOfficeProvider.cs │ │ │ ├── ExcelFormat.cs │ │ │ └── OfficeProvider.cs │ │ └── Crons │ │ │ ├── NotifyInactiveUserConsumerModel.cs │ │ │ ├── ICronProvider.cs │ │ │ ├── Jobs │ │ │ └── NotifyInactiveUserJob.cs │ │ │ └── CronProvider.cs │ ├── Validations │ │ ├── RemoveCustomerCommandValidation.cs │ │ ├── RegisterNewCustomerCommandValidation.cs │ │ ├── UpdateCustomerCommandValidation.cs │ │ └── CustomerValidation.cs │ ├── Events │ │ ├── CustomerRemovedEvent.cs │ │ ├── CustomerUpdatedEvent.cs │ │ └── CustomerRegisteredEvent.cs │ ├── Specifications │ │ ├── CustomerFilterPaginatedSpecification.cs │ │ └── BaseSpecification.cs │ ├── Commands │ │ ├── CustomerCommand.cs │ │ ├── RemoveCustomerCommand.cs │ │ ├── RegisterNewCustomerCommand.cs │ │ └── UpdateCustomerCommand.cs │ ├── DDD.Domain.csproj │ ├── Models │ │ └── Customer.cs │ ├── EventHandlers │ │ └── CustomerEventHandler.cs │ └── CommandHandlers │ │ ├── CommandHandler.cs │ │ └── CustomerCommandHandler.cs ├── DDD.CLI.Migration │ ├── Scripts │ │ ├── 002_Drop_Table_Person.sql │ │ └── 001_Create_Table_Person.sql │ ├── README.md │ ├── DDD.CLI.Migration.csproj │ └── Program.cs ├── DDD.Domain.Core │ ├── Events │ │ ├── IHandler.cs │ │ ├── IEventStore.cs │ │ ├── Event.cs │ │ ├── Message.cs │ │ └── StoredEvent.cs │ ├── DDD.Domain.Core.csproj │ ├── Models │ │ ├── EntityAudit.cs │ │ ├── ValueObject.cs │ │ └── Entity.cs │ ├── Bus │ │ └── IMediatorHandler.cs │ ├── Commands │ │ └── Command.cs │ └── Notifications │ │ ├── DomainNotification.cs │ │ └── DomainNotificationHandler.cs ├── DDD.Services.Api │ ├── appsettings.Staging.json │ ├── appsettings.Production.json │ ├── StartupExtensions │ │ ├── ErrorHandlingExtension.cs │ │ ├── HashingExtension.cs │ │ ├── SignalRExtension.cs │ │ ├── HttpExtension.cs │ │ ├── HealthCheckExtension.cs │ │ ├── QuartzExtensions.cs │ │ ├── DatabaseExtension.cs │ │ ├── AuthExtension.cs │ │ └── SwaggerExtension.cs │ ├── appsettings.json │ ├── Configurations │ │ ├── AddRequiredHeaderParameter.cs │ │ └── AutoMapperSetup.cs │ ├── appsettings.Development.json │ ├── Controllers │ │ ├── v1 │ │ │ ├── RoleController.cs │ │ │ └── CustomerController.cs │ │ └── ApiController.cs │ ├── DDD.Services.Api.csproj │ ├── Properties │ │ └── launchSettings.json │ └── Program.cs ├── DDD.Infra.CrossCutting.Identity │ ├── Models │ │ ├── RoleViewModels │ │ │ └── CreateViewModel.cs │ │ ├── ManageViewModels │ │ │ ├── GenerateRecoveryCodesViewModel.cs │ │ │ ├── RemoveLoginViewModel.cs │ │ │ ├── TwoFactorAuthenticationViewModel.cs │ │ │ ├── ExternalLoginsViewModel.cs │ │ │ ├── IndexViewModel.cs │ │ │ ├── EnableAuthenticatorViewModel.cs │ │ │ ├── SetPasswordViewModel.cs │ │ │ └── ChangePasswordViewModel.cs │ │ ├── AccountViewModels │ │ │ ├── TokenViewModel.cs │ │ │ ├── ExternalLoginViewModel.cs │ │ │ ├── ForgotPasswordViewModel.cs │ │ │ ├── LoginWithRecoveryCodeViewModel.cs │ │ │ ├── LoginViewModel.cs │ │ │ ├── LoginWith2faViewModel.cs │ │ │ ├── ResetPasswordViewModel.cs │ │ │ └── RegisterViewModel.cs │ │ ├── ApplicationUser.cs │ │ ├── RefreshToken.cs │ │ └── AspNetUser.cs │ ├── Services │ │ ├── JwtToken.cs │ │ ├── ISmsSender.cs │ │ ├── IEmailSender.cs │ │ ├── IJwtFactory.cs │ │ ├── AuthSMSMessageSender.cs │ │ ├── AuthEmailMessageSender.cs │ │ └── JwtFactory.cs │ ├── Authorization │ │ ├── Roles.cs │ │ ├── ClaimRequirement.cs │ │ ├── ClaimsRequirementHandler.cs │ │ └── JwtIssuerOptions.cs │ ├── Data │ │ ├── AuthDbContext.cs │ │ └── Migrations │ │ │ └── AuthDbContextModelSnapshot.cs │ ├── Extensions │ │ └── EmailSenderExtensions.cs │ └── DDD.Infra.CrossCutting.Identity.csproj ├── DDD.Infra.CrossCutting.Bus │ ├── DDD.Infra.CrossCutting.Bus.csproj │ └── InMemoryBus.cs ├── DDD.Application │ ├── DDD.Application.csproj │ ├── AutoMapper │ │ ├── DomainToViewModelMappingProfile.cs │ │ ├── ViewModelToDomainMappingProfile.cs │ │ └── AutoMapperConfig.cs │ ├── EventSourcedNormalizers │ │ ├── CustomerHistoryData.cs │ │ └── CustomerHistory.cs │ ├── Interfaces │ │ └── ICustomerAppService.cs │ ├── ViewModels │ │ └── CustomerViewModel.cs │ └── Services │ │ └── CustomerAppService.cs ├── DDD.Infra.Data │ ├── Repository │ │ ├── EventSourcing │ │ │ ├── IEventStoreRepository.cs │ │ │ └── EventStoreSQLRepository.cs │ │ ├── CustomerRepository.cs │ │ ├── SpecificationEvaluator.cs │ │ └── Repository.cs │ ├── DDD.Infra.Data.csproj │ ├── UoW │ │ └── UnitOfWork.cs │ ├── Mappings │ │ ├── StoredEventMap.cs │ │ └── CustomerMap.cs │ ├── Context │ │ ├── EventStoreSQLContext.cs │ │ └── ApplicationDbContext.cs │ ├── EventSourcing │ │ └── SqlEventStore.cs │ └── Migrations │ │ ├── 20161213130431_Initial.cs │ │ ├── EventStoreSQL │ │ ├── 20161213130520_Initial.cs │ │ └── EventStoreSQLContextModelSnapshot.cs │ │ └── ApplicationDbContextModelSnapshot.cs └── DDD.Infra.CrossCutting.IoC │ ├── DDD.Infra.CrossCutting.IoC.csproj │ └── NativeInjectorBootStrapper.cs ├── global.json ├── .gitattributes ├── Docs ├── code-flow.jpg ├── flowchart.png ├── overview.png ├── architecture.png ├── dependencies.jpg ├── project-dependencies.jpg └── custom-repo-versus-db-context.png ├── Sql └── GenerateDataBase.sql ├── omnisharp.json ├── docker-compose.yml ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── nuget.config ├── manifests ├── service.yml └── deployment.yml ├── .github ├── workflows │ ├── release.yml │ ├── container.yml │ ├── sonar.yml │ └── ci.yml └── dependabot.yml ├── Tests ├── DDD.Services.Api.IntegrationTests │ ├── CustomWebApplicationFactory.cs │ ├── DDD.Services.Api.IntegrationTests.csproj │ └── Controllers │ │ └── WeatherForecastControllerIntegrationTests.cs.txt └── DDD.Application.UnitTests │ ├── DDD.Application.UnitTests.csproj │ └── Services │ └── CustomerAppServiceTests.cs ├── .dockerignore ├── stylecop.json ├── LICENSE ├── Directory.Build.props ├── Dockerfile ├── Containerfile ├── templates └── include-throw-error-steps.yml ├── Directory.Packages.props ├── azure-pipelines.yml └── api.http /Src/DDD.Domain/Services/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.100" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.cs diff=csharp 3 | *.sh eol=lf 4 | *.sln eol=crlf 5 | -------------------------------------------------------------------------------- /Src/DDD.CLI.Migration/Scripts/002_Drop_Table_Person.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE [dbo].[Person]; 2 | -------------------------------------------------------------------------------- /Docs/code-flow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntxinh/AspNetCore-DDD/HEAD/Docs/code-flow.jpg -------------------------------------------------------------------------------- /Docs/flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntxinh/AspNetCore-DDD/HEAD/Docs/flowchart.png -------------------------------------------------------------------------------- /Docs/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntxinh/AspNetCore-DDD/HEAD/Docs/overview.png -------------------------------------------------------------------------------- /Docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntxinh/AspNetCore-DDD/HEAD/Docs/architecture.png -------------------------------------------------------------------------------- /Docs/dependencies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntxinh/AspNetCore-DDD/HEAD/Docs/dependencies.jpg -------------------------------------------------------------------------------- /Sql/GenerateDataBase.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntxinh/AspNetCore-DDD/HEAD/Sql/GenerateDataBase.sql -------------------------------------------------------------------------------- /Docs/project-dependencies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntxinh/AspNetCore-DDD/HEAD/Docs/project-dependencies.jpg -------------------------------------------------------------------------------- /Docs/custom-repo-versus-db-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntxinh/AspNetCore-DDD/HEAD/Docs/custom-repo-versus-db-context.png -------------------------------------------------------------------------------- /Src/DDD.Domain/Common/Constants/GlobalConstant.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Domain.Common.Constants; 2 | 3 | public static class GlobalConstant 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/Events/IHandler.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Domain.Core.Events; 2 | 3 | public interface IHandler 4 | where T : Message 5 | { 6 | void Handle(T message); 7 | } 8 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Interfaces/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DDD.Domain.Interfaces; 4 | 5 | public interface IUnitOfWork : IDisposable 6 | { 7 | bool Commit(); 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/Events/IEventStore.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Domain.Core.Events; 2 | 3 | public interface IEventStore 4 | { 5 | void Save(T theEvent) 6 | where T : Event; 7 | } 8 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Hubs/HubRoutes.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Domain.Providers.Hubs; 2 | 3 | public static class HubRoutes 4 | { 5 | public static readonly string Notification = "/notification"; 6 | } 7 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Mail/IMailProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace DDD.Domain.Providers.Mail; 4 | 5 | public interface IMailProvider 6 | { 7 | Task Send(MailMessage message); 8 | } 9 | -------------------------------------------------------------------------------- /omnisharp.json: -------------------------------------------------------------------------------- 1 | // { 2 | // "RoslynExtensionsOptions": { 3 | // "enableAnalyzersSupport": true 4 | // }, 5 | // "FormattingOptions": { 6 | // "enableEditorConfigSupport": true 7 | // }, 8 | // } 9 | -------------------------------------------------------------------------------- /Src/DDD.CLI.Migration/README.md: -------------------------------------------------------------------------------- 1 | # Build & Run 2 | 3 | ```ps 4 | dotnet build .\Src\DDD.CLI.Migration\DDD.CLI.Migration.csproj 5 | dotnet run --project Src\DDD.CLI.Migration\DDD.CLI.Migration.csproj 6 | ``` 7 | 8 | 9 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Mail/MailAddress.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Domain.Providers.Mail; 2 | 3 | public class MailAddress 4 | { 5 | public string Email { get; set; } 6 | 7 | public string Name { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/appsettings.Staging.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "System": "Warning", 6 | "Microsoft": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Webhooks/IWebhookProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace DDD.Domain.Providers.Webhooks; 4 | 5 | public interface IWebhookProvider 6 | { 7 | Task Send(string message); 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "System": "Warning", 6 | "Microsoft": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Mail/MailAttachment.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Domain.Providers.Mail; 2 | 3 | public class MailAttachment 4 | { 5 | public string FileName { get; set; } 6 | 7 | public string MimeType { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/RoleViewModels/CreateViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Infra.CrossCutting.Identity.Models.RoleViewModels; 2 | 3 | public class CreateViewModel 4 | { 5 | public string Name { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Interfaces/ICustomerRepository.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Models; 2 | 3 | namespace DDD.Domain.Interfaces; 4 | 5 | public interface ICustomerRepository : IRepository 6 | { 7 | Customer GetByEmail(string email); 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Bus/DDD.Infra.CrossCutting.Bus.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | image: ghcr.io/ntxinh/aspnetcore-ddd:latest 4 | build: . 5 | ports: 6 | - "80:80" 7 | - "443:443" 8 | # environment: 9 | # ASPNETCORE_ENVIRONMENT: ${MyEnv} 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "esbenp.prettier-vscode", 5 | "ms-dotnettools.csharp", 6 | "josefpihrt-vscode.roslynator", 7 | "humao.rest-client" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Hash/HashingOptions.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Domain.Providers.Hash; 2 | 3 | public sealed class HashingOptions 4 | { 5 | public const string Hashing = "Hashing"; 6 | 7 | public int Iterations { get; set; } = 10000; 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Services/JwtToken.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Infra.CrossCutting.Identity.Services; 2 | 3 | public class JwtToken 4 | { 5 | public string JwtId { get; set; } 6 | 7 | public string AccessToken { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/DDD.Domain.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Http/IFooClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Refit; 4 | 5 | namespace DDD.Domain.Providers.Http; 6 | 7 | public interface IFooClient 8 | { 9 | [Get("/")] 10 | Task GetAll(); 11 | } 12 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Authorization/Roles.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Infra.CrossCutting.Identity.Authorization; 2 | 3 | public static class Roles 4 | { 5 | public const string Admin = "Admin"; 6 | public const string User = "User"; 7 | } 8 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Services/ISmsSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Services; 4 | 5 | public interface ISmsSender 6 | { 7 | Task SendSmsAsync(string number, string message); 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Hash/IPasswordHasher.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Domain.Providers.Hash; 2 | 3 | public interface IPasswordHasher 4 | { 5 | string Hash(string password); 6 | 7 | (bool Verified, bool NeedsUpgrade) Check(string hash, string password); 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Infra.CrossCutting.Identity.Models.ManageViewModels; 2 | 3 | public class GenerateRecoveryCodesViewModel 4 | { 5 | public string[] RecoveryCodes { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Services/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Services; 4 | 5 | public interface IEmailSender 6 | { 7 | Task SendEmailAsync(string email, string subject, string message); 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Office/IOfficeProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace DDD.Domain.Providers.Office; 4 | 5 | public interface IOfficeProvider 6 | { 7 | string ExportAndUploadExcel(IList data, IList formats, string fileName); 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/AccountViewModels/TokenViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Infra.CrossCutting.Identity.Models.AccountViewModels; 2 | 3 | public class TokenViewModel 4 | { 5 | public string AccessToken { get; set; } 6 | 7 | public string RefreshToken { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/ManageViewModels/RemoveLoginViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Infra.CrossCutting.Identity.Models.ManageViewModels; 2 | 3 | public class RemoveLoginViewModel 4 | { 5 | public string LoginProvider { get; set; } 6 | 7 | public string ProviderKey { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Models; 4 | 5 | // Add profile data for application users by adding properties to the ApplicationUser class 6 | public class ApplicationUser : IdentityUser 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Services/IJwtFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using System.Threading.Tasks; 3 | 4 | namespace DDD.Infra.CrossCutting.Identity.Services; 5 | 6 | public interface IJwtFactory 7 | { 8 | Task GenerateJwtToken(ClaimsIdentity claimsIdentity); 9 | } 10 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Interfaces/IUser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Security.Claims; 3 | 4 | namespace DDD.Domain.Interfaces; 5 | 6 | public interface IUser 7 | { 8 | string Name { get; } 9 | 10 | bool IsAuthenticated(); 11 | 12 | IEnumerable GetClaimsIdentity(); 13 | } 14 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Hubs/Notification/NotificationItem.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Domain.Providers.Hubs; 2 | 3 | public class NotificationItem 4 | { 5 | public bool Status { get; set; } = true; 6 | 7 | public string Message { get; set; } = string.Empty; 8 | 9 | public int UserId { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Hubs/Notification/INotificationHub.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace DDD.Domain.Providers.Hubs; 4 | 5 | public interface INotificationHub 6 | { 7 | Task Send(NotificationItem item); 8 | 9 | Task JoinGroup(string groupName); 10 | 11 | Task LeaveGroup(string groupName); 12 | } 13 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Validations/RemoveCustomerCommandValidation.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Commands; 2 | 3 | namespace DDD.Domain.Validations; 4 | 5 | public class RemoveCustomerCommandValidation : CustomerValidation 6 | { 7 | public RemoveCustomerCommandValidation() 8 | { 9 | ValidateId(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /manifests/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: webapi 5 | labels: 6 | app: aspnetcore-ddd 7 | service: webapi 8 | spec: 9 | ports: 10 | - name: http 11 | port: 80 12 | protocol: TCP 13 | targetPort: 80 14 | type: LoadBalancer 15 | selector: 16 | service: webapi 17 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/Events/Event.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using MediatR; 4 | 5 | namespace DDD.Domain.Core.Events; 6 | 7 | public abstract class Event : Message, INotification 8 | { 9 | public DateTime Timestamp { get; private set; } 10 | 11 | protected Event() 12 | { 13 | Timestamp = DateTime.Now; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Hubs/Notification/INotificationProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace DDD.Domain.Providers.Hubs; 4 | 5 | public interface INotificationProvider 6 | { 7 | Task Send(NotificationItem item); 8 | 9 | Task JoinGroup(string groupName); 10 | 11 | Task LeaveGroup(string groupName); 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | # branches: ["main"] 6 | tags: 7 | - "v*.*.*" 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Release 16 | uses: softprops/action-gh-release@v2 17 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/AccountViewModels/ExternalLoginViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Models.AccountViewModels; 4 | 5 | public class ExternalLoginViewModel 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string Email { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/AccountViewModels/ForgotPasswordViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Models.AccountViewModels; 4 | 5 | public class ForgotPasswordViewModel 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string Email { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Crons/NotifyInactiveUserConsumerModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace DDD.Domain.Providers.Crons; 4 | 5 | public class NotifyInactiveUserConsumerModel 6 | { 7 | public List Data { get; set; } 8 | 9 | public int UserId { get; set; } 10 | 11 | public short TenantId { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/Models/EntityAudit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DDD.Domain.Core.Models; 4 | 5 | public abstract class EntityAudit : Entity 6 | { 7 | public DateTime CreatedAt { get; set; } 8 | 9 | public int CreatedBy { get; set; } 10 | 11 | public DateTime UpdatedAt { get; set; } 12 | 13 | public int UpdatedBy { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Events/CustomerRemovedEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Domain.Core.Events; 4 | 5 | namespace DDD.Domain.Events; 6 | 7 | public class CustomerRemovedEvent : Event 8 | { 9 | public CustomerRemovedEvent(Guid id) 10 | { 11 | Id = id; 12 | AggregateId = id; 13 | } 14 | 15 | public Guid Id { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /Src/DDD.CLI.Migration/DDD.CLI.Migration.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Common/Exceptions/EntityNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DDD.Domain.Common.Exceptions; 4 | 5 | public class EntityNotFoundException : Exception 6 | { 7 | public EntityNotFoundException() 8 | { 9 | } 10 | 11 | public EntityNotFoundException(string message) 12 | : base(message) 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Http/Response.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Domain.Providers.Http; 2 | 3 | public class Response 4 | { 5 | public string Version { get; set; } 6 | 7 | public int StatusCode { get; set; } 8 | 9 | public string RequestId { get; set; } 10 | 11 | public string Message { get; set; } 12 | 13 | public object Result { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/StartupExtensions/ErrorHandlingExtension.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Services.Api.StartupExtensions; 2 | 3 | public static class ErrorHandlingExtension 4 | { 5 | public static IApplicationBuilder UseCustomizedErrorHandling(this IApplicationBuilder app) 6 | { 7 | app.UseDeveloperExceptionPage(); 8 | 9 | return app; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Src/DDD.Application/DDD.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Common/Exceptions/EntityAlreadyExistsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DDD.Domain.Common.Exceptions; 4 | 5 | public class EntityAlreadyExistsException : Exception 6 | { 7 | public EntityAlreadyExistsException() 8 | { 9 | } 10 | 11 | public EntityAlreadyExistsException(string message) 12 | : base(message) 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Common/Extensions/StringExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DDD.Domain.Common.Extensions; 4 | 5 | public static class StringExtension 6 | { 7 | public static int WordCount(this string str) 8 | { 9 | return str.Split( 10 | new char[] { ' ', '.', '?' }, 11 | StringSplitOptions.RemoveEmptyEntries).Length; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/ManageViewModels/TwoFactorAuthenticationViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Infra.CrossCutting.Identity.Models.ManageViewModels; 2 | 3 | public class TwoFactorAuthenticationViewModel 4 | { 5 | public bool HasAuthenticator { get; set; } 6 | 7 | public int RecoveryCodesLeft { get; set; } 8 | 9 | public bool Is2faEnabled { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/Bus/IMediatorHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using DDD.Domain.Core.Commands; 4 | using DDD.Domain.Core.Events; 5 | 6 | namespace DDD.Domain.Core.Bus; 7 | 8 | public interface IMediatorHandler 9 | { 10 | Task SendCommand(T command) 11 | where T : Command; 12 | 13 | Task RaiseEvent(T @event) 14 | where T : Event; 15 | } 16 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Repository/EventSourcing/IEventStoreRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using DDD.Domain.Core.Events; 5 | 6 | namespace DDD.Infra.Data.Repository.EventSourcing; 7 | 8 | public interface IEventStoreRepository : IDisposable 9 | { 10 | void Store(StoredEvent theEvent); 11 | 12 | IList All(Guid aggregateId); 13 | } 14 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Specifications/CustomerFilterPaginatedSpecification.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Models; 2 | 3 | namespace DDD.Domain.Specifications; 4 | 5 | public class CustomerFilterPaginatedSpecification : BaseSpecification 6 | { 7 | public CustomerFilterPaginatedSpecification(int skip, int take) 8 | : base(i => true) 9 | { 10 | ApplyPaging(skip, take); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/DDD.Infra.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Src/DDD.Application/AutoMapper/DomainToViewModelMappingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | using DDD.Application.ViewModels; 4 | using DDD.Domain.Models; 5 | 6 | namespace DDD.Application.AutoMapper; 7 | 8 | public class DomainToViewModelMappingProfile : Profile 9 | { 10 | public DomainToViewModelMappingProfile() 11 | { 12 | CreateMap(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/Events/Message.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using MediatR; 4 | 5 | namespace DDD.Domain.Core.Events; 6 | 7 | public abstract class Message : IRequest 8 | { 9 | public string MessageType { get; protected set; } 10 | 11 | public Guid AggregateId { get; protected set; } 12 | 13 | protected Message() 14 | { 15 | MessageType = GetType().Name; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Validations/RegisterNewCustomerCommandValidation.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Commands; 2 | 3 | namespace DDD.Domain.Validations; 4 | 5 | public class RegisterNewCustomerCommandValidation : CustomerValidation 6 | { 7 | public RegisterNewCustomerCommandValidation() 8 | { 9 | ValidateName(); 10 | ValidateBirthDate(); 11 | ValidateEmail(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Services/AuthSMSMessageSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Services; 4 | 5 | public class AuthSMSMessageSender : ISmsSender 6 | { 7 | public Task SendSmsAsync(string number, string message) 8 | { 9 | // Plug in your SMS service here to send a text message. 10 | return Task.FromResult(0); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/AccountViewModels/LoginWithRecoveryCodeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Models.AccountViewModels; 4 | 5 | public class LoginWithRecoveryCodeViewModel 6 | { 7 | [Required] 8 | [DataType(DataType.Text)] 9 | [Display(Name = "Recovery Code")] 10 | public string RecoveryCode { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Common/Utils/ThrowIf.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DDD.Domain.Common.Utils; 4 | 5 | public static class ThrowIf 6 | { 7 | public static class Argument 8 | { 9 | public static void IsNull(T argument) 10 | { 11 | if (argument is null) 12 | { 13 | throw new ArgumentNullException(typeof(T).Name); 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Validations/UpdateCustomerCommandValidation.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Commands; 2 | 3 | namespace DDD.Domain.Validations; 4 | 5 | public class UpdateCustomerCommandValidation : CustomerValidation 6 | { 7 | public UpdateCustomerCommandValidation() 8 | { 9 | ValidateId(); 10 | ValidateName(); 11 | ValidateBirthDate(); 12 | ValidateEmail(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Commands/CustomerCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Domain.Core.Commands; 4 | 5 | namespace DDD.Domain.Commands; 6 | 7 | public abstract class CustomerCommand : Command 8 | { 9 | public Guid Id { get; protected set; } 10 | 11 | public string Name { get; protected set; } 12 | 13 | public string Email { get; protected set; } 14 | 15 | public DateTime BirthDate { get; protected set; } 16 | } 17 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Services/AuthEmailMessageSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Services; 4 | 5 | public class AuthEmailMessageSender : IEmailSender 6 | { 7 | public Task SendEmailAsync(string email, string subject, string message) 8 | { 9 | // Plug in your email service here to send an email. 10 | return Task.FromResult(0); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Src/DDD.CLI.Migration/Scripts/001_Create_Table_Person.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Person ( 2 | Id int IDENTITY(1,1) NOT NULL PRIMARY KEY, 3 | LastName varchar(255) NOT NULL, 4 | FirstName varchar(255), 5 | Age int, 6 | [CreatedAt] [DATETIME2] NOT NULL, 7 | [CreatedBy] [INT] NOT NULL, 8 | [UpdatedAt] [DATETIME2] NOT NULL, 9 | [UpdatedBy] [INT] NOT NULL, 10 | [Active] [BIT] NOT NULL, 11 | [IsDelete] [BIT] NOT NULL 12 | ); 13 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.AspNetCore": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | }, 10 | "AllowedHosts": "*", 11 | "HealthChecksUI": { 12 | "HealthChecks": [ 13 | { 14 | "Name": "Web App", 15 | "Uri": "/hc" 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Src/DDD.Application/EventSourcedNormalizers/CustomerHistoryData.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Application.EventSourcedNormalizers; 2 | 3 | public class CustomerHistoryData 4 | { 5 | public string Action { get; set; } 6 | 7 | public string Id { get; set; } 8 | 9 | public string Name { get; set; } 10 | 11 | public string Email { get; set; } 12 | 13 | public string BirthDate { get; set; } 14 | 15 | public string When { get; set; } 16 | 17 | public string Who { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.IoC/DDD.Infra.CrossCutting.IoC.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/Commands/Command.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Domain.Core.Events; 4 | 5 | using FluentValidation.Results; 6 | 7 | namespace DDD.Domain.Core.Commands; 8 | 9 | public abstract class Command : Message 10 | { 11 | public DateTime Timestamp { get; private set; } 12 | 13 | public ValidationResult ValidationResult { get; set; } 14 | 15 | protected Command() 16 | { 17 | Timestamp = DateTime.Now; 18 | } 19 | 20 | public abstract bool IsValid(); 21 | } 22 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Office/ExcelFormat.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Domain.Providers.Office; 2 | 3 | public class ExcelFormat 4 | { 5 | public string ColId { get; set; } 6 | 7 | public string ColName { get; set; } 8 | 9 | public int ColIndex { get; set; } 10 | 11 | public int Width { get; set; } 12 | 13 | public bool IsBold { get; set; } 14 | 15 | public bool IsCurrency { get; set; } 16 | 17 | public bool IsDate { get; set; } 18 | 19 | public bool IsHide { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /Src/DDD.Domain/DDD.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Authorization/ClaimRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Authorization; 4 | 5 | public class ClaimRequirement : IAuthorizationRequirement 6 | { 7 | public ClaimRequirement(string claimName, string claimValue) 8 | { 9 | ClaimName = claimName; 10 | ClaimValue = claimValue; 11 | } 12 | 13 | public string ClaimName { get; set; } 14 | 15 | public string ClaimValue { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Commands/RemoveCustomerCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Domain.Validations; 4 | 5 | namespace DDD.Domain.Commands; 6 | 7 | public class RemoveCustomerCommand : CustomerCommand 8 | { 9 | public RemoveCustomerCommand(Guid id) 10 | { 11 | Id = id; 12 | AggregateId = id; 13 | } 14 | 15 | public override bool IsValid() 16 | { 17 | ValidationResult = new RemoveCustomerCommandValidation().Validate(this); 18 | return ValidationResult.IsValid; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/AccountViewModels/LoginViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Models.AccountViewModels; 4 | 5 | public class LoginViewModel 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string Email { get; set; } 10 | 11 | [Required] 12 | [DataType(DataType.Password)] 13 | public string Password { get; set; } 14 | 15 | [Display(Name = "Remember me?")] 16 | public bool RememberMe { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Crons/ICronProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | using Quartz; 5 | 6 | namespace DDD.Domain.Providers.Crons; 7 | 8 | public interface ICronProvider 9 | { 10 | // Fire and Forget, One-Off Job 11 | // https://www.quartz-scheduler.net/documentation/quartz-3.x/how-tos/one-off-job.html 12 | // Task NotifyInactiveUser(NotifyInactiveUserConsumerModel payload); 13 | Task OneOffJob(IDictionary jobData) 14 | where T : IJob; 15 | } 16 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Data/AuthDbContext.cs: -------------------------------------------------------------------------------- 1 | using DDD.Infra.CrossCutting.Identity.Models; 2 | 3 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace DDD.Infra.CrossCutting.Identity.Data; 7 | 8 | public class AuthDbContext : IdentityDbContext 9 | { 10 | public AuthDbContext(DbContextOptions options) 11 | : base(options) 12 | { 13 | } 14 | 15 | public DbSet RefreshTokens { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/StartupExtensions/HashingExtension.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Providers.Hash; 2 | 3 | namespace DDD.Services.Api.StartupExtensions; 4 | 5 | public static class HashingExtension 6 | { 7 | public static IServiceCollection AddCustomizedHash(this IServiceCollection services, IConfiguration configuration) 8 | { 9 | services.Configure(configuration.GetSection(HashingOptions.Hashing)); 10 | services.AddScoped(); 11 | 12 | return services; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/DDD.Services.Api.IntegrationTests/CustomWebApplicationFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.Mvc.Testing; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace DDD.Services.Api.IntegrationTests; 6 | 7 | public class CustomWebApplicationFactory 8 | : WebApplicationFactory 9 | where TStartup : class 10 | { 11 | protected override IHostBuilder CreateHostBuilder() 12 | { 13 | return base.CreateHostBuilder() 14 | .UseEnvironment("Development"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/UoW/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Interfaces; 2 | using DDD.Infra.Data.Context; 3 | 4 | namespace DDD.Infra.Data.UoW; 5 | 6 | public class UnitOfWork : IUnitOfWork 7 | { 8 | private readonly ApplicationDbContext _context; 9 | 10 | public UnitOfWork(ApplicationDbContext context) 11 | { 12 | _context = context; 13 | } 14 | 15 | public bool Commit() 16 | { 17 | return _context.SaveChanges() > 0; 18 | } 19 | 20 | public void Dispose() 21 | { 22 | _context.Dispose(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitattributes 6 | **/.gitignore 7 | **/.project 8 | **/.settings 9 | **/.toolstarget 10 | **/.vs 11 | **/.vscode 12 | **/*.*proj.user 13 | **/*.dbmdl 14 | **/*.jfm 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/obj 21 | **/*.bat 22 | **/*.log 23 | **/*.md 24 | **/*.yml 25 | **/*.yaml 26 | **/.editorconfig 27 | **/package-lock.json 28 | **/appsettings.localhost.json 29 | **/test-results 30 | **/TestResults 31 | LICENSE 32 | global.json 33 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Interfaces/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace DDD.Domain.Interfaces; 5 | 6 | public interface IRepository : IDisposable 7 | where TEntity : class 8 | { 9 | void Add(TEntity obj); 10 | 11 | TEntity GetById(Guid id); 12 | 13 | IQueryable GetAll(); 14 | 15 | IQueryable GetAll(ISpecification spec); 16 | 17 | IQueryable GetAllSoftDeleted(); 18 | 19 | void Update(TEntity obj); 20 | 21 | void Remove(Guid id); 22 | 23 | int SaveChanges(); 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/bin": true, 4 | "**/obj": true 5 | }, 6 | "[csharp]": { 7 | "editor.codeActionsOnSave": { 8 | "source.organizeImports": "explicit", 9 | "source.fixAll": "explicit", 10 | "source.sortMembers": "explicit" 11 | }, 12 | "editor.formatOnSave": true 13 | }, 14 | // "omnisharp.enableEditorConfigSupport": true, 15 | // "omnisharp.organizeImportsOnFormat": true, 16 | // "omnisharp.useEditorFormattingSettings": true, 17 | // "omnisharp.disableMSBuildDiagnosticWarning": true, 18 | } 19 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/ManageViewModels/ExternalLoginsViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | using Microsoft.AspNetCore.Authentication; 4 | using Microsoft.AspNetCore.Identity; 5 | 6 | namespace DDD.Infra.CrossCutting.Identity.Models.ManageViewModels; 7 | 8 | public class ExternalLoginsViewModel 9 | { 10 | public IList CurrentLogins { get; set; } 11 | 12 | public IList OtherLogins { get; set; } 13 | 14 | public bool ShowRemoveButton { get; set; } 15 | 16 | public string StatusMessage { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "indentation": { 5 | "indentationSize": 4, 6 | "tabSize": 4, 7 | "useTabs": false 8 | }, 9 | "orderingRules": { 10 | "usingDirectivesPlacement": "outsideNamespace", 11 | "blankLinesBetweenUsingGroups": "allow", 12 | "systemUsingDirectivesFirst": true 13 | }, 14 | "documentationRules": { 15 | "xmlHeader": false 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/Configurations/AddRequiredHeaderParameter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Models; 2 | 3 | using Swashbuckle.AspNetCore.SwaggerGen; 4 | 5 | namespace DDD.Services.Api.Configurations; 6 | 7 | public class AddRequiredHeaderParameter : IOperationFilter 8 | { 9 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 10 | { 11 | operation.Parameters.Add(new OpenApiParameter 12 | { 13 | Name = "YourCustomHeader", 14 | In = ParameterLocation.Header, 15 | Required = true, 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/ManageViewModels/IndexViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Models.ManageViewModels; 4 | 5 | public class IndexViewModel 6 | { 7 | public string Username { get; set; } 8 | 9 | public bool IsEmailConfirmed { get; set; } 10 | 11 | [Required] 12 | [EmailAddress] 13 | public string Email { get; set; } 14 | 15 | [Phone] 16 | [Display(Name = "Phone number")] 17 | public string PhoneNumber { get; set; } 18 | 19 | public string StatusMessage { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "JwtIssuerOptions": { 3 | "Issuer": "webApi", 4 | "Audience": "http://localhost:5000/" 5 | }, 6 | "SecretKey": "iNivDmHLpUA223sqsfhqGbMRdRj1PVkH", 7 | "HttpClients": { 8 | "Foo": "https://YOUR_ANOTHER_SERVICE/" 9 | }, 10 | "Webhook": { 11 | "Slack": "" 12 | }, 13 | "Hashing": { 14 | "Iterations": 10000 15 | }, 16 | "Logging": { 17 | "LogLevel": { 18 | "Default": "Debug", 19 | "System": "Information", 20 | "Microsoft": "Information", 21 | "Microsoft.AspNetCore": "Warning" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Commands/RegisterNewCustomerCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Domain.Validations; 4 | 5 | namespace DDD.Domain.Commands; 6 | 7 | public class RegisterNewCustomerCommand : CustomerCommand 8 | { 9 | public RegisterNewCustomerCommand(string name, string email, DateTime birthDate) 10 | { 11 | Name = name; 12 | Email = email; 13 | BirthDate = birthDate; 14 | } 15 | 16 | public override bool IsValid() 17 | { 18 | ValidationResult = new RegisterNewCustomerCommandValidation().Validate(this); 19 | return ValidationResult.IsValid; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Repository/CustomerRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | using DDD.Domain.Interfaces; 4 | using DDD.Domain.Models; 5 | using DDD.Infra.Data.Context; 6 | 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace DDD.Infra.Data.Repository; 10 | 11 | public class CustomerRepository : Repository, ICustomerRepository 12 | { 13 | public CustomerRepository(ApplicationDbContext context) 14 | : base(context) 15 | { 16 | } 17 | 18 | public Customer GetByEmail(string email) 19 | { 20 | return _dbSet.AsNoTracking().FirstOrDefault(c => c.Email == email); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Commands/UpdateCustomerCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Domain.Validations; 4 | 5 | namespace DDD.Domain.Commands; 6 | 7 | public class UpdateCustomerCommand : CustomerCommand 8 | { 9 | public UpdateCustomerCommand(Guid id, string name, string email, DateTime birthDate) 10 | { 11 | Id = id; 12 | Name = name; 13 | Email = email; 14 | BirthDate = birthDate; 15 | } 16 | 17 | public override bool IsValid() 18 | { 19 | ValidationResult = new UpdateCustomerCommandValidation().Validate(this); 20 | return ValidationResult.IsValid; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Extensions/EmailSenderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Encodings.Web; 2 | using System.Threading.Tasks; 3 | 4 | using DDD.Infra.CrossCutting.Identity.Services; 5 | 6 | namespace DDD.Infra.CrossCutting.Identity.Extensions; 7 | 8 | public static class EmailSenderExtensions 9 | { 10 | public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link) 11 | { 12 | return emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking this link: link"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Mappings/StoredEventMap.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Core.Events; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace DDD.Infra.Data.Mappings; 7 | 8 | public class StoredEventMap : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.Property(c => c.Timestamp) 13 | .HasColumnName("CreationDate"); 14 | 15 | builder.Property(c => c.MessageType) 16 | .HasColumnName("Action") 17 | .HasColumnType("varchar(100)"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Models/Customer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Domain.Core.Models; 4 | 5 | namespace DDD.Domain.Models; 6 | 7 | public class Customer : EntityAudit 8 | { 9 | public Customer(Guid id, string name, string email, DateTime birthDate) 10 | { 11 | Id = id; 12 | Name = name; 13 | Email = email; 14 | BirthDate = birthDate; 15 | } 16 | 17 | // Empty constructor for EF 18 | protected Customer() 19 | { 20 | } 21 | 22 | public string Name { get; private set; } 23 | 24 | public string Email { get; private set; } 25 | 26 | public DateTime BirthDate { get; private set; } 27 | } 28 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/Notifications/DomainNotification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Domain.Core.Events; 4 | 5 | namespace DDD.Domain.Core.Notifications; 6 | 7 | public class DomainNotification : Event 8 | { 9 | public Guid DomainNotificationId { get; private set; } 10 | 11 | public string Key { get; private set; } 12 | 13 | public string Value { get; private set; } 14 | 15 | public int Version { get; private set; } 16 | 17 | public DomainNotification(string key, string value) 18 | { 19 | DomainNotificationId = Guid.NewGuid(); 20 | Version = 1; 21 | Key = key; 22 | Value = value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/Events/StoredEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DDD.Domain.Core.Events; 4 | 5 | public class StoredEvent : Event 6 | { 7 | public StoredEvent(Event theEvent, string data, string user) 8 | { 9 | Id = Guid.NewGuid(); 10 | AggregateId = theEvent.AggregateId; 11 | MessageType = theEvent.MessageType; 12 | Data = data; 13 | User = user; 14 | } 15 | 16 | // EF Constructor 17 | protected StoredEvent() 18 | { 19 | } 20 | 21 | public Guid Id { get; private set; } 22 | 23 | public string Data { get; private set; } 24 | 25 | public string User { get; private set; } 26 | } 27 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Events/CustomerUpdatedEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Domain.Core.Events; 4 | 5 | namespace DDD.Domain.Events; 6 | 7 | public class CustomerUpdatedEvent : Event 8 | { 9 | public CustomerUpdatedEvent(Guid id, string name, string email, DateTime birthDate) 10 | { 11 | Id = id; 12 | Name = name; 13 | Email = email; 14 | BirthDate = birthDate; 15 | AggregateId = id; 16 | } 17 | 18 | public Guid Id { get; set; } 19 | 20 | public string Name { get; private set; } 21 | 22 | public string Email { get; private set; } 23 | 24 | public DateTime BirthDate { get; private set; } 25 | } 26 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/StartupExtensions/SignalRExtension.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Services.Api.StartupExtensions; 2 | 3 | public static class SignalRExtension 4 | { 5 | public static IServiceCollection AddCustomizedSignalR(this IServiceCollection services) 6 | { 7 | services.AddSignalR(options => 8 | { 9 | options.ClientTimeoutInterval = TimeSpan.FromMinutes(30); 10 | options.KeepAliveInterval = TimeSpan.FromMinutes(15); 11 | }); 12 | return services; 13 | } 14 | 15 | public static IApplicationBuilder UseCustomizedSignalR(this IApplicationBuilder app) 16 | { 17 | return app; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Mail/MailMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace DDD.Domain.Providers.Mail; 4 | 5 | public class MailMessage 6 | { 7 | public MailAddress From { get; set; } 8 | 9 | public List To { get; set; } 10 | 11 | public List Cc { get; set; } 12 | 13 | public List Bcc { get; set; } 14 | 15 | public List Attachments { get; set; } 16 | 17 | public string Subject { get; set; } 18 | 19 | public string PlainTextContent { get; set; } 20 | 21 | public string TemplateId { get; set; } 22 | 23 | public object DynamicTemplateData { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/DDD.Infra.CrossCutting.Identity.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Events/CustomerRegisteredEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Domain.Core.Events; 4 | 5 | namespace DDD.Domain.Events; 6 | 7 | public class CustomerRegisteredEvent : Event 8 | { 9 | public CustomerRegisteredEvent(Guid id, string name, string email, DateTime birthDate) 10 | { 11 | Id = id; 12 | Name = name; 13 | Email = email; 14 | BirthDate = birthDate; 15 | AggregateId = id; 16 | } 17 | 18 | public Guid Id { get; set; } 19 | 20 | public string Name { get; private set; } 21 | 22 | public string Email { get; private set; } 23 | 24 | public DateTime BirthDate { get; private set; } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/DDD.Application.UnitTests/DDD.Application.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | runtime; build; native; contentfiles; analyzers; buildtransitive 9 | all 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Src/DDD.Application/AutoMapper/ViewModelToDomainMappingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | using DDD.Application.ViewModels; 4 | using DDD.Domain.Commands; 5 | 6 | namespace DDD.Application.AutoMapper; 7 | 8 | public class ViewModelToDomainMappingProfile : Profile 9 | { 10 | public ViewModelToDomainMappingProfile() 11 | { 12 | CreateMap() 13 | .ConstructUsing(c => new RegisterNewCustomerCommand(c.Name, c.Email, c.BirthDate)); 14 | CreateMap() 15 | .ConstructUsing(c => new UpdateCustomerCommand(c.Id, c.Name, c.Email, c.BirthDate)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/RefreshToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace DDD.Infra.CrossCutting.Identity.Models; 5 | 6 | public class RefreshToken 7 | { 8 | [Key] 9 | public string Token { get; set; } 10 | 11 | public string JwtId { get; set; } 12 | 13 | public DateTime CreationDate { get; set; } 14 | 15 | public DateTime ExpiryDate { get; set; } 16 | 17 | public bool Used { get; set; } 18 | 19 | public bool Invalidated { get; set; } 20 | 21 | public string UserId { get; set; } 22 | 23 | // [ForeignKey(nameof(UserId))] 24 | // public ApplicationUser User { get; set; } 25 | } 26 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Interfaces/ISpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | 5 | namespace DDD.Domain.Interfaces; 6 | 7 | public interface ISpecification 8 | { 9 | Expression> Criteria { get; } 10 | 11 | List>> Includes { get; } 12 | 13 | List IncludeStrings { get; } 14 | 15 | Expression> OrderBy { get; } 16 | 17 | Expression> OrderByDescending { get; } 18 | 19 | Expression> GroupBy { get; } 20 | 21 | int Take { get; } 22 | 23 | int Skip { get; } 24 | 25 | bool IsPagingEnabled { get; } 26 | } 27 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/AccountViewModels/LoginWith2faViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Models.AccountViewModels; 4 | 5 | public class LoginWith2faViewModel 6 | { 7 | [Required] 8 | [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 9 | [DataType(DataType.Text)] 10 | [Display(Name = "Authenticator code")] 11 | public string TwoFactorCode { get; set; } 12 | 13 | [Display(Name = "Remember this machine")] 14 | public bool RememberMachine { get; set; } 15 | 16 | public bool RememberMe { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Context/EventStoreSQLContext.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Core.Events; 2 | using DDD.Infra.Data.Mappings; 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace DDD.Infra.Data.Context; 7 | 8 | public class EventStoreSqlContext : DbContext 9 | { 10 | public EventStoreSqlContext(DbContextOptions options) 11 | : base(options) 12 | { 13 | } 14 | 15 | public DbSet StoredEvent { get; set; } 16 | 17 | protected override void OnModelCreating(ModelBuilder modelBuilder) 18 | { 19 | modelBuilder.ApplyConfiguration(new StoredEventMap()); 20 | 21 | base.OnModelCreating(modelBuilder); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/ManageViewModels/EnableAuthenticatorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace DDD.Infra.CrossCutting.Identity.Models.ManageViewModels; 5 | 6 | public class EnableAuthenticatorViewModel 7 | { 8 | [Required] 9 | [StringLength(10, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 10 | [DataType(DataType.Text)] 11 | [Display(Name = "Verification Code")] 12 | public string Code { get; set; } 13 | 14 | [ReadOnly(true)] 15 | public string SharedKey { get; set; } 16 | 17 | public string AuthenticatorUri { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/DDD.Services.Api.IntegrationTests/DDD.Services.Api.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Src/DDD.Application/Interfaces/ICustomerAppService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using DDD.Application.EventSourcedNormalizers; 5 | using DDD.Application.ViewModels; 6 | 7 | namespace DDD.Application.Interfaces; 8 | 9 | public interface ICustomerAppService : IDisposable 10 | { 11 | void Register(CustomerViewModel customerViewModel); 12 | 13 | IEnumerable GetAll(); 14 | 15 | IEnumerable GetAll(int skip, int take); 16 | 17 | CustomerViewModel GetById(Guid id); 18 | 19 | void Update(CustomerViewModel customerViewModel); 20 | 21 | void Remove(Guid id); 22 | 23 | IList GetAllHistory(Guid id); 24 | } 25 | -------------------------------------------------------------------------------- /Src/DDD.Application/AutoMapper/AutoMapperConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DDD.Application.AutoMapper; 4 | 5 | public class AutoMapperConfig 6 | { 7 | // public static MapperConfiguration RegisterMappings() 8 | // { 9 | // return new MapperConfiguration(cfg => 10 | // { 11 | // cfg.AddProfile(new DomainToViewModelMappingProfile()); 12 | // cfg.AddProfile(new ViewModelToDomainMappingProfile()); 13 | // }); 14 | // } 15 | 16 | public static Type[] RegisterMappings() 17 | { 18 | return new Type[] 19 | { 20 | typeof(DomainToViewModelMappingProfile), 21 | typeof(ViewModelToDomainMappingProfile), 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Authorization/ClaimsRequirementHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | 4 | using Microsoft.AspNetCore.Authorization; 5 | 6 | namespace DDD.Infra.CrossCutting.Identity.Authorization; 7 | 8 | public class ClaimsRequirementHandler : AuthorizationHandler 9 | { 10 | protected override Task HandleRequirementAsync( 11 | AuthorizationHandlerContext context, 12 | ClaimRequirement requirement) 13 | { 14 | var claim = context.User.Claims.FirstOrDefault(c => c.Type == requirement.ClaimName); 15 | if (claim != null && claim.Value.Contains(requirement.ClaimValue)) 16 | { 17 | context.Succeed(requirement); 18 | } 19 | 20 | return Task.CompletedTask; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/ManageViewModels/SetPasswordViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Models.ManageViewModels; 4 | 5 | public class SetPasswordViewModel 6 | { 7 | [Required] 8 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 9 | [DataType(DataType.Password)] 10 | [Display(Name = "New password")] 11 | public string NewPassword { get; set; } 12 | 13 | [DataType(DataType.Password)] 14 | [Display(Name = "Confirm new password")] 15 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 16 | public string ConfirmPassword { get; set; } 17 | 18 | public string StatusMessage { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/AspNetUser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Security.Claims; 3 | 4 | using DDD.Domain.Interfaces; 5 | 6 | using Microsoft.AspNetCore.Http; 7 | 8 | namespace DDD.Infra.CrossCutting.Identity.Models; 9 | 10 | public class AspNetUser : IUser 11 | { 12 | private readonly IHttpContextAccessor _accessor; 13 | 14 | public AspNetUser(IHttpContextAccessor accessor) 15 | { 16 | _accessor = accessor; 17 | } 18 | 19 | public string Name => _accessor.HttpContext.User.Identity.Name; 20 | 21 | public bool IsAuthenticated() 22 | { 23 | return _accessor.HttpContext.User.Identity.IsAuthenticated; 24 | } 25 | 26 | public IEnumerable GetClaimsIdentity() 27 | { 28 | return _accessor.HttpContext.User.Claims; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Webhooks/WebhookProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Net.Http.Json; 3 | using System.Threading.Tasks; 4 | 5 | using Microsoft.Extensions.Configuration; 6 | 7 | namespace DDD.Domain.Providers.Webhooks; 8 | 9 | public class WebhookProvider : IWebhookProvider 10 | { 11 | private readonly IConfiguration _configuration; 12 | 13 | public WebhookProvider(IConfiguration configuration) 14 | { 15 | _configuration = configuration; 16 | } 17 | 18 | public async Task Send(string message) 19 | { 20 | var client = new HttpClient(); 21 | var uri = _configuration.GetValue("Webhook:Slack"); 22 | if (string.IsNullOrEmpty(uri)) 23 | { 24 | return; 25 | } 26 | 27 | await client.PostAsJsonAsync(uri, new { text = message }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/AccountViewModels/ResetPasswordViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Models.AccountViewModels; 4 | 5 | public class ResetPasswordViewModel 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string Email { get; set; } 10 | 11 | [Required] 12 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 13 | [DataType(DataType.Password)] 14 | public string Password { get; set; } 15 | 16 | [DataType(DataType.Password)] 17 | [Display(Name = "Confirm password")] 18 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 19 | public string ConfirmPassword { get; set; } 20 | 21 | public string Code { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/AccountViewModels/RegisterViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Models.AccountViewModels; 4 | 5 | public class RegisterViewModel 6 | { 7 | [Required] 8 | [EmailAddress] 9 | [Display(Name = "Email")] 10 | public string Email { get; set; } 11 | 12 | [Required] 13 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 14 | [DataType(DataType.Password)] 15 | [Display(Name = "Password")] 16 | public string Password { get; set; } 17 | 18 | [DataType(DataType.Password)] 19 | [Display(Name = "Confirm password")] 20 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 21 | public string ConfirmPassword { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /manifests/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: webapi 5 | labels: 6 | app: aspnetcore-ddd 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | service: webapi 12 | template: 13 | metadata: 14 | labels: 15 | app: aspnetcore-ddd 16 | service: webapi 17 | spec: 18 | containers: 19 | - name: webapi 20 | # image: aspnetcoreddd.azurecr.io/aspnetcoredddwebapi:#{Build.BuildId}# 21 | image: aspnetcoreddd.azurecr.io/aspnetcoredddwebapi:latest 22 | imagePullPolicy: Always 23 | ports: 24 | - containerPort: 80 25 | protocol: TCP 26 | # env: 27 | # - name: ASPNETCORE_ENVIRONMENT 28 | # value: Development 29 | # - name: ASPNETCORE_URLS 30 | # value: https://+:443;http://+:80 31 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Hubs/Notification/NotificationHub.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Microsoft.AspNetCore.SignalR; 4 | 5 | namespace DDD.Domain.Providers.Hubs; 6 | 7 | public class NotificationHub : Hub 8 | { 9 | public Task Send(NotificationItem item) 10 | { 11 | var groupName = $"{nameof(NotificationItem.UserId)}_{item.UserId}"; 12 | return Clients.Group(groupName).Send(item); 13 | } 14 | 15 | public async Task JoinGroup(string groupName) 16 | { 17 | await Groups.AddToGroupAsync(Context.ConnectionId, groupName); 18 | await Clients.Group(groupName).JoinGroup(groupName); 19 | } 20 | 21 | public async Task LeaveGroup(string groupName) 22 | { 23 | await Clients.Group(groupName).LeaveGroup(groupName); 24 | await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/StartupExtensions/HttpExtension.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Providers.Http; 2 | 3 | using Polly; 4 | 5 | namespace DDD.Services.Api.StartupExtensions; 6 | 7 | public static class HttpExtension 8 | { 9 | public static IServiceCollection AddCustomizedHttp(this IServiceCollection services, IConfiguration configuration) 10 | { 11 | var url = configuration.GetValue("HttpClients:Foo"); 12 | 13 | if (!string.IsNullOrEmpty(url)) 14 | { 15 | services 16 | .AddHttpClient("Foo", c => 17 | { 18 | c.BaseAddress = new Uri(url); 19 | }) 20 | .AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(5, _ => TimeSpan.FromMilliseconds(500))) 21 | .AddTypedClient(c => Refit.RestService.For(c)); 22 | } 23 | 24 | return services; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for Nuget 4 | - package-ecosystem: "nuget" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | open-pull-requests-limit: 10 9 | 10 | # Enable version updates for Docker 11 | - package-ecosystem: "docker" 12 | # Look for a `Dockerfile` in the `root` directory 13 | directory: "/" 14 | # Check for updates once a week 15 | schedule: 16 | interval: "weekly" 17 | open-pull-requests-limit: 10 18 | 19 | # Enable version updates for Github Actions 20 | - package-ecosystem: "github-actions" 21 | directory: "/" 22 | schedule: 23 | interval: "weekly" 24 | open-pull-requests-limit: 10 25 | 26 | # Enable version updates for Terraform 27 | - package-ecosystem: "terraform" 28 | directory: "/" 29 | schedule: 30 | interval: "weekly" 31 | open-pull-requests-limit: 10 32 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Mappings/CustomerMap.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Models; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace DDD.Infra.Data.Mappings; 7 | 8 | public class CustomerMap : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.Property(c => c.Id) 13 | .HasColumnName("Id"); 14 | 15 | builder.Property(c => c.Name) 16 | .HasColumnType("varchar(100)") 17 | .HasMaxLength(100) 18 | .IsRequired(); 19 | 20 | builder.Property(c => c.Email) 21 | .HasColumnType("varchar(100)") 22 | .HasMaxLength(100) 23 | .IsRequired(); 24 | 25 | // builder.HasQueryFilter(m => EF.Property(m, "IsDeleted") == false); 26 | builder.HasQueryFilter(p => !p.IsDeleted); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Http/IHttpProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | 6 | namespace DDD.Domain.Providers.Http; 7 | 8 | public interface IHttpProvider 9 | { 10 | Task GetAsync(HttpClient httpClient, string url, Dictionary queryParams = null, Dictionary headers = null); 11 | 12 | Task PostAsJsonAsync(HttpClient httpClient, string url, object data, Dictionary queryParams = null, Dictionary headers = null); 13 | 14 | Task PostAsFormUrlEncodedAsync(HttpClient httpClient, string url, Dictionary data, Dictionary queryParams = null, Dictionary headers = null); 15 | 16 | Task GetStreamAsync(HttpClient httpClient, string url, Dictionary queryParams = null, Dictionary headers = null); 17 | } 18 | -------------------------------------------------------------------------------- /Src/DDD.Application/ViewModels/CustomerViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace DDD.Application.ViewModels; 6 | 7 | public class CustomerViewModel 8 | { 9 | [Key] 10 | public Guid Id { get; set; } 11 | 12 | [Required(ErrorMessage = "The Name is Required")] 13 | [MinLength(2)] 14 | [MaxLength(100)] 15 | [DisplayName("Name")] 16 | public string Name { get; set; } 17 | 18 | [Required(ErrorMessage = "The E-mail is Required")] 19 | [EmailAddress] 20 | [DisplayName("E-mail")] 21 | public string Email { get; set; } 22 | 23 | [Required(ErrorMessage = "The BirthDate is Required")] 24 | [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy-MM-dd}")] 25 | [DataType(DataType.Date, ErrorMessage = "Data em formato inválido")] 26 | [DisplayName("Birth Date")] 27 | public DateTime BirthDate { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/EventSourcing/SqlEventStore.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | using DDD.Domain.Core.Events; 4 | using DDD.Domain.Interfaces; 5 | using DDD.Infra.Data.Repository.EventSourcing; 6 | 7 | namespace DDD.Infra.Data.EventSourcing; 8 | 9 | public class SqlEventStore : IEventStore 10 | { 11 | private readonly IEventStoreRepository _eventStoreRepository; 12 | private readonly IUser _user; 13 | 14 | public SqlEventStore(IEventStoreRepository eventStoreRepository, IUser user) 15 | { 16 | _eventStoreRepository = eventStoreRepository; 17 | _user = user; 18 | } 19 | 20 | public void Save(T theEvent) 21 | where T : Event 22 | { 23 | var serializedData = JsonSerializer.Serialize(theEvent); 24 | 25 | var storedEvent = new StoredEvent( 26 | theEvent, 27 | serializedData, 28 | _user.Name); 29 | 30 | _eventStoreRepository.Store(storedEvent); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Repository/EventSourcing/EventStoreSQLRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using DDD.Domain.Core.Events; 6 | using DDD.Infra.Data.Context; 7 | 8 | namespace DDD.Infra.Data.Repository.EventSourcing; 9 | 10 | public class EventStoreSqlRepository : IEventStoreRepository 11 | { 12 | private readonly EventStoreSqlContext _context; 13 | 14 | public EventStoreSqlRepository(EventStoreSqlContext context) 15 | { 16 | _context = context; 17 | } 18 | 19 | public IList All(Guid aggregateId) 20 | { 21 | return (from e in _context.StoredEvent where e.AggregateId == aggregateId select e).ToList(); 22 | } 23 | 24 | public void Store(StoredEvent theEvent) 25 | { 26 | _context.StoredEvent.Add(theEvent); 27 | _context.SaveChanges(); 28 | } 29 | 30 | public void Dispose() 31 | { 32 | _context.Dispose(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Models/ManageViewModels/ChangePasswordViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace DDD.Infra.CrossCutting.Identity.Models.ManageViewModels; 4 | 5 | public class ChangePasswordViewModel 6 | { 7 | [Required] 8 | [DataType(DataType.Password)] 9 | [Display(Name = "Current password")] 10 | public string OldPassword { get; set; } 11 | 12 | [Required] 13 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 14 | [DataType(DataType.Password)] 15 | [Display(Name = "New password")] 16 | public string NewPassword { get; set; } 17 | 18 | [DataType(DataType.Password)] 19 | [Display(Name = "Confirm new password")] 20 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 21 | public string ConfirmPassword { get; set; } 22 | 23 | public string StatusMessage { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/Models/ValueObject.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.Domain.Core.Models; 2 | 3 | public abstract class ValueObject 4 | where T : ValueObject 5 | { 6 | public override bool Equals(object obj) 7 | { 8 | return obj is T valueObject && EqualsCore(valueObject); 9 | } 10 | 11 | public override int GetHashCode() 12 | { 13 | return GetHashCodeCore(); 14 | } 15 | 16 | protected abstract int GetHashCodeCore(); 17 | 18 | protected abstract bool EqualsCore(T other); 19 | 20 | public static bool operator ==(ValueObject a, ValueObject b) 21 | { 22 | if (a is null && b is null) 23 | { 24 | return true; 25 | } 26 | 27 | if (a is null || b is null) 28 | { 29 | return false; 30 | } 31 | 32 | return a.Equals(b); 33 | } 34 | 35 | public static bool operator !=(ValueObject a, ValueObject b) 36 | { 37 | return !(a == b); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Bus/InMemoryBus.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using DDD.Domain.Core.Bus; 4 | using DDD.Domain.Core.Commands; 5 | using DDD.Domain.Core.Events; 6 | 7 | using MediatR; 8 | 9 | namespace DDD.Infra.CrossCutting.Bus; 10 | 11 | public sealed class InMemoryBus : IMediatorHandler 12 | { 13 | private readonly IMediator _mediator; 14 | private readonly IEventStore _eventStore; 15 | 16 | public InMemoryBus(IEventStore eventStore, IMediator mediator) 17 | { 18 | _eventStore = eventStore; 19 | _mediator = mediator; 20 | } 21 | 22 | public Task SendCommand(T command) 23 | where T : Command 24 | { 25 | return _mediator.Send(command); 26 | } 27 | 28 | public Task RaiseEvent(T @event) 29 | where T : Event 30 | { 31 | if (!@event.MessageType.Equals("DomainNotification")) 32 | { 33 | _eventStore?.Save(@event); 34 | } 35 | 36 | return _mediator.Publish(@event); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Src/DDD.Domain/EventHandlers/CustomerEventHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | using DDD.Domain.Events; 5 | 6 | using MediatR; 7 | 8 | namespace DDD.Domain.EventHandlers; 9 | 10 | public class CustomerEventHandler : 11 | INotificationHandler, 12 | INotificationHandler, 13 | INotificationHandler 14 | { 15 | public Task Handle(CustomerUpdatedEvent message, CancellationToken cancellationToken) 16 | { 17 | // Send some notification e-mail 18 | 19 | return Task.CompletedTask; 20 | } 21 | 22 | public Task Handle(CustomerRegisteredEvent message, CancellationToken cancellationToken) 23 | { 24 | // Send some greetings e-mail 25 | 26 | return Task.CompletedTask; 27 | } 28 | 29 | public Task Handle(CustomerRemovedEvent message, CancellationToken cancellationToken) 30 | { 31 | // Send some see you soon e-mail 32 | 33 | return Task.CompletedTask; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/Notifications/DomainNotificationHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | using MediatR; 7 | 8 | namespace DDD.Domain.Core.Notifications; 9 | 10 | public class DomainNotificationHandler : INotificationHandler 11 | { 12 | private List _notifications; 13 | 14 | public DomainNotificationHandler() 15 | { 16 | _notifications = new List(); 17 | } 18 | 19 | public Task Handle(DomainNotification message, CancellationToken cancellationToken) 20 | { 21 | _notifications.Add(message); 22 | 23 | return Task.CompletedTask; 24 | } 25 | 26 | public virtual List GetNotifications() 27 | { 28 | return _notifications; 29 | } 30 | 31 | public virtual bool HasNotifications() 32 | { 33 | return GetNotifications().Any(); 34 | } 35 | 36 | public void Dispose() 37 | { 38 | _notifications = new List(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Migrations/20161213130431_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | namespace DDD.Infra.Data.Migrations; 6 | 7 | public partial class Initial : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Customers", 13 | columns: table => new 14 | { 15 | Id = table.Column(nullable: false), 16 | BirthDate = table.Column(nullable: false), 17 | Email = table.Column(type: "varchar(100)", maxLength: 11, nullable: false), 18 | Name = table.Column(type: "varchar(100)", maxLength: 100, nullable: false), 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Customers", x => x.Id); 23 | }); 24 | } 25 | 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.DropTable( 29 | name: "Customers"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Xinh Nguyen 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 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | 5 | enable 6 | true 7 | 8 | 9 | 11 | $(SolutionDir)StyleCop.ruleset 12 | 13 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Migrations/EventStoreSQL/20161213130520_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | namespace DDD.Infra.Data.Migrations.EventStoreSql; 6 | 7 | public partial class Initial : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "StoredEvent", 13 | columns: table => new 14 | { 15 | Id = table.Column(nullable: false), 16 | AggregateId = table.Column(nullable: false), 17 | Data = table.Column(nullable: true), 18 | Action = table.Column(type: "varchar(100)", nullable: true), 19 | CreationDate = table.Column(nullable: false), 20 | User = table.Column(nullable: true), 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_StoredEvent", x => x.Id); 25 | }); 26 | } 27 | 28 | protected override void Down(MigrationBuilder migrationBuilder) 29 | { 30 | migrationBuilder.DropTable( 31 | name: "StoredEvent"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Validations/CustomerValidation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Domain.Commands; 4 | 5 | using FluentValidation; 6 | 7 | namespace DDD.Domain.Validations; 8 | 9 | public abstract class CustomerValidation : AbstractValidator 10 | where T : CustomerCommand 11 | { 12 | protected static bool HaveMinimumAge(DateTime birthDate) 13 | { 14 | return birthDate <= DateTime.Now.AddYears(-18); 15 | } 16 | 17 | protected void ValidateName() 18 | { 19 | RuleFor(c => c.Name) 20 | .NotEmpty().WithMessage("Please ensure you have entered the Name") 21 | .Length(2, 150).WithMessage("The Name must have between 2 and 150 characters"); 22 | } 23 | 24 | protected void ValidateBirthDate() 25 | { 26 | RuleFor(c => c.BirthDate) 27 | .NotEmpty() 28 | .Must(HaveMinimumAge) 29 | .WithMessage("The customer must have 18 years or more"); 30 | } 31 | 32 | protected void ValidateEmail() 33 | { 34 | RuleFor(c => c.Email) 35 | .NotEmpty() 36 | .EmailAddress(); 37 | } 38 | 39 | protected void ValidateId() 40 | { 41 | RuleFor(c => c.Id) 42 | .NotEqual(Guid.Empty); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/Configurations/AutoMapperSetup.cs: -------------------------------------------------------------------------------- 1 | using DDD.Application.AutoMapper; 2 | 3 | namespace DDD.Services.Api.Configurations; 4 | 5 | public static class AutoMapperSetup 6 | { 7 | public static void AddAutoMapperSetup(this IServiceCollection services) 8 | { 9 | if (services == null) 10 | { 11 | throw new ArgumentNullException(nameof(services)); 12 | } 13 | 14 | services.AddAutoMapper(AutoMapperConfig.RegisterMappings()); 15 | 16 | // services.AddAutoMapper(typeof(Startup)); 17 | // services.AddAutoMapper(Assembly.GetAssembly(this.GetType())); 18 | // services.AddAutoMapper(Assembly.GetExecutingAssembly()); 19 | // services.AddAutoMapper(typeof(DomainToViewModelMappingProfile), typeof(ViewModelToDomainMappingProfile)); 20 | // services.AddAutoMapper(cfg => 21 | // { 22 | // cfg.AddProfile(new DomainToViewModelMappingProfile()); 23 | // cfg.AddProfile(new ViewModelToDomainMappingProfile()); 24 | // }, Assembly.GetExecutingAssembly()); 25 | 26 | // Registering Mappings automatically only works if the 27 | // Automapper Profile classes are in ASP.NET project 28 | // AutoMapperConfig.RegisterMappings(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Src/DDD.Domain.Core/Models/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DDD.Domain.Core.Models; 4 | 5 | public abstract class Entity 6 | { 7 | public Guid Id { get; protected set; } 8 | 9 | public bool IsDeleted { get; set; } 10 | 11 | public override bool Equals(object obj) 12 | { 13 | var compareTo = obj as Entity; 14 | 15 | if (ReferenceEquals(this, compareTo)) 16 | { 17 | return true; 18 | } 19 | 20 | if (compareTo is null) 21 | { 22 | return false; 23 | } 24 | 25 | return Id.Equals(compareTo.Id); 26 | } 27 | 28 | public static bool operator ==(Entity a, Entity b) 29 | { 30 | if (a is null && b is null) 31 | { 32 | return true; 33 | } 34 | 35 | if (a is null || b is null) 36 | { 37 | return false; 38 | } 39 | 40 | return a.Equals(b); 41 | } 42 | 43 | public static bool operator !=(Entity a, Entity b) 44 | { 45 | return !(a == b); 46 | } 47 | 48 | public override int GetHashCode() 49 | { 50 | return (GetType().GetHashCode() * 907) + Id.GetHashCode(); 51 | } 52 | 53 | public override string ToString() 54 | { 55 | return GetType().Name + " [Id=" + Id + "]"; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/Controllers/v1/RoleController.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Core.Bus; 2 | using DDD.Domain.Core.Notifications; 3 | using DDD.Infra.CrossCutting.Identity.Models.RoleViewModels; 4 | 5 | using MediatR; 6 | 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.AspNetCore.Mvc; 9 | 10 | namespace DDD.Services.Api.Controllers.V1; 11 | 12 | [ApiVersion("1.0")] 13 | public class RoleController : ApiController 14 | { 15 | private readonly RoleManager _roleManager; 16 | 17 | public RoleController( 18 | RoleManager roleManager, 19 | INotificationHandler notifications, 20 | IMediatorHandler mediator) 21 | : base(notifications, mediator) 22 | { 23 | _roleManager = roleManager; 24 | } 25 | 26 | [HttpPost] 27 | public async Task Create(CreateViewModel model) 28 | { 29 | if (!ModelState.IsValid) 30 | { 31 | NotifyModelStateErrors(); 32 | return Response(model); 33 | } 34 | 35 | // Add Role 36 | var role = new IdentityRole(model.Name); 37 | await _roleManager.CreateAsync(role); 38 | 39 | // Add RoleClaims 40 | // var roleClaim = new Claim("Customers", "Write"); 41 | // await _roleManager.AddClaimAsync(role, roleClaim); 42 | 43 | return Response(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Src/DDD.Domain/CommandHandlers/CommandHandler.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Core.Bus; 2 | using DDD.Domain.Core.Commands; 3 | using DDD.Domain.Core.Notifications; 4 | using DDD.Domain.Interfaces; 5 | 6 | using MediatR; 7 | 8 | namespace DDD.Domain.CommandHandlers; 9 | 10 | public class CommandHandler 11 | { 12 | private readonly IUnitOfWork _uow; 13 | private readonly IMediatorHandler _bus; 14 | private readonly DomainNotificationHandler _notifications; 15 | 16 | public CommandHandler(IUnitOfWork uow, IMediatorHandler bus, INotificationHandler notifications) 17 | { 18 | _uow = uow; 19 | _notifications = (DomainNotificationHandler)notifications; 20 | _bus = bus; 21 | } 22 | 23 | public bool Commit() 24 | { 25 | if (_notifications.HasNotifications()) 26 | { 27 | return false; 28 | } 29 | 30 | if (_uow.Commit()) 31 | { 32 | return true; 33 | } 34 | 35 | _bus.RaiseEvent(new DomainNotification("Commit", "We had a problem during saving your data.")); 36 | return false; 37 | } 38 | 39 | protected void NotifyValidationErrors(Command message) 40 | { 41 | foreach (var error in message.ValidationResult.Errors) 42 | { 43 | _bus.RaiseEvent(new DomainNotification(message.MessageType, error.ErrorMessage)); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/DDD.Services.Api.IntegrationTests/Controllers/WeatherForecastControllerIntegrationTests.cs.txt: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | 4 | using FluentAssertions; 5 | 6 | using Microsoft.AspNetCore.Mvc.Testing; 7 | 8 | using Xunit; 9 | 10 | namespace DDD.Services.Api.IntegrationTests.IntegrationTests.Controllers; 11 | 12 | public class WeatherForecastControllerIntegrationTests : IClassFixture> 13 | { 14 | private readonly HttpClient _client; 15 | private readonly CustomWebApplicationFactory _factory; 16 | 17 | public WeatherForecastControllerIntegrationTests(CustomWebApplicationFactory factory) 18 | { 19 | _factory = factory; 20 | _client = _factory.CreateClient(new WebApplicationFactoryClientOptions 21 | { 22 | AllowAutoRedirect = false, 23 | }); 24 | } 25 | 26 | [Theory] 27 | [InlineData("/WeatherForecast/HelloWorld", "text/plain; charset=utf-8")] 28 | public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url, string expected) 29 | { 30 | // Arrange 31 | 32 | // Act 33 | var response = await _client.GetAsync(url); 34 | 35 | // Assert 36 | response.EnsureSuccessStatusCode(); // Status Code 200-299 37 | response.Content.Headers.ContentType.ToString().Should().Be(expected); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Migrations/ApplicationDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Infra.Data.Context; 4 | 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Infrastructure; 7 | using Microsoft.EntityFrameworkCore.Metadata; 8 | 9 | namespace DDD.Infra.Data.Migrations; 10 | 11 | [DbContext(typeof(ApplicationDbContext))] 12 | public partial class ApplicationDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | modelBuilder 17 | .HasAnnotation("ProductVersion", "1.1.0-rtm-22752") 18 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 19 | 20 | modelBuilder.Entity("DDD.Domain.Models.Customer", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnName("Id"); 25 | 26 | b.Property("BirthDate"); 27 | 28 | b.Property("Email") 29 | .IsRequired() 30 | .HasColumnType("varchar(100)") 31 | .HasMaxLength(11); 32 | 33 | b.Property("Name") 34 | .IsRequired() 35 | .HasColumnType("varchar(100)") 36 | .HasMaxLength(100); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.ToTable("Customers"); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Migrations/EventStoreSQL/EventStoreSQLContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DDD.Infra.Data.Context; 4 | 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Infrastructure; 7 | using Microsoft.EntityFrameworkCore.Metadata; 8 | 9 | namespace DDD.Infra.Data.Migrations.EventStoreSql; 10 | 11 | [DbContext(typeof(EventStoreSqlContext))] 12 | public partial class EventStoreSqlContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | modelBuilder 17 | .HasAnnotation("ProductVersion", "1.1.0-rtm-22752") 18 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 19 | 20 | modelBuilder.Entity("DDD.Domain.Core.Events.StoredEvent", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd(); 24 | 25 | b.Property("AggregateId"); 26 | 27 | b.Property("Data"); 28 | 29 | b.Property("MessageType") 30 | .HasColumnName("Action") 31 | .HasColumnType("varchar(100)"); 32 | 33 | b.Property("Timestamp") 34 | .HasColumnName("CreationDate"); 35 | 36 | b.Property("User"); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.ToTable("StoredEvent"); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/container.yml: -------------------------------------------------------------------------------- 1 | name: Publish container 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | tags: 8 | - "v*.*.*" 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | jobs: 14 | login: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Login to GitHub Container Registry 23 | uses: docker/login-action@v3 24 | with: 25 | registry: ghcr.io 26 | username: ${{ github.actor }} 27 | password: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | # Method 1 30 | # - 31 | # name: Docker Build and Push 32 | # continue-on-error: true 33 | # run: | 34 | # docker build . --tag ghcr.io/ntxinh/aspnetcore-ddd:latest 35 | # docker push ghcr.io/ntxinh/aspnetcore-ddd:latest 36 | 37 | # Method 2 38 | - 39 | name: Docker Build and Push by Action 40 | # continue-on-error: true 41 | uses: docker/build-push-action@v5 42 | with: 43 | push: true 44 | tags: ghcr.io/ntxinh/aspnetcore-ddd:latest 45 | 46 | # Method 3: 47 | # - 48 | # name: Docker Build and Push by Docker Compose 49 | # continue-on-error: true 50 | # run: | 51 | # docker compose build 52 | # docker compose push 53 | -------------------------------------------------------------------------------- /Tests/DDD.Application.UnitTests/Services/CustomerAppServiceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using AutoMapper; 4 | 5 | using DDD.Application.Services; 6 | using DDD.Application.ViewModels; 7 | using DDD.Domain.Interfaces; 8 | 9 | using Moq; 10 | 11 | using Xunit; 12 | 13 | namespace DDD.Application.UnitTests.Services; 14 | 15 | public class CustomerAppServiceTests 16 | { 17 | [Fact] 18 | public void GetById() 19 | { 20 | // Arrange 21 | var customer = new Domain.Models.Customer(Guid.Empty, "Alan", "alab@test.com", default); 22 | 23 | var customerRepositoryMock = new Mock(); 24 | customerRepositoryMock.Setup(x => x.GetById(customer.Id)) 25 | .Returns(customer); 26 | var mapperMock = new Mock(); 27 | mapperMock.Setup(x => x.Map(customer)).Returns(new CustomerViewModel 28 | { 29 | Id = customer.Id, 30 | Name = customer.Name, 31 | Email = customer.Email, 32 | BirthDate = customer.BirthDate, 33 | }); 34 | 35 | // Act 36 | var sut = new CustomerAppService(mapperMock.Object, customerRepositoryMock.Object, null, null); 37 | var result = sut.GetById(customer.Id); 38 | 39 | // Assert 40 | Assert.Equal(result.Id, customer.Id); 41 | Assert.Equal(result.Name, customer.Name); 42 | Assert.Equal(result.Email, customer.Email); 43 | Assert.Equal(result.BirthDate, customer.BirthDate); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # .NET Core SDK 2 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 3 | 4 | # Sets the working directory 5 | WORKDIR /app 6 | 7 | # Copy Projects 8 | #COPY *.sln . 9 | COPY Src/DDD.Application/DDD.Application.csproj ./Src/DDD.Application/ 10 | COPY Src/DDD.Domain/DDD.Domain.csproj ./Src/DDD.Domain/ 11 | COPY Src/DDD.Domain.Core/DDD.Domain.Core.csproj ./Src/DDD.Domain.Core/ 12 | COPY Src/DDD.Infra.CrossCutting.Bus/DDD.Infra.CrossCutting.Bus.csproj ./Src/DDD.Infra.CrossCutting.Bus/ 13 | COPY Src/DDD.Infra.CrossCutting.Identity/DDD.Infra.CrossCutting.Identity.csproj ./Src/DDD.Infra.CrossCutting.Identity/ 14 | COPY Src/DDD.Infra.CrossCutting.IoC/DDD.Infra.CrossCutting.IoC.csproj ./Src/DDD.Infra.CrossCutting.IoC/ 15 | COPY Src/DDD.Infra.Data/DDD.Infra.Data.csproj ./Src/DDD.Infra.Data/ 16 | COPY Src/DDD.Services.Api/DDD.Services.Api.csproj ./Src/DDD.Services.Api/ 17 | COPY Directory.Build.props ./Src 18 | COPY Directory.Packages.props ./Src 19 | 20 | # .NET Core Restore 21 | RUN dotnet restore ./Src/DDD.Services.Api/DDD.Services.Api.csproj 22 | 23 | # Copy All Files 24 | COPY Src ./Src 25 | 26 | # .NET Core Build and Publish 27 | RUN dotnet publish ./Src/DDD.Services.Api/DDD.Services.Api.csproj -c Release -o /publish 28 | 29 | # ASP.NET Core Runtime 30 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime 31 | WORKDIR /app 32 | COPY --from=build /publish ./ 33 | 34 | # Expose ports 35 | EXPOSE 80 36 | EXPOSE 443 37 | 38 | # Setup your variables before running. 39 | ARG MyEnv 40 | ENV ASPNETCORE_ENVIRONMENT $MyEnv 41 | 42 | ENTRYPOINT ["dotnet", "DDD.Services.Api.dll"] 43 | -------------------------------------------------------------------------------- /Containerfile: -------------------------------------------------------------------------------- 1 | # .NET Core SDK 2 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 3 | 4 | # Sets the working directory 5 | WORKDIR /app 6 | 7 | # Copy Projects 8 | #COPY *.sln . 9 | COPY Src/DDD.Application/DDD.Application.csproj ./Src/DDD.Application/ 10 | COPY Src/DDD.Domain/DDD.Domain.csproj ./Src/DDD.Domain/ 11 | COPY Src/DDD.Domain.Core/DDD.Domain.Core.csproj ./Src/DDD.Domain.Core/ 12 | COPY Src/DDD.Infra.CrossCutting.Bus/DDD.Infra.CrossCutting.Bus.csproj ./Src/DDD.Infra.CrossCutting.Bus/ 13 | COPY Src/DDD.Infra.CrossCutting.Identity/DDD.Infra.CrossCutting.Identity.csproj ./Src/DDD.Infra.CrossCutting.Identity/ 14 | COPY Src/DDD.Infra.CrossCutting.IoC/DDD.Infra.CrossCutting.IoC.csproj ./Src/DDD.Infra.CrossCutting.IoC/ 15 | COPY Src/DDD.Infra.Data/DDD.Infra.Data.csproj ./Src/DDD.Infra.Data/ 16 | COPY Src/DDD.Services.Api/DDD.Services.Api.csproj ./Src/DDD.Services.Api/ 17 | COPY Directory.Build.props ./Src 18 | COPY Directory.Packages.props ./Src 19 | 20 | # .NET Core Restore 21 | RUN dotnet restore ./Src/DDD.Services.Api/DDD.Services.Api.csproj 22 | 23 | # Copy All Files 24 | COPY Src ./Src 25 | 26 | # .NET Core Build and Publish 27 | RUN dotnet publish ./Src/DDD.Services.Api/DDD.Services.Api.csproj -c Release -o /publish 28 | 29 | # ASP.NET Core Runtime 30 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime 31 | WORKDIR /app 32 | COPY --from=build /publish ./ 33 | 34 | # Expose ports 35 | EXPOSE 80 36 | EXPOSE 443 37 | 38 | # Setup your variables before running. 39 | ARG MyEnv 40 | ENV ASPNETCORE_ENVIRONMENT $MyEnv 41 | 42 | ENTRYPOINT ["dotnet", "DDD.Services.Api.dll"] 43 | -------------------------------------------------------------------------------- /.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}/Src/DDD.Services.Api/DDD.Services.Api.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/Src/DDD.Services.Api/DDD.Services.Api.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/Src/DDD.Services.Api/DDD.Services.Api.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile", 40 | "options": { 41 | "cwd": "${workspaceFolder}/Src/DDD.Services.Api/" 42 | } 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /Src/DDD.CLI.Migration/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | using DbUp; 7 | 8 | namespace DDD.CLI.Migration; 9 | 10 | class Program 11 | { 12 | static void Main(string[] args) 13 | { 14 | var connectionString = 15 | args.FirstOrDefault() 16 | ?? ConfigurationManager.ConnectionStrings["PublishToTargetDB"].ConnectionString; 17 | 18 | if (string.IsNullOrEmpty(connectionString)) 19 | { 20 | return; 21 | } 22 | 23 | // If you want your application to create the database for you 24 | // EnsureDatabase.For.SqlDatabase(connectionString); 25 | 26 | var upgrader = 27 | DeployChanges.To 28 | .SqlDatabase(connectionString) 29 | .WithScriptsEmbeddedInAssembly( 30 | Assembly.GetExecutingAssembly(), 31 | (string script) => script.StartsWith("DDD.CLI.Migration.Scripts.")) 32 | .LogToConsole() 33 | .Build(); 34 | 35 | var result = upgrader.PerformUpgrade(); 36 | 37 | if (!result.Successful) 38 | { 39 | Console.ForegroundColor = ConsoleColor.Red; 40 | Console.WriteLine(result.Error); 41 | Console.ResetColor(); 42 | #if DEBUG 43 | Console.ReadLine(); 44 | #endif 45 | return; 46 | } 47 | 48 | Console.ForegroundColor = ConsoleColor.Green; 49 | Console.WriteLine("Success!"); 50 | Console.ResetColor(); 51 | return; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/StartupExtensions/HealthCheckExtension.cs: -------------------------------------------------------------------------------- 1 | using DDD.Infra.Data.Context; 2 | 3 | using HealthChecks.UI.Client; 4 | 5 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 6 | 7 | namespace DDD.Services.Api.StartupExtensions; 8 | 9 | public static class HealthCheckExtension 10 | { 11 | public static IServiceCollection AddCustomizedHealthCheck(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment env) 12 | { 13 | if (env.IsProduction() || env.IsStaging()) 14 | { 15 | services.AddHealthChecks() 16 | .AddSqlServer(configuration.GetConnectionString("DefaultConnection")) 17 | .AddDbContextCheck(); 18 | services.AddHealthChecksUI(opt => 19 | { 20 | opt.SetEvaluationTimeInSeconds(15); // time in seconds between check 21 | }).AddInMemoryStorage(); 22 | } 23 | 24 | return services; 25 | } 26 | 27 | public static void UseCustomizedHealthCheck(IEndpointRouteBuilder endpoints, IWebHostEnvironment env) 28 | { 29 | if (env.IsProduction() || env.IsStaging()) 30 | { 31 | endpoints.MapHealthChecks("/hc", new HealthCheckOptions 32 | { 33 | Predicate = _ => true, 34 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, 35 | }); 36 | 37 | endpoints.MapHealthChecksUI(setup => 38 | { 39 | setup.UIPath = "/hc-ui"; 40 | setup.ApiPath = "/hc-json"; 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/StartupExtensions/QuartzExtensions.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Providers.Crons; 2 | 3 | using Quartz; 4 | using Quartz.AspNetCore; 5 | 6 | namespace DDD.Services.Api.StartupExtensions; 7 | 8 | public static class QuartzExtensions 9 | { 10 | public static IServiceCollection AddCustomizedQuartz(this IServiceCollection services, IConfiguration configuration) 11 | { 12 | services.AddQuartz(q => 13 | { 14 | var myTz = "SE Asia Standard Time"; 15 | var tz = TimeZoneInfo.FindSystemTimeZoneById(myTz); 16 | 17 | // Create a "key" for the job 18 | var jobKey = new JobKey(nameof(NotifyInactiveUserJob)); 19 | 20 | // Register the job with the DI container 21 | q.AddJob(opts => opts.WithIdentity(jobKey)); 22 | 23 | // Create a trigger for the job 24 | q.AddTrigger(opts => opts 25 | .ForJob(jobKey) 26 | .WithIdentity($"{nameof(NotifyInactiveUserJob)}-trigger") 27 | 28 | // run at 6:00 PM every weekdays 29 | .WithCronSchedule("0 0 18 ? * MON,TUE,WED,THU,FRI *", x => x.InTimeZone(tz))); 30 | }); 31 | 32 | // ASP.NET Core hosting 33 | services.AddQuartzServer(options => 34 | { 35 | // when shutting down we want jobs to complete gracefully 36 | options.WaitForJobsToComplete = true; 37 | }); 38 | 39 | return services; 40 | } 41 | 42 | public static IApplicationBuilder UseCustomizedQuartz(this IApplicationBuilder app) 43 | { 44 | return app; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/DDD.Services.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 51c0770a-8c88-4362-b3b5-a8936796ecef 5 | 6 | $(NoWarn);AD0001;NETSDK1206 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/sonar.yml: -------------------------------------------------------------------------------- 1 | name: Sonar 2 | on: [workflow_dispatch] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: windows-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | with: 10 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 11 | - name: Cache SonarCloud packages 12 | uses: actions/cache@v4 13 | with: 14 | path: ~\sonar\cache 15 | key: ${{ runner.os }}-sonar 16 | restore-keys: ${{ runner.os }}-sonar 17 | - name: Cache SonarCloud scanner 18 | id: cache-sonar-scanner 19 | uses: actions/cache@v4 20 | with: 21 | path: .\.sonar\scanner 22 | key: ${{ runner.os }}-sonar-scanner 23 | restore-keys: ${{ runner.os }}-sonar-scanner 24 | - name: Install SonarCloud scanner 25 | if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' 26 | shell: powershell 27 | run: | 28 | New-Item -Path .\.sonar\scanner -ItemType Directory 29 | dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner 30 | - name: Build and analyze 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 33 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 34 | shell: powershell 35 | run: | 36 | .\.sonar\scanner\dotnet-sonarscanner begin /k:"nguyentrucxinh_AspNetCore-DDD" /o:"nguyentrucxinh" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" 37 | dotnet build 38 | .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [workflow_dispatch] 4 | 5 | env: 6 | WEBAPP_PACKAGE_PATH: './published' 7 | # NETCORE_VERSION: '8.0.0' 8 | # SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Setup .NET 19 | uses: actions/setup-dotnet@v4 20 | with: 21 | global-json-file: global.json 22 | # dotnet-version: ${{ env.NETCORE_VERSION }} 23 | 24 | - name: Install dependencies 25 | run: dotnet restore 26 | 27 | - name: Build 28 | run: dotnet build --configuration Release --no-restore 29 | 30 | # - name: Test 31 | # run: dotnet test --no-restore --verbosity normal 32 | 33 | # - name: Slack Notify CI 34 | # if: always() 35 | # uses: adamkdean/simple-slack-notify@master 36 | # with: 37 | # channel: '#cicd' 38 | # status: ${{ job.status }} 39 | # success_text: 'Build (#${env.GITHUB_RUN_NUMBER}) completed successfully' 40 | # failure_text: 'Build (#${env.GITHUB_RUN_NUMBER}) failed' 41 | # cancelled_text: 'Build (#${env.GITHUB_RUN_NUMBER}) was cancelled' 42 | # fields: | 43 | # [{ "title": "Action URL", "value": "${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}"}] 44 | 45 | - name: Publish app for deploy 46 | run: dotnet publish --configuration Release --no-build --output ${{ env.WEBAPP_PACKAGE_PATH }} 47 | 48 | - name: Publish Artifacts 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: webapp 52 | path: ${{ env.WEBAPP_PACKAGE_PATH }} 53 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Hubs/Notification/NotificationProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Microsoft.AspNetCore.SignalR; 4 | 5 | namespace DDD.Domain.Providers.Hubs; 6 | 7 | public class NotificationProvider : INotificationProvider 8 | { 9 | private readonly IHubContext _hubContext; 10 | 11 | public NotificationProvider(IHubContext hubContext) 12 | { 13 | _hubContext = hubContext; 14 | } 15 | 16 | public async Task JoinGroup(string groupName) 17 | { 18 | // TODO: Find connectionId 19 | // https://learn.microsoft.com/en-us/aspnet/core/signalr/hubcontext?view=aspnetcore-6.0 20 | // When client methods are called from outside of the Hub class, there's no caller associated 21 | // with the invocation. Therefore, there's no access to the ConnectionId, Caller, and Others 22 | // properties. 23 | var connectionId = string.Empty; 24 | await _hubContext.Groups.AddToGroupAsync(connectionId, groupName); 25 | await _hubContext.Clients.Group(groupName).JoinGroup(groupName); 26 | } 27 | 28 | public async Task LeaveGroup(string groupName) 29 | { 30 | // TODO: Find connectionId 31 | var connectionId = string.Empty; 32 | await _hubContext.Clients.Group(groupName).LeaveGroup(groupName); 33 | await _hubContext.Groups.RemoveFromGroupAsync(connectionId, groupName); 34 | } 35 | 36 | public async Task Send(NotificationItem item) 37 | { 38 | var groupName = $"{nameof(NotificationItem.UserId)}_{item.UserId}"; 39 | await _hubContext.Clients.Group(groupName).Send(item); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Crons/Jobs/NotifyInactiveUserJob.cs: -------------------------------------------------------------------------------- 1 | // using System.Collections.Generic; 2 | // using System.Linq; 3 | using System.Threading.Tasks; 4 | 5 | using DDD.Domain.Providers.Webhooks; 6 | 7 | using Microsoft.AspNetCore.Hosting; 8 | 9 | using Quartz; 10 | 11 | namespace DDD.Domain.Providers.Crons; 12 | 13 | [DisallowConcurrentExecution] 14 | public class NotifyInactiveUserJob : IJob 15 | { 16 | private readonly IWebHostEnvironment _env; 17 | private readonly IWebhookProvider _webhookProvider; 18 | 19 | public NotifyInactiveUserJob(IWebHostEnvironment env, IWebhookProvider webhookProvider) 20 | { 21 | _env = env; 22 | _webhookProvider = webhookProvider; 23 | } 24 | 25 | public async Task Execute(IJobExecutionContext context) 26 | { 27 | await _webhookProvider.Send($"START CheckInactiveUser, Env {_env.EnvironmentName}"); 28 | 29 | // // Get JobData 30 | // var jobData = context.MergedJobDataMap; 31 | 32 | // // Validate 33 | // if (!jobData.ContainsKey(nameof(NotifyInactiveUserConsumerModel.TenantId)) 34 | // || !jobData.ContainsKey(nameof(NotifyInactiveUserConsumerModel.UserId)) 35 | // || !jobData.ContainsKey(nameof(NotifyInactiveUserConsumerModel.Data))) return; 36 | 37 | // // Parse data 38 | // var tenantId = (short)jobData.Get(nameof(NotifyInactiveUserConsumerModel.TenantId)); 39 | // var userId = (int)jobData.Get(nameof(NotifyInactiveUserConsumerModel.UserId)); 40 | // var data = (List)jobData.Get(nameof(NotifyInactiveUserConsumerModel.Data)); 41 | 42 | // if (!data.Any() || tenantId <= 0 || userId <= 0) return; 43 | 44 | // TODO: Your logic here 45 | 46 | await _webhookProvider.Send($"END CheckInactiveUser, Env {_env.EnvironmentName}"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Repository/SpecificationEvaluator.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | using DDD.Domain.Interfaces; 4 | 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace DDD.Infra.Data.Repository; 8 | 9 | public class SpecificationEvaluator 10 | where T : class 11 | { 12 | public static IQueryable GetQuery(IQueryable inputQuery, ISpecification specification) 13 | { 14 | var query = inputQuery; 15 | 16 | // modify the IQueryable using the specification's criteria expression 17 | if (specification.Criteria != null) 18 | { 19 | query = query.Where(specification.Criteria); 20 | } 21 | 22 | // Includes all expression-based includes 23 | query = specification.Includes.Aggregate( 24 | query, 25 | (current, include) => current.Include(include)); 26 | 27 | // Include any string-based include statements 28 | query = specification.IncludeStrings.Aggregate( 29 | query, 30 | (current, include) => current.Include(include)); 31 | 32 | // Apply ordering if expressions are set 33 | if (specification.OrderBy != null) 34 | { 35 | query = query.OrderBy(specification.OrderBy); 36 | } 37 | else if (specification.OrderByDescending != null) 38 | { 39 | query = query.OrderByDescending(specification.OrderByDescending); 40 | } 41 | 42 | if (specification.GroupBy != null) 43 | { 44 | query = query.GroupBy(specification.GroupBy).SelectMany(x => x); 45 | } 46 | 47 | // Apply paging if enabled 48 | if (specification.IsPagingEnabled) 49 | { 50 | query = query.Skip(specification.Skip) 51 | .Take(specification.Take); 52 | } 53 | 54 | return query; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Repository/Repository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using DDD.Domain.Interfaces; 5 | using DDD.Infra.Data.Context; 6 | 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace DDD.Infra.Data.Repository; 10 | 11 | public abstract class Repository : IRepository 12 | where TEntity : class 13 | { 14 | #pragma warning disable SA1401 // Fields should be private 15 | protected readonly ApplicationDbContext _db; 16 | protected readonly DbSet _dbSet; 17 | #pragma warning restore SA1401 // Fields should be private 18 | 19 | public Repository(ApplicationDbContext context) 20 | { 21 | _db = context; 22 | _dbSet = _db.Set(); 23 | } 24 | 25 | public virtual void Add(TEntity obj) 26 | { 27 | _dbSet.Add(obj); 28 | } 29 | 30 | public virtual TEntity GetById(Guid id) 31 | { 32 | return _dbSet.Find(id); 33 | } 34 | 35 | public virtual IQueryable GetAll() 36 | { 37 | return _dbSet; 38 | } 39 | 40 | public virtual IQueryable GetAll(ISpecification spec) 41 | { 42 | return ApplySpecification(spec); 43 | } 44 | 45 | public virtual IQueryable GetAllSoftDeleted() 46 | { 47 | return _dbSet.IgnoreQueryFilters() 48 | .Where(e => EF.Property(e, "IsDeleted") == true); 49 | } 50 | 51 | public virtual void Update(TEntity obj) 52 | { 53 | _dbSet.Update(obj); 54 | } 55 | 56 | public virtual void Remove(Guid id) 57 | { 58 | _dbSet.Remove(_dbSet.Find(id)); 59 | } 60 | 61 | public int SaveChanges() 62 | { 63 | return _db.SaveChanges(); 64 | } 65 | 66 | public void Dispose() 67 | { 68 | _db.Dispose(); 69 | GC.SuppressFinalize(this); 70 | } 71 | 72 | private IQueryable ApplySpecification(ISpecification spec) 73 | { 74 | return SpecificationEvaluator.GetQuery(_dbSet.AsQueryable(), spec); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Hash/PasswordHasher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Security.Cryptography; 4 | 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace DDD.Domain.Providers.Hash; 8 | 9 | public sealed class PasswordHasher : IPasswordHasher 10 | { 11 | private const int SaltSize = 16; // 128 bit 12 | private const int KeySize = 32; // 256 bit 13 | 14 | public PasswordHasher(IOptions options) 15 | { 16 | Options = options.Value; 17 | } 18 | 19 | private HashingOptions Options { get; } 20 | 21 | public string Hash(string password) 22 | { 23 | using (var algorithm = new Rfc2898DeriveBytes( 24 | password, 25 | SaltSize, 26 | Options.Iterations, 27 | HashAlgorithmName.SHA512)) 28 | { 29 | var key = Convert.ToBase64String(algorithm.GetBytes(KeySize)); 30 | var salt = Convert.ToBase64String(algorithm.Salt); 31 | 32 | return $"{Options.Iterations}.{salt}.{key}"; 33 | } 34 | } 35 | 36 | public (bool Verified, bool NeedsUpgrade) Check(string hash, string password) 37 | { 38 | var parts = hash.Split('.', 3); 39 | 40 | if (parts.Length != 3) 41 | { 42 | throw new FormatException("Unexpected hash format. " + 43 | "Should be formatted as `{iterations}.{salt}.{hash}`"); 44 | } 45 | 46 | var iterations = Convert.ToInt32(parts[0]); 47 | var salt = Convert.FromBase64String(parts[1]); 48 | var key = Convert.FromBase64String(parts[2]); 49 | 50 | var needsUpgrade = iterations != Options.Iterations; 51 | 52 | using (var algorithm = new Rfc2898DeriveBytes( 53 | password, 54 | salt, 55 | iterations, 56 | HashAlgorithmName.SHA512)) 57 | { 58 | var keyToCheck = algorithm.GetBytes(KeySize); 59 | 60 | var verified = keyToCheck.SequenceEqual(key); 61 | 62 | return (verified, needsUpgrade); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:5000", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "Dev - IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "Staging - IIS Express": { 21 | "commandName": "IISExpress", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Staging" 24 | } 25 | }, 26 | "Prod - IIS Express": { 27 | "commandName": "IISExpress", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Production" 30 | } 31 | }, 32 | "Watch-Dev": { 33 | "commandName": "Project", 34 | "hotReloadProfile": "aspnetcore", 35 | "dotnetRunMessages": true, 36 | "launchBrowser": true, 37 | "launchUrl": "swagger", 38 | "applicationUrl": "http://localhost:5000", 39 | "environmentVariables": { 40 | "ASPNETCORE_ENVIRONMENT": "Development" 41 | } 42 | }, 43 | "Dev": { 44 | "commandName": "Project", 45 | "dotnetRunMessages": true, 46 | "launchBrowser": true, 47 | "launchUrl": "swagger", 48 | "applicationUrl": "http://localhost:5000", 49 | "environmentVariables": { 50 | "ASPNETCORE_ENVIRONMENT": "Development" 51 | } 52 | }, 53 | "Staging": { 54 | "commandName": "Project", 55 | "applicationUrl": "http://localhost:5000", 56 | "environmentVariables": { 57 | "ASPNETCORE_ENVIRONMENT": "Staging" 58 | } 59 | }, 60 | "Prod": { 61 | "commandName": "Project", 62 | "applicationUrl": "http://localhost:5000", 63 | "environmentVariables": { 64 | "ASPNETCORE_ENVIRONMENT": "Production" 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /templates/include-throw-error-steps.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: PowerShell@2 3 | inputs: 4 | targetType: 'inline' 5 | script: | 6 | $branchSource = "$(Build.SourceBranch)" 7 | $commitId = "$(Build.SourceVersion)" 8 | $commitMsg = "$(Build.SourceVersionMessage)" 9 | 10 | $createProjectURL = "https://hooks.slack.com/services/..." 11 | 12 | $projectJSON = @{ 13 | attachments = @( 14 | @{ 15 | fallback = 'CICD' 16 | color = '#ff0000' 17 | author_name = 'ntxinh' 18 | author_icon = 'https://play-lh.googleusercontent.com/8ddL1kuoNUB5vUvgDVjYY3_6HwQcrg1K2fd_R8soD-e2QYj8fT9cfhfh3G0hnSruLKec' 19 | fields = @( 20 | @{ 21 | title = 'Ref' 22 | value = "$($branchSource)" 23 | short = $true 24 | } 25 | @{ 26 | title = 'Event' 27 | value = 'push' 28 | short = $true 29 | } 30 | @{ 31 | title = 'Actions URL' 32 | value = '' 33 | short = $true 34 | } 35 | @{ 36 | title = 'Commit' 37 | value = "$($commitId)" 38 | short = $true 39 | } 40 | @{ 41 | title = 'Message' 42 | value = "$($commitMsg)" 43 | short = $false 44 | } 45 | ) 46 | } 47 | ) 48 | } | ConvertTo-Json -Depth 5 49 | 50 | $response = Invoke-RestMethod -Uri $createProjectURL -Method Post -ContentType "application/json" -Body ($projectJSON) 51 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Specifications/BaseSpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | 5 | using DDD.Domain.Interfaces; 6 | 7 | namespace DDD.Domain.Specifications; 8 | 9 | public abstract class BaseSpecification : ISpecification 10 | { 11 | protected BaseSpecification(Expression> criteria) 12 | { 13 | Criteria = criteria; 14 | } 15 | 16 | public Expression> Criteria { get; } 17 | 18 | public List>> Includes { get; } = new List>>(); 19 | 20 | public List IncludeStrings { get; } = new List(); 21 | 22 | public Expression> OrderBy { get; private set; } 23 | 24 | public Expression> OrderByDescending { get; private set; } 25 | 26 | public Expression> GroupBy { get; private set; } 27 | 28 | public int Take { get; private set; } 29 | 30 | public int Skip { get; private set; } 31 | 32 | public bool IsPagingEnabled { get; private set; } = false; 33 | 34 | protected virtual void AddInclude(Expression> includeExpression) 35 | { 36 | Includes.Add(includeExpression); 37 | } 38 | 39 | protected virtual void AddInclude(string includeString) 40 | { 41 | IncludeStrings.Add(includeString); 42 | } 43 | 44 | protected virtual void ApplyPaging(int skip, int take) 45 | { 46 | Skip = skip; 47 | Take = take; 48 | IsPagingEnabled = true; 49 | } 50 | 51 | protected virtual void ApplyOrderBy(Expression> orderByExpression) 52 | { 53 | OrderBy = orderByExpression; 54 | } 55 | 56 | protected virtual void ApplyOrderByDescending(Expression> orderByDescendingExpression) 57 | { 58 | OrderByDescending = orderByDescendingExpression; 59 | } 60 | 61 | // Not used anywhere at the moment, but someone requested an example of setting this up. 62 | protected virtual void ApplyGroupBy(Expression> groupByExpression) 63 | { 64 | GroupBy = groupByExpression; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Authorization/JwtIssuerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | using Microsoft.IdentityModel.Tokens; 5 | 6 | namespace DDD.Infra.CrossCutting.Identity.Models; 7 | 8 | public class JwtIssuerOptions 9 | { 10 | /// 11 | /// 4.1.1. "iss" (Issuer) Claim - The "iss" (issuer) claim identifies the principal that issued the JWT. 12 | /// 13 | public string Issuer { get; set; } 14 | 15 | /// 16 | /// 4.1.2. "sub" (Subject) Claim - The "sub" (subject) claim identifies the principal that is the subject of the JWT. 17 | /// 18 | public string Subject { get; set; } 19 | 20 | /// 21 | /// 4.1.3. "aud" (Audience) Claim - The "aud" (audience) claim identifies the recipients that the JWT is intended for. 22 | /// 23 | public string Audience { get; set; } 24 | 25 | /// 26 | /// 4.1.4. "exp" (Expiration Time) Claim - The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. 27 | /// 28 | public DateTime Expiration => IssuedAt.Add(ValidFor); 29 | 30 | /// 31 | /// 4.1.5. "nbf" (Not Before) Claim - The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. 32 | /// 33 | public DateTime NotBefore => DateTime.UtcNow; 34 | 35 | /// 36 | /// 4.1.6. "iat" (Issued At) Claim - The "iat" (issued at) claim identifies the time at which the JWT was issued. 37 | /// 38 | public DateTime IssuedAt => DateTime.UtcNow; 39 | 40 | /// 41 | /// Set the timespan the token will be valid for (default is 120 min) 42 | /// 43 | public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(120); 44 | 45 | /// 46 | /// "jti" (JWT ID) Claim (default ID is a GUID) 47 | /// 48 | public Func> JtiGenerator => 49 | () => Task.FromResult(Guid.NewGuid().ToString()); 50 | 51 | /// 52 | /// The signing key to use when generating tokens. 53 | /// 54 | public SigningCredentials SigningCredentials { get; set; } 55 | } 56 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Mail/MailProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | // using Microsoft.Extensions.Configuration; 4 | 5 | // using SendGrid; 6 | // using SendGrid.Helpers.Mail; 7 | 8 | namespace DDD.Domain.Providers.Mail; 9 | 10 | public class MailProvider : IMailProvider 11 | { 12 | // private readonly IConfiguration _configuration; 13 | // private readonly string devOpsEmail; 14 | // private readonly ISendGridClient _sendGridClient; 15 | 16 | public MailProvider(/*IConfiguration configuration, ISendGridClient sendGridClient*/) 17 | { 18 | // _configuration = configuration; 19 | // _sendGridClient = sendGridClient; 20 | // devOpsEmail = _configuration.GetSection("DevOpsEmail").Value; 21 | } 22 | 23 | public Task Send(MailMessage message) 24 | { 25 | throw new System.NotImplementedException(); 26 | } 27 | 28 | // public async Task Send(MailMessage message) 29 | // { 30 | // if (message.From is null || message.From.Email is null) return; 31 | // if (message.To is null || !message.To.Any()) return; 32 | 33 | // var msg = new SendGridMessage 34 | // { 35 | // From = new EmailAddress(message.From.Email, message.From.Name), 36 | // Subject = message.Subject, 37 | // PlainTextContent = message.PlainTextContent, 38 | // TemplateId = message.TemplateId, 39 | // }; 40 | // if (message.DynamicTemplateData != null) 41 | // { 42 | // msg.SetTemplateData(message.DynamicTemplateData); 43 | // } 44 | // if (message.Cc != null && message.Cc.Any()) 45 | // { 46 | // msg.AddCcs(message.Cc.Select(x => new EmailAddress(x.Email, x.Name)).ToList()); 47 | // } 48 | // if (message.Bcc != null && message.Bcc.Any()) 49 | // { 50 | // msg.AddBccs(message.Bcc.Select(x => new EmailAddress(x.Email, x.Name)).ToList()); 51 | // } 52 | // msg.AddTos(message.To.Select(x => new EmailAddress(x.Email, x.Name)).ToList()); 53 | // msg.AddBcc(devOpsEmail); 54 | // var response = await _sendGridClient.SendEmailAsync(msg); 55 | // } 56 | } 57 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/StartupExtensions/DatabaseExtension.cs: -------------------------------------------------------------------------------- 1 | using DDD.Infra.CrossCutting.Identity.Data; 2 | using DDD.Infra.Data.Context; 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace DDD.Services.Api.StartupExtensions; 7 | 8 | public static class DatabaseExtension 9 | { 10 | public static IServiceCollection AddCustomizedDatabase(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment env) 11 | { 12 | services.AddDbContext(options => 13 | { 14 | options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); 15 | 16 | // options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); 17 | // Configuring it to throw an exception when a query is evaluated client side 18 | // This is no longer logged in Entity Framework Core 3.0. 19 | // options.ConfigureWarnings(warnings => 20 | // { 21 | // warnings.Throw(RelationalEventId.QueryClientEvaluationWarning); 22 | // }); 23 | if (!env.IsProduction()) 24 | { 25 | options.EnableDetailedErrors(); 26 | options.EnableSensitiveDataLogging(); 27 | } 28 | }); 29 | 30 | services.AddDbContext(options => 31 | { 32 | options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); 33 | 34 | // options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); 35 | if (!env.IsProduction()) 36 | { 37 | options.EnableDetailedErrors(); 38 | options.EnableSensitiveDataLogging(); 39 | } 40 | }); 41 | 42 | services.AddDbContext(options => 43 | { 44 | options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); 45 | 46 | // options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); 47 | if (!env.IsProduction()) 48 | { 49 | options.EnableDetailedErrors(); 50 | options.EnableSensitiveDataLogging(); 51 | } 52 | }); 53 | 54 | return services; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/Controllers/ApiController.cs: -------------------------------------------------------------------------------- 1 | using DDD.Domain.Core.Bus; 2 | using DDD.Domain.Core.Notifications; 3 | 4 | using MediatR; 5 | 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace DDD.Services.Api.Controllers; 10 | 11 | [ApiController] 12 | [Route("api/v{version:apiVersion}/[controller]")] 13 | 14 | // [Route("api/[controller]/[action]")] 15 | public abstract class ApiController : ControllerBase 16 | { 17 | private readonly DomainNotificationHandler _notifications; 18 | private readonly IMediatorHandler _mediator; 19 | 20 | protected ApiController( 21 | INotificationHandler notifications, 22 | IMediatorHandler mediator) 23 | { 24 | _notifications = (DomainNotificationHandler)notifications; 25 | _mediator = mediator; 26 | } 27 | 28 | protected IEnumerable Notifications => _notifications.GetNotifications(); 29 | 30 | protected bool IsValidOperation() 31 | { 32 | return !_notifications.HasNotifications(); 33 | } 34 | 35 | protected new IActionResult Response(object result = null) 36 | { 37 | if (IsValidOperation()) 38 | { 39 | return Ok(new 40 | { 41 | success = true, 42 | data = result, 43 | }); 44 | } 45 | 46 | return BadRequest(new 47 | { 48 | success = false, 49 | errors = _notifications.GetNotifications().Select(n => n.Value), 50 | }); 51 | } 52 | 53 | protected void NotifyModelStateErrors() 54 | { 55 | var erros = ModelState.Values.SelectMany(v => v.Errors); 56 | foreach (var erro in erros) 57 | { 58 | var erroMsg = erro.Exception == null ? erro.ErrorMessage : erro.Exception.Message; 59 | NotifyError(string.Empty, erroMsg); 60 | } 61 | } 62 | 63 | protected void NotifyError(string code, string message) 64 | { 65 | _mediator.RaiseEvent(new DomainNotification(code, message)); 66 | } 67 | 68 | protected void AddIdentityErrors(IdentityResult result) 69 | { 70 | foreach (var error in result.Errors) 71 | { 72 | NotifyError(result.ToString(), error.Description); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Crons/CronProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | using Quartz; 5 | 6 | namespace DDD.Domain.Providers.Crons; 7 | 8 | public class CronProvider : ICronProvider 9 | { 10 | private readonly ISchedulerFactory _schedulerFactory; 11 | 12 | public CronProvider(ISchedulerFactory schedulerFactory) 13 | { 14 | _schedulerFactory = schedulerFactory; 15 | } 16 | 17 | // public async Task NotifyInactiveUser(NotifyInactiveUserConsumerModel payload) 18 | // { 19 | // IScheduler sched = await _schedulerFactory.GetScheduler(); 20 | 21 | // IDictionary jobData = new Dictionary 22 | // { 23 | // { nameof(NotifyInactiveUserConsumerModel.Data), payload.Data }, 24 | // { nameof(NotifyInactiveUserConsumerModel.TenantId), payload.TenantId }, 25 | // { nameof(NotifyInactiveUserConsumerModel.UserId), payload.UserId }, 26 | // }; 27 | 28 | // var job = JobBuilder.Create() 29 | // .WithIdentity(nameof(NotifyInactiveUserJob)) 30 | // .Build(); 31 | 32 | // var replace = true; 33 | // var durable = true; 34 | // await sched.AddJob(job, replace, durable); 35 | 36 | // await sched.TriggerJob(new JobKey(nameof(NotifyInactiveUserJob)), new JobDataMap(jobData)); 37 | // } 38 | 39 | public async Task OneOffJob(IDictionary jobData) 40 | where T : IJob 41 | { 42 | IScheduler sched = await _schedulerFactory.GetScheduler(); 43 | 44 | var jobKey = typeof(T).Name; 45 | 46 | var job = JobBuilder.Create() 47 | .WithIdentity(jobKey) 48 | .Build(); 49 | 50 | var replace = true; 51 | var durable = true; 52 | await sched.AddJob(job, replace, durable); 53 | 54 | await sched.TriggerJob(new JobKey(jobKey), new JobDataMap(jobData)); 55 | 56 | // How to use 57 | // var jobData = new Dictionary 58 | // { 59 | // { nameof(NotifyInactiveUserConsumerModel.Data), payload.Data }, 60 | // { nameof(NotifyInactiveUserConsumerModel.TenantId), payload.TenantId }, 61 | // { nameof(NotifyInactiveUserConsumerModel.UserId), payload.UserId }, 62 | // }; 63 | // await _quartzProvider.OneOffJob(jobData); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Src/DDD.Application/Services/CustomerAppService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using AutoMapper; 5 | using AutoMapper.QueryableExtensions; 6 | 7 | using DDD.Application.EventSourcedNormalizers; 8 | using DDD.Application.Interfaces; 9 | using DDD.Application.ViewModels; 10 | using DDD.Domain.Commands; 11 | using DDD.Domain.Core.Bus; 12 | using DDD.Domain.Interfaces; 13 | using DDD.Domain.Specifications; 14 | using DDD.Infra.Data.Repository.EventSourcing; 15 | 16 | namespace DDD.Application.Services; 17 | 18 | public class CustomerAppService : ICustomerAppService 19 | { 20 | private readonly IMapper _mapper; 21 | private readonly ICustomerRepository _customerRepository; 22 | private readonly IEventStoreRepository _eventStoreRepository; 23 | private readonly IMediatorHandler _bus; 24 | 25 | public CustomerAppService( 26 | IMapper mapper, 27 | ICustomerRepository customerRepository, 28 | IMediatorHandler bus, 29 | IEventStoreRepository eventStoreRepository) 30 | { 31 | _mapper = mapper; 32 | _customerRepository = customerRepository; 33 | _bus = bus; 34 | _eventStoreRepository = eventStoreRepository; 35 | } 36 | 37 | public IEnumerable GetAll() 38 | { 39 | return _customerRepository.GetAll().ProjectTo(_mapper.ConfigurationProvider); 40 | } 41 | 42 | public IEnumerable GetAll(int skip, int take) 43 | { 44 | return _customerRepository.GetAll(new CustomerFilterPaginatedSpecification(skip, take)) 45 | .ProjectTo(_mapper.ConfigurationProvider); 46 | } 47 | 48 | public CustomerViewModel GetById(Guid id) 49 | { 50 | return _mapper.Map(_customerRepository.GetById(id)); 51 | } 52 | 53 | public void Register(CustomerViewModel customerViewModel) 54 | { 55 | var registerCommand = _mapper.Map(customerViewModel); 56 | _bus.SendCommand(registerCommand); 57 | } 58 | 59 | public void Update(CustomerViewModel customerViewModel) 60 | { 61 | var updateCommand = _mapper.Map(customerViewModel); 62 | _bus.SendCommand(updateCommand); 63 | } 64 | 65 | public void Remove(Guid id) 66 | { 67 | var removeCommand = new RemoveCustomerCommand(id); 68 | _bus.SendCommand(removeCommand); 69 | } 70 | 71 | public IList GetAllHistory(Guid id) 72 | { 73 | return CustomerHistory.ToJavaScriptCustomerHistory(_eventStoreRepository.All(id)); 74 | } 75 | 76 | public void Dispose() 77 | { 78 | GC.SuppressFinalize(this); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Services/JwtFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IdentityModel.Tokens.Jwt; 3 | using System.Security.Claims; 4 | using System.Threading.Tasks; 5 | 6 | using DDD.Infra.CrossCutting.Identity.Models; 7 | 8 | using Microsoft.Extensions.Options; 9 | using Microsoft.IdentityModel.Tokens; 10 | 11 | namespace DDD.Infra.CrossCutting.Identity.Services; 12 | 13 | public class JwtFactory : IJwtFactory 14 | { 15 | private readonly JwtIssuerOptions _jwtOptions; 16 | 17 | public JwtFactory(IOptions jwtOptions) 18 | { 19 | _jwtOptions = jwtOptions.Value; 20 | ThrowIfInvalidOptions(_jwtOptions); 21 | } 22 | 23 | public async Task GenerateJwtToken(ClaimsIdentity claimsIdentity) 24 | { 25 | claimsIdentity.AddClaims(new Claim[] 26 | { 27 | // new Claim(JwtRegisteredClaimNames.Sub, user.Username), 28 | new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()), 29 | new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64), 30 | }); 31 | 32 | var tokenHandler = new JwtSecurityTokenHandler(); 33 | var token = tokenHandler.CreateToken(new SecurityTokenDescriptor 34 | { 35 | Issuer = _jwtOptions.Issuer, 36 | Audience = _jwtOptions.Audience, 37 | Subject = claimsIdentity, 38 | NotBefore = _jwtOptions.NotBefore, 39 | Expires = _jwtOptions.Expiration, 40 | SigningCredentials = _jwtOptions.SigningCredentials, 41 | }); 42 | 43 | return new JwtToken 44 | { 45 | JwtId = token.Id, 46 | AccessToken = tokenHandler.WriteToken(token), 47 | }; 48 | } 49 | 50 | private static void ThrowIfInvalidOptions(JwtIssuerOptions options) 51 | { 52 | if (options == null) 53 | { 54 | throw new ArgumentNullException(nameof(options)); 55 | } 56 | 57 | if (options.ValidFor <= TimeSpan.Zero) 58 | { 59 | throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(JwtIssuerOptions.ValidFor)); 60 | } 61 | 62 | if (options.SigningCredentials == null) 63 | { 64 | throw new ArgumentNullException(nameof(JwtIssuerOptions.SigningCredentials)); 65 | } 66 | 67 | if (options.JtiGenerator == null) 68 | { 69 | throw new ArgumentNullException(nameof(JwtIssuerOptions.JtiGenerator)); 70 | } 71 | } 72 | 73 | /// Date converted to seconds since Unix epoch (Jan 1, 1970, midnight UTC). 74 | private static long ToUnixEpochDate(DateTime date) 75 | => (long)Math.Round((date.ToUniversalTime() - 76 | new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)) 77 | .TotalSeconds); 78 | } 79 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "watch-dev", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "watch", 12 | "program": "${workspaceFolder}/Src/DDD.Services.Api/bin/Debug/net8.0/DDD.Services.Api.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/Src/DDD.Services.Api", 15 | "stopAtEntry": false, 16 | "console": "internalConsole", 17 | "env": { 18 | "ASPNETCORE_ENVIRONMENT": "Development", 19 | "ASPNETCORE_URLS": "http://localhost:5000/" 20 | }, 21 | }, 22 | { 23 | "name": "dev", 24 | "type": "coreclr", 25 | "request": "launch", 26 | "preLaunchTask": "build", 27 | "program": "${workspaceFolder}/Src/DDD.Services.Api/bin/Debug/net8.0/DDD.Services.Api.dll", 28 | "args": [], 29 | "cwd": "${workspaceFolder}/Src/DDD.Services.Api", 30 | "stopAtEntry": false, 31 | "console": "internalConsole", 32 | "env": { 33 | "ASPNETCORE_ENVIRONMENT": "Development", 34 | "ASPNETCORE_URLS": "http://localhost:5000/" 35 | }, 36 | }, 37 | { 38 | "name": "staging", 39 | "type": "coreclr", 40 | "request": "launch", 41 | "preLaunchTask": "build", 42 | "program": "${workspaceFolder}/Src/DDD.Services.Api/bin/Debug/net8.0/DDD.Services.Api.dll", 43 | "args": [], 44 | "cwd": "${workspaceFolder}/Src/DDD.Services.Api", 45 | "stopAtEntry": false, 46 | "console": "internalConsole", 47 | "env": { 48 | "ASPNETCORE_ENVIRONMENT": "Staging", 49 | "ASPNETCORE_URLS": "http://localhost:5000/" 50 | }, 51 | }, 52 | { 53 | "name": "prod", 54 | "type": "coreclr", 55 | "request": "launch", 56 | "preLaunchTask": "build", 57 | "program": "${workspaceFolder}/Src/DDD.Services.Api/bin/Debug/net8.0/DDD.Services.Api.dll", 58 | "args": [], 59 | "cwd": "${workspaceFolder}/Src/DDD.Services.Api", 60 | "stopAtEntry": false, 61 | "console": "internalConsole", 62 | "env": { 63 | "ASPNETCORE_ENVIRONMENT": "Production", 64 | "ASPNETCORE_URLS": "http://localhost:5000/" 65 | }, 66 | }, 67 | { 68 | "name": "CLI Migration", 69 | "type": "coreclr", 70 | "request": "launch", 71 | "preLaunchTask": "build", 72 | "program": "${workspaceFolder}/Src/DDD.CLI.Migration/bin/Debug/netcoreapp3.1/DDD.CLI.Migration.dll", 73 | "args": [], 74 | "cwd": "${workspaceFolder}", 75 | "stopAtEntry": false, 76 | // "console": "internalConsole" 77 | "console": "integratedTerminal" 78 | } 79 | ], 80 | "compounds": [ 81 | { 82 | "name": "Run-All-Dev", 83 | "configurations": [ 84 | "dev", 85 | "CLI Migration" 86 | ] 87 | }, 88 | { 89 | "name": "Run-All-Staging", 90 | "configurations": [ 91 | "staging", 92 | ] 93 | } 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/Controllers/v1/CustomerController.cs: -------------------------------------------------------------------------------- 1 | using DDD.Application.Interfaces; 2 | using DDD.Application.ViewModels; 3 | using DDD.Domain.Core.Bus; 4 | using DDD.Domain.Core.Notifications; 5 | using DDD.Infra.CrossCutting.Identity.Authorization; 6 | 7 | using MediatR; 8 | 9 | using Microsoft.AspNetCore.Authorization; 10 | using Microsoft.AspNetCore.Mvc; 11 | 12 | namespace DDD.Services.Api.Controllers.V1; 13 | 14 | [Authorize] 15 | [ApiVersion("1.0")] 16 | public class CustomerController : ApiController 17 | { 18 | private readonly ICustomerAppService _customerAppService; 19 | 20 | public CustomerController( 21 | ICustomerAppService customerAppService, 22 | INotificationHandler notifications, 23 | IMediatorHandler mediator) 24 | : base(notifications, mediator) 25 | { 26 | _customerAppService = customerAppService; 27 | } 28 | 29 | [HttpGet] 30 | [AllowAnonymous] 31 | [Route("customer-management")] 32 | public IActionResult Get() 33 | { 34 | return Response(_customerAppService.GetAll()); 35 | } 36 | 37 | [HttpGet] 38 | [AllowAnonymous] 39 | [Route("customer-management/{id:guid}")] 40 | public IActionResult Get(Guid id) 41 | { 42 | var customerViewModel = _customerAppService.GetById(id); 43 | 44 | return Response(customerViewModel); 45 | } 46 | 47 | [HttpPost] 48 | [Authorize(Policy = "CanWriteCustomerData", Roles = Roles.Admin)] 49 | [Route("customer-management")] 50 | public IActionResult Post([FromBody] CustomerViewModel customerViewModel) 51 | { 52 | if (!ModelState.IsValid) 53 | { 54 | NotifyModelStateErrors(); 55 | return Response(customerViewModel); 56 | } 57 | 58 | _customerAppService.Register(customerViewModel); 59 | 60 | return Response(customerViewModel); 61 | } 62 | 63 | [HttpPut] 64 | [Authorize(Policy = "CanWriteCustomerData", Roles = Roles.Admin)] 65 | [Route("customer-management")] 66 | public IActionResult Put([FromBody] CustomerViewModel customerViewModel) 67 | { 68 | if (!ModelState.IsValid) 69 | { 70 | NotifyModelStateErrors(); 71 | return Response(customerViewModel); 72 | } 73 | 74 | _customerAppService.Update(customerViewModel); 75 | 76 | return Response(customerViewModel); 77 | } 78 | 79 | [HttpDelete] 80 | [Authorize(Policy = "CanRemoveCustomerData", Roles = Roles.Admin)] 81 | [Route("customer-management")] 82 | public IActionResult Delete(Guid id) 83 | { 84 | _customerAppService.Remove(id); 85 | 86 | return Response(); 87 | } 88 | 89 | [HttpGet] 90 | [AllowAnonymous] 91 | [Route("customer-management/history/{id:guid}")] 92 | public IActionResult History(Guid id) 93 | { 94 | var customerHistoryData = _customerAppService.GetAllHistory(id); 95 | return Response(customerHistoryData); 96 | } 97 | 98 | [HttpGet] 99 | [AllowAnonymous] 100 | [Route("customer-management/pagination")] 101 | public IActionResult Pagination(int skip, int take) 102 | { 103 | return Response(_customerAppService.GetAll(skip, take)); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Deploy to Azure Kubernetes Service 2 | # Build and push image to Azure Container Registry; Deploy to Azure Kubernetes Service 3 | # https://docs.microsoft.com/azure/devops/pipelines/languages/docker 4 | 5 | trigger: 6 | - main 7 | 8 | resources: 9 | - repo: self 10 | 11 | variables: 12 | # Container registry service connection established during pipeline creation 13 | dockerRegistryServiceConnection: "" 14 | imageRepository: "aspnetcoredddwebapi" 15 | containerRegistry: "aspnetcoreddd.azurecr.io" 16 | dockerfilePath: "**/Dockerfile" 17 | tag: "$(Build.BuildId)" 18 | imagePullSecret: "aspnetcoredddwebapi-auth" 19 | 20 | # Agent VM image name 21 | vmImageName: "ubuntu-latest" 22 | 23 | stages: 24 | - stage: Build 25 | displayName: Build stage 26 | variables: 27 | - group: DEV 28 | jobs: 29 | - job: Build 30 | displayName: Build 31 | pool: 32 | vmImage: $(vmImageName) 33 | steps: 34 | 35 | - task: Docker@2 36 | displayName: Build an image to container registry 37 | inputs: 38 | command: build 39 | repository: $(imageRepository) 40 | dockerfile: $(dockerfilePath) 41 | containerRegistry: $(dockerRegistryServiceConnection) 42 | arguments: "--build-arg MyEnv=$(MyEnv)" 43 | tags: | 44 | latest 45 | $(tag) 46 | 47 | - task: Docker@2 48 | displayName: Push an image to container registry 49 | inputs: 50 | command: push 51 | repository: $(imageRepository) 52 | containerRegistry: $(dockerRegistryServiceConnection) 53 | tags: | 54 | latest 55 | $(tag) 56 | 57 | - upload: manifests 58 | artifact: manifests 59 | - job: NotifyIfError 60 | dependsOn: Build 61 | condition: failed() 62 | steps: 63 | - script: echo -----ThrowError----- 64 | displayName: Echo 65 | - template: templates/include-throw-error-steps.yml 66 | 67 | - stage: Deploy 68 | displayName: Deploy stage 69 | dependsOn: Build 70 | 71 | jobs: 72 | - deployment: Deploy 73 | displayName: Deploy 74 | pool: 75 | vmImage: $(vmImageName) 76 | environment: "aspnetcoreddd.default" 77 | strategy: 78 | runOnce: 79 | deploy: 80 | steps: 81 | - task: KubernetesManifest@0 82 | displayName: Create imagePullSecret 83 | inputs: 84 | action: createSecret 85 | secretName: $(imagePullSecret) 86 | dockerRegistryEndpoint: $(dockerRegistryServiceConnection) 87 | 88 | - task: KubernetesManifest@0 89 | displayName: Deploy to Kubernetes cluster 90 | inputs: 91 | action: deploy 92 | manifests: | 93 | $(Pipeline.Workspace)/manifests/deployment.yml 94 | $(Pipeline.Workspace)/manifests/service.yml 95 | imagePullSecrets: | 96 | $(imagePullSecret) 97 | containers: | 98 | $(containerRegistry)/$(imageRepository):$(tag) 99 | -------------------------------------------------------------------------------- /api.http: -------------------------------------------------------------------------------- 1 | # Local 2 | @protocol = http 3 | @hostname = localhost 4 | @port = 5000 5 | 6 | # Prod 7 | # @protocol = https 8 | # @hostname = 9 | # @port = 443 10 | 11 | @apiVersion = v1 12 | @host = {{hostname}}:{{port}} 13 | @baseUrl = {{protocol}}://{{host}}/api/{{apiVersion}} 14 | @contentType = application/json 15 | 16 | @accountCtrl = Account 17 | @customerCtrl = Customer/customer-management 18 | 19 | ### 20 | 21 | GET {{protocol}}://{{host}}/swagger 22 | 23 | ### 24 | # @name register 25 | # @prompt email 26 | # @prompt pw 27 | POST {{baseUrl}}/{{accountCtrl}}/register 28 | Content-Type: {{contentType}} 29 | 30 | { 31 | "email": "{{email}}", 32 | "password": "{{pw}}", 33 | "confirmPassword": "{{pw}}" 34 | } 35 | 36 | ### 37 | # @name login 38 | # @prompt email 39 | # @prompt pw 40 | POST {{baseUrl}}/{{accountCtrl}}/login 41 | Content-Type: {{contentType}} 42 | 43 | { 44 | "email": "{{email}}", 45 | "password": "{{pw}}", 46 | "rememberMe": false 47 | } 48 | 49 | ### 50 | # @name refresh 51 | # @prompt accessToken 52 | # @prompt refreshToken 53 | POST {{baseUrl}}/{{accountCtrl}}/refresh 54 | Content-Type: {{contentType}} 55 | 56 | { 57 | "accessToken": "{{accessToken}}", 58 | "refreshToken": "{{refreshToken}}", 59 | } 60 | 61 | ### 62 | 63 | @authToken = {{login.response.body.data.accessToken}} 64 | 65 | # @name current 66 | GET {{baseUrl}}/{{accountCtrl}}/current 67 | Content-Type: {{contentType}} 68 | Authorization: Bearer {{authToken}} 69 | 70 | ### 71 | # @name GetCustomers 72 | GET {{baseUrl}}/{{customerCtrl}} 73 | Content-Type: {{contentType}} 74 | Authorization: Bearer {{authToken}} 75 | 76 | ### 77 | # @name CreateCustomer 78 | # @prompt customerId 79 | # @prompt customerName 80 | # @prompt customerEmail 81 | # @prompt customerBirthDate 82 | POST {{baseUrl}}/{{customerCtrl}} 83 | Content-Type: {{contentType}} 84 | Authorization: Bearer {{authToken}} 85 | 86 | { 87 | "id": "{{customerId}}", 88 | "name": "{{customerName}}", 89 | "email": "{{customerEmail}}", 90 | "birthDate": "{{customerBirthDate}}" 91 | } 92 | 93 | ### 94 | # @name UpdateCustomer 95 | # @prompt customerId 96 | # @prompt customerName 97 | # @prompt customerEmail 98 | # @prompt customerBirthDate 99 | PUT {{baseUrl}}/{{customerCtrl}} 100 | Content-Type: {{contentType}} 101 | Authorization: Bearer {{authToken}} 102 | 103 | { 104 | "id": "{{customerId}}", 105 | "name": "{{customerName}}", 106 | "email": "{{customerEmail}}", 107 | "birthDate": "{{customerBirthDate}}" 108 | } 109 | 110 | ### 111 | # @name DeleteCustomer 112 | # @prompt customerId 113 | DELETE {{baseUrl}}/{{customerCtrl}} 114 | ?id={{customerId}} 115 | Content-Type: {{contentType}} 116 | Authorization: Bearer {{authToken}} 117 | 118 | ### 119 | # @name GetCustomer 120 | # @prompt customerId 121 | GET {{baseUrl}}/{{customerCtrl}}/{{customerId}} 122 | Content-Type: {{contentType}} 123 | Authorization: Bearer {{authToken}} 124 | 125 | ### 126 | # @name GetCustomerHistory 127 | # @prompt customerId 128 | GET {{baseUrl}}/{{customerCtrl}}/history/{{customerId}} 129 | Content-Type: {{contentType}} 130 | Authorization: Bearer {{authToken}} 131 | 132 | 133 | ### 134 | # @name GetCustomerByPagination 135 | # @prompt skip 136 | # @prompt take 137 | GET {{baseUrl}}/{{customerCtrl}}/pagination 138 | ?skip={{skip}} 139 | &take={{take}} 140 | Content-Type: {{contentType}} 141 | Authorization: Bearer {{authToken}} 142 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.IoC/NativeInjectorBootStrapper.cs: -------------------------------------------------------------------------------- 1 | using DDD.Application.Interfaces; 2 | using DDD.Application.Services; 3 | using DDD.Domain.CommandHandlers; 4 | using DDD.Domain.Commands; 5 | using DDD.Domain.Core.Bus; 6 | using DDD.Domain.Core.Events; 7 | using DDD.Domain.Core.Notifications; 8 | using DDD.Domain.EventHandlers; 9 | using DDD.Domain.Events; 10 | using DDD.Domain.Interfaces; 11 | using DDD.Domain.Providers.Crons; 12 | using DDD.Domain.Providers.Http; 13 | using DDD.Domain.Providers.Hubs; 14 | using DDD.Domain.Providers.Mail; 15 | using DDD.Domain.Providers.Office; 16 | using DDD.Domain.Providers.Webhooks; 17 | using DDD.Infra.CrossCutting.Bus; 18 | using DDD.Infra.CrossCutting.Identity.Authorization; 19 | using DDD.Infra.CrossCutting.Identity.Models; 20 | using DDD.Infra.CrossCutting.Identity.Services; 21 | using DDD.Infra.Data.EventSourcing; 22 | using DDD.Infra.Data.Repository; 23 | using DDD.Infra.Data.Repository.EventSourcing; 24 | using DDD.Infra.Data.UoW; 25 | 26 | using MediatR; 27 | 28 | using Microsoft.AspNetCore.Authorization; 29 | using Microsoft.Extensions.DependencyInjection; 30 | 31 | namespace DDD.Infra.CrossCutting.IoC; 32 | 33 | public class NativeInjectorBootStrapper 34 | { 35 | public static void RegisterServices(IServiceCollection services) 36 | { 37 | // ASP.NET HttpContext dependency 38 | services.AddHttpContextAccessor(); 39 | 40 | // services.AddSingleton(); 41 | 42 | // Domain Bus (Mediator) 43 | services.AddScoped(); 44 | 45 | // ASP.NET Authorization Polices 46 | services.AddSingleton(); 47 | 48 | // Application 49 | services.AddScoped(); 50 | 51 | // Domain - Events 52 | services.AddScoped, DomainNotificationHandler>(); 53 | services.AddScoped, CustomerEventHandler>(); 54 | services.AddScoped, CustomerEventHandler>(); 55 | services.AddScoped, CustomerEventHandler>(); 56 | 57 | // Domain - Commands 58 | services.AddScoped, CustomerCommandHandler>(); 59 | services.AddScoped, CustomerCommandHandler>(); 60 | services.AddScoped, CustomerCommandHandler>(); 61 | 62 | // Domain - Providers, 3rd parties 63 | services.AddScoped(); 64 | services.AddScoped(); 65 | services.AddScoped(); 66 | services.AddScoped(); 67 | services.AddScoped(); 68 | services.AddScoped(); 69 | 70 | // Infra - Data 71 | services.AddScoped(); 72 | services.AddScoped(); 73 | 74 | // Infra - Data EventSourcing 75 | services.AddScoped(); 76 | services.AddScoped(); 77 | 78 | // Infra - Identity Services 79 | services.AddTransient(); 80 | services.AddTransient(); 81 | 82 | // Infra - Identity 83 | services.AddScoped(); 84 | services.AddSingleton(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Src/DDD.Application/EventSourcedNormalizers/CustomerHistory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json; 5 | 6 | using DDD.Domain.Core.Events; 7 | 8 | namespace DDD.Application.EventSourcedNormalizers; 9 | 10 | public class CustomerHistory 11 | { 12 | public static IList HistoryData { get; set; } 13 | 14 | public static IList ToJavaScriptCustomerHistory(IList storedEvents) 15 | { 16 | HistoryData = new List(); 17 | CustomerHistoryDeserializer(storedEvents); 18 | 19 | var sorted = HistoryData.OrderBy(c => c.When); 20 | var list = new List(); 21 | var last = new CustomerHistoryData(); 22 | 23 | foreach (var change in sorted) 24 | { 25 | var jsSlot = new CustomerHistoryData 26 | { 27 | Id = change.Id == Guid.Empty.ToString() || change.Id == last.Id 28 | ? string.Empty 29 | : change.Id, 30 | Name = string.IsNullOrWhiteSpace(change.Name) || change.Name == last.Name 31 | ? string.Empty 32 | : change.Name, 33 | Email = string.IsNullOrWhiteSpace(change.Email) || change.Email == last.Email 34 | ? string.Empty 35 | : change.Email, 36 | BirthDate = string.IsNullOrWhiteSpace(change.BirthDate) || change.BirthDate == last.BirthDate 37 | ? string.Empty 38 | : change.BirthDate.Substring(0, 10), 39 | Action = string.IsNullOrWhiteSpace(change.Action) ? string.Empty : change.Action, 40 | When = change.When, 41 | Who = change.Who, 42 | }; 43 | 44 | list.Add(jsSlot); 45 | last = change; 46 | } 47 | 48 | return list; 49 | } 50 | 51 | private static void CustomerHistoryDeserializer(IEnumerable storedEvents) 52 | { 53 | foreach (var e in storedEvents) 54 | { 55 | var slot = new CustomerHistoryData(); 56 | dynamic values; 57 | 58 | switch (e.MessageType) 59 | { 60 | case "CustomerRegisteredEvent": 61 | values = JsonSerializer.Deserialize>(e.Data); 62 | slot.BirthDate = values["BirthDate"]; 63 | slot.Email = values["Email"]; 64 | slot.Name = values["Name"]; 65 | slot.Action = "Registered"; 66 | slot.When = values["Timestamp"]; 67 | slot.Id = values["Id"]; 68 | slot.Who = e.User; 69 | break; 70 | case "CustomerUpdatedEvent": 71 | values = JsonSerializer.Deserialize>(e.Data); 72 | slot.BirthDate = values["BirthDate"]; 73 | slot.Email = values["Email"]; 74 | slot.Name = values["Name"]; 75 | slot.Action = "Updated"; 76 | slot.When = values["Timestamp"]; 77 | slot.Id = values["Id"]; 78 | slot.Who = e.User; 79 | break; 80 | case "CustomerRemovedEvent": 81 | values = JsonSerializer.Deserialize>(e.Data); 82 | slot.Action = "Removed"; 83 | slot.When = values["Timestamp"]; 84 | slot.Id = values["Id"]; 85 | slot.Who = e.User; 86 | break; 87 | } 88 | 89 | HistoryData.Add(slot); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/StartupExtensions/AuthExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | using DDD.Infra.CrossCutting.Identity.Authorization; 4 | using DDD.Infra.CrossCutting.Identity.Data; 5 | using DDD.Infra.CrossCutting.Identity.Models; 6 | 7 | using Microsoft.AspNetCore.Authentication.JwtBearer; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Identity; 10 | using Microsoft.IdentityModel.Tokens; 11 | 12 | namespace DDD.Services.Api.StartupExtensions; 13 | 14 | public static class AuthExtension 15 | { 16 | public static IServiceCollection AddCustomizedAuth(this IServiceCollection services, IConfiguration configuration) 17 | { 18 | var secretKey = configuration.GetValue("SecretKey"); 19 | if (string.IsNullOrEmpty(secretKey)) 20 | { 21 | return services; 22 | } 23 | 24 | var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)); 25 | 26 | services.AddIdentity() 27 | .AddRoles() 28 | .AddEntityFrameworkStores() 29 | .AddDefaultTokenProviders(); 30 | 31 | var jwtAppSettingOptions = configuration.GetSection(nameof(JwtIssuerOptions)); 32 | 33 | services.Configure(options => 34 | { 35 | options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)]; 36 | options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)]; 37 | options.SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); 38 | }); 39 | 40 | var tokenValidationParameters = new TokenValidationParameters 41 | { 42 | ValidateIssuer = true, 43 | ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)], 44 | 45 | ValidateAudience = true, 46 | ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)], 47 | 48 | ValidateIssuerSigningKey = true, 49 | IssuerSigningKey = signingKey, 50 | 51 | RequireExpirationTime = false, 52 | ValidateLifetime = true, 53 | ClockSkew = TimeSpan.Zero, 54 | }; 55 | 56 | services.AddAuthentication(options => 57 | { 58 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 59 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 60 | }).AddJwtBearer(configureOptions => 61 | { 62 | configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)]; 63 | configureOptions.TokenValidationParameters = tokenValidationParameters; 64 | configureOptions.SaveToken = true; 65 | }); 66 | 67 | services.AddAuthorization(options => 68 | { 69 | var policy1 = new AuthorizationPolicyBuilder() 70 | .RequireAuthenticatedUser() 71 | .RequireRole("Admin") 72 | .AddRequirements(new ClaimRequirement("Customers_Write", "Write")) 73 | .Build(); 74 | var policy2 = new AuthorizationPolicyBuilder() 75 | .RequireAuthenticatedUser() 76 | .RequireRole("Admin") 77 | .AddRequirements(new ClaimRequirement("Customers_Remove", "Remove")) 78 | .Build(); 79 | options.AddPolicy("CanWriteCustomerData", policy1); 80 | options.AddPolicy("CanRemoveCustomerData", policy2); 81 | }); 82 | 83 | return services; 84 | } 85 | 86 | public static IApplicationBuilder UseCustomizedAuth(this IApplicationBuilder app) 87 | { 88 | app.UseAuthentication(); 89 | app.UseAuthorization(); 90 | 91 | return app; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Src/DDD.Infra.Data/Context/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using DDD.Domain.Core.Models; 8 | using DDD.Domain.Models; 9 | using DDD.Infra.Data.Mappings; 10 | 11 | using Microsoft.EntityFrameworkCore; 12 | using Microsoft.EntityFrameworkCore.ChangeTracking; 13 | 14 | namespace DDD.Infra.Data.Context; 15 | 16 | public class ApplicationDbContext : DbContext 17 | { 18 | public ApplicationDbContext(DbContextOptions options) 19 | : base(options) 20 | { 21 | } 22 | 23 | public DbSet Customers { get; set; } 24 | 25 | // public override int SaveChanges() 26 | // { 27 | // OnBeforeSaving(); 28 | // return base.SaveChanges(); 29 | // } 30 | 31 | // public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) 32 | // { 33 | // OnBeforeSaving(); 34 | // return await base.SaveChangesAsync(cancellationToken); 35 | // } 36 | 37 | public override int SaveChanges(bool acceptAllChangesOnSuccess) 38 | { 39 | OnBeforeSaving(); 40 | return base.SaveChanges(acceptAllChangesOnSuccess); 41 | } 42 | 43 | public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) 44 | { 45 | OnBeforeSaving(); 46 | return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); 47 | } 48 | 49 | protected override void OnModelCreating(ModelBuilder modelBuilder) 50 | { 51 | modelBuilder.ApplyConfiguration(new CustomerMap()); 52 | 53 | base.OnModelCreating(modelBuilder); 54 | } 55 | 56 | private void OnBeforeSaving() 57 | { 58 | var entities = ChangeTracker.Entries() 59 | .Where(x => x.Entity is EntityAudit) 60 | .ToList(); 61 | UpdateSoftDelete(entities); 62 | UpdateTimestamps(entities); 63 | } 64 | 65 | private void UpdateSoftDelete(List entries) 66 | { 67 | var filtered = entries 68 | .Where(x => x.State == EntityState.Added 69 | || x.State == EntityState.Deleted); 70 | 71 | foreach (var entry in filtered) 72 | { 73 | switch (entry.State) 74 | { 75 | case EntityState.Added: 76 | // entry.CurrentValues["IsDeleted"] = false; 77 | ((EntityAudit)entry.Entity).IsDeleted = false; 78 | break; 79 | case EntityState.Deleted: 80 | entry.State = EntityState.Modified; 81 | 82 | // entry.CurrentValues["IsDeleted"] = true; 83 | ((EntityAudit)entry.Entity).IsDeleted = true; 84 | break; 85 | } 86 | } 87 | } 88 | 89 | private void UpdateTimestamps(List entries) 90 | { 91 | var filtered = entries 92 | .Where(x => x.State == EntityState.Added 93 | || x.State == EntityState.Modified); 94 | 95 | // TODO: Get real current user id 96 | var currentUserId = 1; 97 | 98 | foreach (var entry in filtered) 99 | { 100 | if (entry.State == EntityState.Added) 101 | { 102 | ((EntityAudit)entry.Entity).CreatedAt = DateTime.UtcNow; 103 | ((EntityAudit)entry.Entity).CreatedBy = currentUserId; 104 | } 105 | 106 | ((EntityAudit)entry.Entity).UpdatedAt = DateTime.UtcNow; 107 | ((EntityAudit)entry.Entity).UpdatedBy = currentUserId; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json.Serialization; 3 | 4 | using DDD.Domain.Providers.Hubs; 5 | using DDD.Infra.CrossCutting.IoC; 6 | using DDD.Services.Api.Configurations; 7 | using DDD.Services.Api.StartupExtensions; 8 | 9 | using MediatR; 10 | 11 | using Microsoft.AspNetCore.Mvc.Versioning; 12 | 13 | var builder = WebApplication.CreateBuilder(args); 14 | 15 | // START: Variables 16 | // END: Variables 17 | 18 | // START: Custom services 19 | // ----- Database ----- 20 | builder.Services.AddCustomizedDatabase(builder.Configuration, builder.Environment); 21 | 22 | // ----- Auth ----- 23 | builder.Services.AddCustomizedAuth(builder.Configuration); 24 | 25 | // ----- Http ----- 26 | builder.Services.AddCustomizedHttp(builder.Configuration); 27 | 28 | // ----- AutoMapper ----- 29 | builder.Services.AddAutoMapperSetup(); 30 | 31 | // Adding MediatR for Domain Events and Notifications 32 | builder.Services.AddMediatR(cfg => 33 | { 34 | cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); 35 | }); 36 | 37 | // ----- Hash ----- 38 | builder.Services.AddCustomizedHash(builder.Configuration); 39 | 40 | // ----- SignalR ----- 41 | builder.Services.AddCustomizedSignalR(); 42 | 43 | // ----- Quartz ----- 44 | builder.Services.AddCustomizedQuartz(builder.Configuration); 45 | 46 | // .NET Native DI Abstraction 47 | NativeInjectorBootStrapper.RegisterServices(builder.Services); 48 | 49 | builder.Services.AddControllers() 50 | .AddJsonOptions(x => 51 | { 52 | x.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; 53 | 54 | // x.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; 55 | }); 56 | 57 | builder.Services.AddApiVersioning(opt => 58 | { 59 | opt.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1, 0); 60 | opt.AssumeDefaultVersionWhenUnspecified = true; 61 | opt.ReportApiVersions = true; 62 | opt.ApiVersionReader = ApiVersionReader.Combine( 63 | new UrlSegmentApiVersionReader(), 64 | new HeaderApiVersionReader("x-api-version"), 65 | new MediaTypeApiVersionReader("x-api-version")); 66 | }); 67 | 68 | // Add ApiExplorer to discover versions 69 | builder.Services.AddVersionedApiExplorer(setup => 70 | { 71 | setup.GroupNameFormat = "'v'VVV"; 72 | setup.SubstituteApiVersionInUrl = true; 73 | }); 74 | 75 | builder.Services.AddEndpointsApiExplorer(); 76 | 77 | // ----- Swagger UI ----- 78 | builder.Services.AddCustomizedSwagger(builder.Environment); 79 | 80 | // ----- Health check ----- 81 | builder.Services.AddCustomizedHealthCheck(builder.Configuration, builder.Environment); 82 | // END: Custom services 83 | 84 | var app = builder.Build(); 85 | 86 | // Configure the HTTP request pipeline. 87 | 88 | // START: Custom middlewares 89 | 90 | if (app.Environment.IsDevelopment()) 91 | { 92 | // ----- Error Handling ----- 93 | app.UseCustomizedErrorHandling(); 94 | } 95 | 96 | app.UseRouting(); 97 | 98 | // ----- CORS ----- 99 | app.UseCors(x => x 100 | .AllowAnyOrigin() 101 | .AllowAnyMethod() 102 | .AllowAnyHeader()); 103 | 104 | // ----- Auth ----- 105 | app.UseCustomizedAuth(); 106 | 107 | // ----- SignalR ----- 108 | app.UseCustomizedSignalR(); 109 | 110 | // ----- Quartz ----- 111 | app.UseCustomizedQuartz(); 112 | 113 | // ----- Controller ----- 114 | app.MapControllers(); 115 | 116 | // ----- SignalR ----- 117 | app.MapHub($"/hub{HubRoutes.Notification}"); 118 | 119 | // ----- Health check ----- 120 | HealthCheckExtension.UseCustomizedHealthCheck(app, builder.Environment); 121 | 122 | // ----- Swagger UI ----- 123 | app.UseCustomizedSwagger(builder.Environment); 124 | // END: Custom middlewares 125 | 126 | app.Run(); 127 | -------------------------------------------------------------------------------- /Src/DDD.Domain/CommandHandlers/CustomerCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | using DDD.Domain.Commands; 6 | using DDD.Domain.Core.Bus; 7 | using DDD.Domain.Core.Notifications; 8 | using DDD.Domain.Events; 9 | using DDD.Domain.Interfaces; 10 | using DDD.Domain.Models; 11 | 12 | using MediatR; 13 | 14 | namespace DDD.Domain.CommandHandlers; 15 | 16 | public class CustomerCommandHandler : CommandHandler, 17 | IRequestHandler, 18 | IRequestHandler, 19 | IRequestHandler 20 | { 21 | private readonly ICustomerRepository _customerRepository; 22 | private readonly IMediatorHandler _bus; 23 | 24 | public CustomerCommandHandler( 25 | ICustomerRepository customerRepository, 26 | IUnitOfWork uow, 27 | IMediatorHandler bus, 28 | INotificationHandler notifications) 29 | : base(uow, bus, notifications) 30 | { 31 | _customerRepository = customerRepository; 32 | _bus = bus; 33 | } 34 | 35 | public Task Handle(RegisterNewCustomerCommand message, CancellationToken cancellationToken) 36 | { 37 | if (!message.IsValid()) 38 | { 39 | NotifyValidationErrors(message); 40 | return Task.FromResult(false); 41 | } 42 | 43 | var customer = new Customer(Guid.NewGuid(), message.Name, message.Email, message.BirthDate); 44 | 45 | if (_customerRepository.GetByEmail(customer.Email) != null) 46 | { 47 | _bus.RaiseEvent(new DomainNotification(message.MessageType, "The customer e-mail has already been taken.")); 48 | return Task.FromResult(false); 49 | } 50 | 51 | _customerRepository.Add(customer); 52 | 53 | if (Commit()) 54 | { 55 | _bus.RaiseEvent(new CustomerRegisteredEvent(customer.Id, customer.Name, customer.Email, customer.BirthDate)); 56 | } 57 | 58 | return Task.FromResult(true); 59 | } 60 | 61 | public Task Handle(UpdateCustomerCommand message, CancellationToken cancellationToken) 62 | { 63 | if (!message.IsValid()) 64 | { 65 | NotifyValidationErrors(message); 66 | return Task.FromResult(false); 67 | } 68 | 69 | var customer = new Customer(message.Id, message.Name, message.Email, message.BirthDate); 70 | var existingCustomer = _customerRepository.GetByEmail(customer.Email); 71 | 72 | if (existingCustomer != null && existingCustomer.Id != customer.Id) 73 | { 74 | if (!existingCustomer.Equals(customer)) 75 | { 76 | _bus.RaiseEvent(new DomainNotification(message.MessageType, "The customer e-mail has already been taken.")); 77 | return Task.FromResult(false); 78 | } 79 | } 80 | 81 | _customerRepository.Update(customer); 82 | 83 | if (Commit()) 84 | { 85 | _bus.RaiseEvent(new CustomerUpdatedEvent(customer.Id, customer.Name, customer.Email, customer.BirthDate)); 86 | } 87 | 88 | return Task.FromResult(true); 89 | } 90 | 91 | public Task Handle(RemoveCustomerCommand message, CancellationToken cancellationToken) 92 | { 93 | if (!message.IsValid()) 94 | { 95 | NotifyValidationErrors(message); 96 | return Task.FromResult(false); 97 | } 98 | 99 | _customerRepository.Remove(message.Id); 100 | 101 | if (Commit()) 102 | { 103 | _bus.RaiseEvent(new CustomerRemovedEvent(message.Id)); 104 | } 105 | 106 | return Task.FromResult(true); 107 | } 108 | 109 | public void Dispose() 110 | { 111 | _customerRepository.Dispose(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Src/DDD.Services.Api/StartupExtensions/SwaggerExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Models; 2 | 3 | namespace DDD.Services.Api.StartupExtensions; 4 | 5 | public static class SwaggerExtension 6 | { 7 | public static IServiceCollection AddCustomizedSwagger(this IServiceCollection services, IWebHostEnvironment env) 8 | { 9 | if (env.IsDevelopment()) 10 | { 11 | services.AddSwaggerGen(c => 12 | { 13 | c.SwaggerDoc("v1", new OpenApiInfo 14 | { 15 | Version = "v1", 16 | Title = "ASPNET Core DDD Project", 17 | Description = string.Empty, 18 | Contact = new OpenApiContact { Name = "Xinh Nguyen", Email = "nguyentrucxjnh@gmail.com", Url = new Uri("https://ntxinh.github.io/") }, 19 | License = new OpenApiLicense() { Name = "MIT", Url = new Uri("https://github.com/ntxinh/AspNetCore-DDD/blob/master/LICENSE") }, 20 | }); 21 | c.SwaggerDoc("v2", new OpenApiInfo 22 | { 23 | Version = "v2", 24 | Title = "ASPNET Core DDD Project", 25 | Description = string.Empty, 26 | Contact = new OpenApiContact { Name = "Xinh Nguyen", Email = "nguyentrucxjnh@gmail.com", Url = new Uri("https://ntxinh.github.io/") }, 27 | License = new OpenApiLicense() { Name = "MIT", Url = new Uri("https://github.com/ntxinh/AspNetCore-DDD/blob/master/LICENSE") }, 28 | }); 29 | 30 | c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme 31 | { 32 | Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", 33 | Name = "Authorization", 34 | In = ParameterLocation.Header, 35 | Type = SecuritySchemeType.ApiKey, 36 | Scheme = "Bearer", 37 | }); 38 | 39 | c.AddSecurityRequirement(new OpenApiSecurityRequirement 40 | { 41 | { 42 | new OpenApiSecurityScheme 43 | { 44 | Reference = new OpenApiReference 45 | { 46 | Type = ReferenceType.SecurityScheme, 47 | Id = "Bearer", 48 | }, 49 | Scheme = "oauth2", 50 | Name = "Bearer", 51 | In = ParameterLocation.Header, 52 | }, 53 | new List() 54 | 55 | // new string[] { } 56 | }, 57 | }); 58 | 59 | // Add custom header request 60 | // c.OperationFilter(); 61 | }); 62 | } 63 | 64 | return services; 65 | } 66 | 67 | public static IApplicationBuilder UseCustomizedSwagger(this IApplicationBuilder app, IWebHostEnvironment env) 68 | { 69 | if (env.IsDevelopment()) 70 | { 71 | // Enable middleware to serve generated Swagger as a JSON endpoint. 72 | app.UseSwagger(); 73 | 74 | // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), 75 | // specifying the Swagger JSON endpoint. 76 | app.UseSwaggerUI(c => 77 | { 78 | c.SwaggerEndpoint("/swagger/v1/swagger.json", "ASPNET Core DDD Project API v1.0"); 79 | c.SwaggerEndpoint("/swagger/v2/swagger.json", "ASPNET Core DDD Project API v2.0"); 80 | 81 | // Show V1 first in Swagger 82 | // foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions) 83 | // { 84 | // c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", 85 | // description.GroupName.ToUpperInvariant()); 86 | // } 87 | 88 | // Show V2 first in Swagger 89 | // foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions.Reverse()) 90 | // { 91 | // options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", 92 | // description.GroupName.ToUpperInvariant()); 93 | // } 94 | }); 95 | } 96 | 97 | return app; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Http/HttpProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net.Http; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace DDD.Domain.Providers.Http; 9 | 10 | public class HttpProvider : IHttpProvider 11 | { 12 | public async Task GetAsync(HttpClient httpClient, string url, Dictionary queryParams = null, Dictionary headers = null) 13 | { 14 | try 15 | { 16 | var request = CreateGetRequest(url, queryParams, headers); 17 | var response = await httpClient.SendAsync(request); 18 | if (response.IsSuccessStatusCode) 19 | { 20 | return await ReadAsAsync(response); 21 | } 22 | 23 | return default; 24 | } 25 | catch (Exception ex) 26 | { 27 | Console.WriteLine(ex.Message); 28 | throw; 29 | } 30 | } 31 | 32 | public async Task GetStreamAsync(HttpClient httpClient, string url, Dictionary queryParams = null, Dictionary headers = null) 33 | { 34 | try 35 | { 36 | // TODO: Handle queryParams, headers 37 | return await httpClient.GetStreamAsync(url); 38 | } 39 | catch (Exception ex) 40 | { 41 | Console.WriteLine(ex.Message); 42 | throw; 43 | } 44 | } 45 | 46 | public async Task PostAsJsonAsync(HttpClient httpClient, string url, object data, Dictionary queryParams = null, Dictionary headers = null) 47 | { 48 | try 49 | { 50 | var request = CreatePostAsJsonRequest(url, data, queryParams, headers); 51 | var response = await httpClient.SendAsync(request); 52 | if (response.IsSuccessStatusCode) 53 | { 54 | return await ReadAsAsync(response); 55 | } 56 | 57 | return default(T); 58 | } 59 | catch (Exception ex) 60 | { 61 | Console.WriteLine(ex.Message); 62 | throw; 63 | } 64 | } 65 | 66 | public async Task PostAsFormUrlEncodedAsync(HttpClient httpClient, string url, Dictionary data, Dictionary queryParams = null, Dictionary headers = null) 67 | { 68 | try 69 | { 70 | var request = CreatePostAsFormUrlEncodedRequest(url, data, queryParams, headers); 71 | var response = await httpClient.SendAsync(request); 72 | if (response.IsSuccessStatusCode) 73 | { 74 | return await ReadAsAsync(response); 75 | } 76 | 77 | return default(T); 78 | } 79 | catch (Exception ex) 80 | { 81 | Console.WriteLine(ex.Message); 82 | throw; 83 | } 84 | } 85 | 86 | // START: Private Method 87 | private HttpRequestMessage CreatePostAsJsonRequest(string url, object data, Dictionary queryParams, Dictionary headers) 88 | { 89 | // TODO: Handle queryParams 90 | var dataAsString = JsonSerializer.Serialize(data); 91 | var content = new StringContent(dataAsString, System.Text.Encoding.UTF8, "application/json"); 92 | var request = new HttpRequestMessage(HttpMethod.Post, url) 93 | { 94 | Content = content, 95 | }; 96 | if (headers != null) 97 | { 98 | foreach (KeyValuePair entry in headers) 99 | { 100 | request.Headers.Add(entry.Key, entry.Value); 101 | } 102 | } 103 | 104 | return request; 105 | } 106 | 107 | private HttpRequestMessage CreatePostAsFormUrlEncodedRequest(string url, Dictionary data, Dictionary queryParams, Dictionary headers) 108 | { 109 | // TODO: Handle queryParams 110 | var request = new HttpRequestMessage(HttpMethod.Post, url) 111 | { 112 | Content = new FormUrlEncodedContent(data), 113 | }; 114 | if (headers != null) 115 | { 116 | foreach (KeyValuePair entry in headers) 117 | { 118 | request.Headers.Add(entry.Key, entry.Value); 119 | } 120 | } 121 | 122 | return request; 123 | } 124 | 125 | private HttpRequestMessage CreateGetRequest(string url, Dictionary queryParams, Dictionary headers) 126 | { 127 | // TODO: Handle queryParams 128 | var request = new HttpRequestMessage(HttpMethod.Get, url); 129 | foreach (KeyValuePair entry in headers) 130 | { 131 | request.Headers.Add(entry.Key, entry.Value); 132 | } 133 | 134 | return request; 135 | } 136 | 137 | private async Task ReadAsAsync(HttpResponseMessage response) 138 | { 139 | using var responseStream = await response.Content.ReadAsStreamAsync(); 140 | return await JsonSerializer.DeserializeAsync(responseStream); 141 | } 142 | 143 | // END: Private Method 144 | } 145 | -------------------------------------------------------------------------------- /Src/DDD.Domain/Providers/Office/OfficeProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Data; 5 | using System.IO; 6 | using System.Linq; 7 | 8 | using NPOI.SS.UserModel; 9 | using NPOI.XSSF.UserModel; 10 | 11 | namespace DDD.Domain.Providers.Office; 12 | 13 | public class OfficeProvider : IOfficeProvider 14 | { 15 | public string ExportAndUploadExcel(IList data, IList formats, string fileName) 16 | { 17 | var dt = ToDataTable(data); 18 | 19 | // dt.TableName = ""; 20 | var dataSet = new DataSet(); 21 | dataSet.Tables.Add(dt); 22 | 23 | var str = ExportExcel(dataSet, formats, fileName); 24 | return str; 25 | } 26 | 27 | // START: Static 28 | private static void CreateSheetFromDataTable(IWorkbook workbook, int dataTableIndex, DataTable dataTable, IList formats) 29 | { 30 | var tableName = string.IsNullOrEmpty(dataTable.TableName) ? $"Sheet {dataTableIndex}" : dataTable.TableName; 31 | var sheet = (XSSFSheet)workbook.CreateSheet(tableName); 32 | var columnCount = dataTable.Columns.Count; 33 | var rowCount = dataTable.Rows.Count; 34 | 35 | // create the format instance 36 | IDataFormat format = workbook.CreateDataFormat(); 37 | 38 | // add column headers 39 | var row = sheet.CreateRow(0); 40 | for (var columnIndex = 0; columnIndex < columnCount; columnIndex++) 41 | { 42 | var col = dataTable.Columns[columnIndex]; 43 | 44 | var colFormat = formats.FirstOrDefault(x => x.ColId == col.ColumnName); 45 | if (colFormat is null) 46 | { 47 | row.CreateCell(columnIndex).SetCellValue(col.ColumnName); 48 | continue; 49 | } 50 | 51 | if (colFormat.IsHide) 52 | { 53 | continue; 54 | } 55 | 56 | var cell = row.CreateCell(columnIndex); 57 | 58 | if (!string.IsNullOrEmpty(colFormat.ColName)) 59 | { 60 | cell.SetCellValue(colFormat.ColName); 61 | } 62 | 63 | // if (colFormat.IsBold) 64 | // { 65 | // var font = workbook.CreateFont(); 66 | // font.IsBold = true; 67 | // cell.CellStyle.SetFont(font); 68 | // } 69 | } 70 | 71 | // add data rows 72 | for (var rowIndex = 0; rowIndex < rowCount; rowIndex++) 73 | { 74 | var dataRow = dataTable.Rows[rowIndex]; 75 | var sheetRow = sheet.CreateRow(rowIndex + 1); 76 | 77 | for (var columnIndex = 0; columnIndex < columnCount; columnIndex++) 78 | { 79 | var cellRawValue = dataRow[columnIndex]; 80 | if (string.IsNullOrEmpty(cellRawValue.ToString())) 81 | { 82 | continue; 83 | } 84 | 85 | var col = dataTable.Columns[columnIndex]; 86 | var colFormat = formats.FirstOrDefault(x => x.ColId == col.ColumnName); 87 | if (colFormat is null) 88 | { 89 | sheetRow.CreateCell(columnIndex).SetCellValue(cellRawValue.ToString()); 90 | continue; 91 | } 92 | 93 | if (colFormat.IsHide) 94 | { 95 | continue; 96 | } 97 | 98 | var cell = sheetRow.CreateCell(columnIndex); 99 | 100 | if (col.DataType == typeof(string)) 101 | { 102 | cell.SetCellValue(cellRawValue.ToString()); 103 | } 104 | else if (col.DataType == typeof(double) || col.DataType == typeof(decimal)) 105 | { 106 | SetValueAndFormat(workbook, cell, (double)cellRawValue, format.GetFormat("$#,##")); 107 | } 108 | else if (col.DataType == typeof(short) || col.DataType == typeof(int) || col.DataType == typeof(long)) 109 | { 110 | SetValueAndFormat(workbook, cell, (int)cellRawValue, format.GetFormat("0.00")); 111 | } 112 | else if (col.DataType == typeof(DateTime)) 113 | { 114 | SetValueAndFormat(workbook, cell, (DateTime)cellRawValue, format.GetFormat("mm/dd/yyyy")); 115 | } 116 | } 117 | } 118 | 119 | // auto size columns 120 | for (var columnIndex = 0; columnIndex < columnCount; columnIndex++) 121 | { 122 | sheet.AutoSizeColumn(columnIndex); 123 | } 124 | } 125 | 126 | private static void SetValueAndFormat(IWorkbook workbook, ICell cell, int value, short formatId) 127 | { 128 | cell.SetCellValue(value); 129 | ICellStyle cellStyle = workbook.CreateCellStyle(); 130 | cellStyle.DataFormat = formatId; 131 | cell.CellStyle = cellStyle; 132 | } 133 | 134 | private static void SetValueAndFormat(IWorkbook workbook, ICell cell, double value, short formatId) 135 | { 136 | cell.SetCellValue(value); 137 | ICellStyle cellStyle = workbook.CreateCellStyle(); 138 | cellStyle.DataFormat = formatId; 139 | cell.CellStyle = cellStyle; 140 | } 141 | 142 | private static void SetValueAndFormat(IWorkbook workbook, ICell cell, DateTime value, short formatId) 143 | { 144 | // set value for the cell 145 | cell.SetCellValue(value); 146 | 147 | ICellStyle cellStyle = workbook.CreateCellStyle(); 148 | cellStyle.DataFormat = formatId; 149 | cell.CellStyle = cellStyle; 150 | } 151 | 152 | // END: Static 153 | 154 | private string ExportExcel(DataSet dataSet, IList formats, string fileName) 155 | { 156 | using (IWorkbook workbook = new XSSFWorkbook()) 157 | { 158 | for (var tableIndex = 0; tableIndex < dataSet.Tables.Count; tableIndex++) 159 | { 160 | var dt = dataSet.Tables[tableIndex]; 161 | CreateSheetFromDataTable(workbook, tableIndex, dt, formats); 162 | } 163 | 164 | var stream = new MemoryStream(); 165 | workbook.Write(stream, leaveOpen: true); 166 | 167 | // TODO: Upload 168 | 169 | stream.Close(); 170 | 171 | return string.Empty; 172 | } 173 | } 174 | 175 | private DataTable ToDataTable(IList data) 176 | { 177 | PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T)); 178 | DataTable table = new DataTable(); 179 | foreach (PropertyDescriptor prop in properties) 180 | { 181 | table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType); 182 | } 183 | 184 | foreach (T item in data) 185 | { 186 | DataRow row = table.NewRow(); 187 | foreach (PropertyDescriptor prop in properties) 188 | { 189 | row[prop.Name] = prop.GetValue(item) ?? DBNull.Value; 190 | } 191 | 192 | table.Rows.Add(row); 193 | } 194 | 195 | return table; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /Src/DDD.Infra.CrossCutting.Identity/Data/Migrations/AuthDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | 7 | namespace DDD.Infra.CrossCutting.Identity.Data.Migrations; 8 | 9 | [DbContext(typeof(AuthDbContext))] 10 | public partial class AuthDbContextModelSnapshot : ModelSnapshot 11 | { 12 | protected override void BuildModel(ModelBuilder modelBuilder) 13 | { 14 | modelBuilder 15 | .HasAnnotation("ProductVersion", "1.0.0-rc3") 16 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 17 | 18 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b => 19 | { 20 | b.Property("Id"); 21 | 22 | b.Property("ConcurrencyStamp") 23 | .IsConcurrencyToken(); 24 | 25 | b.Property("Name") 26 | .HasAnnotation("MaxLength", 256); 27 | 28 | b.Property("NormalizedName") 29 | .HasAnnotation("MaxLength", 256); 30 | 31 | b.HasKey("Id"); 32 | 33 | b.HasIndex("NormalizedName") 34 | .HasDatabaseName("RoleNameIndex"); 35 | 36 | b.ToTable("AspNetRoles"); 37 | }); 38 | 39 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => 40 | { 41 | b.Property("Id") 42 | .ValueGeneratedOnAdd(); 43 | 44 | b.Property("ClaimType"); 45 | 46 | b.Property("ClaimValue"); 47 | 48 | b.Property("RoleId") 49 | .IsRequired(); 50 | 51 | b.HasKey("Id"); 52 | 53 | b.HasIndex("RoleId"); 54 | 55 | b.ToTable("AspNetRoleClaims"); 56 | }); 57 | 58 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => 59 | { 60 | b.Property("Id") 61 | .ValueGeneratedOnAdd(); 62 | 63 | b.Property("ClaimType"); 64 | 65 | b.Property("ClaimValue"); 66 | 67 | b.Property("UserId") 68 | .IsRequired(); 69 | 70 | b.HasKey("Id"); 71 | 72 | b.HasIndex("UserId"); 73 | 74 | b.ToTable("AspNetUserClaims"); 75 | }); 76 | 77 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => 78 | { 79 | b.Property("LoginProvider"); 80 | 81 | b.Property("ProviderKey"); 82 | 83 | b.Property("ProviderDisplayName"); 84 | 85 | b.Property("UserId") 86 | .IsRequired(); 87 | 88 | b.HasKey("LoginProvider", "ProviderKey"); 89 | 90 | b.HasIndex("UserId"); 91 | 92 | b.ToTable("AspNetUserLogins"); 93 | }); 94 | 95 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => 96 | { 97 | b.Property("UserId"); 98 | 99 | b.Property("RoleId"); 100 | 101 | b.HasKey("UserId", "RoleId"); 102 | 103 | b.HasIndex("RoleId"); 104 | 105 | b.HasIndex("UserId"); 106 | 107 | b.ToTable("AspNetUserRoles"); 108 | }); 109 | 110 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserToken", b => 111 | { 112 | b.Property("UserId"); 113 | 114 | b.Property("LoginProvider"); 115 | 116 | b.Property("Name"); 117 | 118 | b.Property("Value"); 119 | 120 | b.HasKey("UserId", "LoginProvider", "Name"); 121 | 122 | b.ToTable("AspNetUserTokens"); 123 | }); 124 | 125 | modelBuilder.Entity("WebApplication1.Models.ApplicationUser", b => 126 | { 127 | b.Property("Id"); 128 | 129 | b.Property("AccessFailedCount"); 130 | 131 | b.Property("ConcurrencyStamp") 132 | .IsConcurrencyToken(); 133 | 134 | b.Property("Email") 135 | .HasAnnotation("MaxLength", 256); 136 | 137 | b.Property("EmailConfirmed"); 138 | 139 | b.Property("LockoutEnabled"); 140 | 141 | b.Property("LockoutEnd"); 142 | 143 | b.Property("NormalizedEmail") 144 | .HasAnnotation("MaxLength", 256); 145 | 146 | b.Property("NormalizedUserName") 147 | .HasAnnotation("MaxLength", 256); 148 | 149 | b.Property("PasswordHash"); 150 | 151 | b.Property("PhoneNumber"); 152 | 153 | b.Property("PhoneNumberConfirmed"); 154 | 155 | b.Property("SecurityStamp"); 156 | 157 | b.Property("TwoFactorEnabled"); 158 | 159 | b.Property("UserName") 160 | .HasAnnotation("MaxLength", 256); 161 | 162 | b.HasKey("Id"); 163 | 164 | b.HasIndex("NormalizedEmail") 165 | .HasDatabaseName("EmailIndex"); 166 | 167 | b.HasIndex("NormalizedUserName") 168 | .IsUnique() 169 | .HasDatabaseName("UserNameIndex"); 170 | 171 | b.ToTable("AspNetUsers"); 172 | }); 173 | 174 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => 175 | { 176 | b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") 177 | .WithMany("Claims") 178 | .HasForeignKey("RoleId") 179 | .OnDelete(DeleteBehavior.Cascade); 180 | }); 181 | 182 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => 183 | { 184 | b.HasOne("WebApplication1.Models.ApplicationUser") 185 | .WithMany("Claims") 186 | .HasForeignKey("UserId") 187 | .OnDelete(DeleteBehavior.Cascade); 188 | }); 189 | 190 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => 191 | { 192 | b.HasOne("WebApplication1.Models.ApplicationUser") 193 | .WithMany("Logins") 194 | .HasForeignKey("UserId") 195 | .OnDelete(DeleteBehavior.Cascade); 196 | }); 197 | 198 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => 199 | { 200 | b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") 201 | .WithMany("Users") 202 | .HasForeignKey("RoleId") 203 | .OnDelete(DeleteBehavior.Cascade); 204 | 205 | b.HasOne("WebApplication1.Models.ApplicationUser") 206 | .WithMany("Roles") 207 | .HasForeignKey("UserId") 208 | .OnDelete(DeleteBehavior.Cascade); 209 | }); 210 | } 211 | } 212 | --------------------------------------------------------------------------------