WebApi + ClientApp, GraphQL, Reflection

This commit is contained in:
Boris Milašinović
2026-05-06 20:55:05 +02:00
parent 8f7c704a90
commit 4fb3de19f6
196 changed files with 10395 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
using Events.FilesAPI.Infrastructure.Files;
using Events.FilesAPI.Features.RegistrationsExcel.Synchronize;
using Events.WebAPI.Handlers.EF.Data.Postgres;
using Microsoft.EntityFrameworkCore;
using MediatR;
namespace Events.FilesAPI.Features.RegistrationsExcel.Download;
public sealed class DownloadRegistrationsExcelHandler(
EventsContext context,
RegistrationsExcelFileGenerator generator,
RegistrationsExcelFileLocator fileLocator) : IRequestHandler<DownloadRegistrationsExcelQuery, DownloadRegistrationsExcelResult>
{
public async Task<DownloadRegistrationsExcelResult> Handle(DownloadRegistrationsExcelQuery request, CancellationToken cancellationToken)
{
bool exists = await context.Events
.AsNoTracking()
.AnyAsync(e => e.Id == request.EventId, cancellationToken);
if (!exists)
return new DownloadRegistrationsExcelResult(false, null);
GeneratedFileReference? file = fileLocator.TryGet(request.EventId);
if (file == null)
{
await generator.GenerateAsync(request.EventId, cancellationToken);
file = fileLocator.TryGet(request.EventId);
}
return new DownloadRegistrationsExcelResult(true, file);
}
}

View File

@@ -0,0 +1,8 @@
using Events.FilesAPI.Infrastructure.Files;
using MediatR;
namespace Events.FilesAPI.Features.RegistrationsExcel.Download;
public sealed record DownloadRegistrationsExcelQuery(int EventId) : IRequest<DownloadRegistrationsExcelResult>;
public sealed record DownloadRegistrationsExcelResult(bool EventFound, GeneratedFileReference? File);

View File

@@ -0,0 +1,27 @@
using Events.FilesAPI.Infrastructure.Files;
using Events.FilesAPI.Infrastructure.Options;
using Microsoft.Extensions.Options;
namespace Events.FilesAPI.Features.RegistrationsExcel.Download;
public sealed class RegistrationsExcelFileLocator(
IHostEnvironment hostEnvironment,
IOptions<GeneratedFilesOptions> generatedFilesOptions)
{
public GeneratedFileReference? TryGet(int eventId)
{
string rootPath = Path.IsPathRooted(generatedFilesOptions.Value.OutputPath)
? generatedFilesOptions.Value.OutputPath
: Path.GetFullPath(Path.Combine(hostEnvironment.ContentRootPath, generatedFilesOptions.Value.OutputPath));
string excelPath = Path.Combine(rootPath, $"{eventId}.xlsx");
if (!File.Exists(excelPath))
return null;
return new GeneratedFileReference
{
FileName = Path.GetFileName(excelPath),
PhysicalPath = excelPath
};
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Mvc;
using Events.FilesAPI.Features.RegistrationsExcel.Download;
using MediatR;
namespace Events.FilesAPI.Features.RegistrationsExcel;
[ApiController]
[Route("Events")]
public class DownloadRegistrationsExcelController : ControllerBase
{
private const string XlsxContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
[HttpGet("{id}/RegistrationsExcel")]
[ProducesResponseType(typeof(PhysicalFileResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DownloadRegistrationsExcel(
int id,
[FromServices] IMediator mediator,
CancellationToken cancellationToken)
{
DownloadRegistrationsExcelResult result = await mediator.Send(new DownloadRegistrationsExcelQuery(id), cancellationToken);
if (!result.EventFound)
return Problem(statusCode: StatusCodes.Status404NotFound, detail: $"Invalid id = {id}");
if (result.File == null)
return Problem(statusCode: StatusCodes.Status404NotFound, detail: "Registrations Excel could not be generated.");
return PhysicalFile(result.File.PhysicalPath, XlsxContentType, result.File.FileName);
}
}

View File

@@ -0,0 +1,102 @@
using LargeXlsx;
using Microsoft.EntityFrameworkCore;
namespace Events.FilesAPI.Features.RegistrationsExcel;
internal static class EventRegistrationsExcelWriter
{
public static async Task WriteAsync(
string path,
IQueryable<RowData> rows,
RowData firstRow,
CancellationToken cancellationToken)
{
await using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
await using var writer = new XlsxWriter(stream);
string[] headers =
[
"Registration ID",
"Registration date",
"Person ID",
"Last name",
"First name",
"Last name transcription",
"First name transcription",
"Country",
"Sport"
];
string[] firstRowValues = GetRowValues(firstRow);
var columns = headers
.Select((header, index) => XlsxColumn.Formatted(GetWidth(header, firstRowValues[index])))
.ToArray();
writer
.BeginWorksheet("Registrations", columns: columns)
.BeginRow();
foreach (string header in headers)
{
writer.Write(header);
}
await foreach (RowData row in rows.AsAsyncEnumerable().WithCancellation(cancellationToken))
{
writer
.BeginRow()
.Write(row.RegistrationId)
.Write(FormatRegisteredAt(row.RegisteredAt))
.Write(row.PersonId)
.Write(row.LastName)
.Write(row.FirstName)
.Write(row.LastNameTranscription)
.Write(row.FirstNameTranscription)
.Write(row.CountryName)
.Write(row.SportName);
}
await writer.CommitAsync();
}
private static string[] GetRowValues(RowData row)
{
return
[
row.RegistrationId.ToString(),
FormatRegisteredAt(row.RegisteredAt),
row.PersonId.ToString(),
row.LastName,
row.FirstName,
row.LastNameTranscription,
row.FirstNameTranscription,
row.CountryName,
row.SportName
];
}
private static double GetWidth(string header, string sample)
{
int maxLength = Math.Max(header.Length, sample.Length);
double paddedWidth = Math.Ceiling(maxLength * 1.25d + 4d);
return Math.Clamp(paddedWidth, 10d, 60d);
}
private static string FormatRegisteredAt(DateTime? value)
{
return value.HasValue ? value.Value.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty;
}
internal sealed class RowData
{
public int RegistrationId { get; init; }
public DateTime? RegisteredAt { get; init; }
public int PersonId { get; init; }
public string FirstName { get; init; } = string.Empty;
public string LastName { get; init; } = string.Empty;
public string FirstNameTranscription { get; init; } = string.Empty;
public string LastNameTranscription { get; init; } = string.Empty;
public string CountryName { get; init; } = string.Empty;
public string SportName { get; init; } = string.Empty;
}
}

View File

@@ -0,0 +1,43 @@
using Events.WebAPI.Contract.Messages;
using Events.FilesAPI.Features.RegistrationsExcel.Synchronize;
using MassTransit;
using MediatR;
namespace Events.FilesAPI.Features.RegistrationsExcel;
public class RegistrationsExcelEventsConsumer :
IConsumer<RegistrationCreated>,
IConsumer<RegistrationUpdated>,
IConsumer<RegistrationDeleted>
{
private readonly IMediator mediator;
public RegistrationsExcelEventsConsumer(IMediator mediator)
{
this.mediator = mediator;
}
public Task Consume(ConsumeContext<RegistrationCreated> context)
{
return mediator.Send(new SynchronizeRegistrationsExcelCommand(
context.Message.EventId), context.CancellationToken);
}
public async Task Consume(ConsumeContext<RegistrationUpdated> context)
{
await mediator.Send(new SynchronizeRegistrationsExcelCommand(
context.Message.EventId), context.CancellationToken);
if (context.Message.PreviousEventId != context.Message.EventId)
{
await mediator.Send(new SynchronizeRegistrationsExcelCommand(
context.Message.PreviousEventId), context.CancellationToken);
}
}
public Task Consume(ConsumeContext<RegistrationDeleted> context)
{
return mediator.Send(new SynchronizeRegistrationsExcelCommand(
context.Message.EventId), context.CancellationToken);
}
}

View File

@@ -0,0 +1,70 @@
using Events.FilesAPI.Infrastructure.Options;
using Events.WebAPI.Handlers.EF.Data.Postgres;
using Events.WebAPI.Handlers.EF.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace Events.FilesAPI.Features.RegistrationsExcel.Synchronize;
public sealed class RegistrationsExcelFileGenerator(
EventsContext context,
IHostEnvironment hostEnvironment,
IOptions<GeneratedFilesOptions> generatedFilesOptions,
ILogger<RegistrationsExcelFileGenerator> logger)
{
public async Task GenerateAsync(int eventId, CancellationToken cancellationToken)
{
var registrations = context.Set<Registration>()
.AsNoTracking()
.Where(r => r.EventId == eventId)
.OrderBy(r => r.Person.LastName)
.ThenBy(r => r.Person.FirstName)
.ThenBy(r => r.Sport.Name)
.Select(r => new EventRegistrationsExcelWriter.RowData
{
RegistrationId = r.Id,
RegisteredAt = r.RegisteredAt,
PersonId = r.PersonId,
FirstName = r.Person.FirstName,
LastName = r.Person.LastName,
FirstNameTranscription = r.Person.FirstNameTranscription,
LastNameTranscription = r.Person.LastNameTranscription,
CountryName = r.Person.CountryCodeNavigation.Name,
SportName = r.Sport.Name
});
string excelPath = GetPath(eventId);
EventRegistrationsExcelWriter.RowData? firstRow = await registrations.FirstOrDefaultAsync(cancellationToken);
if (firstRow == null)
{
DeleteFileIfExists(excelPath);
return;
}
Directory.CreateDirectory(Path.GetDirectoryName(excelPath)!);
await EventRegistrationsExcelWriter.WriteAsync(excelPath, registrations, firstRow, cancellationToken);
logger.LogInformation(
"Event registrations Excel generated for event #{EventId} at {Path}",
eventId,
excelPath);
}
private string GetPath(int eventId)
{
string rootPath = Path.IsPathRooted(generatedFilesOptions.Value.OutputPath)
? generatedFilesOptions.Value.OutputPath
: Path.GetFullPath(Path.Combine(hostEnvironment.ContentRootPath, generatedFilesOptions.Value.OutputPath));
return Path.Combine(rootPath, $"{eventId}.xlsx");
}
private void DeleteFileIfExists(string path)
{
if (!File.Exists(path))
return;
File.Delete(path);
logger.LogInformation("Event registrations Excel deleted at {Path}", path);
}
}

View File

@@ -0,0 +1,5 @@
using MediatR;
namespace Events.FilesAPI.Features.RegistrationsExcel.Synchronize;
public sealed record SynchronizeRegistrationsExcelCommand(int EventId) : IRequest;

View File

@@ -0,0 +1,12 @@
using MediatR;
namespace Events.FilesAPI.Features.RegistrationsExcel.Synchronize;
public sealed class SynchronizeRegistrationsExcelHandler(
RegistrationsExcelFileGenerator generator) : IRequestHandler<SynchronizeRegistrationsExcelCommand>
{
public async Task Handle(SynchronizeRegistrationsExcelCommand request, CancellationToken cancellationToken)
{
await generator.GenerateAsync(request.EventId, cancellationToken);
}
}