Klant Statistieken — Techniek (Backend)
Deze pagina beschrijft de back-end implementatie van Tenant Metrics, met concrete codevoorbeelden uit de repository.
Overzicht
Scheduler: Hangfire recurring jobs (iam-metrics-daily en iam-metrics-monthly)
Job class: IamMetricsJobs
Command: CreateTenantMetricsRequest
Handler: CreateTenantMetricsHandler
Repository: IMetricsRepository/TenantMetricRepository
Events: TenantMetricsCreatedEvent, TenantMetricsFailedEvent
Hangfire Jobs (Scheduler)
De metrics worden gegenereerd via twee Hangfire recurring jobs, geregistreerd in Program.cs:
// Dagelijkse metrics om 01:00 UTC
recurring.AddOrUpdate<IamMetricsJobs>(
recurringJobId: "iam-metrics-daily",
methodCall: x => x.RunGenerateIamMetricsDailyAsync(CancellationToken.None),
cronExpression: "0 1 * * *",
recurringJobOptions);
// Maandelijkse metrics op de 1e van de maand om 05:00 UTC
recurring.AddOrUpdate<IamMetricsJobs>(
recurringJobId: "iam-metrics-monthly",
methodCall: x => x.RunGenerateIamMetricsMonthlyAsync(CancellationToken.None),
cronExpression: "0 5 1 * *",
recurringJobOptions);
Beide jobs roepen methoden aan op de IamMetricsJobs class, die intern de CreateTenantMetricsRequest verzendt via MediatR.
Command & Handler
Bronnen:
Belangrijkste logica uit de handler:
public async ValueTask<CreateTenantMetricsResponse> Handle(CreateTenantMetricsRequest request, CancellationToken ct)
{
var tenants = await sender.Send(new GetTenantListQuery(), ct);
List<TenantMetric> tenantMetrics = [];
foreach (var tenantId in tenants.Tenants)
{
multiTenantService.SetTenantId(tenantId);
var currentTenant = await tenantRepository.GetAsync(tenantId, ct);
var users = await sender.Send(new EmployeeGetAllQuery(), ct);
var payload = new Dictionary<string, object> { { "TotalUsers", users.Count } };
var metric = TenantMetric.Create(payload);
metric.SetTenant(currentTenant);
tenantMetrics.Add(metric);
}
var response = new CreateTenantMetricsResponse(tenantMetrics);
if (!request.SaveToDatabase) return response;
await tenantMetricRepository.CreateMetrics(tenantMetrics, ct);
await unitOfWork.SaveChangesAsync(ct);
return response;
}
Haalt de tenantlijst op, stelt per tenant de multi-tenant context in, telt medewerkers en bouwt de metric.
Slaat alleen op als SaveToDatabase waar is (standaard bij de Hangfire jobs).
Domein Entiteit
Bron: .../Domain/Entities/Metrics/TenantMetric.cs
public class TenantMetric : AggregateRoot<Guid>
{
public Tenant Tenant { get; set; }
public Dictionary<string, object> Payload { get; set; }
public static TenantMetric Create(Dictionary<string, object> payload) => new(payload);
public void SetTenant(Tenant tenant) => Tenant = tenant;
}
De flexibele Payload maakt het eenvoudig om nieuwe KPI's toe te voegen zonder schemawijziging.
Voorbeeld payload
{
"TotalUsers": 128
}
Repositories
Bron: .../Infrastructure/Repositories/TenantMetricRepository.cs
public async Task CreateMetrics(List<TenantMetric> tenantMetrics, CancellationToken ct)
=> await context.TenantMetrics.AddRangeAsync(tenantMetrics, ct);
public async Task<List<TenantMetric>> GetMetrics(CancellationToken ct)
=> await context.TenantMetrics.ToListAsync(ct);
public async Task<List<TenantMetric>> GetMetrics(int month, CancellationToken ct)
=> await context.TenantMetrics
.IgnoreQueryFilters()
.Where(m => m.CreatedAt.Month == month)
.Include(m => m.Tenant)
.ToListAsync(ct);
GetMetrics(month) wordt gebruikt voor rapportages met maandfiltering en laadt de gekoppelde tenant mee.
Idempotentie en datakwaliteit
Overweeg een check vóór opslag om dubbele metrics te voorkomen:
// Bestaat er al een metric voor (tenantId, jaar, maand)?
var exists = await context.TenantMetrics.AnyAsync(
m => m.Tenant.Id == tenantId
&& m.CreatedAt.Year == now.Year
&& m.CreatedAt.Month == now.Month, ct);
if (!exists) { await repo.CreateMetric(metric, ct); }
Multi-tenant waarborgen
Per tenant wordt IMultiTenantService.SetTenantId(tenantId) aangeroepen vóórdat data wordt gelezen of geschreven. Repositories werken daarmee impliciet in de juiste tenantcontext.
Uitbreiden van metrieken
Extra KPI's toevoegen in de handler:
var activeUsers = users.Count(u => u.IsActive);
payload["ActiveUsers"] = activeUsers;
De front-end kan deze vervolgens uitlezen zonder schemawijziging.
Last modified: 24 March 2026