feat: refinamentos

This commit is contained in:
Jean Gatto
2023-02-17 16:22:16 -03:00
parent 36a9d975e6
commit 10666a1471
35 changed files with 349 additions and 73 deletions
+4
View File
@@ -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:
+3 -1
View File
@@ -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>
+1 -2
View File
@@ -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
View File
@@ -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();
}
}
+3 -3
View File
@@ -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>
+3
View File
@@ -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();
+1 -1
View File
@@ -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();
}
+3
View File
@@ -8,5 +8,8 @@ namespace Shop.Core.Interfaces;
/// </summary>
public interface IDomainEvent : INotification
{
/// <summary>
/// Quando ocorreu o evento.
/// </summary>
DateTime OccurredOn { get; }
}
+3 -3
View File
@@ -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>
+21 -5
View File
@@ -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);
}
+3 -1
View File
@@ -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)]
+6
View File
@@ -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)
{
+12
View File
@@ -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
+12
View File
@@ -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 };
}
+4 -1
View File
@@ -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();
+4 -4
View File
@@ -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">
+2 -2
View File
@@ -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">