Neste artigo
veremos como implementar o padrão Repository fazendo o cache e usar o
HangFire de forma a obter um melhor desempenho em uma aplicação ASP .NET Core usando a arquitetura cebola. |
Continuando o artigo anterior vamos iniciar a criação dos serviços do Cache.
Adicionando o serviço de cache
Vamos precisar especificar certas configurações relacionadas ao cache. Vamos usar o padrão IOptions para ler as configurações diretamente de appsettings.json.
No projeto principal, adicione uma nova pasta e nomeie-a como Configurations e inclua nesta pasta uma nova classe chamada CacheConfiguration.cs
public class CacheConfiguration
{
public int AbsoluteExpirationInHours { get; set; }
public int SlidingExpirationInMinutes { get; set; }
}
|
A seguir vamos adicionar o serviço ao contêiner de serviço do aplicativo na classe Program do projeto Api:
... builder.Services.Configure<CacheConfiguration> (builder.Configuration.GetSection("CacheConfiguration")); ... |
Agora vamos definir os valores no arquivo
appsettings.json :
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=<seu_servidor>;Initial Catalog=AlunosWebDB;Integrated Security=True"
},
"CacheConfiguration": {
"AbsoluteExpirationInHours": 1,
"SlidingExpirationInMinutes": 30
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
|
A
propriedade SlidingExpirationInMinutes
refere-se à duração em minutos em que o cache será limpo se houver solicitação
de qualquer usuário.
E AbsoluteExpirationInHours refere-se à duração em
horas em que o cache será limpo mesmo se houver solicitações dos usuários. Essas
configurações permitem máxima eficiência e tempos de resposta mínimos, mantendo
a verificação de que o cache está sempre válido e os usuários não estão
recebendo informações desatualizadas.
Interface única com várias implementações
A seguir vamos preparar o sistema para acomodar várias técnicas de
cache, como in-memory e Redis, e assim por diante. Isso exige um conceito de
interface única com várias implementações.
Assim, a ideia é que saibamos de antemão que todas as técnicas de cache terão 3
funções principais:
Obter os dados,
Definir os dados
Remover os dados em cache.
Assim
vamos construir uma interface comum, ICacheService
que define essas 3 funções, e, a implementação será múltipla, como
MemoryCacheService e RedisCacheService.
Vamos iniciar incluindo um Enum que consiste nas técnicas de Cache suportadas.
No projeto principal, adicione uma nova pasta,
Enumerations e inclua nesta pasta a classe
CacheTech :
enum público CacheTech { Redis, Memory } |
Aqui incluimos as opções Redis e Memory representando as opções para implementar o cache.
Agora vamos construir nossa interface ICacheService.
No
projeto principal, adicione outra pasta chamada Interfaces.
Lembre-se que, com arquitetura limpa, toda a interface deve estar no Core da
aplicação. Isso Inverte as dependências e a aplicação não depende mais da
implementação, mas apenas da interface.
Nesta pasta inclua a interface ICacheService :
interface pública ICacheService { bool TryGet<T>(string cacheKey, valor T de saída); T Set<T>(string cacheKey, valor T); void Remove(string cacheKey); } |
Com isso definimos nossas 3 funções principais na interface. Para este artigo, vamos implementar apenas o cache na memória. (Se desejar pode implementar o cache do Redis)
Continuando vamos implementar esta interface no Infra.Data na pasta Services criando a classe MemoryCacheService :
public class MemoryCacheService : ICacheService
{
private readonly IMemoryCache _memoryCache;
private readonly CacheConfiguration _cacheConfig;
private MemoryCacheEntryOptions _cacheOptions;
public MemoryCacheService(IMemoryCache memoryCache,
IOptions<CacheConfiguration> cacheConfig)
{
_memoryCache = memoryCache;
_cacheConfig = cacheConfig.Value;
if (_cacheConfig != null)
{
_cacheOptions = new MemoryCacheEntryOptions
{
AbsoluteExpiration = DateTime.Now.AddHours(_cacheConfig.AbsoluteExpirationInHours),
Priority = CacheItemPriority.High,
SlidingExpiration = TimeSpan.FromMinutes(_cacheConfig.SlidingExpirationInMinutes)
};
}
}
public bool TryGet<T>(string cacheKey, out T value)
{
_memoryCache.TryGetValue(cacheKey, out value);
if (value == null) return false;
else return true;
}
public T Set<T>(string cacheKey, T value)
{
return _memoryCache.Set(cacheKey, value, _cacheOptions);
}
public void Remove(string cacheKey)
{
_memoryCache.Remove(cacheKey);
}
}
|
Agora vamos registrar o serviço de Cache no projeto Infra.IoC, na classe DependencyInjection:
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructureApi(this IServiceCollection services,
IConfiguration configuration)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"
), b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
services.AddMemoryCache();
services.AddTransient<MemoryCacheService>();
//services.AddTransient<RedisCacheService>();
services.AddTransient<Func<CacheTech, ICacheService>>(serviceProvider => key => { switch (key) { case CacheTech.Memory: return serviceProvider.GetService<MemoryCacheService>(); case CacheTech.Redis: return serviceProvider.GetService<RedisCacheService>(); default: return serviceProvider.GetService<MemoryCacheService>(); } }); return services;
}
}
|
Neste
código adicionamos as configurações de configurações ao contêiner para que ele
possa ser acessado posteriormente por meio do Padrão
IOptions e incluimos também o InMemoryCache.
Para que uma única injeção de interface funcione com múltiplas implementações,
temos que defini-la como uma função que aceita o Enum
criado anteriormente como parâmetro. Dentro dele, haverá uma instrução switch
muito simples que retorna o serviço conforme solicitado.
Configurando o Hangfire
O Hangfire é uma das melhores bibliotecas de processamento
de trabalho em segundo plano , e, vamos usar esta biblioteca incrível para
melhorar ainda mais a eficiência do nosso aplicativo. Novamente, o Hangfire
pertence ao projeto Infraestrutura.
Portanto, instale o pacote do Hangfire no projeto de
Infra.Data usando um dos comandos a seguir:
Instalar-package Hangfire
dotnet add package Hangfire
Para este artigo, usaremos o projeto de API como o servidor que processará os
trabalhos enfileirados no Hangfire. Em alguns casos, faz sentido criar um
aplicativo ASP.NET Core em branco e instalar o Hangfire nele. Isso isola o
aplicativo e o Hangfire não interfere nos recursos do servidor de API. Mas aqui,
vamos usar nossa API como server do Hangfire.
No projeto Infra.IoC na classe
DependencyInjection vamos adicionar o código para
configurar o Hangfire onde usaremos a mesma string de conexão para armazenar
dados de trabalho do Hangfire.
services.AddHangfire(x =>
x.UseSqlServerStorage(Configuration.GetConnectionString("DefaultConnection")));
serviços.AddHangfireServer();
Por fim, no arquivo Program do projeto
API vamos habilitar o middlweare e definir o
caminho no qual você poderá monitorar os jobs do Hangfire por meio de seu
painel:
app.UseHangfireDashboard("/jobs");
Implementando o padrão Repository
Com o Caching e o Hangfire concluídos, a maioria das partes complexas da implementação foram concluídas.
Vamos agora focar na implementação básica do padrão Repository criando na pasta Interfaces do projeto Core as interfaces IGenericRepository.cs e IAlunoRepository.cs
1- IGenericRepository
public interface IGenericRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IReadOnlyList<T>> GetAllAsync();
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}
|
2- IAlunoRepository
public
interface
IAlunoRepository :
IGenericRepository<Aluno> { } |
A seguir vamos implementar a interface IGenericRepository na classe GenericRepository no projeto Infra.Data na pasta Repositories :
using AlunosWeb.Core.Enumerations;
using AlunosWeb.Core.Interfaces;
using AlunosWeb.Infra.Data.Context;
using Hangfire;
using Microsoft.EntityFrameworkCore;
namespace AlunosWeb.Infra.Data.Repositories;
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
private readonly static CacheTech cacheTech = CacheTech.Memory;
private readonly string cacheKey = $"{typeof(T)}";
private readonly ApplicationDbContext _dbContext;
private readonly Func<CacheTech, ICacheService> _cacheService;
public GenericRepository(ApplicationDbContext dbContext, Func<CacheTech,
ICacheService> cacheService)
{
_dbContext = dbContext;
_cacheService = cacheService;
}
public virtual async Task<T> GetByIdAsync(int id)
{
return await _dbContext.Set<T>().FindAsync(id);
}
public async Task<IReadOnlyList<T>> GetAllAsync()
{
if (!_cacheService(cacheTech).TryGet(cacheKey,
out IReadOnlyList<T> cachedList))
{
cachedList = await _dbContext.Set<T>().ToListAsync();
_cacheService(cacheTech).Set(cacheKey, cachedList);
}
return cachedList;
}
public async Task<T> AddAsync(T entity)
{
await _dbContext.Set<T>().AddAsync(entity);
await _dbContext.SaveChangesAsync();
BackgroundJob.Enqueue(() => RefreshCache());
return entity;
}
public async Task UpdateAsync(T entity)
{
_dbContext.Entry(entity).State = EntityState.Modified;
await _dbContext.SaveChangesAsync();
BackgroundJob.Enqueue(() => RefreshCache());
}
public async Task DeleteAsync(T entity)
{
_dbContext.Set<T>().Remove(entity);
await _dbContext.SaveChangesAsync();
BackgroundJob.Enqueue(() => RefreshCache());
}
public async Task RefreshCache()
{
_cacheService(cacheTech).Remove(cacheKey);
var cachedList = await _dbContext.Set<T>().ToListAsync();
_cacheService(cacheTech).Set(cacheKey, cachedList);
}
}
|
Neste código
estamos especificando a técnica de Cache que desejamos usar , ou seja, o cache
na memória.
Como esta é uma Implementação genérica, teremos que definir o nome da chave
usada para cache. O cache é mais como um dicionário com um par de chave-valores
onde a chave é o identificador, os dados são armazenados. Portanto, aqui a chave
será o nome da própria Classe, ou seja, Aluno.
Não vamos armazenare em cache o resultado de
GetByIdAsync
porque geralmente
esta é uma consulta de execução muito rápida. Precisaremos de cache apenas
quando o usuário solicitar todos os dados ou modificar algum.
Para o _cacheService, passamos
o cacheTech (Memory) selecionado como parâmetro e
tentamos verificar se existe algum dado. Caso contrário, vamos obter os dados do
banco de dados e configurá-los para o cache e, finalmente, devolver os dados ao
usuário. Até agora, os dados estão prontamente disponíveis no cache para as
solicitações subsequentes.
Observe que se, em vez de Memory, você especificar
a tecnologia de cache como Redis, a API lançará uma
exceção não implementada, pois o próprio serviço ainda não foi implementado.
Na adição, atualização e deleção de dados temos uma modificação na Coleta de
Dados, e, por isso, criamos um método comum,
RefreshCache, que remove os dados existentes do cache para essa chave de
cache específica e consulta novamente o banco de dados para carregar o cache
novamente.
Observe que esse método pode ser demorado, dependendo da quantidade de dados
envolvidos. Portanto, estamos adicionando o método
RefreshCache como um trabalho em segundo plano ao Hangfire para facilitar
o processo.
Por fim, vamos implementar o AlunoRepository que
não conterá nada de especial por enquanto, já que temos a implementação feita no
repositório genérico. Mas, observe que a injeção do construtor permanece
semelhante.
public class AlunoRepository : GenericRepository<Aluno>, IAlunoRepository
{
private readonly DbSet<Aluno> _aluno;
public AlunoRepository(ApplicationDbContext dbContext, Func<CacheTech,
ICacheService> cacheService) : base(dbContext, cacheService)
{
_aluno = dbContext.Set<Aluno>();
}
}
|
Agora vamos registrar os serviços destes repositórios no projeto Infra.IoC na classe DependencyInjection:
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructureApi(this IServiceCollection services,
IConfiguration configuration)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"
), b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>));
services.AddTransient<IAlunoRepository, AlunoRepository>();
services.AddMemoryCache();
services.AddTransient<MemoryCacheService>();
//services.AddTransient<RedisCacheService>();
services.AddTransient<Func<CacheTech, ICacheService>>(serviceProvider => key =>
{
switch (key)
{
case CacheTech.Memory:
return serviceProvider.GetService<MemoryCacheService>();
case CacheTech.Redis:
return serviceProvider.GetService<RedisCacheService>();
default:
return serviceProvider.GetService<MemoryCacheService>();
}
});
services.AddHangfire(x => x.UseSqlServerStorage(configuration
.GetConnectionString("DefaultConnection")));
services.AddHangfireServer();
return services;
}
}
|
Pronto ! agora podemos criar o controlador e realizar os testes em nosso projeto.
Na próxima parte do artigo vamos fazer isso...
"Olhai para as aves do céu, que nem semeiam, nem
segam, nem ajuntam em celeiros; e vosso Pai celestial as alimenta. Não tendes
vós muito mais valor do que elas?"
Mateus 6:26
Referências: