feat: refinamentos

This commit is contained in:
Jean Gatto
2023-05-08 17:26:01 -03:00
parent 1a8f9c6071
commit 7a0381241a
15 changed files with 120 additions and 103 deletions
-2
View File
@@ -3,8 +3,6 @@ version: '3.9'
services:
shop-webapi:
ports:
- "5072:5072"
environment:
- ASPNETCORE_ENVIRONMENT=Docker
volumes:
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Shop.Core.Abstractions;
@@ -8,15 +7,11 @@ namespace Shop.Core.Abstractions;
/// Repositório (somente escrita).
/// </summary>
/// <typeparam name="TEntity">O tipo da entidade.</typeparam>
public interface IWriteOnlyRepository<TEntity> : IDisposable
where TEntity : IEntity<Guid>
public interface IWriteOnlyRepository<TEntity> : IDisposable where TEntity : IEntity<Guid>
{
void Add(TEntity entity);
void AddRange(IEnumerable<TEntity> entities);
void Update(TEntity entity);
void UpdateRange(IEnumerable<TEntity> entities);
void Remove(TEntity entity);
void RemoveRange(IEnumerable<TEntity> entities);
/// <summary>
/// Obtém a entidade por Id.
+17 -17
View File
@@ -10,21 +10,10 @@ namespace Shop.Core.Extensions;
/// </summary>
public static class JsonExtensions
{
private static readonly CamelCaseNamingStrategy NamingStrategy = new();
private static readonly StringEnumConverter EnumConverter = new(NamingStrategy);
private static readonly PrivateSetterContractResolver ContractResolver = new() { NamingStrategy = NamingStrategy };
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings().Configure();
public static JsonSerializerSettings Configure(this JsonSerializerSettings jsonSettings)
{
jsonSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
jsonSettings.PreserveReferencesHandling = PreserveReferencesHandling.None;
jsonSettings.NullValueHandling = NullValueHandling.Ignore;
jsonSettings.Formatting = Formatting.None;
jsonSettings.ContractResolver = ContractResolver;
jsonSettings.Converters.Add(EnumConverter);
return jsonSettings;
}
private static readonly CamelCaseNamingStrategy CamelCaseNaming = new();
private static readonly StringEnumConverter StringEnumConverter = new(CamelCaseNaming);
private static readonly PrivateSetterContractResolver ContractResolver = new() { NamingStrategy = CamelCaseNaming };
private static readonly JsonSerializerSettings DefaultJsonSettings = new JsonSerializerSettings().Configure();
/// <summary>
/// Desserializa o JSON para o tipo especificado.
@@ -33,7 +22,7 @@ public static class JsonExtensions
/// <param name="value">O objeto a ser desserializado.</param>
/// <returns>O objeto desserializado da string JSON.</returns>
public static T FromJson<T>(this string value) =>
value != null ? JsonConvert.DeserializeObject<T>(value, JsonSettings) : default;
value != null ? JsonConvert.DeserializeObject<T>(value, DefaultJsonSettings) : default;
/// <summary>
/// Serializa o objeto especificado em uma string JSON.
@@ -41,5 +30,16 @@ public static class JsonExtensions
/// <param name="value">O objeto a ser serializado.</param>
/// <returns>Uma representação de string JSON do objeto.</returns>
public static string ToJson<T>(this T value) =>
value != null ? JsonConvert.SerializeObject(value, JsonSettings) : default;
value != null ? JsonConvert.SerializeObject(value, DefaultJsonSettings) : default;
public static JsonSerializerSettings Configure(this JsonSerializerSettings jsonSettings)
{
jsonSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
jsonSettings.PreserveReferencesHandling = PreserveReferencesHandling.None;
jsonSettings.NullValueHandling = NullValueHandling.Ignore;
jsonSettings.Formatting = Formatting.None;
jsonSettings.ContractResolver = ContractResolver;
jsonSettings.Converters.Add(StringEnumConverter);
return jsonSettings;
}
}
@@ -21,9 +21,7 @@ public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest,
{
var commandName = request.GetGenericTypeName();
_logger.LogInformation(
"----- Handling command {CommandName} ({Command})",
commandName, request);
_logger.LogInformation("----- Handling command {CommandName} ({Command})", commandName, request);
var timer = new Stopwatch();
timer.Start();
@@ -32,9 +30,8 @@ public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest,
timer.Stop();
_logger.LogInformation(
"----- Command {CommandName} handled ({TimeTaken} seconds) - response: {Response} ",
commandName, timer.Elapsed.Seconds, response);
var timeTaken = timer.Elapsed.Seconds;
_logger.LogInformation("----- Command {CommandName} handled ({TimeTaken} seconds)", commandName, timeTaken);
return response;
}
@@ -14,5 +14,7 @@ public abstract class BaseDbContext<TContext> : DbContext
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.UseCollation(Collation).RemoveCascadeDeleteConvention();
=> modelBuilder
.UseCollation(Collation)
.RemoveCascadeDeleteConvention();
}
@@ -9,7 +9,7 @@ public static class ModelBuilderExtensions
/// Remove a delação em cascata de chaves estrangeiras (FK).
/// </summary>
/// <param name="modelBuilder">The model builder.</param>
public static ModelBuilder RemoveCascadeDeleteConvention(this ModelBuilder modelBuilder)
public static void RemoveCascadeDeleteConvention(this ModelBuilder modelBuilder)
{
var foreignKeys = modelBuilder.Model
.GetEntityTypes()
@@ -19,7 +19,5 @@ public static class ModelBuilderExtensions
foreach (var fk in foreignKeys)
fk.DeleteBehavior = DeleteBehavior.Restrict;
return modelBuilder;
}
}
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Shop.Core.Abstractions;
@@ -22,21 +21,12 @@ internal abstract class BaseWriteOnlyRepository<TEntity> : IWriteOnlyRepository<
public void Add(TEntity entity)
=> DbSet.Add(entity);
public void AddRange(IEnumerable<TEntity> entities)
=> DbSet.AddRange(entities);
public void Update(TEntity entity)
=> DbSet.Update(entity);
public void UpdateRange(IEnumerable<TEntity> entities)
=> DbSet.UpdateRange(entities);
public void Remove(TEntity entity)
=> DbSet.Remove(entity);
public void RemoveRange(IEnumerable<TEntity> entities)
=> DbSet.RemoveRange(entities);
public async Task<TEntity> GetByIdAsync(Guid id)
=> await DbSet.AsNoTracking().FirstOrDefaultAsync(entity => entity.Id == id);
@@ -12,9 +12,9 @@ namespace Shop.Infrastructure.Data.Services;
internal class MemoryCacheService : ICacheService
{
private readonly IMemoryCache _memoryCache;
private readonly ILogger<MemoryCacheService> _logger;
private readonly MemoryCacheEntryOptions _cacheOptions;
private readonly ILogger<MemoryCacheService> _logger;
private readonly IMemoryCache _memoryCache;
public MemoryCacheService(
ILogger<MemoryCacheService> logger,
@@ -32,7 +32,7 @@ internal class MemoryCacheService : ICacheService
public async Task<TItem> GetOrCreateAsync<TItem>(string cacheKey, Func<Task<TItem>> factory)
{
return await _memoryCache.GetOrCreateAsync(cacheKey, async (cacheEntry) =>
return await _memoryCache.GetOrCreateAsync(cacheKey, async cacheEntry =>
{
var cacheValue = cacheEntry?.Value;
if (cacheValue != null)
@@ -52,9 +52,10 @@ internal class MemoryCacheService : ICacheService
});
}
public async Task<IEnumerable<TItem>> GetOrCreateAsync<TItem>(string cacheKey, Func<Task<IEnumerable<TItem>>> factory)
public async Task<IEnumerable<TItem>> GetOrCreateAsync<TItem>(string cacheKey,
Func<Task<IEnumerable<TItem>>> factory)
{
return await _memoryCache.GetOrCreateAsync(cacheKey, async (cacheEntry) =>
return await _memoryCache.GetOrCreateAsync(cacheKey, async cacheEntry =>
{
var cacheValues = cacheEntry?.Value;
if (cacheValues != null)
@@ -73,6 +74,7 @@ internal class MemoryCacheService : ICacheService
return items;
});
}
public Task RemoveAsync(params string[] cacheKeys)
{
foreach (var cacheKey in cacheKeys)
@@ -53,6 +53,8 @@ public static class ServicesCollectionExtensions
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath, true);
});
services.AddSwaggerGenNewtonsoftSupport();
}
public static void AddHealthChecks(this IServiceCollection services, IConfiguration configuration)
@@ -127,9 +129,7 @@ public static class ServicesCollectionExtensions
eventData =>
{
if (eventData is not ExecutionStrategyEventData retryEventData)
{
return;
}
var exceptions = retryEventData.ExceptionsEncountered;
+6 -5
View File
@@ -111,11 +111,12 @@ var app = builder.Build();
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();
app.UseHealthChecks("/health", new HealthCheckOptions
{
AllowCachingResponses = false,
ResponseWriter = (httpContext, healthReport) => httpContext.Response.WriteAsync(healthReport.ToJson())
});
app.UseHealthChecks("/health",
new HealthCheckOptions
{
AllowCachingResponses = false,
ResponseWriter = (httpContext, healthReport) => httpContext.Response.WriteAsync(healthReport.ToJson())
});
app.UseSwagger();
app.UseSwaggerUI();
app.UseResponseCompression();
+10 -3
View File
@@ -8,8 +8,15 @@ namespace Shop.Query.Abstractions;
public interface IReadDbContext
{
string ConnectionString { get; }
IMongoCollection<TQueryModel> GetCollection<TQueryModel>() where TQueryModel : IQueryModel;
Task UpsertAsync<TQueryModel>(TQueryModel queryModel, Expression<Func<TQueryModel, bool>> upsertFilter) where TQueryModel : IQueryModel;
Task DeleteAsync<TQueryModel>(Expression<Func<TQueryModel, bool>> deleteFilter) where TQueryModel : IQueryModel;
IMongoCollection<TQueryModel> GetCollection<TQueryModel>()
where TQueryModel : IQueryModel;
Task UpsertAsync<TQueryModel>(TQueryModel queryModel, Expression<Func<TQueryModel, bool>> upsertFilter)
where TQueryModel : IQueryModel;
Task DeleteAsync<TQueryModel>(Expression<Func<TQueryModel, bool>> deleteFilter)
where TQueryModel : IQueryModel;
Task CreateCollectionsAsync();
}
+38 -33
View File
@@ -21,6 +21,22 @@ public sealed class ReadDbContext : IReadDbContext
private const string DatabaseName = "Shop";
private static readonly Random Rnd = new();
private static readonly ReplaceOptions DefaultReplaceOptions = new()
{
// Indica se o documento deve ser inserido se ele não existir.
IsUpsert = true
};
private static readonly CreateIndexOptions DefaultCreateIndexOptions = new()
{
// Restrição Exclusiva
Unique = true,
// Índices esparsos são como índices não esparsos, exceto que eles omitem referências a documentos que não incluem o campo indexado.
// Para campos que estão presentes apenas em alguns documentos, índices esparsos podem fornecer uma economia significativa de espaço.
Sparse = true
};
private readonly IMongoDatabase _database;
private readonly ILogger<ReadDbContext> _logger;
private readonly AsyncRetryPolicy _mongoRetryPolicy;
@@ -29,8 +45,8 @@ public sealed class ReadDbContext : IReadDbContext
{
ConnectionString = options.Value.NoSqlConnection;
var client = new MongoClient(options.Value.NoSqlConnection);
_database = client.GetDatabase(DatabaseName);
var mongoClient = new MongoClient(options.Value.NoSqlConnection);
_database = mongoClient.GetDatabase(DatabaseName);
_logger = logger;
_mongoRetryPolicy = CreateRetryPolicy(logger);
}
@@ -45,13 +61,9 @@ public sealed class ReadDbContext : IReadDbContext
{
var collection = GetCollection<TQueryModel>();
// ReplaceOptions:
// Se o documento existir, será substituído.
// Se o documento não existir, será criado um novo.
var replaceOptions = new ReplaceOptions { IsUpsert = true };
await _mongoRetryPolicy
.ExecuteAsync(async () => await collection.ReplaceOneAsync(upsertFilter, queryModel, replaceOptions));
.ExecuteAsync(async () =>
await collection.ReplaceOneAsync(upsertFilter, queryModel, DefaultReplaceOptions));
}
public async Task DeleteAsync<TQueryModel>(Expression<Func<TQueryModel, bool>> deleteFilter)
@@ -87,17 +99,7 @@ public sealed class ReadDbContext : IReadDbContext
private async Task CreateIndexAsync()
{
var indexDefinition = Builders<CustomerQueryModel>.IndexKeys.Ascending(model => model.Email);
var indexModel = new CreateIndexModel<CustomerQueryModel>(indexDefinition, new CreateIndexOptions
{
// Restrição Exclusiva
Unique = true,
// Índices esparsos são como índices não esparsos, exceto que eles omitem referências a documentos que não incluem o campo indexado.
// Para campos que estão presentes apenas em alguns documentos, índices esparsos podem fornecer uma economia significativa de espaço.
Sparse = true
});
var indexModel = new CreateIndexModel<CustomerQueryModel>(indexDefinition, DefaultCreateIndexOptions);
var collection = GetCollection<CustomerQueryModel>();
await collection.Indexes.CreateOneAsync(indexModel);
}
@@ -109,22 +111,25 @@ public sealed class ReadDbContext : IReadDbContext
.Select(impl => impl.Name)
.ToList();
private static AsyncRetryPolicy CreateRetryPolicy(ILogger<ReadDbContext> logger)
private static AsyncRetryPolicy CreateRetryPolicy(ILogger logger)
{
return Policy
.Handle<MongoException>()
.WaitAndRetryAsync(2, (retryAttempt) =>
{
// Retry with jitter
// A well-known retry strategy is exponential backoff, allowing retries to be made initially quickly,
// but then at progressively longer intervals: for example, after 2, 4, 8, 15, then 30 seconds.
// REF: https://github.com/App-vNext/Polly/wiki/Retry-with-jitter#simple-jitter
var sleepDuration
= TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) + TimeSpan.FromMilliseconds(Rnd.Next(0, 1000));
TimeSpan SleepDurationProvider(int retryAttempt)
{
// Retry with jitter
// A well-known retry strategy is exponential backoff, allowing retries to be made initially quickly,
// but then at progressively longer intervals: for example, after 2, 4, 8, 15, then 30 seconds.
// REF: https://github.com/App-vNext/Polly/wiki/Retry-with-jitter#simple-jitter
var sleepDuration = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) +
TimeSpan.FromMilliseconds(Rnd.Next(0, 1000));
logger.LogWarning("----- MongoDB: Retry #{Count} with delay {Delay}", retryAttempt, sleepDuration);
logger.LogWarning("----- MongoDB: Retry #{Count} with delay {Delay}", retryAttempt, sleepDuration);
return sleepDuration;
}, (ex, _) => logger.LogError(ex, "Ocorreu uma exceção não esperada ao salvar no MongoDB: {Message}", ex.Message));
return sleepDuration;
}
void OnRetry(Exception ex, TimeSpan _) => logger.LogError(ex,
"Ocorreu uma exceção não esperada ao salvar no MongoDB: {Message}", ex.Message);
return Policy.Handle<MongoException>().WaitAndRetryAsync(2, SleepDurationProvider, OnRetry);
}
}
+4 -1
View File
@@ -28,7 +28,10 @@ public class CustomerMap : IReadDbMapping
map.MapMember(customer => customer.DateOfBirth)
.SetIsRequired(true)
.SetSerializer(new DateTimeSerializer(dateOnly: true));
.SetSerializer(new DateTimeSerializer(true));
// Ignore
map.UnmapMember(m => m.FullName);
});
}
}
@@ -2,7 +2,9 @@ using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using MediatR;
using Microsoft.Extensions.Logging;
using Shop.Core.Abstractions;
using Shop.Core.Extensions;
using Shop.Domain.Entities.CustomerAggregate.Events;
using Shop.Query.Abstractions;
using Shop.Query.Application.Customer.Queries;
@@ -18,29 +20,27 @@ public class CustomerEventHandler :
INotificationHandler<CustomerUpdatedEvent>,
INotificationHandler<CustomerDeletedEvent>
{
private readonly ICacheService _cacheService;
private readonly ILogger _logger;
private readonly IMapper _mapper;
private readonly IReadDbContext _readDbContext;
private readonly ICacheService _cacheService;
public CustomerEventHandler(
IMapper mapper,
IReadDbContext readDbContext,
ICacheService cacheService)
ICacheService cacheService,
ILogger<CustomerEventHandler> logger)
{
_mapper = mapper;
_readDbContext = readDbContext;
_cacheService = cacheService;
_logger = logger;
}
public async Task Handle(CustomerCreatedEvent notification, CancellationToken cancellationToken)
{
var customerQueryModel = _mapper.Map<CustomerQueryModel>(notification);
await _readDbContext.UpsertAsync(customerQueryModel, filter => filter.Id == customerQueryModel.Id);
await ClearCacheAsync(notification);
}
LogEvent(notification);
public async Task Handle(CustomerUpdatedEvent notification, CancellationToken cancellationToken)
{
var customerQueryModel = _mapper.Map<CustomerQueryModel>(notification);
await _readDbContext.UpsertAsync(customerQueryModel, filter => filter.Id == customerQueryModel.Id);
await ClearCacheAsync(notification);
@@ -48,7 +48,18 @@ public class CustomerEventHandler :
public async Task Handle(CustomerDeletedEvent notification, CancellationToken cancellationToken)
{
await _readDbContext.DeleteAsync<CustomerQueryModel>(filter => filter.Id == notification.Id);
LogEvent(notification);
await _readDbContext.DeleteAsync<CustomerQueryModel>(filter => filter.Email == notification.Email);
await ClearCacheAsync(notification);
}
public async Task Handle(CustomerUpdatedEvent notification, CancellationToken cancellationToken)
{
LogEvent(notification);
var customerQueryModel = _mapper.Map<CustomerQueryModel>(notification);
await _readDbContext.UpsertAsync(customerQueryModel, filter => filter.Id == customerQueryModel.Id);
await ClearCacheAsync(notification);
}
@@ -57,4 +68,10 @@ public class CustomerEventHandler :
var cacheKeys = new[] { nameof(GetAllCustomerQuery), $"{nameof(GetCustomerByIdQuery)}_{@event.Id}" };
await _cacheService.RemoveAsync(cacheKeys);
}
private void LogEvent<TEvent>(TEvent @event) where TEvent : CustomerBaseEvent
{
_logger.LogInformation("----- Triggering the event {EventName}, model: {EventModel}",
typeof(TEvent).Name, @event.ToJson());
}
}
@@ -9,4 +9,6 @@ public class CustomerQueryModel : BaseQueryModel
public string Gender { get; set; }
public string Email { get; set; }
public DateTime DateOfBirth { get; set; }
public string FullName => (FirstName + " " + LastName).Trim();
}