diff --git a/Controllers/DiDemoController.cs b/Controllers/DiDemoController.cs new file mode 100644 index 0000000..fe8c2bb --- /dev/null +++ b/Controllers/DiDemoController.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using CampusWorkshops.Api.Services; + +namespace CampusWorkshops.Api.Controllers; + +[ApiController] +[AllowAnonymous] // Deixe aberto para facilitar o teste +[Route("api/[controller]")] +public class DiDemoController : ControllerBase +{ + // Visualiza os IDs por lifetime + [HttpGet("lifetimes")] + public IActionResult Lifetimes( + [FromServices] IRequestIdTransient t1, + [FromServices] IRequestIdTransient t2, + [FromServices] IRequestIdScoped s1, + [FromServices] IRequestIdScoped s2, + [FromServices] IRequestIdSingleton g1, + [FromServices] IRequestIdSingleton g2) + { + return Ok(new { + transient1 = t1.Id, transient2 = t2.Id, // normalmente diferentes NA MESMA chamada + scoped1 = s1.Id, scoped2 = s2.Id, // iguais NA MESMA chamada; mudam entre chamadas + singleton1 = g1.Id, singleton2 = g2.Id // sempre iguais + }); + } + + // Demonstra o "cativeiro" do transient dentro de um singleton + [HttpGet("captive-transient")] + public IActionResult Captive( + [FromServices] ReportingSingleton singleton, + [FromServices] IPerRequestClock clockTransient) + { + // clockTransient é Transient: CreatedAt muda entre requisições + var transientCreatedAtNow = clockTransient.CreatedAt; + + // GetClockCreatedAt (TODO) deveria retornar o CreatedAt visto pelo singleton + DateTimeOffset? singletonClockCreatedAt = null; + string? error = null; + try + { + singletonClockCreatedAt = singleton.GetClockCreatedAt(); + } + catch (Exception ex) + { + error = $"{ex.GetType().Name}: {ex.Message}"; + } + + return Ok(new { + transientCreatedAtNow, + singletonClockCreatedAt, + esperado = "Valores DIFERENTES por requisição", + observacao = "Mas como o transient foi resolvido DENTRO do singleton, ele congelou e não muda", + todoImplementado = error is null, + errorAoChamarSingleton = error + }); + } +} diff --git a/Program.cs b/Program.cs index 67ff0c1..ef094a0 100644 --- a/Program.cs +++ b/Program.cs @@ -8,6 +8,9 @@ using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using CampusWorkshops.Api.Services; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Caching.Memory; var builder = WebApplication.CreateBuilder(args); @@ -44,7 +47,32 @@ builder.Services.AddAuthorization(options => builder.Services.AddDbContext(options => options.UseSqlite(builder.Configuration.GetConnectionString("WorkshopsDb"))); -builder.Services.AddScoped(); +builder.Services.AddMemoryCache(); +builder.Services.Configure(builder.Configuration.GetSection("Cache")); + + +// Primeiro registramos a implementação EF concreta +builder.Services.AddScoped(); + +// Depois expomos IWorkshopRepository como o "EF envelopado por cache" +builder.Services.AddScoped(sp => + new CachedWorkshopRepository( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>())); + +// Lifetimes de exemplo +builder.Services.AddTransient(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); + +// Exemplo de "captive dependency" +builder.Services.AddTransient(); // Transient +builder.Services.AddSingleton(); + + +// Removendo para ativar o cached repository +// builder.Services.AddScoped(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(o => diff --git a/Repositories/CachedWorkshopRepository.cs b/Repositories/CachedWorkshopRepository.cs new file mode 100644 index 0000000..e8ea881 --- /dev/null +++ b/Repositories/CachedWorkshopRepository.cs @@ -0,0 +1,82 @@ +using CampusWorkshops.Api.Models; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace CampusWorkshops.Api.Repositories; + +public sealed class CachedWorkshopRepository : IWorkshopRepository +{ + private readonly IWorkshopRepository _inner; + private readonly IMemoryCache _cache; + private readonly TimeSpan _ttl; + + public CachedWorkshopRepository( + IWorkshopRepository inner, + IMemoryCache cache, + IOptionsSnapshot opts) + { + _inner = inner; + _cache = cache; + _ttl = TimeSpan.FromSeconds(opts.Value.DefaultTtlSeconds); + } + + private static string KeyById(Guid id) => $"workshops:byId:{id}"; + private static string KeyList(DateTimeOffset? from, DateTimeOffset? to, string? q) + => $"workshops:list:{from?.ToUnixTimeSeconds()}:{to?.ToUnixTimeSeconds()}:{q?.Trim().ToLowerInvariant()}"; + public async Task> GetAllAsync( + DateTimeOffset? from, DateTimeOffset? to, string? q, CancellationToken ct) + { + // Implementação final + var key = KeyList(from, to, q); + if (_cache.TryGetValue(key, out IReadOnlyList? cached) && cached is not null) + return cached; + + var data = await _inner.GetAllAsync(from, to, q, ct); + _cache.Set(key, data, _ttl); + return data; + } + + + public async Task GetByIdAsync(Guid id, CancellationToken ct) + { + var key = KeyById(id); + if (_cache.TryGetValue(key, out Workshop? cached) && cached is not null) + return cached; + + var w = await _inner.GetByIdAsync(id, ct); + if (w is not null) _cache.Set(key, w, _ttl); + return w; + } + + // Ações de escrita: apenas encaminham e limpam entradas simples + public async Task AddAsync(Workshop workshop, CancellationToken ct) + { + var w = await _inner.AddAsync(workshop, ct); + _cache.Remove(KeyById(w.Id)); + return w; + } + + public async Task UpdateAsync(Workshop workshop, CancellationToken ct) + { + var w = await _inner.UpdateAsync(workshop, ct); + if (w is not null) _cache.Remove(KeyById(w.Id)); + return w; + } + + public async Task DeleteAsync(Guid id, CancellationToken ct) + { + var ok = await _inner.DeleteAsync(id, ct); + if (ok) _cache.Remove(KeyById(id)); + return ok; + } + + public Task ExistsAsync(Guid id, CancellationToken ct) + => _inner.ExistsAsync(id, ct); + + public async Task UpdatePartialAsync(Guid id, Action updateAction, CancellationToken ct) + { + var w = await _inner.UpdatePartialAsync(id, updateAction, ct); + if (w is not null) _cache.Remove(KeyById(w.Id)); + return w; + } +} diff --git a/Services/CacheSettings.cs b/Services/CacheSettings.cs new file mode 100644 index 0000000..d1fc0f3 --- /dev/null +++ b/Services/CacheSettings.cs @@ -0,0 +1,6 @@ +namespace CampusWorkshops.Api.Services; + +public sealed class CacheSettings +{ + public int DefaultTtlSeconds { get; set; } = 15; +} \ No newline at end of file diff --git a/Services/PerRequestClock.cs b/Services/PerRequestClock.cs new file mode 100644 index 0000000..addbac9 --- /dev/null +++ b/Services/PerRequestClock.cs @@ -0,0 +1,12 @@ +namespace CampusWorkshops.Api.Services; + +public interface IPerRequestClock +{ + // Carimbo criado no CONSTRUTOR (não muda depois) + DateTimeOffset CreatedAt { get; } +} + +public sealed class PerRequestClock : IPerRequestClock +{ + public DateTimeOffset CreatedAt { get; } = DateTimeOffset.UtcNow; +} diff --git a/Services/ReportingSingleton.cs b/Services/ReportingSingleton.cs new file mode 100644 index 0000000..a48bc87 --- /dev/null +++ b/Services/ReportingSingleton.cs @@ -0,0 +1,23 @@ +using System; + +namespace CampusWorkshops.Api.Services; + +/// +/// Singleton que recebe um Transient (IPerRequestClock). +/// Na prática, o Transient é resolvido uma única vez, na construção do Singleton, +/// e "vira" efetivamente singleton dentro dele. +/// +public sealed class ReportingSingleton +{ + private readonly IPerRequestClock _clock; + + public ReportingSingleton(IPerRequestClock clock) + { + _clock = clock; + } + + public DateTimeOffset GetClockCreatedAt() + { + return _clock.CreatedAt; + } +} diff --git a/Services/RequestId.cs b/Services/RequestId.cs new file mode 100644 index 0000000..74b6582 --- /dev/null +++ b/Services/RequestId.cs @@ -0,0 +1,9 @@ +namespace CampusWorkshops.Api.Services; + +public interface IRequestIdTransient { Guid Id { get; } } +public interface IRequestIdScoped { Guid Id { get; } } +public interface IRequestIdSingleton { Guid Id { get; } } + +internal sealed class RequestIdTransient : IRequestIdTransient { public Guid Id { get; } = Guid.NewGuid(); } +internal sealed class RequestIdScoped : IRequestIdScoped { public Guid Id { get; } = Guid.NewGuid(); } +internal sealed class RequestIdSingleton : IRequestIdSingleton { public Guid Id { get; } = Guid.NewGuid(); } diff --git a/appsettings.json b/appsettings.json index fb28713..d47372b 100644 --- a/appsettings.json +++ b/appsettings.json @@ -8,7 +8,8 @@ "Jwt": { "Issuer": "CampusWorkshops", "Audience": "CampusWorkshops.Api", - "Key": "some-key" + "Key": "some-key-that-is-super-secret-always-secured" }, + "Cache": { "DefaultTtlSeconds": 15 }, "AllowedHosts": "*" }