Skip to content

Commit dd71ace

Browse files
committed
⚡feat: add distributed cache with Redis using cache-aside pattern
- Added Redis as a distributed cache using the cache-aside pattern to improve read performance and reduce database load. - Implemented cache invalidation on update and delete operations to ensure data consistency. Redis configuration is isolated to the API layer and integrated via Docker for local development.
1 parent 6c75591 commit dd71ace

9 files changed

Lines changed: 178 additions & 28 deletions

File tree

EventFlow.Application/EventFlow.Application.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<PackageReference Include="AutoMapper" Version="14.0.0" />
1111
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
1212
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
13+
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.2" />
1314
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.1.2" />
1415
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
1516
</ItemGroup>

EventFlow.Application/Services/EventService.cs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,37 @@
11
using EventFlow.Core.Models.DTOs;
2+
using Microsoft.Extensions.Caching.Distributed;
3+
using System.Reflection.Metadata.Ecma335;
4+
using System.Text.Json;
25

36
namespace EventFlow.Application.Services;
47

5-
public class EventService(IEventRepository repository, IMapper mapper) : IEventService
8+
public class EventService(IEventRepository repository, IMapper mapper, IDistributedCache cache) : IEventService
69
{
710
public async Task<EventDTO?> GetByIdAsync(int id)
811
{
12+
string cacheKey = $"event-{id}";
13+
14+
var cachedData = await cache.GetStringAsync(cacheKey);
15+
16+
if (!string.IsNullOrEmpty(cachedData))
17+
{
18+
return JsonSerializer.Deserialize<EventDTO>(cachedData)!;
19+
}
20+
921
var entity = await repository.GetEventWithDetailsByIdAsync(id);
1022

11-
return entity is null ?
12-
null :
13-
mapper.Map<EventDTO>(entity);
23+
if (entity == null)
24+
return null;
25+
26+
var dto = mapper.Map<EventDTO>(entity);
27+
28+
await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(dto),
29+
new DistributedCacheEntryOptions
30+
{
31+
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
32+
});
33+
34+
return dto;
1435
}
1536

1637
public async Task<List<EventDTO>> GetAllEventsAsync()
@@ -64,14 +85,21 @@ public async Task<PagedResult<EventDTO>> GetAllPagedEventsAsync(QueryParameters
6485
existing.OrganizerId = command.OrganizerId;
6586

6687
var updated = await repository.UpdateAsync(existing);
67-
return updated is null ?
68-
null :
69-
mapper.Map<EventDTO>(updated);
88+
89+
await cache.RemoveAsync($"event-{id}");
90+
91+
return updated is null ? null : mapper.Map<EventDTO>(updated);
7092
}
7193

7294
public async Task<bool> DeleteAsync(int id)
7395
{
7496
var deleted = await repository.DeleteAsync(id);
97+
98+
if (deleted > 0)
99+
{
100+
await cache.RemoveAsync($"event-{id}");
101+
}
102+
75103
return deleted > 0;
76104
}
77105
}

EventFlow.Application/Services/OrganizerService.cs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
11
using EventFlow.Core.Models.DTOs;
2+
using Microsoft.Extensions.Caching.Distributed;
3+
using System.Text.Json;
24

35
namespace EventFlow.Application.Services;
46

5-
public class OrganizerService(IOrganizerRepository repository, IEventRepository eventRepository, IMapper mapper) : IOrganizerService
7+
public class OrganizerService(IOrganizerRepository repository, IEventRepository eventRepository,
8+
IMapper mapper, IDistributedCache cache) : IOrganizerService
69
{
710
public async Task<OrganizerDTO?> GetByIdAsync(int id)
811
{
12+
string cacheKey = $"organizer-{id}";
13+
14+
var cachedData = await cache.GetStringAsync(cacheKey);
15+
16+
if (!string.IsNullOrEmpty(cachedData))
17+
{
18+
return JsonSerializer.Deserialize<OrganizerDTO>(cachedData)!;
19+
}
20+
921
var entity = await repository.GetOrganizerByIdAsync(id);
10-
return entity is null ?
11-
null :
12-
mapper.Map<OrganizerDTO>(entity);
22+
23+
if (entity == null)
24+
return null;
25+
26+
var dto = mapper.Map<OrganizerDTO>(entity);
27+
28+
await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(dto),
29+
new DistributedCacheEntryOptions
30+
{
31+
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
32+
});
33+
34+
return dto;
1335
}
1436

1537
public async Task<List<OrganizerDTO>> GetAllOrganizersAsync()
@@ -68,14 +90,21 @@ public async Task<bool> RegisterToEventAsync(int eventId, int organizerId)
6890
existing.Email = command.Email;
6991

7092
var updated = await repository.UpdateAsync(existing);
71-
return updated is null ?
72-
null :
73-
mapper.Map<OrganizerDTO>(updated);
93+
94+
await cache.RemoveAsync($"organizer-{id}");
95+
96+
return updated is null ? null : mapper.Map<OrganizerDTO>(updated);
7497
}
7598

7699
public async Task<bool> DeleteAsync(int id)
77100
{
78101
var deleted = await repository.DeleteAsync(id);
102+
103+
if (deleted > 0)
104+
{
105+
await cache.RemoveAsync($"organizer-{id}");
106+
}
107+
79108
return deleted > 0;
80109
}
81110
}

EventFlow.Application/Services/ParticipantService.cs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,37 @@
11
using EventFlow.Core.Models.DTOs;
2+
using Microsoft.Extensions.Caching.Distributed;
3+
using System.Text.Json;
24

35
namespace EventFlow.Application.Services;
46

5-
public class ParticipantService(IParticipantRepository repository, IEventRepository eventRepository, IMapper mapper) : IParticipantService
7+
public class ParticipantService(IParticipantRepository repository, IEventRepository eventRepository,
8+
IMapper mapper, IDistributedCache cache) : IParticipantService
69
{
710
public async Task<ParticipantDTO?> GetByIdAsync(int id)
811
{
12+
string cacheKey = $"participant-{id}";
13+
14+
var cachedData = await cache.GetStringAsync(cacheKey);
15+
16+
if (!string.IsNullOrEmpty(cachedData))
17+
{
18+
return JsonSerializer.Deserialize<ParticipantDTO>(cachedData)!;
19+
}
20+
921
var entity = await repository.GetParticipantByIdAsync(id);
10-
return entity is null ?
11-
null : mapper.Map<ParticipantDTO>(entity);
22+
23+
if (entity == null)
24+
return null;
25+
26+
var dto = mapper.Map<ParticipantDTO>(entity);
27+
28+
await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(dto),
29+
new DistributedCacheEntryOptions
30+
{
31+
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
32+
});
33+
34+
return dto;
1235
}
1336

1437
public async Task<PagedResult<ParticipantDTO>> GetAllPagedParticipantsByEventIdAsync(int eventId, QueryParameters queryParameters)
@@ -47,6 +70,8 @@ public async Task<bool> RegisterToEventAsync(int eventId, int participantId)
4770
participant.Events!.Add(evento);
4871
await repository.UpdateAsync(participant);
4972

73+
await cache.RemoveAsync($"participant-{participantId}");
74+
5075
return true;
5176
}
5277

@@ -61,14 +86,21 @@ public async Task<bool> RegisterToEventAsync(int eventId, int participantId)
6186
existing.Email = command.Email;
6287

6388
var updated = await repository.UpdateAsync(existing);
64-
return updated is null ?
65-
null :
66-
mapper.Map<ParticipantDTO>(updated);
89+
90+
await cache.RemoveAsync($"participant-{id}");
91+
92+
return updated is null ? null : mapper.Map<ParticipantDTO>(updated);
6793
}
6894

6995
public async Task<bool> DeleteAsync(int id)
7096
{
7197
var deleted = await repository.DeleteAsync(id);
98+
99+
if (deleted > 0)
100+
{
101+
await cache.RemoveAsync($"participant-{id}");
102+
}
103+
72104
return deleted > 0;
73105
}
74106

EventFlow.Application/Services/SpeakerService.cs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
11
using EventFlow.Core.Models.DTOs;
2+
using Microsoft.Extensions.Caching.Distributed;
3+
using System.Text.Json;
24

35
namespace EventFlow.Application.Services;
46

5-
public class SpeakerService(ISpeakerRepository repository, IEventRepository eventRepository, IMapper mapper) : ISpeakerService
7+
public class SpeakerService(ISpeakerRepository repository, IEventRepository eventRepository,
8+
IMapper mapper, IDistributedCache cache) : ISpeakerService
69
{
710
public async Task<SpeakerDTO?> GetByIdAsync(int id)
811
{
12+
string cacheKey = $"speaker-{id}";
13+
14+
var cachedData = await cache.GetStringAsync(cacheKey);
15+
16+
if (!string.IsNullOrEmpty(cachedData))
17+
{
18+
return JsonSerializer.Deserialize<SpeakerDTO>(cachedData)!;
19+
}
20+
921
var entity = await repository.GetSpeakerByIdAsync(id);
10-
return entity is null ?
11-
null :
12-
mapper.Map<SpeakerDTO>(entity);
22+
23+
if (entity == null)
24+
return null;
25+
26+
var dto = mapper.Map<SpeakerDTO>(entity);
27+
28+
await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(dto),
29+
new DistributedCacheEntryOptions
30+
{
31+
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
32+
});
33+
34+
return dto;
1335
}
1436

1537
public async Task<List<SpeakerDTO>> GetAllSpeakersAsync()
@@ -62,6 +84,9 @@ public async Task<bool> RegisterToEventAsync(int eventId, int speakerId)
6284
};
6385

6486
await repository.AddSpeakerEventAsync(speakerEvent);
87+
88+
await cache.RemoveAsync($"speaker-{speakerId}");
89+
6590
return true;
6691
}
6792

@@ -75,14 +100,21 @@ public async Task<bool> RegisterToEventAsync(int eventId, int speakerId)
75100
existing.Biography = command.Biography;
76101

77102
var updated = await repository.UpdateAsync(existing);
78-
return updated is null ?
79-
null :
80-
mapper.Map<SpeakerDTO>(updated);
103+
104+
await cache.RemoveAsync($"speaker-{id}");
105+
106+
return updated is null ? null : mapper.Map<SpeakerDTO>(updated);
81107
}
82108

83109
public async Task<bool> DeleteAsync(int id)
84110
{
85111
var deleted = await repository.DeleteAsync(id);
112+
113+
if (deleted > 0)
114+
{
115+
await cache.RemoveAsync($"speaker-{id}");
116+
}
117+
86118
return deleted > 0;
87119
}
88120
}

EventFlow.Presentation/Config/AppConfiguration.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ public static IServiceCollection AddDbContextConfig(this IServiceCollection serv
2323

2424
services.AddDbContext<EventFlowContext>(options =>
2525
{
26-
options.UseSqlServer(ConnectionString);
26+
options.UseSqlServer(ConnectionString, sqlOptions =>
27+
{
28+
sqlOptions.EnableRetryOnFailure(
29+
maxRetryCount: 3,
30+
maxRetryDelay: TimeSpan.FromSeconds(5),
31+
errorNumbersToAdd: null);
32+
});
2733

2834
if (isDevelopment)
2935
{
@@ -110,4 +116,15 @@ public static IServiceCollection ConfigureSwagger(this IServiceCollection servic
110116

111117
return services;
112118
}
119+
120+
public static IServiceCollection AddRedisCacheConfig(this IServiceCollection services)
121+
{
122+
services.AddStackExchangeRedisCache(options =>
123+
{
124+
options.Configuration = "eventflow-redis:6379";
125+
options.InstanceName = "EventFlow_";
126+
});
127+
128+
return services;
129+
}
113130
}

EventFlow.Presentation/EventFlow.Presentation.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
<PrivateAssets>all</PrivateAssets>
2626
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2727
</PackageReference>
28+
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.2" />
29+
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.2.0" />
2830
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
2931
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0" />
3032
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" />

EventFlow.Presentation/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
builder.Services.AddEndpointsApiExplorer();
1818
builder.Services.AddSwaggerGen();
1919
builder.Services.AddDbContextConfig(builder.Configuration, builder.Environment.IsDevelopment());
20+
builder.Services.AddRedisCacheConfig();
2021

2122
builder.Services.AddOpenTelemetry()
2223
.ConfigureResource(resource => resource

docker-compose.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@
6666
- "4317:4317"
6767
networks:
6868
- eventflow-network
69+
70+
eventflow-redis:
71+
image: redis:alpine
72+
container_name: eventflow-redis
73+
ports:
74+
- "6379:6379"
75+
networks:
76+
- eventflow-network
6977

7078
volumes:
7179
eventflow_sql_data:

0 commit comments

Comments
 (0)