ASP.NET Core - Padrão Repositório com Cache e HangFire - II
  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:

  1. Obter os dados,

  2. Definir os dados

  3. 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:


José Carlos Macoratti