feat: melhorias e otimizações

This commit is contained in:
Jean Gatto
2023-05-08 13:56:18 -03:00
parent b0251c9bf6
commit 1a8f9c6071
18 changed files with 137 additions and 69 deletions
+1
View File
@@ -360,3 +360,4 @@ MigrationBackup/
# JetBrains Rider
*.sln.iml
.idea/
.run/*
+3 -3
View File
@@ -19,12 +19,12 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0-1.final" />
<PackageReference Include="Roslynator.Analyzers" Version="4.2.0">
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0-1.final"/>
<PackageReference Include="Roslynator.Analyzers" Version="4.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.53.0.62665">
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.0.0.68202">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
+16
View File
@@ -30,6 +30,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shop.Query", "src\Shop.Query\Shop.Query.csproj", "{C2A82AAD-0348-4B29-B7E7-E36509EE068C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{157AD845-5FAB-4FF2-9980-80FEC878701C}"
ProjectSection(SolutionItems) = preProject
docker-compose.yml = docker-compose.yml
docker-compose.override.yml = docker-compose.override.yml
.dockerignore = .dockerignore
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{45B3273F-DCE8-48C2-B27A-05AF1343C69C}"
ProjectSection(SolutionItems) = preProject
.github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml
.github\workflows\devskim-analysis.yml = .github\workflows\devskim-analysis.yml
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -76,5 +90,7 @@ Global
{D957E297-38EA-402F-B603-25F3EC156E18} = {DFB189FE-F11B-46EA-A5CD-3571CEC8193A}
{E94C4296-DC64-4C22-87A1-B5A054FAADAC} = {8011CDDD-0814-4C74-9B74-C3E3365D50CC}
{C2A82AAD-0348-4B29-B7E7-E36509EE068C} = {8011CDDD-0814-4C74-9B74-C3E3365D50CC}
{157AD845-5FAB-4FF2-9980-80FEC878701C} = {1DAF7B63-F776-4466-908D-56FD1FD02F42}
{45B3273F-DCE8-48C2-B27A-05AF1343C69C} = {1DAF7B63-F776-4466-908D-56FD1FD02F42}
EndGlobalSection
EndGlobal
+1 -1
View File
@@ -4,7 +4,7 @@ services:
shop-webapi:
ports:
- 5072:5072
- "5072:5072"
environment:
- ASPNETCORE_ENVIRONMENT=Docker
volumes:
+4 -4
View File
@@ -17,7 +17,7 @@ services:
expose:
- 5072
ports:
- 5072:5072
- "5072:5072"
links:
- sqlserver
- mongoserver
@@ -39,7 +39,7 @@ services:
expose:
- 1433
ports:
- 1433:1433
- "1433:1433"
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=pMA63033n6tF
@@ -54,7 +54,7 @@ services:
expose:
- 27017
ports:
- 27017:27017
- "27017:27017"
networks:
- app-network
@@ -67,6 +67,6 @@ services:
expose:
- 6379
ports:
- 6379:6379
- "6379:6379"
networks:
- app-network
@@ -1,9 +1,9 @@
using System;
using Shop.Core.Common;
using Shop.Core.Abstractions;
namespace Shop.Application.Customer.Responses;
public class CreatedCustomerResponse : BaseResponse
public class CreatedCustomerResponse : IResponse
{
public CreatedCustomerResponse(Guid id) => Id = id;
@@ -1,8 +1,8 @@
namespace Shop.Core.Common;
namespace Shop.Core.Abstractions;
/// <summary>
/// Classe base usada para representar uma seção do AppSettings.
/// </summary>
public abstract class BaseOptions
public interface IAppOptions
{
}
@@ -1,8 +1,8 @@
namespace Shop.Core.Common;
namespace Shop.Core.Abstractions;
/// <summary>
/// Classe base usada pelas respostas da API.
/// </summary>
public abstract class BaseResponse
public interface IResponse
{
}
+4 -2
View File
@@ -1,8 +1,10 @@
using Shop.Core.Common;
using System.Diagnostics.CodeAnalysis;
using Shop.Core.Abstractions;
namespace Shop.Core.AppSettings;
public sealed class CacheOptions : BaseOptions
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
public sealed class CacheOptions : IAppOptions
{
public const string ConfigSectionPath = nameof(CacheOptions);
@@ -1,18 +1,17 @@
using System.ComponentModel.DataAnnotations;
using Shop.Core.Common;
using System.Diagnostics.CodeAnalysis;
using Shop.Core.Abstractions;
namespace Shop.Core.AppSettings;
public sealed class ConnectionOptions : BaseOptions
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
public sealed class ConnectionOptions : IAppOptions
{
public const string ConfigSectionPath = "ConnectionStrings";
[Required]
public string SqlConnection { get; private init; }
[Required] public string SqlConnection { get; private init; }
[Required]
public string NoSqlConnection { get; private init; }
[Required] public string NoSqlConnection { get; private init; }
[Required]
public string CacheConnection { get; private init; }
[Required] public string CacheConnection { get; private init; }
}
@@ -1,6 +1,6 @@
using System;
using Microsoft.Extensions.Configuration;
using Shop.Core.Common;
using Shop.Core.Abstractions;
namespace Shop.Core.Extensions;
@@ -10,7 +10,7 @@ public static class ConfigurationExtensions
= options => options.BindNonPublicProperties = true;
public static TOptions GetOptions<TOptions>(this IConfiguration configuration, string configSectionPath)
where TOptions : BaseOptions
where TOptions : IAppOptions
{
return configuration.GetSection(configSectionPath).Get<TOptions>(ConfigureBinderOptions);
}
+17 -17
View File
@@ -15,23 +15,6 @@ public static class JsonExtensions
private static readonly PrivateSetterContractResolver ContractResolver = new() { NamingStrategy = NamingStrategy };
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings().Configure();
/// <summary>
/// Desserializa o JSON para o tipo especificado.
/// </summary>
/// <typeparam name="T">O tipo de objeto para o qual desserializar.</typeparam>
/// <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;
/// <summary>
/// Serializa o objeto especificado em uma string JSON.
/// </summary>
/// <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;
public static JsonSerializerSettings Configure(this JsonSerializerSettings jsonSettings)
{
jsonSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
@@ -42,4 +25,21 @@ public static class JsonExtensions
jsonSettings.Converters.Add(EnumConverter);
return jsonSettings;
}
/// <summary>
/// Desserializa o JSON para o tipo especificado.
/// </summary>
/// <typeparam name="T">O tipo de objeto para o qual desserializar.</typeparam>
/// <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;
/// <summary>
/// Serializa o objeto especificado em uma string JSON.
/// </summary>
/// <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;
}
@@ -1,6 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Shop.Core.Abstractions;
using Shop.Core.AppSettings;
using Shop.Core.Common;
namespace Shop.Core.Extensions;
@@ -12,14 +12,13 @@ public static class ServicesCollectionExtensions
services.AddOptionsWithValidation<CacheOptions>(CacheOptions.ConfigSectionPath);
}
public static IServiceCollection AddOptionsWithValidation<TOptions>(this IServiceCollection services, string configSectionPath)
where TOptions : BaseOptions
private static void AddOptionsWithValidation<TOptions>(this IServiceCollection services, string configSectionPath)
where TOptions : class, IAppOptions
{
return services
services
.AddOptions<TOptions>()
.BindConfiguration(configSectionPath, options => options.BindNonPublicProperties = true)
.ValidateDataAnnotations()
.ValidateOnStart()
.Services;
.ValidateOnStart();
}
}
@@ -13,10 +13,10 @@ namespace Shop.Infrastructure.Extensions;
public static class ServicesCollectionExtensions
{
public static IServiceCollection AddMemoryCacheService(this IServiceCollection services)
public static void AddMemoryCacheService(this IServiceCollection services)
=> services.AddScoped<ICacheService, MemoryCacheService>();
public static IServiceCollection AddDistributedCacheService(this IServiceCollection services)
public static void AddDistributedCacheService(this IServiceCollection services)
=> services.AddScoped<ICacheService, DistributedCacheService>();
public static void AddInfrastructure(this IServiceCollection services)
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using Ardalis.Result;
using Microsoft.AspNetCore.Mvc;
using Shop.PublicApi.Models;
@@ -19,11 +19,7 @@ public static class ResultExtensions
private static IActionResult ToHttpNonSuccessResult(this IResult result)
{
var errors = new List<ApiError>();
foreach (var error in result.Errors)
{
errors.Add(new ApiError(error));
}
var errors = result.Errors.Select(error => new ApiError(error)).ToList();
switch (result.Status)
{
@@ -18,8 +18,8 @@ namespace Shop.PublicApi.Extensions;
public static class ServicesCollectionExtensions
{
private static readonly string[] DatabaseTags = new[] { "database" };
private const string MigrationsAssembly = "Shop.PublicApi";
private static readonly string[] DatabaseTags = { "database" };
public static void AddSwagger(this IServiceCollection services)
{
@@ -29,7 +29,8 @@ public static class ServicesCollectionExtensions
{
Version = "v1",
Title = "Shop (e-commerce)",
Description = "ASP.NET Core C# CQRS Event Sourcing, REST API, DDD, Princípios SOLID e Clean Architecture",
Description =
"ASP.NET Core C# CQRS Event Sourcing, REST API, DDD, Princípios SOLID e Clean Architecture",
Contact = new OpenApiContact
{
Name = "Jean Gatto",
@@ -42,7 +43,8 @@ public static class ServicesCollectionExtensions
{
Name = "MIT License",
#pragma warning disable S1075 // Refactor your code not to use hardcoded absolute paths or URIs.
Url = new Uri("https://github.com/jeangatto/ASP.NET-Core-API-CQRS-EVENT-DDD-SOLID/blob/main/LICENSE")
Url = new Uri(
"https://github.com/jeangatto/ASP.NET-Core-API-CQRS-EVENT-DDD-SOLID/blob/main/LICENSE")
#pragma warning restore S1075 // Refactor your code not to use hardcoded absolute paths or URIs.
}
});
@@ -58,13 +60,15 @@ public static class ServicesCollectionExtensions
var connectionOptions = configuration.GetOptions<ConnectionOptions>(ConnectionOptions.ConfigSectionPath);
var healthCheckBuilder = services
.AddHealthChecks()
.AddDbContextCheck<WriteDbContext>(tags: DatabaseTags)
.AddDbContextCheck<EventStoreDbContext>(tags: DatabaseTags)
.AddMongoDb(connectionOptions.NoSqlConnection, tags: DatabaseTags);
.AddHealthChecks()
.AddDbContextCheck<WriteDbContext>(tags: DatabaseTags)
.AddDbContextCheck<EventStoreDbContext>(tags: DatabaseTags)
.AddMongoDb(connectionOptions.NoSqlConnection, tags: DatabaseTags);
if (!connectionOptions.CacheConnection.IsInMemoryCache())
{
healthCheckBuilder.AddRedis(connectionOptions.CacheConnection);
}
}
public static void AddShopDbContext(this IServiceCollection services)
@@ -114,15 +118,18 @@ public static class ServicesCollectionExtensions
// Configurando a resiliência da conexão.
// REF: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
sqlServerOptions.EnableRetryOnFailure(maxRetryCount: 3);
sqlServerOptions.EnableRetryOnFailure(3);
});
// Log das tentativas de repetição.
options.LogTo(
filter: (eventId, _) => eventId.Id == CoreEventId.ExecutionStrategyRetrying,
logger: eventData =>
(eventId, _) => eventId.Id == CoreEventId.ExecutionStrategyRetrying,
eventData =>
{
if (eventData is not ExecutionStrategyEventData retryEventData) return;
if (eventData is not ExecutionStrategyEventData retryEventData)
{
return;
}
var exceptions = retryEventData.ExceptionsEncountered;
@@ -136,6 +143,8 @@ public static class ServicesCollectionExtensions
// Quando o ambiente for o de "desenvolvimento" será logado informações detalhadas.
var environment = serviceProvider.GetRequiredService<IHostEnvironment>();
if (environment.IsDevelopment())
{
options.EnableDetailedErrors().EnableSensitiveDataLogging();
}
}
}
@@ -49,7 +49,7 @@ public static class WebApplicationExtensions
}
else
{
app.Logger.LogInformation("----- {DbName}: Migrações estão em dia.", dbName);
app.Logger.LogInformation("----- {DbName}: Migrações estão em dia", dbName);
}
}
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Shop.Core.AppSettings;
using Shop.Core.Extensions;
using Xunit;
using Xunit.Categories;
namespace Shop.UnitTests.Core.Extensions;
[UnitTest]
public class ServicesCollectionExtensionsTests
{
[Fact]
public void Should_ReturnClassOptions_WhenConfigureAppSettings()
{
// Arrange
const int absoluteExpirationInHours = 4;
const int slidingExpirationInSeconds = 120;
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddInMemoryCollection(new Dictionary<string, string>
{
{ "CacheOptions:AbsoluteExpirationInHours", absoluteExpirationInHours.ToString() },
{ "CacheOptions:SlidingExpirationInSeconds", slidingExpirationInSeconds.ToString() }
});
var configuratio = configurationBuilder.Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(_ => configuratio);
services.ConfigureAppSettings();
var serviceProvider = services.BuildServiceProvider(true);
// Act
var act = serviceProvider.GetRequiredService<IOptions<CacheOptions>>();
// Assert
act.Should().NotBeNull();
act.Value.Should().NotBeNull();
act.Value.AbsoluteExpirationInHours.Should().Be(absoluteExpirationInHours);
act.Value.SlidingExpirationInSeconds.Should().Be(slidingExpirationInSeconds);
}
}