├── DotNetCoreArchitecture.Application ├── Interfaces │ ├── ITransactionalRequest.cs │ ├── IIdempotentRequest.cs │ ├── ICommandStore.cs │ ├── IDomainEventPublishRequest.cs │ ├── IDomainEventContext.cs │ ├── IDbContext.cs │ ├── IUnitOfWork.cs │ └── IRepository.cs ├── CommandBehaviours │ ├── IntegrationEventPublishBehaviour.cs │ ├── LoggingBehavior.cs │ ├── IdempotentBehaviour.cs │ ├── ValidatorBehavior.cs │ ├── DomainEventPublishBehaviour.cs │ └── TransactionBehaviour.cs ├── Validations │ └── CreatePostCommandValidator.cs ├── DotNetCoreArchitecture.Application.csproj ├── Commands │ ├── CreatePostCommand.cs │ └── CreatePostCommandHandler.cs ├── Exceptions │ ├── CommandValidatorException.cs │ ├── ForbiddenOperationException.cs │ ├── ConflictingOperationException.cs │ ├── DotNetCoreArchitectureApplicationException.cs │ └── NotFoundException.cs └── DomainEventHandlers │ └── PostCreatedDomainEventHandler.cs ├── DotNetCoreArchitecture.Domain ├── SeedWork │ ├── IAggregateRoot.cs │ ├── EntityStatus.cs │ ├── EntityArchivedDomainEvent.cs │ ├── Event.cs │ ├── Entity.cs │ ├── IntegrationalEntity.cs │ ├── Enumeration.cs │ ├── ValueObject.cs │ └── EntityBase.cs ├── DotNetCoreArchitecture.Domain.csproj ├── Events │ └── PostCreatedDomainEvent.cs ├── Aggregates │ └── PostAggregate │ │ └── Post.cs └── Exceptions │ └── DotNetCoreArchitectureDomainException.cs ├── DotNetCoreArchitecture.Api ├── Infrastructure │ ├── Services │ │ ├── IIdentityService.cs │ │ └── IdentityService.cs │ ├── ActionResults │ │ └── InternalServerErrorObjectResult.cs │ ├── Exceptions │ │ └── DotNetCoreArchitecturePresentationException.cs │ ├── AutofacModules │ │ ├── MediatorModule.cs │ │ └── ApplicationModule.cs │ ├── Extensions │ │ └── IWebHostExtensions.cs │ └── Filters │ │ └── HttpGlobalExceptionFilter.cs ├── appsettings.json ├── appsettings.Development.json ├── Controllers │ ├── BaseController.cs │ └── PostController.cs ├── Dockerfile ├── Properties │ └── launchSettings.json ├── DotNetCoreArchitecture.Api.csproj ├── Program.cs ├── Shared │ └── JwtToken.cs └── Startup.cs ├── .dockerignore ├── docker-compose.yml ├── docker-compose.override.yml ├── DotNetCoreArchitecture.Persistence ├── CommandStore.cs ├── Exceptions │ ├── DotNetCoreArchitecturePersistenceException.cs │ └── ArithMathPersistenceException.cs ├── DomainEvent.cs ├── EntityConfigurations │ ├── DomainEventConfiguration.cs │ └── PostConfiguration.cs ├── DotNetCoreArchitecture.Persistence.csproj ├── Repositories │ ├── WithSaveRepositoryDecorator.cs │ ├── SqlRepository.cs │ └── DomainEventRepositoryDecorator.cs ├── Migrations │ ├── 20191128133912_InitialMigration.cs │ ├── DotNetCoreArchitectureContextModelSnapshot.cs │ └── 20191128133912_InitialMigration.Designer.cs └── DotNetCoreArchitectureContext.cs ├── README.md ├── docker-compose.dcproj ├── LICENSE ├── DotNetCoreArchitecture.sln └── .gitignore /DotNetCoreArchitecture.Application/Interfaces/ITransactionalRequest.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCoreArchitecture.Application.Interfaces 2 | { 3 | public interface ITransactionalRequest 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Interfaces/IIdempotentRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotNetCoreArchitecture.Application.Interfaces 4 | { 5 | public interface IIdempotentRequest 6 | { 7 | Guid CommandId { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/SeedWork/IAggregateRoot.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace DotNetCoreArchitecture.SeedWork 4 | { 5 | public interface IAggregateRoot 6 | { 7 | IReadOnlyCollection DomainEvents { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Interfaces/ICommandStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace DotNetCoreArchitecture.Application.Interfaces 5 | { 6 | public interface ICommandStore 7 | { 8 | Task Exists(Guid id); 9 | 10 | Task Save(Guid id); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/SeedWork/EntityStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DotNetCoreArchitecture.SeedWork 7 | { 8 | public enum EntityStatus 9 | { 10 | Active = 1, 11 | Archived 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/DotNetCoreArchitecture.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Interfaces/IDomainEventPublishRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DotNetCoreArchitecture.Application.Interfaces 7 | { 8 | public interface IDomainEventPublishRequest 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/CommandBehaviours/IntegrationEventPublishBehaviour.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DotNetCoreArchitecture.Application.CommandBehaviours 7 | { 8 | public class IntegrationEventPublishBehaviour 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Infrastructure/Services/IIdentityService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DotNetCoreArchitecture.Api.Infrastructure.Services 7 | { 8 | public interface IIdentityService 9 | { 10 | Guid GetUserIdentity(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/Events/PostCreatedDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.SeedWork; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace DotNetCoreArchitecture.Domain.Events 7 | { 8 | public class PostCreatedDomainEvent : Event 9 | { 10 | public string Name { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionString": "User ID=postgres;Password=password;Host=dotnetcorearchitecture.sql;Port=5432;Database=dotnetcorearchitecture;Pooling=true;", 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | }, 10 | "AllowedHosts": "*" 11 | } 12 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionString": "User ID=postgres;Password=password;Host=dotnetcorearchitecture.sql;Port=5432;Database=dotnetcorearchitecture;Pooling=true;", 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | }, 10 | "AllowedHosts": "*" 11 | } 12 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Interfaces/IDomainEventContext.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.SeedWork; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace DotNetCoreArchitecture.Application.Interfaces 8 | { 9 | public interface IDomainEventContext 10 | { 11 | IEnumerable GetDomainEvents(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Validations/CreatePostCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Application.Commands; 2 | using FluentValidation; 3 | 4 | namespace ArithMath.Api.ApplicationLayer.Validations 5 | { 6 | public class CreatePostCommandValidator : AbstractValidator 7 | { 8 | public CreatePostCommandValidator() 9 | { 10 | 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Interfaces/IDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace DotNetCoreArchitecture.Application.Interfaces 7 | { 8 | public interface IDbContext 9 | { 10 | Task BeginTransactionAsync(); 11 | Task CommitTransactionAsync(); 12 | void RollbackTransaction(); 13 | Task RetryOnExceptionAsync(Func func); 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Interfaces/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace DotNetCoreArchitecture.Application.Interfaces 6 | { 7 | public interface IUnitOfWork : IDisposable 8 | { 9 | Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); 10 | Task SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/SeedWork/EntityArchivedDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DotNetCoreArchitecture.SeedWork 7 | { 8 | public class EntityArchivedDomainEvent : Event 9 | { 10 | public TIdentity Id { get; private set; } 11 | 12 | public EntityArchivedDomainEvent(TIdentity id) 13 | { 14 | Id = id; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | dotnetcorearchitecture.api: 5 | image: ${DOCKER_REGISTRY-}dotnetcorearchitectureapi 6 | build: 7 | context: . 8 | dockerfile: DotNetCoreArchitecture.Api/Dockerfile 9 | depends_on: 10 | - dotnetcorearchitecture.sql 11 | 12 | dotnetcorearchitecture.sql: 13 | image: postgres 14 | environment: 15 | POSTGRES_USER: postgres 16 | POSTGRES_PASSWORD: password 17 | POSTGRES_DB: dotnetcorearchitecture 18 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Interfaces/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Threading.Tasks; 5 | 6 | namespace DotNetCoreArchitecture.Application.Interfaces 7 | { 8 | public interface IRepository 9 | { 10 | Task GetByIdAsync(Guid id); 11 | 12 | Task AddAsync(T aggregate); 13 | 14 | Task RemoveAsync(T aggregate); 15 | 16 | Task UpdateAsync(T aggregate); 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | dotnetcorearchitecture.api: 5 | environment: 6 | - ASPNETCORE_ENVIRONMENT=Development 7 | - ASPNETCORE_URLS=https://+:443;http://+:80 8 | - ASPNETCORE_HTTPS_PORT=44323 9 | ports: 10 | - "15240:80" 11 | - "44323:443" 12 | volumes: 13 | - ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro 14 | - ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro 15 | dotnetcorearchitecture.sql: 16 | ports: 17 | - "5431:5432" -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/DotNetCoreArchitecture.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/CommandStore.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Application.Interfaces; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace DotNetCoreArchitecture.Persistence 8 | { 9 | public class CommandStore : ICommandStore 10 | { 11 | public Task Exists(Guid id) 12 | { 13 | return Task.FromResult(false); 14 | } 15 | 16 | public Task Save(Guid id) 17 | { 18 | return Task.CompletedTask; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/SeedWork/Event.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace DotNetCoreArchitecture.SeedWork 7 | { 8 | public abstract class Event : INotification 9 | { 10 | public DateTime CreateDate { get; } 11 | public bool IsCommitted { get; private set; } 12 | public string EventType { get; private set; } 13 | 14 | protected Event() 15 | { 16 | CreateDate = DateTime.Now; 17 | IsCommitted = false; 18 | EventType = GetType().Name; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace DotNetCoreArchitecture.Api.Infrastructure.ActionResults 9 | { 10 | public class InternalServerErrorObjectResult : ObjectResult 11 | { 12 | public InternalServerErrorObjectResult(object error) 13 | : base(error) 14 | { 15 | StatusCode = StatusCodes.Status500InternalServerError; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Commands/CreatePostCommand.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Application.Interfaces; 2 | using MediatR; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Runtime.Serialization; 6 | using System.Text; 7 | 8 | namespace DotNetCoreArchitecture.Application.Commands 9 | { 10 | [DataContract] 11 | public class CreatePostCommand : IRequest, ITransactionalRequest, IDomainEventPublishRequest, IIdempotentRequest 12 | { 13 | [DataMember] 14 | public string Name { get; set; } 15 | public Guid UserId { get; set; } 16 | public Guid CommandId { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/Exceptions/DotNetCoreArchitecturePersistenceException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotNetCoreArchitecture.Persistence.Exceptions 4 | { 5 | public class DotNetCoreArchitecturePersistenceException : Exception 6 | { 7 | public DotNetCoreArchitecturePersistenceException() 8 | { 9 | } 10 | 11 | public DotNetCoreArchitecturePersistenceException(string message) : base(message) 12 | { 13 | } 14 | 15 | public DotNetCoreArchitecturePersistenceException(string message, Exception innerException) : base(message, innerException) 16 | { 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Exceptions/CommandValidatorException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DotNetCoreArchitecture.Application.Exceptions 7 | { 8 | public class CommandValidatorException : DotNetCoreArchitectureApplicationException 9 | { 10 | public CommandValidatorException() 11 | { 12 | } 13 | 14 | public CommandValidatorException(string message) : base(message) 15 | { 16 | } 17 | 18 | public CommandValidatorException(string message, Exception innerException) : base(message, innerException) 19 | { 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Exceptions/ForbiddenOperationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DotNetCoreArchitecture.Application.Exceptions 7 | { 8 | public class ForbiddenOperationException : DotNetCoreArchitectureApplicationException 9 | { 10 | public ForbiddenOperationException() 11 | { 12 | } 13 | 14 | public ForbiddenOperationException(string message) : base(message) 15 | { 16 | } 17 | 18 | public ForbiddenOperationException(string message, Exception innerException) : base(message, innerException) 19 | { 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/DomainEventHandlers/PostCreatedDomainEventHandler.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Domain.Events; 2 | using MediatR; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace DotNetCoreArchitecture.Application.DomainEventHandlers 10 | { 11 | public class PostCreatedDomainEventHandler : INotificationHandler 12 | { 13 | public Task Handle(PostCreatedDomainEvent notification, CancellationToken cancellationToken) 14 | { 15 | // handle notification 16 | 17 | return Task.CompletedTask; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/Aggregates/PostAggregate/Post.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Domain.Events; 2 | using DotNetCoreArchitecture.SeedWork; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace DotNetCoreArchitecture.Domain.Aggregates.PostAggregate 8 | { 9 | public class Post : EntityBase, IAggregateRoot 10 | { 11 | public string Name { get; private set; } 12 | 13 | public Post(string name) 14 | { 15 | Id = Guid.NewGuid(); 16 | Name = name; 17 | 18 | AddDomainEvent(new PostCreatedDomainEvent 19 | { 20 | Name = Name 21 | }); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Exceptions/ConflictingOperationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DotNetCoreArchitecture.Application.Exceptions 7 | { 8 | public class ConflictingOperationException : DotNetCoreArchitectureApplicationException 9 | { 10 | public ConflictingOperationException() 11 | { 12 | } 13 | 14 | public ConflictingOperationException(string message) : base(message) 15 | { 16 | } 17 | 18 | public ConflictingOperationException(string message, Exception innerException) : base(message, innerException) 19 | { 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/Exceptions/DotNetCoreArchitectureDomainException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.Serialization; 4 | using System.Text; 5 | 6 | namespace DotNetCoreArchitecture.Domain.Exceptions 7 | { 8 | public class DotNetCoreArchitectureDomainException : Exception 9 | { 10 | public DotNetCoreArchitectureDomainException() 11 | { 12 | } 13 | 14 | public DotNetCoreArchitectureDomainException(string message) : base(message) 15 | { 16 | } 17 | 18 | public DotNetCoreArchitectureDomainException(string message, Exception innerException) : base(message, innerException) 19 | { 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using System; 3 | using DotNetCoreArchitecture.Api.Infrastructure.Services; 4 | 5 | namespace DotNetCoreArchitecture.Api.Controllers 6 | { 7 | public abstract class BaseController : Controller 8 | { 9 | private readonly IIdentityService _identityService; 10 | 11 | protected BaseController(IIdentityService identityService) 12 | { 13 | _identityService = identityService ?? throw new ArgumentNullException(nameof(identityService)); 14 | } 15 | 16 | protected Guid ExtractUserFromToken() 17 | { 18 | return _identityService.GetUserIdentity(); 19 | } 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/DomainEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace DotNetCoreArchitecture.Persistence 8 | { 9 | public class DomainEvent 10 | { 11 | public Guid Id { get; private set; } 12 | [Column(TypeName = "jsonb")] 13 | public string EventData { get; private set; } 14 | public string EventType { get; private set; } 15 | 16 | public DomainEvent(Guid id, string eventData, string eventType) 17 | { 18 | Id = id; 19 | EventData = eventData; 20 | EventType = eventType; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📖 DotNetCoreArchitecture 2 | 3 | This is an example of a clean architecture template using: 4 | * .NET Core 3.0 5 | * Docker 6 | * Entity Framework Core 7 | * MediatR 8 | 9 | # :pencil: Article 10 | See Medium article about [How to be more declarative when implementing CQRS with MediatR in .Net Core](https://medium.com/@akojavakhishvili/how-to-be-more-declarative-when-implementing-cqrs-with-mediatr-in-net-core-c8b9ff7ea2a4) 11 | 12 | # :whale: Running with Docker 13 | You must install Docker & Docker Compose first. 14 | ```bash 15 | docker-compose -f docker-compose.yml -f docker-compose.override.yml up 16 | ``` 17 | 18 | ## The MIT License 19 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 20 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/Exceptions/ArithMathPersistenceException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Threading.Tasks; 6 | 7 | namespace DotNetCoreArchitecture.Persistence.Exceptions 8 | { 9 | public class RockPaperScissorsPersistenceException : Exception 10 | { 11 | public RockPaperScissorsPersistenceException() 12 | { 13 | } 14 | 15 | public RockPaperScissorsPersistenceException(string message) : base(message) 16 | { 17 | } 18 | 19 | public RockPaperScissorsPersistenceException(string message, Exception innerException) : base(message, innerException) 20 | { 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Exceptions/DotNetCoreArchitectureApplicationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Threading.Tasks; 6 | 7 | namespace DotNetCoreArchitecture.Application.Exceptions 8 | { 9 | public class DotNetCoreArchitectureApplicationException : Exception 10 | { 11 | public DotNetCoreArchitectureApplicationException() 12 | { 13 | } 14 | 15 | public DotNetCoreArchitectureApplicationException(string message) : base(message) 16 | { 17 | } 18 | 19 | public DotNetCoreArchitectureApplicationException(string message, Exception innerException) : base(message, innerException) 20 | { 21 | } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | EXPOSE 443 5 | 6 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster AS build 7 | WORKDIR /src 8 | COPY ["DotNetCoreArchitecture.Api/DotNetCoreArchitecture.Api.csproj", "DotNetCoreArchitecture.Api/"] 9 | RUN dotnet restore "DotNetCoreArchitecture.Api/DotNetCoreArchitecture.Api.csproj" 10 | COPY . . 11 | WORKDIR "/src/DotNetCoreArchitecture.Api" 12 | RUN dotnet build "DotNetCoreArchitecture.Api.csproj" -c Release -o /app/build 13 | 14 | FROM build AS publish 15 | RUN dotnet publish "DotNetCoreArchitecture.Api.csproj" -c Release -o /app/publish 16 | 17 | FROM base AS final 18 | WORKDIR /app 19 | COPY --from=publish /app/publish . 20 | ENTRYPOINT ["dotnet", "DotNetCoreArchitecture.Api.dll"] -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Infrastructure/Exceptions/DotNetCoreArchitecturePresentationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Threading.Tasks; 6 | 7 | namespace DotNetCoreArchitecture.Api.Infrastructure.Exceptions 8 | { 9 | public class DotNetCoreArchitecturePresentationException : Exception 10 | { 11 | public DotNetCoreArchitecturePresentationException() 12 | { 13 | } 14 | 15 | public DotNetCoreArchitecturePresentationException(string message) : base(message) 16 | { 17 | } 18 | 19 | public DotNetCoreArchitecturePresentationException(string message, Exception innerException) : base(message, innerException) 20 | { 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docker-compose.dcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.1 5 | Linux 6 | 0a74560c-a692-4d2b-89dc-d4cd3441d33d 7 | LaunchBrowser 8 | {Scheme}://localhost:{ServicePort} 9 | dotnetcorearchitecture.api 10 | 11 | 12 | 13 | docker-compose.yml 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Exceptions/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Threading.Tasks; 6 | 7 | namespace DotNetCoreArchitecture.Application.Exceptions 8 | { 9 | public class NotFoundException : ApplicationException 10 | { 11 | public NotFoundException() 12 | { 13 | } 14 | 15 | public NotFoundException(string message) : base(message) 16 | { 17 | } 18 | 19 | public NotFoundException(string message, Exception innerException) : base(message, innerException) 20 | { 21 | } 22 | 23 | protected NotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) 24 | { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/EntityConfigurations/DomainEventConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace DotNetCoreArchitecture.Persistence.EntityConfigurations 9 | { 10 | public class DomainEventConfiguration : IEntityTypeConfiguration 11 | { 12 | public void Configure(EntityTypeBuilder builder) 13 | { 14 | builder.ToTable("DomainEvents", DotNetCoreArchitectureContext.DefaultSchema); 15 | 16 | builder.HasKey(p => p.Id); 17 | 18 | 19 | builder.Property(p => p.EventData).IsRequired(); 20 | 21 | builder.Property(p => p.EventType).IsRequired(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/EntityConfigurations/PostConfiguration.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Domain.Aggregates.PostAggregate; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace DotNetCoreArchitecture.Persistence.EntityConfigurations 6 | { 7 | public class PostConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.ToTable("Post", DotNetCoreArchitectureContext.DefaultSchema); 12 | 13 | builder.HasKey(p => p.Id); 14 | builder.Property(p => p.Status); 15 | builder.Property(p => p.Name); 16 | 17 | builder.ForNpgsqlUseXminAsConcurrencyToken(); 18 | 19 | builder.Ignore(b => b.DomainEvents); 20 | 21 | builder.Property("RowVersion") 22 | .IsRowVersion(); 23 | 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/Commands/CreatePostCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Application.Interfaces; 2 | using DotNetCoreArchitecture.Domain.Aggregates.PostAggregate; 3 | using MediatR; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace DotNetCoreArchitecture.Application.Commands 11 | { 12 | public class CreatePostCommandHandler : AsyncRequestHandler 13 | { 14 | private readonly IRepository _repository; 15 | 16 | public CreatePostCommandHandler(IRepository repository) 17 | { 18 | _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 19 | } 20 | 21 | protected async override Task Handle(CreatePostCommand request, CancellationToken cancellationToken) 22 | { 23 | var post = new Post(request.Name); 24 | 25 | await _repository.AddAsync(post); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/CommandBehaviours/LoggingBehavior.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace DotNetCoreArchitecture.Application.CommandBehaviours 10 | { 11 | public class LoggingBehavior : IPipelineBehavior 12 | { 13 | private readonly ILogger> _logger; 14 | public LoggingBehavior(ILogger> logger) => _logger = logger; 15 | 16 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 17 | { 18 | _logger.LogInformation($"Handling {typeof(TRequest).Name}"); 19 | var response = await next(); 20 | _logger.LogInformation($"Handled {typeof(TResponse).Name}"); 21 | return response; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aleksandre Javakhishvili 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 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:15230", 7 | "sslPort": 44313 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "DotNetCoreArchitecture.Api": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | }, 26 | "Docker": { 27 | "commandName": "Docker", 28 | "launchBrowser": true, 29 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 30 | "environmentVariables": { 31 | "ASPNETCORE_URLS": "https://+:443;http://+:80", 32 | "ASPNETCORE_HTTPS_PORT": "44314" 33 | }, 34 | "httpPort": 15232, 35 | "useSSL": true, 36 | "sslPort": 44314 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/DotNetCoreArchitecture.Persistence.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | all 10 | runtime; build; native; contentfiles; analyzers; buildtransitive 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Infrastructure/Services/IdentityService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | using System.Threading.Tasks; 6 | using DotNetCoreArchitecture.Api.Infrastructure.Exceptions; 7 | using Microsoft.AspNetCore.Http; 8 | 9 | namespace DotNetCoreArchitecture.Api.Infrastructure.Services 10 | { 11 | public class IdentityService : IIdentityService 12 | { 13 | private readonly IHttpContextAccessor _context; 14 | 15 | public IdentityService(IHttpContextAccessor context) 16 | { 17 | _context = context ?? throw new ArgumentNullException(nameof(context)); 18 | } 19 | 20 | 21 | public Guid GetUserIdentity() 22 | { 23 | return Guid.NewGuid(); 24 | 25 | var user = _context.HttpContext.User; 26 | var sub = user.FindFirst(ClaimTypes.NameIdentifier)?.Value; 27 | 28 | if (!Guid.TryParse(sub, out var result)) 29 | { 30 | throw new DotNetCoreArchitecturePresentationException($"Invalid id specified, cannot parse to guid {sub}"); 31 | } 32 | 33 | return result; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/CommandBehaviours/IdempotentBehaviour.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Application.Interfaces; 2 | using MediatR; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace DotNetCoreArchitecture.Application.CommandBehaviours 10 | { 11 | public class IdempotentBehaviour : IPipelineBehavior 12 | where TRequest : IIdempotentRequest 13 | { 14 | private readonly ICommandStore _commandStore; 15 | 16 | public IdempotentBehaviour(ICommandStore commandStore) 17 | { 18 | _commandStore = commandStore ?? throw new ArgumentNullException(nameof(commandStore)); 19 | } 20 | 21 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 22 | { 23 | var exists = await _commandStore.Exists(request.CommandId); 24 | 25 | if (exists) 26 | { 27 | return default; 28 | } 29 | 30 | var response = await next(); 31 | 32 | await _commandStore.Save(request.CommandId); 33 | 34 | return response; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/DotNetCoreArchitecture.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | cf960d00-7e7f-4830-8d99-489326d06f62 6 | Linux 7 | ..\docker-compose.dcproj 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Controllers/PostController.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Api.Infrastructure.Services; 2 | using DotNetCoreArchitecture.Application.Commands; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Net; 9 | using System.Threading.Tasks; 10 | 11 | namespace DotNetCoreArchitecture.Api.Controllers 12 | { 13 | [Route("api/v1/posts")] 14 | [ApiController] 15 | public class PostController : BaseController 16 | { 17 | private readonly IMediator _mediator; 18 | 19 | public PostController(IMediator mediator, IIdentityService identityService) : base(identityService) 20 | { 21 | _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 22 | } 23 | 24 | [HttpPost] 25 | [ProducesResponseType((int)HttpStatusCode.OK)] 26 | [ProducesResponseType((int)HttpStatusCode.BadRequest)] 27 | [ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)] 28 | public async Task Post([FromBody] CreatePostCommand command) 29 | { 30 | var result = await _mediator.Send(new CreatePostCommand 31 | { 32 | Name = command.Name, 33 | UserId = ExtractUserFromToken(), 34 | CommandId = Guid.NewGuid() 35 | }); 36 | 37 | return Ok(result); 38 | } 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/CommandBehaviours/ValidatorBehavior.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Application.Exceptions; 2 | using FluentValidation; 3 | using MediatR; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.ComponentModel.DataAnnotations; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using ValidationException = FluentValidation.ValidationException; 11 | 12 | namespace DotNetCoreArchitecture.Application.CommandBehaviours 13 | { 14 | public class ValidatorBehavior : IPipelineBehavior 15 | { 16 | private readonly IValidator[] _validators; 17 | public ValidatorBehavior(IValidator[] validators) => _validators = validators; 18 | 19 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 20 | { 21 | var failures = _validators 22 | .Select(v => v.Validate(request)) 23 | .SelectMany(result => result.Errors) 24 | .Where(error => error != null) 25 | .ToList(); 26 | 27 | if (failures.Any()) 28 | { 29 | throw new CommandValidatorException( 30 | $"Command Validation Errors for type {typeof(TRequest).Name}", new ValidationException("Validation exception", failures)); 31 | } 32 | 33 | var response = await next(); 34 | return response; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/Repositories/WithSaveRepositoryDecorator.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Application.Interfaces; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | 7 | namespace DotNetCoreArchitecture.Persistence.Repositories 8 | { 9 | public class WithSaveRepositoryDecorator : IRepository where T : class 10 | { 11 | protected readonly DotNetCoreArchitectureContext Context; 12 | 13 | private readonly IRepository _repository; 14 | 15 | public WithSaveRepositoryDecorator(IRepository repository, DotNetCoreArchitectureContext context) 16 | { 17 | Context = context; 18 | _repository = repository; 19 | } 20 | 21 | public async Task GetByIdAsync(Guid id) 22 | { 23 | return await _repository.GetByIdAsync(id); 24 | } 25 | 26 | public async Task AddAsync(T aggregate) 27 | { 28 | var res = await _repository.AddAsync(aggregate); 29 | 30 | await Context.SaveChangesAsync(); 31 | 32 | return res; 33 | } 34 | 35 | 36 | public async Task RemoveAsync(T aggregate) 37 | { 38 | await _repository.RemoveAsync(aggregate); 39 | 40 | await Context.SaveChangesAsync(); 41 | } 42 | 43 | public async Task UpdateAsync(T aggregate) 44 | { 45 | var res = await _repository.UpdateAsync(aggregate); 46 | 47 | await Context.SaveChangesAsync(); 48 | 49 | return res; 50 | } 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/SeedWork/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace DotNetCoreArchitecture.SeedWork 6 | { 7 | public abstract class Entity : EntityBase 8 | { 9 | private int? _requestedHashCode; 10 | 11 | public override bool Equals(object obj) 12 | { 13 | if (!(obj is Entity)) 14 | return false; 15 | 16 | if (object.ReferenceEquals(this, obj)) 17 | return true; 18 | 19 | if (this.GetType() != obj.GetType()) 20 | return false; 21 | 22 | var item = (Entity)obj; 23 | 24 | if (item.IsTransient() || this.IsTransient()) 25 | return false; 26 | else 27 | return item.Id == this.Id; 28 | } 29 | 30 | public override int GetHashCode() 31 | { 32 | if (!IsTransient()) 33 | { 34 | if (!_requestedHashCode.HasValue) 35 | _requestedHashCode = this.Id.GetHashCode() ^ 31; 36 | 37 | return _requestedHashCode.Value; 38 | } 39 | else 40 | return base.GetHashCode(); 41 | } 42 | 43 | public static bool operator ==(Entity left, Entity right) 44 | { 45 | return left?.Equals(right) ?? Equals(right, null); 46 | } 47 | 48 | public static bool operator !=(Entity left, Entity right) 49 | { 50 | return !(left == right); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/SeedWork/IntegrationalEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotNetCoreArchitecture.SeedWork 4 | { 5 | public abstract class IntegrationalEntity : EntityBase 6 | { 7 | private int? _requestedHashCode; 8 | 9 | public override bool Equals(object obj) 10 | { 11 | if (!(obj is IntegrationalEntity)) 12 | return false; 13 | 14 | if (ReferenceEquals(this, obj)) 15 | return true; 16 | 17 | if (GetType() != obj.GetType()) 18 | return false; 19 | 20 | var item = (IntegrationalEntity)obj; 21 | 22 | if (item.IsTransient() || IsTransient()) 23 | return false; 24 | 25 | return item.Id == this.Id; 26 | } 27 | 28 | public override int GetHashCode() 29 | { 30 | if (IsTransient()) return base.GetHashCode(); 31 | 32 | if (!_requestedHashCode.HasValue) 33 | _requestedHashCode = this.Id.GetHashCode() ^ 31; // XOR for random distribution (http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx) 34 | 35 | return _requestedHashCode.Value; 36 | } 37 | 38 | public static bool operator ==(IntegrationalEntity left, IntegrationalEntity right) 39 | { 40 | return left?.Equals(right) ?? Equals(right, null); 41 | } 42 | 43 | public static bool operator !=(IntegrationalEntity left, IntegrationalEntity right) 44 | { 45 | return !(left == right); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/CommandBehaviours/DomainEventPublishBehaviour.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Application.Interfaces; 2 | using MediatR; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace DotNetCoreArchitecture.Application.CommandBehaviours 11 | { 12 | public class DomainEventPublishBehaviour : IPipelineBehavior 13 | where TRequest : IDomainEventPublishRequest 14 | { 15 | private readonly ILogger> _logger; 16 | private readonly IDomainEventContext _dbContext; 17 | private readonly IMediator _mediator; 18 | 19 | public DomainEventPublishBehaviour(IDomainEventContext dbContext, 20 | IMediator mediator, 21 | ILogger> logger) 22 | { 23 | _dbContext = dbContext ?? throw new ArgumentException(nameof(dbContext)); 24 | _mediator = mediator ?? throw new ArgumentException(nameof(mediator)); 25 | _logger = logger ?? throw new ArgumentException(nameof(ILogger)); 26 | } 27 | 28 | public async Task Handle(TRequest request, CancellationToken cancellationToken, 29 | RequestHandlerDelegate next) 30 | { 31 | TResponse response = await next(); 32 | 33 | var events = _dbContext.GetDomainEvents().ToList(); 34 | 35 | foreach (var @event in events) 36 | { 37 | await _mediator.Publish(@event); 38 | } 39 | 40 | return response; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/Repositories/SqlRepository.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Application.Interfaces; 2 | using DotNetCoreArchitecture.SeedWork; 3 | using Microsoft.EntityFrameworkCore; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using System.Threading.Tasks; 9 | 10 | namespace DotNetCoreArchitecture.Persistence.Repositories 11 | { 12 | public class SqlRepository : IRepository 13 | where TEntity : EntityBase 14 | { 15 | protected readonly DotNetCoreArchitectureContext Context; 16 | 17 | public SqlRepository(DotNetCoreArchitectureContext context) 18 | { 19 | Context = context ?? throw new ArgumentNullException(nameof(context)); 20 | } 21 | 22 | public async Task AddAsync(TEntity aggregate) 23 | { 24 | var res = await Context.Set().AddAsync(aggregate); 25 | 26 | return res.Entity; 27 | } 28 | 29 | public async Task AddListAsync(IEnumerable aggregates) 30 | { 31 | await Context.Set().AddRangeAsync(aggregates); 32 | } 33 | 34 | public Task RemoveAsync(TEntity aggregate) 35 | { 36 | Context.Entry(aggregate).State = EntityState.Deleted; 37 | 38 | return Task.CompletedTask; 39 | } 40 | 41 | public virtual async Task GetByIdAsync(Guid id) 42 | { 43 | var res = await Context.Set().SingleOrDefaultAsync(x => x.Status == EntityStatus.Active && x.Id.Equals(id)); 44 | 45 | return res; 46 | } 47 | 48 | public Task UpdateAsync(TEntity aggregate) 49 | { 50 | Context.Set().Update(aggregate); 51 | 52 | return Task.FromResult(aggregate); 53 | } 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/SeedWork/Enumeration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace DotNetCoreArchitecture.SeedWork 7 | { 8 | public abstract class Enumeration : IComparable 9 | { 10 | public string Name { get; private set; } 11 | 12 | public int Id { get; private set; } 13 | 14 | protected Enumeration() { } 15 | 16 | protected Enumeration(int id, string name) 17 | { 18 | Id = id; 19 | Name = name; 20 | } 21 | 22 | public override string ToString() => Name; 23 | 24 | public static IEnumerable GetAll() where T : Enumeration, new() 25 | { 26 | var type = typeof(T); 27 | var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); 28 | 29 | foreach (var info in fields) 30 | { 31 | var instance = new T(); 32 | 33 | if (info.GetValue(instance) is T locatedValue) 34 | yield return locatedValue; 35 | } 36 | } 37 | 38 | public override bool Equals(object obj) 39 | { 40 | if (!(obj is Enumeration otherValue)) 41 | return false; 42 | 43 | var typeMatches = GetType() == obj.GetType(); 44 | var valueMatches = Id.Equals(otherValue.Id); 45 | 46 | return typeMatches && valueMatches; 47 | } 48 | 49 | public override int GetHashCode() => Id.GetHashCode(); 50 | 51 | public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue) 52 | { 53 | var absoluteDifference = Math.Abs(firstValue.Id - secondValue.Id); 54 | return absoluteDifference; 55 | } 56 | 57 | 58 | public int CompareTo(object other) => Id.CompareTo(((Enumeration)other).Id); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/SeedWork/ValueObject.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace DotNetCoreArchitecture.SeedWork 5 | { 6 | public abstract class ValueObject 7 | { 8 | protected static bool EqualOperator(ValueObject left, ValueObject right) 9 | { 10 | if (left is null ^ right is null) 11 | { 12 | return false; 13 | } 14 | return left is null || left.Equals(right); 15 | } 16 | 17 | protected static bool NotEqualOperator(ValueObject left, ValueObject right) 18 | { 19 | return !(EqualOperator(left, right)); 20 | } 21 | 22 | protected abstract IEnumerable GetAtomicValues(); 23 | 24 | public override bool Equals(object obj) 25 | { 26 | if (obj == null || obj.GetType() != GetType()) 27 | { 28 | return false; 29 | } 30 | var other = (ValueObject)obj; 31 | var thisValues = GetAtomicValues().GetEnumerator(); 32 | var otherValues = other.GetAtomicValues().GetEnumerator(); 33 | while (thisValues.MoveNext() && otherValues.MoveNext()) 34 | { 35 | if (thisValues.Current is null ^ otherValues.Current is null) 36 | { 37 | return false; 38 | } 39 | if (thisValues.Current != null && !thisValues.Current.Equals(otherValues.Current)) 40 | { 41 | return false; 42 | } 43 | } 44 | return !thisValues.MoveNext() && !otherValues.MoveNext(); 45 | } 46 | 47 | public override int GetHashCode() 48 | { 49 | return GetAtomicValues() 50 | .Select(x => x != null ? x.GetHashCode() : 0) 51 | .Aggregate((x, y) => x ^ y); 52 | } 53 | 54 | public ValueObject GetCopy() 55 | { 56 | return MemberwiseClone() as ValueObject; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Infrastructure/AutofacModules/MediatorModule.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using FluentValidation; 3 | using MediatR; 4 | using System.Linq; 5 | using System.Reflection; 6 | using DotNetCoreArchitecture.Application.CommandBehaviours; 7 | using DotNetCoreArchitecture.Application.Commands; 8 | using ArithMath.Api.ApplicationLayer.Validations; 9 | using DotNetCoreArchitecture.Application.DomainEventHandlers; 10 | 11 | namespace DotNetCoreArchitecture.Api.Infrastructure.AutofacModules 12 | { 13 | public class MediatorModule : Autofac.Module 14 | { 15 | protected override void Load(ContainerBuilder builder) 16 | { 17 | builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly) 18 | .AsImplementedInterfaces(); 19 | 20 | builder.RegisterAssemblyTypes(typeof(CreatePostCommand).GetTypeInfo().Assembly) 21 | .AsClosedTypesOf(typeof(IRequestHandler<,>)); 22 | 23 | builder 24 | .RegisterAssemblyTypes(typeof(CreatePostCommandValidator).GetTypeInfo().Assembly) 25 | .Where(t => t.IsClosedTypeOf(typeof(IValidator<>))) 26 | .AsImplementedInterfaces(); 27 | 28 | builder 29 | .RegisterAssemblyTypes(typeof(PostCreatedDomainEventHandler).GetTypeInfo().Assembly) 30 | .AsClosedTypesOf(typeof(INotificationHandler<>)); 31 | 32 | 33 | builder.Register(ctx => 34 | { 35 | var c = ctx.Resolve(); 36 | return t => c.Resolve(t); 37 | }); 38 | 39 | builder.RegisterGeneric(typeof(LoggingBehavior<,>)).As(typeof(IPipelineBehavior<,>)); 40 | builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).As(typeof(IPipelineBehavior<,>)); 41 | builder.RegisterGeneric(typeof(TransactionBehaviour<,>)).As(typeof(IPipelineBehavior<,>)); 42 | builder.RegisterGeneric(typeof(DomainEventPublishBehaviour<,>)).As(typeof(IPipelineBehavior<,>)); 43 | builder.RegisterGeneric(typeof(IdempotentBehaviour<,>)).As(typeof(IPipelineBehavior<,>)); 44 | 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Application/CommandBehaviours/TransactionBehaviour.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Application.Interfaces; 2 | using MediatR; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace DotNetCoreArchitecture.Application.CommandBehaviours 11 | { 12 | public class TransactionBehaviour : IPipelineBehavior where TRequest 13 | : ITransactionalRequest 14 | { 15 | private readonly ILogger> _logger; 16 | private readonly IDbContext _dbContext; 17 | 18 | public TransactionBehaviour(IDbContext dbContext, 19 | ILogger> logger) 20 | { 21 | _dbContext = dbContext ?? throw new ArgumentException(nameof(dbContext)); 22 | _logger = logger ?? throw new ArgumentException(nameof(ILogger)); 23 | } 24 | 25 | public async Task Handle(TRequest request, CancellationToken cancellationToken, 26 | RequestHandlerDelegate next) 27 | { 28 | TResponse response = default; 29 | 30 | try 31 | { 32 | await _dbContext.RetryOnExceptionAsync(async () => 33 | { 34 | _logger.LogInformation($"Begin transaction {typeof(TRequest).Name}"); 35 | 36 | await _dbContext.BeginTransactionAsync(); 37 | 38 | response = await next(); 39 | 40 | await _dbContext.CommitTransactionAsync(); 41 | 42 | _logger.LogInformation($"Committed transaction {typeof(TRequest).Name}"); 43 | }); 44 | 45 | return response; 46 | } 47 | catch (Exception e) 48 | { 49 | _logger.LogInformation($"Rollback transaction executed {typeof(TRequest).Name}"); 50 | 51 | _dbContext.RollbackTransaction(); 52 | 53 | _logger.LogError(e.Message, e.StackTrace); 54 | 55 | throw e; 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Infrastructure/Extensions/IWebHostExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Logging; 6 | using Polly; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Data.SqlClient; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace DotNetCoreArchitecture.Api.Infrastructure.Extensions 14 | { 15 | public static class WebHostExtensions 16 | { 17 | public static IHost MigrateDbContext(this IHost webHost, Action seeder) where TContext : DbContext 18 | { 19 | using (var scope = webHost.Services.CreateScope()) 20 | { 21 | var services = scope.ServiceProvider; 22 | 23 | var logger = services.GetRequiredService>(); 24 | 25 | var context = services.GetService(); 26 | 27 | try 28 | { 29 | logger.LogInformation($"Migrating database associated with context {typeof(TContext).Name}"); 30 | 31 | var retry = Policy.Handle() 32 | .WaitAndRetry(new[] 33 | { 34 | TimeSpan.FromSeconds(3), 35 | TimeSpan.FromSeconds(5), 36 | TimeSpan.FromSeconds(8), 37 | }); 38 | 39 | retry.Execute(() => 40 | { 41 | context.Database 42 | .Migrate(); 43 | 44 | seeder(context, services); 45 | }); 46 | 47 | 48 | logger.LogInformation($"Migrated database associated with context {typeof(TContext).Name}"); 49 | } 50 | catch (Exception ex) 51 | { 52 | logger.LogError(ex, $"An error occurred while migrating the database used on context {typeof(TContext).Name}"); 53 | } 54 | } 55 | 56 | return webHost; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/Repositories/DomainEventRepositoryDecorator.cs: -------------------------------------------------------------------------------- 1 | using DotNetCoreArchitecture.Application.Interfaces; 2 | using DotNetCoreArchitecture.SeedWork; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Text.Json; 8 | using System.Threading.Tasks; 9 | 10 | namespace DotNetCoreArchitecture.Persistence.Repositories 11 | { 12 | public class DomainEventRepositoryDecorator : IRepository where T : IAggregateRoot 13 | { 14 | protected readonly DotNetCoreArchitectureContext Context; 15 | 16 | protected readonly IRepository _repository; 17 | 18 | public DomainEventRepositoryDecorator(IRepository repository, DotNetCoreArchitectureContext context) 19 | { 20 | Context = context; 21 | _repository = repository; 22 | } 23 | 24 | public virtual async Task GetByIdAsync(Guid id) 25 | { 26 | return await _repository.GetByIdAsync(id); 27 | } 28 | 29 | public async Task AddAsync(T aggregate) 30 | { 31 | await AddDomainEvents(aggregate); 32 | 33 | return await _repository.AddAsync(aggregate); 34 | } 35 | 36 | public async Task RemoveAsync(T aggregate) 37 | { 38 | await AddDomainEvents(aggregate); 39 | 40 | await _repository.RemoveAsync(aggregate); 41 | } 42 | 43 | public async Task UpdateAsync(T aggregate) 44 | { 45 | await AddDomainEvents(aggregate); 46 | 47 | await _repository.UpdateAsync(aggregate); 48 | 49 | return aggregate; 50 | } 51 | 52 | protected async Task AddDomainEvents(IAggregateRoot aggregateRoot) 53 | { 54 | var events = aggregateRoot.DomainEvents ?? new List(); 55 | 56 | await AddDomainEvents(events); 57 | } 58 | 59 | protected async Task AddDomainEvents(IEnumerable events) 60 | { 61 | await Context.DomainEvents.AddRangeAsync(events.Select(domainEvent => 62 | new DomainEvent(Guid.NewGuid(), JsonSerializer.Serialize(domainEvent), domainEvent.EventType))); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Infrastructure/AutofacModules/ApplicationModule.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using DotNetCoreArchitecture.Api.Infrastructure.Services; 3 | using DotNetCoreArchitecture.Application.Interfaces; 4 | using DotNetCoreArchitecture.Domain.Aggregates.PostAggregate; 5 | using DotNetCoreArchitecture.Persistence; 6 | using DotNetCoreArchitecture.Persistence.Repositories; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.Extensions.Configuration; 10 | using System; 11 | using Module = Autofac.Module; 12 | 13 | 14 | namespace DotNetCoreArchitecture.Api.Infrastructure.AutofacModules 15 | { 16 | public class ApplicationModule : Module 17 | { 18 | private readonly IConfiguration _configuration; 19 | private readonly IWebHostEnvironment _env; 20 | 21 | public ApplicationModule(IConfiguration configuration, IWebHostEnvironment env) 22 | { 23 | _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); 24 | _env = env ?? throw new ArgumentNullException(nameof(env)); 25 | } 26 | 27 | protected override void Load(ContainerBuilder builder) 28 | { 29 | 30 | #region PostRepository 31 | 32 | builder.RegisterType>() 33 | .Named>(nameof(SqlRepository)) 34 | .InstancePerLifetimeScope(); 35 | 36 | builder.RegisterDecorator>((ctx, inner) => 37 | { 38 | var context = ctx.Resolve(); 39 | 40 | return new WithSaveRepositoryDecorator( 41 | new DomainEventRepositoryDecorator(inner, context), 42 | context); 43 | }, nameof(SqlRepository)); 44 | 45 | #endregion 46 | 47 | builder.RegisterType() 48 | .As() 49 | .InstancePerLifetimeScope(); 50 | 51 | builder.RegisterType() 52 | .As() 53 | .InstancePerLifetimeScope(); 54 | 55 | builder.RegisterType() 56 | .As() 57 | .SingleInstance(); 58 | 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Domain/SeedWork/EntityBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DotNetCoreArchitecture.SeedWork 7 | { 8 | public abstract class EntityBase 9 | { 10 | public virtual TIdentity Id { get; protected set; } 11 | public virtual EntityStatus Status { get; protected set; } = EntityStatus.Active; 12 | public virtual Guid ArchiveIdentifier { get; protected set; } 13 | 14 | public int Version { get; protected set; } 15 | 16 | private List _domainEvents; 17 | public IReadOnlyCollection DomainEvents => _domainEvents?.AsReadOnly(); 18 | 19 | protected EntityBase() 20 | { 21 | Version = 0; 22 | ArchiveIdentifier = new Guid(); 23 | _domainEvents = new List(); 24 | } 25 | 26 | public void AddDomainEvent(Event eventItem) 27 | { 28 | _domainEvents ??= new List(); 29 | Version++; 30 | _domainEvents.Add(eventItem); 31 | } 32 | 33 | public void RemoveDomainEvent(Event eventItem) 34 | { 35 | _domainEvents?.Remove(eventItem); 36 | } 37 | 38 | public void ClearDomainEvents() 39 | { 40 | _domainEvents?.Clear(); 41 | } 42 | 43 | public IEnumerable GetUncommittedEvents() 44 | { 45 | return _domainEvents.Where(domainEvent => !domainEvent.IsCommitted); 46 | } 47 | 48 | protected void Publish(Event e) 49 | { 50 | AddDomainEvent(e); 51 | } 52 | 53 | public bool HasEvent() 54 | { 55 | return _domainEvents.Any(x => typeof(T) == x.GetType()); 56 | } 57 | 58 | public T GetFirstEvent() where T : Event 59 | { 60 | return (T)_domainEvents.FirstOrDefault(x => typeof(T) == x.GetType()); 61 | } 62 | 63 | public bool IsTransient() 64 | { 65 | return this.Id == default; 66 | } 67 | 68 | public virtual void Archive() 69 | { 70 | Status = EntityStatus.Archived; 71 | ArchiveIdentifier = Guid.NewGuid(); 72 | AddDomainEvent(new EntityArchivedDomainEvent(Id)); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/Migrations/20191128133912_InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace DotNetCoreArchitecture.Persistence.Migrations 5 | { 6 | public partial class InitialMigration : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.EnsureSchema( 11 | name: "DotNetCoreArchitecture"); 12 | 13 | migrationBuilder.CreateTable( 14 | name: "DomainEvents", 15 | schema: "DotNetCoreArchitecture", 16 | columns: table => new 17 | { 18 | Id = table.Column(nullable: false), 19 | EventData = table.Column(type: "jsonb", nullable: false), 20 | EventType = table.Column(nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_DomainEvents", x => x.Id); 25 | }); 26 | 27 | migrationBuilder.CreateTable( 28 | name: "Post", 29 | schema: "DotNetCoreArchitecture", 30 | columns: table => new 31 | { 32 | Id = table.Column(nullable: false), 33 | Status = table.Column(nullable: false), 34 | ArchiveIdentifier = table.Column(nullable: false), 35 | Version = table.Column(nullable: false), 36 | Name = table.Column(nullable: true), 37 | RowVersion = table.Column(rowVersion: true, nullable: true), 38 | xmin = table.Column(type: "xid", nullable: false) 39 | }, 40 | constraints: table => 41 | { 42 | table.PrimaryKey("PK_Post", x => x.Id); 43 | }); 44 | } 45 | 46 | protected override void Down(MigrationBuilder migrationBuilder) 47 | { 48 | migrationBuilder.DropTable( 49 | name: "DomainEvents", 50 | schema: "DotNetCoreArchitecture"); 51 | 52 | migrationBuilder.DropTable( 53 | name: "Post", 54 | schema: "DotNetCoreArchitecture"); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Autofac.Extensions.DependencyInjection; 7 | using DotNetCoreArchitecture.Api.Infrastructure.Extensions; 8 | using DotNetCoreArchitecture.Persistence; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.Hosting; 12 | using Microsoft.Extensions.Logging; 13 | 14 | namespace DotNetCoreArchitecture.Api 15 | { 16 | public class Program 17 | { 18 | public static void Main(string[] args) 19 | { 20 | CreateHostBuilder(args).Build() 21 | .MigrateDbContext((context, services) => 22 | { 23 | // seed 24 | }) 25 | .Run(); 26 | } 27 | 28 | public static IHostBuilder CreateHostBuilder(string[] args) 29 | { 30 | return Host.CreateDefaultBuilder(args) 31 | .UseServiceProviderFactory(new AutofacServiceProviderFactory()) 32 | .ConfigureAppConfiguration((hostingContext, config) => 33 | { 34 | var env = hostingContext.HostingEnvironment; 35 | config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 36 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", 37 | optional: true, reloadOnChange: true); 38 | config.AddEnvironmentVariables(); 39 | }) 40 | .ConfigureLogging((hostingContext, logging) => 41 | { 42 | logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); 43 | 44 | if (hostingContext.HostingEnvironment.IsDevelopment()) 45 | { 46 | logging.AddConsole(); 47 | logging.AddEventSourceLogger(); 48 | logging.AddDebug(); 49 | } 50 | 51 | }) 52 | .ConfigureWebHostDefaults(webBuilder => 53 | { 54 | webBuilder.UseStartup(); 55 | webBuilder.UseKestrel(); 56 | webBuilder.UseContentRoot(Directory.GetCurrentDirectory()); 57 | }); 58 | 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/Migrations/DotNetCoreArchitectureContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using DotNetCoreArchitecture.Persistence; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | 9 | namespace DotNetCoreArchitecture.Persistence.Migrations 10 | { 11 | [DbContext(typeof(DotNetCoreArchitectureContext))] 12 | partial class DotNetCoreArchitectureContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) 19 | .HasAnnotation("ProductVersion", "3.0.0") 20 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 21 | 22 | modelBuilder.Entity("DotNetCoreArchitecture.Domain.Aggregates.PostAggregate.Post", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd() 26 | .HasColumnType("uuid"); 27 | 28 | b.Property("ArchiveIdentifier") 29 | .HasColumnType("uuid"); 30 | 31 | b.Property("Name") 32 | .HasColumnType("text"); 33 | 34 | b.Property("RowVersion") 35 | .IsConcurrencyToken() 36 | .ValueGeneratedOnAddOrUpdate() 37 | .HasColumnType("bytea"); 38 | 39 | b.Property("Status") 40 | .HasColumnType("integer"); 41 | 42 | b.Property("Version") 43 | .HasColumnType("integer"); 44 | 45 | b.Property("xmin") 46 | .IsConcurrencyToken() 47 | .ValueGeneratedOnAddOrUpdate() 48 | .HasColumnType("xid"); 49 | 50 | b.HasKey("Id"); 51 | 52 | b.ToTable("Post","DotNetCoreArchitecture"); 53 | }); 54 | 55 | modelBuilder.Entity("DotNetCoreArchitecture.Persistence.DomainEvent", b => 56 | { 57 | b.Property("Id") 58 | .ValueGeneratedOnAdd() 59 | .HasColumnType("uuid"); 60 | 61 | b.Property("EventData") 62 | .IsRequired() 63 | .HasColumnType("jsonb"); 64 | 65 | b.Property("EventType") 66 | .IsRequired() 67 | .HasColumnType("text"); 68 | 69 | b.HasKey("Id"); 70 | 71 | b.ToTable("DomainEvents","DotNetCoreArchitecture"); 72 | }); 73 | #pragma warning restore 612, 618 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/Migrations/20191128133912_InitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using DotNetCoreArchitecture.Persistence; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 9 | 10 | namespace DotNetCoreArchitecture.Persistence.Migrations 11 | { 12 | [DbContext(typeof(DotNetCoreArchitectureContext))] 13 | [Migration("20191128133912_InitialMigration")] 14 | partial class InitialMigration 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) 21 | .HasAnnotation("ProductVersion", "3.0.0") 22 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 23 | 24 | modelBuilder.Entity("DotNetCoreArchitecture.Domain.Aggregates.PostAggregate.Post", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd() 28 | .HasColumnType("uuid"); 29 | 30 | b.Property("ArchiveIdentifier") 31 | .HasColumnType("uuid"); 32 | 33 | b.Property("Name") 34 | .HasColumnType("text"); 35 | 36 | b.Property("RowVersion") 37 | .IsConcurrencyToken() 38 | .ValueGeneratedOnAddOrUpdate() 39 | .HasColumnType("bytea"); 40 | 41 | b.Property("Status") 42 | .HasColumnType("integer"); 43 | 44 | b.Property("Version") 45 | .HasColumnType("integer"); 46 | 47 | b.Property("xmin") 48 | .IsConcurrencyToken() 49 | .ValueGeneratedOnAddOrUpdate() 50 | .HasColumnType("xid"); 51 | 52 | b.HasKey("Id"); 53 | 54 | b.ToTable("Post","DotNetCoreArchitecture"); 55 | }); 56 | 57 | modelBuilder.Entity("DotNetCoreArchitecture.Persistence.DomainEvent", b => 58 | { 59 | b.Property("Id") 60 | .ValueGeneratedOnAdd() 61 | .HasColumnType("uuid"); 62 | 63 | b.Property("EventData") 64 | .IsRequired() 65 | .HasColumnType("jsonb"); 66 | 67 | b.Property("EventType") 68 | .IsRequired() 69 | .HasColumnType("text"); 70 | 71 | b.HasKey("Id"); 72 | 73 | b.ToTable("DomainEvents","DotNetCoreArchitecture"); 74 | }); 75 | #pragma warning restore 612, 618 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29509.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCoreArchitecture.Api", "DotNetCoreArchitecture.Api\DotNetCoreArchitecture.Api.csproj", "{9D6F74CE-66B4-4020-8BD5-C0257956F2AC}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCoreArchitecture.Application", "DotNetCoreArchitecture.Application\DotNetCoreArchitecture.Application.csproj", "{EFD83D69-E421-42AC-8622-A1198AC310CB}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCoreArchitecture.Domain", "DotNetCoreArchitecture.Domain\DotNetCoreArchitecture.Domain.csproj", "{A451DD64-CC5C-46E5-ABB6-506FBCAF4A09}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCoreArchitecture.Persistence", "DotNetCoreArchitecture.Persistence\DotNetCoreArchitecture.Persistence.csproj", "{80C7740A-E8D6-4913-A791-F2A0A26604DE}" 13 | EndProject 14 | Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{0A74560C-A692-4D2B-89DC-D4CD3441D33D}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {9D6F74CE-66B4-4020-8BD5-C0257956F2AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {9D6F74CE-66B4-4020-8BD5-C0257956F2AC}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {9D6F74CE-66B4-4020-8BD5-C0257956F2AC}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {9D6F74CE-66B4-4020-8BD5-C0257956F2AC}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {EFD83D69-E421-42AC-8622-A1198AC310CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {EFD83D69-E421-42AC-8622-A1198AC310CB}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {EFD83D69-E421-42AC-8622-A1198AC310CB}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {EFD83D69-E421-42AC-8622-A1198AC310CB}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {A451DD64-CC5C-46E5-ABB6-506FBCAF4A09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {A451DD64-CC5C-46E5-ABB6-506FBCAF4A09}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {A451DD64-CC5C-46E5-ABB6-506FBCAF4A09}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {A451DD64-CC5C-46E5-ABB6-506FBCAF4A09}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {80C7740A-E8D6-4913-A791-F2A0A26604DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {80C7740A-E8D6-4913-A791-F2A0A26604DE}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {80C7740A-E8D6-4913-A791-F2A0A26604DE}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {80C7740A-E8D6-4913-A791-F2A0A26604DE}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {0A74560C-A692-4D2B-89DC-D4CD3441D33D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {0A74560C-A692-4D2B-89DC-D4CD3441D33D}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {0A74560C-A692-4D2B-89DC-D4CD3441D33D}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {0A74560C-A692-4D2B-89DC-D4CD3441D33D}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {D4F2B07D-7A8D-42CB-863E-4037692E9324} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Persistence/DotNetCoreArchitectureContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Storage; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using DotNetCoreArchitecture.Application.Interfaces; 10 | using DotNetCoreArchitecture.Domain.Aggregates; 11 | using DotNetCoreArchitecture.SeedWork; 12 | using DotNetCoreArchitecture.Domain.Aggregates.PostAggregate; 13 | using DotNetCoreArchitecture.Persistence.EntityConfigurations; 14 | using Microsoft.EntityFrameworkCore.Design; 15 | 16 | namespace DotNetCoreArchitecture.Persistence 17 | { 18 | public class DotNetCoreArchitectureContext : DbContext, IDomainEventContext, IDbContext 19 | { 20 | public const string DefaultSchema = "DotNetCoreArchitecture"; 21 | 22 | public virtual DbSet Posts { get; set; } 23 | public virtual DbSet DomainEvents { get; set; } 24 | 25 | 26 | public IDbContextTransaction GetCurrentTransaction => _currentTransaction; 27 | 28 | private IDbContextTransaction _currentTransaction; 29 | 30 | public DotNetCoreArchitectureContext(DbContextOptions options) : base(options) 31 | { 32 | } 33 | 34 | protected override void OnModelCreating(ModelBuilder modelBuilder) 35 | { 36 | modelBuilder.ApplyConfiguration(new PostConfiguration()); 37 | 38 | modelBuilder.ApplyConfiguration(new DomainEventConfiguration()); 39 | } 40 | 41 | public async Task BeginTransactionAsync() 42 | { 43 | _currentTransaction ??= await Database.BeginTransactionAsync(IsolationLevel.ReadCommitted); 44 | } 45 | 46 | public async Task RetryOnExceptionAsync(Func func) 47 | { 48 | await Database.CreateExecutionStrategy().ExecuteAsync(func); 49 | } 50 | 51 | public async Task CommitTransactionAsync() 52 | { 53 | try 54 | { 55 | await SaveChangesAsync(); 56 | _currentTransaction?.Commit(); 57 | } 58 | catch 59 | { 60 | RollbackTransaction(); 61 | throw; 62 | } 63 | finally 64 | { 65 | if (_currentTransaction != null) 66 | { 67 | _currentTransaction.Dispose(); 68 | _currentTransaction = null; 69 | } 70 | } 71 | } 72 | 73 | public void RollbackTransaction() 74 | { 75 | try 76 | { 77 | _currentTransaction?.Rollback(); 78 | } 79 | finally 80 | { 81 | if (_currentTransaction != null) 82 | { 83 | _currentTransaction.Dispose(); 84 | _currentTransaction = null; 85 | } 86 | } 87 | } 88 | 89 | public IEnumerable GetDomainEvents() 90 | { 91 | var domainEntities = this.ChangeTracker 92 | .Entries() 93 | .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()); 94 | 95 | return domainEntities 96 | .SelectMany(x => x.Entity.DomainEvents); 97 | } 98 | } 99 | 100 | public class PostingContextDesignFactory : IDesignTimeDbContextFactory 101 | { 102 | public DotNetCoreArchitectureContext CreateDbContext(string[] args) 103 | { 104 | var optionsBuilder = new DbContextOptionsBuilder() 105 | .UseNpgsql("User ID=postgres;Password=password;Host=dotnetcorearchitecture.sql;Port=5432;Database=dotnetcorearchitecture;Pooling=true;"); 106 | 107 | return new DotNetCoreArchitectureContext(optionsBuilder.Options); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Shared/JwtToken.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.IdentityModel.Tokens; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IdentityModel.Tokens.Jwt; 5 | using System.Linq; 6 | using System.Security.Claims; 7 | using System.Security.Cryptography; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace DotNetCoreArchitecture.Api.Shared 12 | { 13 | public class JwtTokenGenerator : IJwtTokenGenerator 14 | { 15 | 16 | public JwtTokenGenerator() 17 | { 18 | } 19 | 20 | public JwtTokenSettings With(string issuer, string audience, string key, DateTime? expireDate = null) 21 | { 22 | return new JwtTokenSettings(Encode(issuer, audience, key, expireDate), Decode(issuer, audience, key), DecodeExpired(issuer, audience, key)); 23 | } 24 | 25 | private static SymmetricSecurityKey SecurityKey(string key) 26 | { 27 | return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); 28 | } 29 | 30 | private static Func Encode(string issuer, string audience, string key, DateTime? expireDate = null) 31 | { 32 | return claims => 33 | { 34 | var credentials = new SigningCredentials(SecurityKey(key), SecurityAlgorithms.HmacSha256); 35 | 36 | var securityToken = new JwtSecurityToken(issuer, audience, claims, null, expireDate ?? DateTime.Now.AddMonths(2), 37 | signingCredentials: credentials); 38 | 39 | return new JwtSecurityTokenHandler() 40 | .WriteToken(securityToken); 41 | }; 42 | } 43 | 44 | private Func Decode(string issuer, string audience, string key) 45 | { 46 | return token => 47 | { 48 | var claims = new JwtSecurityTokenHandler().ValidateToken(token, new TokenValidationParameters 49 | { 50 | ValidIssuer = issuer, 51 | ValidAudience = audience, 52 | IssuerSigningKey = SecurityKey(key), 53 | ValidateIssuerSigningKey = true 54 | }, out var securityToken); 55 | 56 | 57 | if (!(securityToken is JwtSecurityToken jwtSecurityToken) || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) 58 | throw new SecurityTokenException("Invalid token"); 59 | 60 | return claims; 61 | }; 62 | } 63 | 64 | private Func DecodeExpired(string issuer, string audience, string key) 65 | { 66 | return token => 67 | { 68 | var claims = new JwtSecurityTokenHandler().ValidateToken(token, new TokenValidationParameters 69 | { 70 | ValidIssuer = issuer, 71 | ValidAudience = audience, 72 | IssuerSigningKey = SecurityKey(key), 73 | ValidateIssuerSigningKey = true, 74 | ValidateLifetime = false 75 | }, out var securityToken); 76 | 77 | 78 | if (!(securityToken is JwtSecurityToken jwtSecurityToken) || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) 79 | throw new SecurityTokenException("Invalid token"); 80 | 81 | return claims; 82 | }; 83 | } 84 | 85 | public string GetRefreshToken() 86 | { 87 | var randomNumber = new byte[32]; 88 | using (var rng = RandomNumberGenerator.Create()) 89 | { 90 | rng.GetBytes(randomNumber); 91 | return Convert.ToBase64String(randomNumber); 92 | } 93 | } 94 | } 95 | 96 | public class JwtTokenSettings 97 | { 98 | private readonly Func _encodeFunc; 99 | private readonly Func _decodeFunc; 100 | private readonly Func _decodeExpiredFunc; 101 | 102 | public JwtTokenSettings(Func encodeFunc, 103 | Func decodeFunc, 104 | Func decodeExpiredFunc) 105 | { 106 | _encodeFunc = encodeFunc; 107 | _decodeFunc = decodeFunc; 108 | _decodeExpiredFunc = decodeExpiredFunc; 109 | } 110 | 111 | public string Encode(params Claim[] claims) 112 | { 113 | return _encodeFunc(claims); 114 | } 115 | 116 | public ClaimsPrincipal Decode(string token) 117 | { 118 | return _decodeFunc(token); 119 | } 120 | 121 | public ClaimsPrincipal DecodeExpired(string token) 122 | { 123 | return _decodeExpiredFunc(token); 124 | } 125 | 126 | } 127 | public interface IJwtTokenGenerator 128 | { 129 | JwtTokenSettings With(string issuer, string audience, string key, DateTime? expireDate = null); 130 | 131 | string GetRefreshToken(); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Infrastructure/Filters/HttpGlobalExceptionFilter.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Filters; 7 | using Microsoft.Extensions.Logging; 8 | using System.Linq; 9 | using System.Net; 10 | using DotNetCoreArchitecture.Application.Exceptions; 11 | using DotNetCoreArchitecture.Domain.Exceptions; 12 | using DotNetCoreArchitecture.Persistence.Exceptions; 13 | using DotNetCoreArchitecture.Api.Infrastructure.ActionResults; 14 | using DotNetCoreArchitecture.Api.Infrastructure.Exceptions; 15 | 16 | namespace DotNetCoreArchitecture.Api.Infrastructure.Filters 17 | { 18 | public class HttpGlobalExceptionFilter : IExceptionFilter 19 | { 20 | private readonly IWebHostEnvironment _env; 21 | private readonly ILogger _logger; 22 | 23 | public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger logger) 24 | { 25 | _env = env; 26 | _logger = logger; 27 | } 28 | 29 | public void OnException(ExceptionContext context) 30 | { 31 | var exception = context.Exception; 32 | var exceptionId = exception.HResult; 33 | 34 | _logger.LogError(new EventId(exception.HResult), 35 | exception, 36 | exception.Message); 37 | 38 | switch (exception) 39 | { 40 | case ForbiddenOperationException _: 41 | { 42 | var problemDetails = new ErrorResponse() 43 | { 44 | Status = StatusCodes.Status403Forbidden, 45 | Message = exception.Message 46 | }; 47 | 48 | context.Result = new ObjectResult(problemDetails);//not content? 49 | context.HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden; 50 | break; 51 | } 52 | case ConflictingOperationException _: 53 | { 54 | var problemDetails = new ErrorResponse() 55 | { 56 | Status = StatusCodes.Status409Conflict, 57 | Message = exception.Message 58 | }; 59 | 60 | context.Result = new ConflictObjectResult(problemDetails);//not content? 61 | context.HttpContext.Response.StatusCode = StatusCodes.Status409Conflict; 62 | break; 63 | } 64 | case NotFoundException _: 65 | { 66 | var problemDetails = new ErrorResponse() 67 | { 68 | Status = StatusCodes.Status404NotFound, 69 | Message = exception.Message 70 | }; 71 | 72 | context.Result = new NotFoundObjectResult(problemDetails);//not content? 73 | context.HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; 74 | break; 75 | } 76 | case CommandValidatorException e when e.InnerException is ValidationException ex: 77 | { 78 | var problemDetails = new ValidationProblemDetails() 79 | { 80 | Instance = context.HttpContext.Request.Path, 81 | Status = StatusCodes.Status400BadRequest, 82 | Detail = "Please refer to the errors property for additional details." 83 | }; 84 | 85 | problemDetails.Errors.Add("DomainValidations", ex.Errors.Select(error => $"{error.PropertyName}: {error.ErrorMessage}").ToArray()); 86 | 87 | context.Result = new BadRequestObjectResult(problemDetails); 88 | context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; 89 | break; 90 | } 91 | case DotNetCoreArchitecturePresentationException _: 92 | case DotNetCoreArchitectureApplicationException _: 93 | case DotNetCoreArchitecturePersistenceException _: 94 | case DotNetCoreArchitectureDomainException _: 95 | { 96 | var problemDetails = new ValidationProblemDetails() 97 | { 98 | Instance = context.HttpContext.Request.Path, 99 | Status = StatusCodes.Status422UnprocessableEntity, 100 | Detail = "Please refer to the errors property for additional details." 101 | }; 102 | 103 | problemDetails.Errors.Add("DomainValidations", new string[] { exception.Message }); 104 | 105 | context.Result = new UnprocessableEntityObjectResult(problemDetails); 106 | context.HttpContext.Response.StatusCode = StatusCodes.Status422UnprocessableEntity; 107 | break; 108 | } 109 | default: 110 | var json = new ErrorResponse 111 | { 112 | Message = $"An error has occured. Please refer to support with this id {exceptionId}.", 113 | Status = StatusCodes.Status500InternalServerError 114 | }; 115 | 116 | if (_env.IsDevelopment()) 117 | { 118 | json.DeveloperMessage = context.Exception; 119 | } 120 | 121 | context.Result = new InternalServerErrorObjectResult(json); 122 | context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 123 | break; 124 | } 125 | 126 | 127 | context.ExceptionHandled = true; 128 | } 129 | 130 | 131 | public class ErrorResponse 132 | { 133 | public int Status { get; set; } 134 | public string Message { get; set; } 135 | public object DeveloperMessage { get; set; } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /DotNetCoreArchitecture.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Autofac; 6 | using DotNetCoreArchitecture.Api.Infrastructure.AutofacModules; 7 | using DotNetCoreArchitecture.Api.Infrastructure.Filters; 8 | using DotNetCoreArchitecture.Application.Interfaces; 9 | using DotNetCoreArchitecture.Persistence; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Hosting; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.AspNetCore.Mvc; 14 | using Microsoft.EntityFrameworkCore; 15 | using Microsoft.Extensions.Configuration; 16 | using Microsoft.Extensions.DependencyInjection; 17 | using Microsoft.Extensions.Hosting; 18 | using Microsoft.OpenApi.Models; 19 | using Newtonsoft.Json; 20 | using Swashbuckle.AspNetCore.Filters; 21 | 22 | namespace DotNetCoreArchitecture.Api 23 | { 24 | public class Startup 25 | { 26 | public Startup(IConfiguration configuration, IWebHostEnvironment env) 27 | { 28 | Configuration = configuration; 29 | Env = env; 30 | } 31 | 32 | public IConfiguration Configuration { get; } 33 | public IWebHostEnvironment Env { get; } 34 | 35 | 36 | public void ConfigureContainer(ContainerBuilder builder) 37 | { 38 | builder.RegisterModule(new MediatorModule()); 39 | builder.RegisterModule(new ApplicationModule(Configuration, Env)); 40 | } 41 | 42 | public void ConfigureServices(IServiceCollection services) 43 | { 44 | services 45 | .AddCustomMvc() 46 | .AddCustomDbContext(Configuration, Env) 47 | .AddCustomConfiguration(Configuration) 48 | .AddCustomSwagger(Configuration); 49 | 50 | } 51 | 52 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 53 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 54 | { 55 | if (env.IsDevelopment()) 56 | { 57 | app.UseDeveloperExceptionPage(); 58 | } 59 | 60 | app.UseRouting(); 61 | app.UseCors("CorsPolicy"); 62 | app.UseAuthentication(); 63 | app.UseAuthorization(); 64 | 65 | var pathBase = Configuration["PATH_BASE"]; 66 | if (!string.IsNullOrEmpty(pathBase)) 67 | { 68 | app.UsePathBase(pathBase); 69 | } 70 | 71 | app.UseSwagger() 72 | .UseSwaggerUI(c => 73 | { 74 | c.SwaggerEndpoint( 75 | $"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", 76 | "DotNetCoreAchitecture.API V1"); 77 | }); 78 | 79 | 80 | app.UseEndpoints(endpoints => 81 | { 82 | endpoints.MapControllers(); 83 | }); 84 | } 85 | } 86 | internal static class CustomExtensionsMethods 87 | { 88 | public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment env) 89 | { 90 | services.AddEntityFrameworkNpgsql() 91 | .AddDbContext(options => 92 | { 93 | options.UseNpgsql(configuration["ConnectionString"], sqlOptions => 94 | { 95 | sqlOptions.EnableRetryOnFailure(10, TimeSpan.FromSeconds(30), null); 96 | }); 97 | }, ServiceLifetime.Scoped); 98 | 99 | services.AddScoped(sp => sp.GetRequiredService()); 100 | services.AddScoped(sp => sp.GetRequiredService()); 101 | 102 | return services; 103 | } 104 | 105 | public static IServiceCollection AddCustomMvc(this IServiceCollection services) 106 | { 107 | services.AddMvc(options => 108 | { 109 | options.Filters.Add(typeof(HttpGlobalExceptionFilter)); 110 | }).AddControllersAsServices() 111 | .SetCompatibilityVersion(CompatibilityVersion.Version_3_0) 112 | .AddNewtonsoftJson(opt => opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore); 113 | 114 | services.AddCors(options => 115 | { 116 | options.AddPolicy("CorsPolicy", 117 | builder => builder.AllowAnyOrigin() 118 | .AllowAnyMethod() 119 | .AllowAnyHeader()); 120 | }); 121 | 122 | return services; 123 | } 124 | 125 | public static IServiceCollection AddCustomSwagger(this IServiceCollection services, IConfiguration configuration) 126 | { 127 | services.AddSwaggerGen(options => 128 | { 129 | options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() 130 | { 131 | Description = "Please insert JWT with Bearer into field", 132 | Name = "Authorization", 133 | }); 134 | options.SwaggerDoc("v1", new OpenApiInfo 135 | { 136 | Title = "DotNetCoreArchitecture HTTP API", 137 | Version = "v1", 138 | Description = "The DotNetCoreArchitecture Service HTTP API", 139 | }); 140 | options.OperationFilter(); 141 | options.OperationFilter(); 142 | }); 143 | 144 | return services; 145 | } 146 | 147 | public static IServiceCollection AddCustomConfiguration(this IServiceCollection services, IConfiguration configuration) 148 | { 149 | services.AddOptions(); 150 | 151 | services.Configure(options => 152 | { 153 | options.InvalidModelStateResponseFactory = context => 154 | { 155 | var problemDetails = new ValidationProblemDetails(context.ModelState) 156 | { 157 | Instance = context.HttpContext.Request.Path, 158 | Status = StatusCodes.Status400BadRequest, 159 | Detail = "Please refer to the errors property for additional details." 160 | }; 161 | 162 | return new BadRequestObjectResult(problemDetails) 163 | { 164 | ContentTypes = { "application/problem+json", "application/problem+xml" } 165 | }; 166 | }; 167 | }); 168 | 169 | return services; 170 | } 171 | 172 | 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb --------------------------------------------------------------------------------