diff --git a/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs b/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs
index 134f592b4..ac5c880fa 100644
--- a/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs
+++ b/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs
@@ -212,7 +212,9 @@ FORM_SUBMISSIONS AS (
'Text',
"Text",
'TimeSubmitted',
- "LastUpdatedAt"
+ "LastUpdatedAt",
+ 'MonitoringObserverId',
+ N."MonitoringObserverId"
)
)
FROM
@@ -433,7 +435,11 @@ @HASATTACHMENTS IS NULL
}
var notes = submissions
- .SelectMany(x => x.Notes.Select(note => note with { SubmissionId = x.SubmissionId }))
+ .SelectMany(x => x.Notes.Select(note => note with
+ {
+ SubmissionId = x.SubmissionId,
+ MonitoringObserverId = x.MonitoringObserverId
+ }))
.ToList();
var attachments = submissions
diff --git a/api/src/Feature.Forms/Feature.Forms.csproj b/api/src/Feature.Forms/Feature.Forms.csproj
index 3a939cb2e..b48ea8c65 100644
--- a/api/src/Feature.Forms/Feature.Forms.csproj
+++ b/api/src/Feature.Forms/Feature.Forms.csproj
@@ -11,6 +11,7 @@
+
diff --git a/api/src/Feature.Forms/FetchCitizenReportingForms/Endpoint.cs b/api/src/Feature.Forms/FetchCitizenReportingForms/Endpoint.cs
index 229890a3d..e92a1f1d2 100644
--- a/api/src/Feature.Forms/FetchCitizenReportingForms/Endpoint.cs
+++ b/api/src/Feature.Forms/FetchCitizenReportingForms/Endpoint.cs
@@ -1,13 +1,14 @@
using Feature.Forms.Models;
using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.DependencyInjection;
using Vote.Monitor.Domain;
using Vote.Monitor.Domain.Entities.FormAggregate;
using Vote.Monitor.Domain.Entities.FormBase;
+using ZiggyCreatures.Caching.Fusion;
namespace Feature.Forms.FetchCitizenReportingForms;
-public class Endpoint(VoteMonitorContext context, IMemoryCache cache)
+public class Endpoint(VoteMonitorContext context, IFusionCache cache, IServiceScopeFactory scopeFactory)
: Endpoint>>
{
public override void Configure()
@@ -44,26 +45,32 @@ public override async Task>> Execute
var cacheKey =
$"election-rounds/{req.ElectionRoundId}/monitoring-ngo/{monitoringNgo.MonitoringNgoForCitizenReportingId}/citizen-reports-forms/{monitoringNgo.FormsVersion}";
- var cachedResponse = await cache.GetOrCreateAsync(cacheKey, async (e) =>
- {
- var forms = await context.Forms
- .Where(x => x.Status == FormStatus.Published)
- .Where(x => x.ElectionRoundId == req.ElectionRoundId)
- .Where(x => x.MonitoringNgoId == monitoringNgo.MonitoringNgoForCitizenReportingId)
- .Where(x => x.FormType == FormType.CitizenReporting)
- .OrderBy(x => x.Code)
- .ToListAsync(cancellationToken: ct);
+ var cachedResponse = await cache.GetOrSetAsync(
+ cacheKey,
+ async factoryCt =>
+ {
+ // Fresh scope: FusionCache eager refresh may outlive the request DbContext.
+ await using var scope = scopeFactory.CreateAsyncScope();
+ var db = scope.ServiceProvider.GetRequiredService();
- e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
+ var forms = await db.Forms
+ .Where(x => x.Status == FormStatus.Published)
+ .Where(x => x.ElectionRoundId == req.ElectionRoundId)
+ .Where(x => x.MonitoringNgoId == monitoringNgo.MonitoringNgoForCitizenReportingId)
+ .Where(x => x.FormType == FormType.CitizenReporting)
+ .OrderBy(x => x.Code)
+ .ToListAsync(cancellationToken: factoryCt);
- return new NgoFormsResponseModel
- {
- ElectionRoundId = monitoringNgo.ElectionRoundId,
- Version = monitoringNgo.FormsVersion.ToString(),
- Forms = forms.Select(FormFullModel.FromEntity).OrderBy(x => x.DisplayOrder).ToList()
- };
- });
+ return new NgoFormsResponseModel
+ {
+ ElectionRoundId = monitoringNgo.ElectionRoundId,
+ Version = monitoringNgo.FormsVersion.ToString(),
+ Forms = forms.Select(FormFullModel.FromEntity).OrderBy(x => x.DisplayOrder).ToList()
+ };
+ },
+ options => options.SetDuration(TimeSpan.FromMinutes(30)),
+ token: ct);
- return TypedResults.Ok(cachedResponse!);
+ return TypedResults.Ok(cachedResponse);
}
}
diff --git a/api/src/Feature.Locations/Feature.Locations.csproj b/api/src/Feature.Locations/Feature.Locations.csproj
index ab10b17e8..fc845d4cb 100644
--- a/api/src/Feature.Locations/Feature.Locations.csproj
+++ b/api/src/Feature.Locations/Feature.Locations.csproj
@@ -15,6 +15,7 @@
+
diff --git a/api/src/Feature.Locations/FetchAll/Endpoint.cs b/api/src/Feature.Locations/FetchAll/Endpoint.cs
index 8c35838ef..af89109aa 100644
--- a/api/src/Feature.Locations/FetchAll/Endpoint.cs
+++ b/api/src/Feature.Locations/FetchAll/Endpoint.cs
@@ -1,9 +1,10 @@
-using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.DependencyInjection;
using Vote.Monitor.Core.Extensions;
+using ZiggyCreatures.Caching.Fusion;
namespace Feature.Locations.FetchAll;
-public class Endpoint(VoteMonitorContext context, IMemoryCache cache)
+public class Endpoint(VoteMonitorContext context, IFusionCache cache, IServiceScopeFactory scopeFactory)
: Endpoint, NotFound>>
{
public override void Configure()
@@ -34,36 +35,42 @@ public override async Task, NotFound>> ExecuteAsync(Request
var cacheKey = $"election-rounds/{request.ElectionRoundId}/locations/{electionRound.LocationsVersion}";
- var cachedResponse = await cache.GetOrCreateAsync(cacheKey, async (e) =>
- {
- var locations = await context.Locations
- .Where(x => x.ElectionRoundId == request.ElectionRoundId)
- .OrderBy(x => x.DisplayOrder)
- .Select(x => new LocationModel
- {
- Id = x.Id,
- Level1 = x.Level1,
- Level2 = x.Level2,
- Level3 = x.Level3,
- Level4 = x.Level4,
- Level5 = x.Level5,
- DisplayOrder = x.DisplayOrder
- })
- .ToListAsync(cancellationToken: ct);
-
- e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
-
- var nodes = GetLocationNodes(locations);
-
- return new Response
+ var cachedResponse = await cache.GetOrSetAsync(
+ cacheKey,
+ async factoryCt =>
{
- ElectionRoundId = electionRound.Id,
- Version = electionRound.LocationsVersion.ToString(),
- Nodes = nodes
- };
- });
-
- return TypedResults.Ok(cachedResponse!);
+ // Fresh scope: FusionCache eager refresh may outlive the request DbContext.
+ await using var scope = scopeFactory.CreateAsyncScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ var locations = await db.Locations
+ .Where(x => x.ElectionRoundId == request.ElectionRoundId)
+ .OrderBy(x => x.DisplayOrder)
+ .Select(x => new LocationModel
+ {
+ Id = x.Id,
+ Level1 = x.Level1,
+ Level2 = x.Level2,
+ Level3 = x.Level3,
+ Level4 = x.Level4,
+ Level5 = x.Level5,
+ DisplayOrder = x.DisplayOrder
+ })
+ .ToListAsync(cancellationToken: factoryCt);
+
+ var nodes = GetLocationNodes(locations);
+
+ return new Response
+ {
+ ElectionRoundId = electionRound.Id,
+ Version = electionRound.LocationsVersion.ToString(),
+ Nodes = nodes
+ };
+ },
+ options => options.SetDuration(TimeSpan.FromMinutes(30)),
+ token: ct);
+
+ return TypedResults.Ok(cachedResponse);
}
private static List GetLocationNodes(List locations)
@@ -144,4 +151,4 @@ private static List GetLocationNodes(List locations
return root.Traverse().Where(x => x.Id != 0).ToList();
}
-}
\ No newline at end of file
+}
diff --git a/api/src/Feature.PollingStations/FetchAll/Endpoint.cs b/api/src/Feature.PollingStations/FetchAll/Endpoint.cs
index 857865e8d..8583abe89 100644
--- a/api/src/Feature.PollingStations/FetchAll/Endpoint.cs
+++ b/api/src/Feature.PollingStations/FetchAll/Endpoint.cs
@@ -1,8 +1,9 @@
-using Vote.Monitor.Core.Extensions;
+using Microsoft.Extensions.DependencyInjection;
+using Vote.Monitor.Core.Extensions;
using ZiggyCreatures.Caching.Fusion;
namespace Feature.PollingStations.FetchAll;
-public class Endpoint(VoteMonitorContext context, IFusionCache cache) : Endpoint, NotFound>>
+public class Endpoint(VoteMonitorContext context, IFusionCache cache, IServiceScopeFactory scopeFactory) : Endpoint, NotFound>>
{
public override void Configure()
{
@@ -33,9 +34,13 @@ public override async Task, NotFound>> ExecuteAsync(Request
var cachedResponse = await cache.GetOrSetAsync(
cacheKey,
- async _ =>
+ async factoryCt =>
{
- var pollingStations = await context.PollingStations
+ // Fresh scope: FusionCache eager refresh may outlive the request DbContext.
+ await using var scope = scopeFactory.CreateAsyncScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ var pollingStations = await db.PollingStations
.Where(x => x.ElectionRoundId == request.ElectionRoundId)
.Select(x => new PollingStationModel
{
@@ -51,7 +56,7 @@ public override async Task, NotFound>> ExecuteAsync(Request
Latitude = x.Latitude,
Longitude = x.Longitude
})
- .ToListAsync(cancellationToken: ct);
+ .ToListAsync(cancellationToken: factoryCt);
var nodes = GetLocationNodes(pollingStations);
diff --git a/api/src/Feature.PollingStations/FetchLevels/Endpoint.cs b/api/src/Feature.PollingStations/FetchLevels/Endpoint.cs
index 1f77db4a1..d09969e6e 100644
--- a/api/src/Feature.PollingStations/FetchLevels/Endpoint.cs
+++ b/api/src/Feature.PollingStations/FetchLevels/Endpoint.cs
@@ -1,8 +1,9 @@
-using Vote.Monitor.Core.Extensions;
+using Microsoft.Extensions.DependencyInjection;
+using Vote.Monitor.Core.Extensions;
using ZiggyCreatures.Caching.Fusion;
namespace Feature.PollingStations.FetchLevels;
-public class Endpoint(VoteMonitorContext context, IFusionCache cache) : Endpoint, NotFound>>
+public class Endpoint(VoteMonitorContext context, IFusionCache cache, IServiceScopeFactory scopeFactory) : Endpoint, NotFound>>
{
public override void Configure()
{
@@ -33,9 +34,13 @@ public override async Task, NotFound>> ExecuteAsync(Request
var cachedResponse = await cache.GetOrSetAsync(
cacheKey,
- async _ =>
+ async factoryCt =>
{
- var pollingStations = await context.PollingStations
+ // Fresh scope: FusionCache eager refresh may outlive the request DbContext.
+ await using var scope = scopeFactory.CreateAsyncScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ var pollingStations = await db.PollingStations
.Where(x => x.ElectionRoundId == request.ElectionRoundId)
.Select(x => new
{
@@ -46,7 +51,7 @@ public override async Task, NotFound>> ExecuteAsync(Request
x.Level5
})
.Distinct()
- .ToListAsync(cancellationToken: ct);
+ .ToListAsync(cancellationToken: factoryCt);
Dictionary nodes = new();
int id = 0;