feat: refinamentos
This commit is contained in:
@@ -10,6 +10,10 @@ on:
|
||||
|
||||
env:
|
||||
DOTNET_VERSION: "7.0.x"
|
||||
# Disable .NET Core welcome and telemetry messages
|
||||
DOTNET_NOLOGO: true
|
||||
# Disable .NET Core SDK telemetry
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<Authors>Jean Gatto</Authors>
|
||||
<MaintainerEmail>jean_gatto@hotmail.com</MaintainerEmail>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/jeangatto/ASP.NET-Core-API-CQRS-DDD-SOLID</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/jeangatto/ASP.NET-Core-API-CQRS-EVENT-DDD-SOLID</RepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
@@ -12,6 +12,8 @@
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
|
||||
<RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -4,12 +4,11 @@ version: '3.9'
|
||||
|
||||
services:
|
||||
|
||||
webapi:
|
||||
shop-webapi:
|
||||
ports:
|
||||
- 5072:5072
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Docker
|
||||
volumes:
|
||||
- ~/.vsdbg:/remote_debugger:rw
|
||||
- ~/.aspnet/https:/root/.aspnet/https:ro
|
||||
- ~/.microsoft/usersecrets:/root/.microsoft/usersecrets:ro
|
||||
|
||||
+8
-3
@@ -1,5 +1,10 @@
|
||||
# Please refer https://aka.ms/HTTPSinContainer on how to setup an https developer certificate for your ASP .NET Core service.
|
||||
|
||||
# docker-compose up -d
|
||||
# docker-compose up --build
|
||||
# docker-compose down
|
||||
# docker-compose down -v
|
||||
|
||||
version: '3.9'
|
||||
|
||||
networks:
|
||||
@@ -9,9 +14,9 @@ networks:
|
||||
services:
|
||||
|
||||
# ASPNET Core Web API Application
|
||||
webapi:
|
||||
image: webapi
|
||||
container_name: webapi
|
||||
shop-webapi:
|
||||
image: shop-webapi
|
||||
container_name: shop-webapi
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/Shop.PublicApi/Dockerfile
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Ardalis.Result;
|
||||
using MediatR;
|
||||
|
||||
namespace Shop.Application.Commands;
|
||||
|
||||
public class UpdateCustomerCommand : IRequest<Result>
|
||||
{
|
||||
[Required]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
[DataType(DataType.EmailAddress)]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
@@ -8,7 +8,12 @@ using Shop.Domain.QueriesModel;
|
||||
|
||||
namespace Shop.Application.Customer.Events;
|
||||
|
||||
public class CustomerEventHandler : INotificationHandler<CustomerCreatedEvent>
|
||||
/// <summary>
|
||||
/// Manipulador de Eventos do Cliente.
|
||||
/// </summary>
|
||||
public class CustomerEventHandler :
|
||||
INotificationHandler<CustomerCreatedEvent>,
|
||||
INotificationHandler<CustomerUpdatedEvent>
|
||||
{
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ISyncDataBase _syncDataBase;
|
||||
@@ -24,4 +29,10 @@ public class CustomerEventHandler : INotificationHandler<CustomerCreatedEvent>
|
||||
var queryModel = _mapper.Map<CustomerQueryModel>(notification);
|
||||
await _syncDataBase.SaveAsync(queryModel, filter => filter.Id == queryModel.Id);
|
||||
}
|
||||
|
||||
public async Task Handle(CustomerUpdatedEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var queryModel = _mapper.Map<CustomerQueryModel>(notification);
|
||||
await _syncDataBase.SaveAsync(queryModel, filter => filter.Id == queryModel.Id);
|
||||
}
|
||||
}
|
||||
@@ -2,44 +2,56 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ardalis.Result;
|
||||
using Ardalis.Result.FluentValidation;
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Shop.Application.Commands;
|
||||
using Shop.Application.Customer.Responses;
|
||||
using Shop.Application.Customer.Validators;
|
||||
using Shop.Core.Interfaces;
|
||||
using Shop.Domain.Interfaces.ReadOnly;
|
||||
using Shop.Domain.Interfaces.WriteOnly;
|
||||
using Shop.Domain.ValueObjects;
|
||||
|
||||
namespace Shop.Application.Customer.Handlers;
|
||||
|
||||
public class CustomerCommandHandler : IRequestHandler<CreateCustomerCommand, Result<CreatedCustomerResponse>>
|
||||
public class CustomerCommandHandler :
|
||||
IRequestHandler<CreateCustomerCommand, Result<CreatedCustomerResponse>>,
|
||||
IRequestHandler<UpdateCustomerCommand, Result>
|
||||
{
|
||||
private readonly CreateCustomerCommandValidator _commandValidator;
|
||||
private readonly CreateCustomerCommandValidator _createCommandValidator;
|
||||
private readonly UpdateCustomerCommandValidator _updateCommandValidator;
|
||||
private readonly ICustomerWriteOnlyRepository _writeOnlyRepository;
|
||||
private readonly ICustomerReadOnlyRepository _readOnlyRepository;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public CustomerCommandHandler(
|
||||
CreateCustomerCommandValidator commandValidator,
|
||||
CreateCustomerCommandValidator createCommandValidator,
|
||||
UpdateCustomerCommandValidator updateCommandValidator,
|
||||
ICustomerWriteOnlyRepository writeOnlyRepository,
|
||||
ICustomerReadOnlyRepository readOnlyRepository,
|
||||
IMapper mapper,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
_commandValidator = commandValidator;
|
||||
_createCommandValidator = createCommandValidator;
|
||||
_updateCommandValidator = updateCommandValidator;
|
||||
_writeOnlyRepository = writeOnlyRepository;
|
||||
_readOnlyRepository = readOnlyRepository;
|
||||
_mapper = mapper;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<CreatedCustomerResponse>> Handle(
|
||||
CreateCustomerCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
public async Task<Result<CreatedCustomerResponse>> Handle(CreateCustomerCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Validanto a requisição.
|
||||
var validationResult = await _commandValidator.ValidateAsync(request, cancellationToken);
|
||||
var validationResult = await _createCommandValidator.ValidateAsync(request, cancellationToken);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
// Retorna o resultado com os erros da validação.
|
||||
return Result.Invalid(validationResult.AsErrors());
|
||||
}
|
||||
|
||||
// Instanciando o VO Email.
|
||||
var email = new Email(request.Email);
|
||||
|
||||
// Verificiando se já existe um cliente com o endereço de e-mail.
|
||||
@@ -67,4 +79,42 @@ public class CustomerCommandHandler : IRequestHandler<CreateCustomerCommand, Res
|
||||
// Retornando o ID e a mensagem de sucesso.
|
||||
return Result.Success(new CreatedCustomerResponse(customer.Id), "Cadastrado com sucesso!");
|
||||
}
|
||||
|
||||
public async Task<Result> Handle(UpdateCustomerCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Validanto a requisição.
|
||||
var validationResult = await _updateCommandValidator.ValidateAsync(request, cancellationToken);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
// Retorna o resultado com os erros da validação.
|
||||
return Result.Invalid(validationResult.AsErrors());
|
||||
}
|
||||
|
||||
// Obtendo o cliente da base.
|
||||
var customerQueryModel = await _readOnlyRepository.GetByIdAsync(request.Id);
|
||||
if (customerQueryModel == null)
|
||||
return Result.NotFound($"Nenhum cliente encontrado pelo Id: {request.Id}");
|
||||
|
||||
// Instanciando o VO Email.
|
||||
var newEmail = new Email(request.Email);
|
||||
|
||||
// Verificiando se já existe um cliente com o endereço de e-mail.
|
||||
if (await _writeOnlyRepository.ExistsByEmailAsync(newEmail, customerQueryModel.Id))
|
||||
{
|
||||
// Retorna o resultado com o erro informado:
|
||||
return Result.Error("O endereço de e-mail informado já está sendo utilizado.");
|
||||
}
|
||||
|
||||
var customer = _mapper.Map<Domain.Entities.Customer.Customer>(customerQueryModel);
|
||||
customer.ChangeEmail(newEmail);
|
||||
|
||||
// Atualizando a entidade cliente no repositório.
|
||||
_writeOnlyRepository.Update(customer);
|
||||
|
||||
// Salvando as alterações no banco e disparando os eventos.
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
// Retornando o ID e a mensagem de sucesso.
|
||||
return Result.SuccessWithMessage("Atualizado com sucesso!");
|
||||
}
|
||||
}
|
||||
@@ -2,31 +2,49 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ardalis.Result;
|
||||
using Ardalis.Result.FluentValidation;
|
||||
using MediatR;
|
||||
using Shop.Application.Customer.Queries;
|
||||
using Shop.Application.Customer.Validators;
|
||||
using Shop.Domain.Interfaces.ReadOnly;
|
||||
using Shop.Domain.QueriesModel;
|
||||
|
||||
namespace Shop.Application.Customer.Handlers;
|
||||
|
||||
public class CustomerQueryHandler :
|
||||
IRequestHandler<GetCustomerByIdQuery, Result<CustomerQueryModel>>,
|
||||
IRequestHandler<GetAllCustomerQuery, Result<IEnumerable<CustomerQueryModel>>>
|
||||
IRequestHandler<GetAllCustomerQuery, Result<IEnumerable<CustomerQueryModel>>>,
|
||||
IRequestHandler<GetCustomerByIdQuery, Result<CustomerQueryModel>>
|
||||
{
|
||||
private readonly GetCustomerByIdQueryValidator _validator;
|
||||
private readonly ICustomerReadOnlyRepository _readOnlyRepository;
|
||||
|
||||
public CustomerQueryHandler(ICustomerReadOnlyRepository readOnlyRepository)
|
||||
=> _readOnlyRepository = readOnlyRepository;
|
||||
|
||||
public async Task<Result<CustomerQueryModel>> Handle(GetCustomerByIdQuery request, CancellationToken cancellationToken)
|
||||
public CustomerQueryHandler(
|
||||
GetCustomerByIdQueryValidator validator,
|
||||
ICustomerReadOnlyRepository readOnlyRepository)
|
||||
{
|
||||
var customer = await _readOnlyRepository.GetByIdAsync(request.Id);
|
||||
if (customer == null)
|
||||
return Result.NotFound($"Nenhum cliente encontrado pelo Id: {request.Id}");
|
||||
|
||||
return Result.Success(customer);
|
||||
_validator = validator;
|
||||
_readOnlyRepository = readOnlyRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<IEnumerable<CustomerQueryModel>>> Handle(GetAllCustomerQuery request, CancellationToken cancellationToken)
|
||||
=> Result.Success(await _readOnlyRepository.GetAllAsync());
|
||||
|
||||
public async Task<Result<CustomerQueryModel>> Handle(GetCustomerByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Validanto a requisição.
|
||||
var validationResult = await _validator.ValidateAsync(request, cancellationToken);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
// Retorna o resultado com os erros da validação.
|
||||
return Result.Invalid(validationResult.AsErrors());
|
||||
}
|
||||
|
||||
// Obtendo o cliente da base.
|
||||
var customerQueryModel = await _readOnlyRepository.GetByIdAsync(request.Id);
|
||||
if (customerQueryModel == null)
|
||||
return Result.NotFound($"Nenhum cliente encontrado pelo Id: {request.Id}");
|
||||
|
||||
// Retornando o cliente no resultado.
|
||||
return Result.Success(customerQueryModel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using FluentValidation;
|
||||
using Shop.Application.Customer.Queries;
|
||||
|
||||
namespace Shop.Application.Customer.Validators;
|
||||
|
||||
public class GetCustomerByIdQueryValidator : AbstractValidator<GetCustomerByIdQuery>
|
||||
{
|
||||
public GetCustomerByIdQueryValidator()
|
||||
{
|
||||
RuleFor(command => command.Id)
|
||||
.NotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using FluentValidation;
|
||||
using Shop.Application.Commands;
|
||||
|
||||
namespace Shop.Application.Customer.Validators;
|
||||
|
||||
public class UpdateCustomerCommandValidator : AbstractValidator<UpdateCustomerCommand>
|
||||
{
|
||||
public UpdateCustomerCommandValidator()
|
||||
{
|
||||
RuleFor(command => command.Id)
|
||||
.NotEmpty();
|
||||
|
||||
RuleFor(command => command.Email)
|
||||
.NotEmpty()
|
||||
.MaximumLength(254)
|
||||
.EmailAddress();
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@
|
||||
<PackageReference Include="Ardalis.Result.FluentValidation" Version="4.2.0" />
|
||||
<PackageReference Include="Ardalis.Result" Version="4.2.0" />
|
||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.4.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.4.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.5.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.5.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||
<PackageReference Include="MediatR" Version="11.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -4,6 +4,9 @@ using Shop.Core.Interfaces;
|
||||
|
||||
namespace Shop.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Classe base que contém os comportamentos de uma entidade.
|
||||
/// </summary>
|
||||
public abstract class BaseEntity : IEntityKey<Guid>
|
||||
{
|
||||
private readonly List<IDomainEvent> _domainEvents = new();
|
||||
|
||||
@@ -8,6 +8,6 @@ public static partial class RegexPatterns
|
||||
|
||||
[GeneratedRegex(
|
||||
"^([0-9a-zA-Z]([\\+\\-_\\.][0-9a-zA-Z]+)*)+@(([0-9a-zA-Z][-\\w]*[0-9a-zA-Z]*\\.)+[a-zA-Z0-9]{2,17})$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant)]
|
||||
RegexOptions.Compiled)]
|
||||
private static partial Regex EmailRegexPatternAttr();
|
||||
}
|
||||
@@ -8,5 +8,8 @@ namespace Shop.Core.Interfaces;
|
||||
/// </summary>
|
||||
public interface IDomainEvent : INotification
|
||||
{
|
||||
/// <summary>
|
||||
/// Quando ocorreu o evento.
|
||||
/// </summary>
|
||||
DateTime OccurredOn { get; }
|
||||
}
|
||||
@@ -7,17 +7,17 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JsonNet.ContractResolvers" Version="2.0.0" />
|
||||
<PackageReference Include="MediatR" Version="11.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -8,12 +8,12 @@ using Shop.Domain.ValueObjects;
|
||||
namespace Shop.Domain.Entities.Customer;
|
||||
|
||||
/// <summary>
|
||||
/// Cliente
|
||||
/// Entidade Cliente.
|
||||
/// </summary>
|
||||
public class Customer : BaseEntity, IAggregateRoot
|
||||
{
|
||||
/// <summary>
|
||||
/// Inicializa uma nova inståncia do cliente.
|
||||
/// Inicializa uma inståncia de um novo cliente.
|
||||
/// </summary>
|
||||
/// <param name="firstName">Primeiro Nome.</param>
|
||||
/// <param name="lastName">Sobrenome.</param>
|
||||
@@ -28,11 +28,11 @@ public class Customer : BaseEntity, IAggregateRoot
|
||||
Email = email;
|
||||
DateOfBirth = dateOfBirth;
|
||||
|
||||
// Adicionando nos eventos de domínio.
|
||||
// Adicionando a nova instãncia nos eventos de domínio.
|
||||
AddDomainEvent(new CustomerCreatedEvent(Id, firstName, lastName, gender, email.Address, dateOfBirth));
|
||||
}
|
||||
|
||||
private Customer() { } // ORM
|
||||
public Customer() { } // ORM
|
||||
|
||||
/// <summary>
|
||||
/// Primeiro Nome.
|
||||
@@ -52,10 +52,26 @@ public class Customer : BaseEntity, IAggregateRoot
|
||||
/// <summary>
|
||||
/// Endereço de e-mail.
|
||||
/// </summary>
|
||||
public Email Email { get; private init; }
|
||||
public Email Email { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Data de Nascimento.
|
||||
/// </summary>
|
||||
public DateTime DateOfBirth { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Altera o endereço de e-mail atual.
|
||||
/// </summary>
|
||||
/// <param name="newEmail">Novo endereço de e-mail.</param>
|
||||
public void ChangeEmail(Email newEmail)
|
||||
{
|
||||
// Só será alterado se for diferente do existente.
|
||||
if (!Email.Equals(newEmail))
|
||||
{
|
||||
Email = newEmail;
|
||||
|
||||
// Adicionando a alteração nos eventos de domínio.
|
||||
AddDomainEvent(new CustomerUpdatedEvent(Id, FirstName, LastName, Gender, newEmail.Address, DateOfBirth));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using Shop.Core.Abstractions;
|
||||
using Shop.Domain.Enums;
|
||||
|
||||
namespace Shop.Domain.Entities.Customer.Events;
|
||||
|
||||
public abstract class CustomerBaseEvent : BaseDomainEvent
|
||||
{
|
||||
protected CustomerBaseEvent(
|
||||
Guid id,
|
||||
string firstName,
|
||||
string lastName,
|
||||
EGender gender,
|
||||
string email,
|
||||
DateTime dateOfBirth)
|
||||
{
|
||||
Id = id;
|
||||
FirstName = firstName;
|
||||
LastName = lastName;
|
||||
Gender = gender;
|
||||
Email = email;
|
||||
DateOfBirth = dateOfBirth;
|
||||
}
|
||||
|
||||
public Guid Id { get; private init; }
|
||||
public string FirstName { get; private init; }
|
||||
public string LastName { get; private init; }
|
||||
public EGender Gender { get; private init; }
|
||||
public string Email { get; private init; }
|
||||
public DateTime DateOfBirth { get; private init; }
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using Shop.Core.Abstractions;
|
||||
using Shop.Domain.Enums;
|
||||
|
||||
namespace Shop.Domain.Entities.Customer.Events;
|
||||
|
||||
public class CustomerCreatedEvent : BaseDomainEvent
|
||||
/// <summary>
|
||||
/// Evento que represente um novo cliente.
|
||||
/// </summary>
|
||||
public class CustomerCreatedEvent : CustomerBaseEvent
|
||||
{
|
||||
public CustomerCreatedEvent(
|
||||
Guid id,
|
||||
@@ -12,20 +14,7 @@ public class CustomerCreatedEvent : BaseDomainEvent
|
||||
string lastName,
|
||||
EGender gender,
|
||||
string email,
|
||||
DateTime dateOfBirth)
|
||||
DateTime dateOfBirth) : base(id, firstName, lastName, gender, email, dateOfBirth)
|
||||
{
|
||||
Id = id;
|
||||
FirstName = firstName;
|
||||
LastName = lastName;
|
||||
Gender = gender;
|
||||
Email = email;
|
||||
DateOfBirth = dateOfBirth;
|
||||
}
|
||||
|
||||
public Guid Id { get; private init; }
|
||||
public string FirstName { get; private init; }
|
||||
public string LastName { get; private init; }
|
||||
public EGender Gender { get; private init; }
|
||||
public string Email { get; private init; }
|
||||
public DateTime DateOfBirth { get; private init; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using Shop.Domain.Enums;
|
||||
|
||||
namespace Shop.Domain.Entities.Customer.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Evento que representa um atualização de um cliente.
|
||||
/// </summary>
|
||||
public class CustomerUpdatedEvent : CustomerBaseEvent
|
||||
{
|
||||
public CustomerUpdatedEvent(
|
||||
Guid id,
|
||||
string firstName,
|
||||
string lastName,
|
||||
EGender gender,
|
||||
string email,
|
||||
DateTime dateOfBirth) : base(id, firstName, lastName, gender, email, dateOfBirth)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Shop.Core.Interfaces;
|
||||
using Shop.Domain.Entities.Customer;
|
||||
@@ -8,4 +9,5 @@ namespace Shop.Domain.Interfaces.WriteOnly;
|
||||
public interface ICustomerWriteOnlyRepository : IWriteOnlyRepository<Customer>
|
||||
{
|
||||
Task<bool> ExistsByEmailAsync(Email email);
|
||||
Task<bool> ExistsByEmailAsync(Email email, Guid currentId);
|
||||
}
|
||||
@@ -9,5 +9,6 @@ public class EventToQueryModelProfile : Profile
|
||||
public EventToQueryModelProfile()
|
||||
{
|
||||
CreateMap<CustomerCreatedEvent, CustomerQueryModel>(MemberList.Destination);
|
||||
CreateMap<CustomerUpdatedEvent, CustomerQueryModel>(MemberList.Destination);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using AutoMapper;
|
||||
using Shop.Domain.Entities.Customer;
|
||||
using Shop.Domain.QueriesModel;
|
||||
using Shop.Domain.ValueObjects;
|
||||
|
||||
namespace Shop.Domain.Profiles;
|
||||
|
||||
public class QueryModelToEntityProfile : Profile
|
||||
{
|
||||
public QueryModelToEntityProfile()
|
||||
{
|
||||
// Mapeando propriedades com "setters" privados.
|
||||
ShouldMapProperty = _ => true;
|
||||
|
||||
// Desabilitando o mapeando via construtor.
|
||||
// A classe só deverá ser inicializada no construtor manualmente (via dev)
|
||||
DisableConstructorMapping();
|
||||
|
||||
CreateMap<CustomerQueryModel, Customer>(MemberList.Destination)
|
||||
.ForMember(dest => dest.Email, cfg => cfg.MapFrom(src => new Email(src.Email)))
|
||||
.ForMember(dest => dest.DomainEvents, cfg => cfg.Ignore())
|
||||
.AfterMap((_, e) => e.ClearDomainEvents());
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shop.Domain.Entities.Customer;
|
||||
@@ -14,5 +15,8 @@ public class CustomerWriteOnlyRepository : BaseWriteOnlyRepository<Customer>, IC
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsByEmailAsync(Email email)
|
||||
=> await DbSet.AsNoTracking().AnyAsync(customer => customer.Email.Address == email.Address);
|
||||
=> await DbSet.AsNoTracking().AnyAsync(c => c.Email.Address == email.Address);
|
||||
|
||||
public async Task<bool> ExistsByEmailAsync(Email email, Guid currentId)
|
||||
=> await DbSet.AsNoTracking().AnyAsync(c => c.Email.Address == email.Address && c.Id != currentId);
|
||||
}
|
||||
@@ -35,8 +35,10 @@ public class UnitOfWork : IUnitOfWork
|
||||
|
||||
public async Task SaveChangesAsync()
|
||||
{
|
||||
// A estratégia (Connection resiliency and database retries) é criada de acordo com a configuração informada no AddDbContext
|
||||
// Criando a estratégia de execução (Connection resiliency and database retries).
|
||||
var strategy = _writeDbContext.Database.CreateExecutionStrategy();
|
||||
|
||||
// Executando a estratégia.
|
||||
await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
using var transaction
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="11.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="MongoDB.Bson" Version="2.19.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.19.0" />
|
||||
|
||||
@@ -32,6 +32,15 @@ public class CustomersController : ControllerBase
|
||||
public async Task<IActionResult> Create([FromBody] CreateCustomerCommand command)
|
||||
=> (await _mediator.Send(command)).ToActionResult();
|
||||
|
||||
[HttpPut]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> Update([FromBody] UpdateCustomerCommand command)
|
||||
=> (await _mediator.Send(command)).ToActionResult();
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
|
||||
@@ -4,6 +4,12 @@ EXPOSE 5072
|
||||
|
||||
ENV ASPNETCORE_URLS=http://+:5072
|
||||
|
||||
# Disable .NET Core welcome and telemetry messages
|
||||
ENV DOTNET_NOLOGO=true
|
||||
|
||||
# Disable .NET Core SDK telemetry
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=true
|
||||
|
||||
# Creates a non-root user with an explicit UID and adds permission to access the /app folder
|
||||
# For more info, please refer to https://aka.ms/vscode-docker-dotnet-configure-containers
|
||||
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
|
||||
|
||||
@@ -40,7 +40,7 @@ internal static class ServicesCollectionExtensions
|
||||
});
|
||||
});
|
||||
|
||||
// Quando for ambiente de desenvolvimento será logado informações detalhadas.
|
||||
// Quando o ambiente for o de "desenvolvimento" será logado informações detalhadas.
|
||||
var environment = serviceProvider.GetRequiredService<IHostEnvironment>();
|
||||
if (environment.IsDevelopment())
|
||||
options.EnableDetailedErrors().EnableSensitiveDataLogging();
|
||||
|
||||
@@ -7,13 +7,15 @@ namespace Shop.PublicApi.Extensions;
|
||||
|
||||
public static class ResultExtensions
|
||||
{
|
||||
private static readonly OkObjectResult EmptyOkResult = new(ApiResponse.Ok());
|
||||
|
||||
public static IActionResult ToActionResult(this Result result)
|
||||
=> result.IsSuccess ? EmptyOkResult : result.ToHttpNonSuccessResult();
|
||||
=> result.IsSuccess
|
||||
? new OkObjectResult(ApiResponse.Ok(result.SuccessMessage))
|
||||
: result.ToHttpNonSuccessResult();
|
||||
|
||||
public static IActionResult ToActionResult<T>(this Result<T> result)
|
||||
=> result.IsSuccess ? new OkObjectResult(ApiResponse<T>.Ok(result.Value)) : result.ToHttpNonSuccessResult();
|
||||
=> result.IsSuccess
|
||||
? new OkObjectResult(ApiResponse<T>.Ok(result.Value, result.SuccessMessage))
|
||||
: result.ToHttpNonSuccessResult();
|
||||
|
||||
private static IActionResult ToHttpNonSuccessResult(this IResult result)
|
||||
{
|
||||
|
||||
@@ -16,6 +16,11 @@ public class ApiResponse
|
||||
/// </summary>
|
||||
public bool Success { get; protected init; }
|
||||
|
||||
/// <summary>
|
||||
/// Mensagem de sucesso.
|
||||
/// </summary>
|
||||
public string SuccessMessage { get; protected init; }
|
||||
|
||||
/// <summary>
|
||||
/// O código do status HTTP.
|
||||
/// </summary>
|
||||
@@ -36,6 +41,13 @@ public class ApiResponse
|
||||
public static ApiResponse Ok()
|
||||
=> new() { Success = true, StatusCode = StatusCodes.Status200OK };
|
||||
|
||||
/// <summary>
|
||||
/// Cria uma resposta com HTTP Status 200
|
||||
/// </summary>
|
||||
/// <param name="successMessage">Mensagem de sucesso a ser exibida na resposta.</param>
|
||||
public static ApiResponse Ok(string successMessage)
|
||||
=> new() { Success = true, StatusCode = StatusCodes.Status200OK, SuccessMessage = successMessage };
|
||||
|
||||
#endregion
|
||||
|
||||
#region HTTP Status 400 BadRequest
|
||||
|
||||
@@ -5,11 +5,23 @@ namespace Shop.PublicApi.Models;
|
||||
/// <inheritdoc />
|
||||
public sealed class ApiResponse<T> : ApiResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Resultado da resposta.
|
||||
/// </summary>
|
||||
public T Result { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Cria uma resposta com HTTP Status 200.
|
||||
/// </summary>
|
||||
/// <param name="result">Resultado da resposta.</param>
|
||||
public static ApiResponse<T> Ok(T result)
|
||||
=> new() { Success = true, StatusCode = StatusCodes.Status200OK, Result = result };
|
||||
|
||||
/// <summary>
|
||||
/// Cria uma resposta com HTTP Status 200.
|
||||
/// </summary>
|
||||
/// <param name="result">Resultado da resposta.</param>
|
||||
/// <param name="successMessage">Mensagem de sucesso a ser exibida na resposta.</param>
|
||||
public static ApiResponse<T> Ok(T result, string successMessage)
|
||||
=> new() { Success = true, StatusCode = StatusCodes.Status200OK, Result = result, SuccessMessage = successMessage };
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AutoMapper;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Resources;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -71,7 +73,7 @@ builder.Services.AddSwaggerGen(options =>
|
||||
License = new OpenApiLicense
|
||||
{
|
||||
Name = "MIT License",
|
||||
Url = new Uri("https://github.com/jeangatto/ASP.NET-Core-API-CQRS-DDD-SOLID/blob/main/LICENSE")
|
||||
Url = new Uri("https://github.com/jeangatto/ASP.NET-Core-API-CQRS-EVENT-DDD-SOLID/blob/main/LICENSE")
|
||||
}
|
||||
});
|
||||
|
||||
@@ -106,6 +108,7 @@ builder.WebHost.UseKestrel(options => options.AddServerHeader = false);
|
||||
|
||||
// Exibindo os nomes das propriedades sem espaços na validação.
|
||||
ValidatorOptions.Global.DisplayNameResolver = (_, member, _) => member?.Name;
|
||||
ValidatorOptions.Global.LanguageManager = new LanguageManager { Culture = new CultureInfo("pt-Br") };
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
<PackageReference Include="MediatR" Version="11.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.2">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="Moq.Analyzers" Version="0.0.9">
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="Moq.Analyzers" Version="0.0.9">
|
||||
|
||||
Reference in New Issue
Block a user