Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ FORM_SUBMISSIONS AS (
'Text',
"Text",
'TimeSubmitted',
"LastUpdatedAt"
"LastUpdatedAt",
'MonitoringObserverId',
N."MonitoringObserverId"
)
)
FROM
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions api/src/Feature.Forms/Feature.Forms.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageReference Include="FastEndpoints" />
<PackageReference Include="FastEndpoints.Security" />
<PackageReference Include="PolyJson" />
<PackageReference Include="ZiggyCreatures.FusionCache" />
</ItemGroup>

<ItemGroup>
Expand Down
47 changes: 27 additions & 20 deletions api/src/Feature.Forms/FetchCitizenReportingForms/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -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<Request, Results<NotFound, Ok<NgoFormsResponseModel>>>
{
public override void Configure()
Expand Down Expand Up @@ -44,26 +45,32 @@ public override async Task<Results<NotFound, Ok<NgoFormsResponseModel>>> 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<VoteMonitorContext>();

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);
}
}
1 change: 1 addition & 0 deletions api/src/Feature.Locations/Feature.Locations.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<ItemGroup>
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" />
<PackageReference Include="FastEndpoints" />
<PackageReference Include="ZiggyCreatures.FusionCache" />
</ItemGroup>

<ItemGroup>
Expand Down
71 changes: 39 additions & 32 deletions api/src/Feature.Locations/FetchAll/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -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<Request, Results<Ok<Response>, NotFound>>
{
public override void Configure()
Expand Down Expand Up @@ -34,36 +35,42 @@ public override async Task<Results<Ok<Response>, 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<VoteMonitorContext>();

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<LocationNode> GetLocationNodes(List<LocationModel> locations)
Expand Down Expand Up @@ -144,4 +151,4 @@ private static List<LocationNode> GetLocationNodes(List<LocationModel> locations

return root.Traverse().Where(x => x.Id != 0).ToList();
}
}
}
15 changes: 10 additions & 5 deletions api/src/Feature.PollingStations/FetchAll/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -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<Request, Results<Ok<Response>, NotFound>>
public class Endpoint(VoteMonitorContext context, IFusionCache cache, IServiceScopeFactory scopeFactory) : Endpoint<Request, Results<Ok<Response>, NotFound>>
{
public override void Configure()
{
Expand Down Expand Up @@ -33,9 +34,13 @@ public override async Task<Results<Ok<Response>, 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<VoteMonitorContext>();

var pollingStations = await db.PollingStations
.Where(x => x.ElectionRoundId == request.ElectionRoundId)
.Select(x => new PollingStationModel
{
Expand All @@ -51,7 +56,7 @@ public override async Task<Results<Ok<Response>, NotFound>> ExecuteAsync(Request
Latitude = x.Latitude,
Longitude = x.Longitude
})
.ToListAsync(cancellationToken: ct);
.ToListAsync(cancellationToken: factoryCt);

var nodes = GetLocationNodes(pollingStations);

Expand Down
15 changes: 10 additions & 5 deletions api/src/Feature.PollingStations/FetchLevels/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -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<Request, Results<Ok<Response>, NotFound>>
public class Endpoint(VoteMonitorContext context, IFusionCache cache, IServiceScopeFactory scopeFactory) : Endpoint<Request, Results<Ok<Response>, NotFound>>
{
public override void Configure()
{
Expand Down Expand Up @@ -33,9 +34,13 @@ public override async Task<Results<Ok<Response>, 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<VoteMonitorContext>();

var pollingStations = await db.PollingStations
.Where(x => x.ElectionRoundId == request.ElectionRoundId)
.Select(x => new
{
Expand All @@ -46,7 +51,7 @@ public override async Task<Results<Ok<Response>, NotFound>> ExecuteAsync(Request
x.Level5
})
.Distinct()
.ToListAsync(cancellationToken: ct);
.ToListAsync(cancellationToken: factoryCt);

Dictionary<string, LevelNode> nodes = new();
int id = 0;
Expand Down
Loading