Compare commits
21 Commits
90031db6a5
...
e5295b8302
| Author | SHA1 | Date | |
|---|---|---|---|
| e5295b8302 | |||
| 00a9cf06da | |||
| 1b387238e8 | |||
| bda4f3dbef | |||
| 2458d0c07a | |||
| a72cbab195 | |||
| bcf4e63f7c | |||
| 5aabeb4510 | |||
| 32edc6474d | |||
| 71bfe3b323 | |||
| 089d2bd1cb | |||
| 65c72bcf77 | |||
| 2d8375f26a | |||
| a7cfb099fa | |||
| 7a0d4e2fa7 | |||
| 3955ee9f39 | |||
| 9bdf24d7d5 | |||
| 993ca82596 | |||
| ce9958a8b1 | |||
| 6c54473d5a | |||
| 9ad4352e02 |
@@ -0,0 +1,47 @@
|
|||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repräsentiert eine Abfrage für Umschläge.
|
||||||
|
/// </summary>
|
||||||
|
public record ReadSingleEnvelopeDocResultQuery() : IRequest<byte[]>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public ReadSingleEnvelopeQuery Envelope { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verarbeitet <see cref="ReadEnvelopeQuery"/> und liefert passende <see cref="EnvelopeDto"/>-Ergebnisse.
|
||||||
|
/// </summary>
|
||||||
|
public class ReadSingleEnvelopeDocResultQueryHandler : IRequestHandler<ReadSingleEnvelopeDocResultQuery, byte[]>
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mediator"></param>
|
||||||
|
public ReadSingleEnvelopeDocResultQueryHandler(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<byte[]> Handle(ReadSingleEnvelopeDocResultQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(request.Envelope, cancellationToken);
|
||||||
|
return result.DocResult is byte[] docResult && docResult.Length > 0
|
||||||
|
? docResult
|
||||||
|
: throw new NotFoundException($"Document for Envelope with ID {request.Envelope.Id} not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,14 @@ using AutoMapper;
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Repräsentiert eine Abfrage für einen einzelnen Umschlag.
|
/// Repräsentiert eine Abfrage für einen einzelnen Umschlag.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadSingleEnvelopeQuery : EnvelopeQueryBase, IRequest<EnvelopeDto?>
|
public record ReadSingleEnvelopeQuery : EnvelopeQueryBase, IRequest<EnvelopeDto>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
|
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
|
||||||
@@ -27,7 +28,7 @@ public record ReadSingleEnvelopeQuery : EnvelopeQueryBase, IRequest<EnvelopeDto?
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verarbeitet <see cref="ReadSingleEnvelopeQuery"/> und liefert ein einzelnes <see cref="EnvelopeDto"/>-Ergebnis.
|
/// Verarbeitet <see cref="ReadSingleEnvelopeQuery"/> und liefert ein einzelnes <see cref="EnvelopeDto"/>-Ergebnis.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ReadSingleEnvelopeQueryHandler : IRequestHandler<ReadSingleEnvelopeQuery, EnvelopeDto?>
|
public class ReadSingleEnvelopeQueryHandler : IRequestHandler<ReadSingleEnvelopeQuery, EnvelopeDto>
|
||||||
{
|
{
|
||||||
private readonly IRepository<Envelope> _repository;
|
private readonly IRepository<Envelope> _repository;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
@@ -49,7 +50,7 @@ public class ReadSingleEnvelopeQueryHandler : IRequestHandler<ReadSingleEnvelope
|
|||||||
/// <param name="request"></param>
|
/// <param name="request"></param>
|
||||||
/// <param name="cancel"></param>
|
/// <param name="cancel"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<EnvelopeDto?> Handle(ReadSingleEnvelopeQuery request, CancellationToken cancel)
|
public async Task<EnvelopeDto> Handle(ReadSingleEnvelopeQuery request, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var query = _repository.Query;
|
var query = _repository.Query;
|
||||||
|
|
||||||
@@ -61,11 +62,17 @@ public class ReadSingleEnvelopeQueryHandler : IRequestHandler<ReadSingleEnvelope
|
|||||||
|
|
||||||
if (request.Uuid is string uuid)
|
if (request.Uuid is string uuid)
|
||||||
query = query.Where(e => e.Uuid == uuid);
|
query = query.Where(e => e.Uuid == uuid);
|
||||||
|
|
||||||
var envelope = await query
|
var envelopes = await query
|
||||||
.Include(e => e.Documents)
|
.Include(e => e.Documents)
|
||||||
.FirstOrDefaultAsync(cancel);
|
.Take(2)
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
return envelope is null ? null : _mapper.Map<EnvelopeDto>(envelope);
|
if (envelopes.Count > 1)
|
||||||
|
throw new BadRequestException($"Multiple envelopes found for the given criteria: Id={request.Id}, Uuid={request.Uuid}, UserId={request.UserId}");
|
||||||
|
|
||||||
|
return envelopes.SingleOrDefault() is Envelope envelope
|
||||||
|
? _mapper.Map<EnvelopeDto>(envelope)
|
||||||
|
: throw new NotFoundException($"Envelope with Id={request.Id}, Uuid={request.Uuid} not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,6 +202,10 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
.HasForeignKey(annot => annot.ElementId);
|
.HasForeignKey(annot => annot.ElementId);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region EnvelopeReport
|
||||||
|
modelBuilder.Entity<EnvelopeReport>().HasNoKey();
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Trigger
|
#region Trigger
|
||||||
// Configure entities to handle database triggers
|
// Configure entities to handle database triggers
|
||||||
void AddTrigger<T>() where T : class => _triggers
|
void AddTrigger<T>() where T : class => _triggers
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Controllers;
|
||||||
|
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class DocResultController(IMediator mediator) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAsync([FromQuery] ReadSingleEnvelopeDocResultQuery query, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
return File(await mediator.Send(query, cancel), "application/pdf", $"envelope_{query.Envelope.Uuid}.pdf");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
|
using EnvelopeGenerator.ServiceHost.Jobs;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mediator"></param>
|
||||||
|
/// <param name="job"></param>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class FinalizeDocumentController(IMediator mediator, FinalizeDocumentJob job) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> FinalizeEnvelopeDocument([FromQuery] ReadSingleEnvelopeQuery query, [FromQuery] bool forceRegenerate = false, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var envelope = await mediator.Send(query, cancel);
|
||||||
|
|
||||||
|
// TODO: migrate forceRegenerate input to FinalizeDocumentJob as property and remove this check from controller
|
||||||
|
if (envelope.DocResult is null || forceRegenerate)
|
||||||
|
await job.ExecuteAsync(envelope, cancel);
|
||||||
|
|
||||||
|
var docResult = await mediator.Send(new ReadSingleEnvelopeDocResultQuery() { Envelope = query }, cancel);
|
||||||
|
return File(docResult, "application/pdf", $"envelope_{query.Uuid}.pdf");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
<PackageReference Include="GdPicture" Version="14.3.3" />
|
<PackageReference Include="GdPicture" Version="14.3.3" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||||
@@ -18,9 +20,11 @@
|
|||||||
<PackageReference Include="System.Drawing.Common" Version="8.0.16" />
|
<PackageReference Include="System.Drawing.Common" Version="8.0.16" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||||
<PackageReference Include="DevExpress.Reporting.Core" Version="24.2.*" />
|
<PackageReference Include="DevExpress.Reporting.Core" Version="24.2.*" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.17" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
|
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
|
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ public class FinalizeDocumentJob(IOptions<WorkerOptions> options, ILogger<Finali
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task ExecuteAsync(EnvelopeDto envelope, CancellationToken cancel = default) => ExecuteAsync([envelope], cancel);
|
||||||
|
|
||||||
public async Task ExecuteAsync(CancellationToken cancel = default)
|
public async Task ExecuteAsync(CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var envelopes = await mediator.Send(new ReadEnvelopeQuery()
|
var envelopes = await mediator.Send(new ReadEnvelopeQuery()
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
namespace EnvelopeGenerator.ServiceHost.Middleware;
|
||||||
|
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Middleware for handling exceptions globally in the application.
|
||||||
|
/// Captures exceptions thrown during the request pipeline execution,
|
||||||
|
/// logs them, and returns an appropriate HTTP response with a JSON error message.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Initializes a new instance of the <see cref="ExceptionHandlingMiddleware"/> class.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="next">The next middleware in the request pipeline.</param>
|
||||||
|
/// <param name="logger">The logger instance for logging exceptions.</param>
|
||||||
|
public class ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the middleware to handle the HTTP request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The HTTP context of the current request.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await next(context); // Continue down the pipeline
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await HandleExceptionAsync(context, ex, logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles exceptions by logging them and writing an appropriate JSON response.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The HTTP context of the current request.</param>
|
||||||
|
/// <param name="exception">The exception that occurred.</param>
|
||||||
|
/// <param name="logger">The logger instance for logging the exception.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
private static async Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger logger)
|
||||||
|
{
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
|
||||||
|
string message;
|
||||||
|
|
||||||
|
switch (exception)
|
||||||
|
{
|
||||||
|
case BadRequestException badRequestEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||||
|
message = badRequestEx.Message;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotFoundException notFoundEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||||
|
message = notFoundEx.Message;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.LogError(exception, "Unhandled exception occurred.");
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
|
message = "An unexpected error occurred.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
message
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,65 @@
|
|||||||
|
using DigitalData.UserManager.DependencyInjection;
|
||||||
|
using EnvelopeGenerator.Application;
|
||||||
|
using EnvelopeGenerator.Infrastructure;
|
||||||
using EnvelopeGenerator.ServiceHost;
|
using EnvelopeGenerator.ServiceHost;
|
||||||
using EnvelopeGenerator.ServiceHost.Extensions;
|
using EnvelopeGenerator.ServiceHost.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Localization;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
#region Kestrel & Windows Service Configuration
|
||||||
|
if (builder.Configuration.GetValue<bool>("UseWindowsService"))
|
||||||
|
{
|
||||||
|
builder.Host.UseWindowsService();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder.Configuration.GetValue<bool>("UseKestrelConfig"))
|
||||||
|
{
|
||||||
|
builder.WebHost.ConfigureKestrel((context, serverOptions) =>
|
||||||
|
{
|
||||||
|
var kestrelSection = context.Configuration.GetSection("Kestrel");
|
||||||
|
serverOptions.Configure(kestrelSection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var config = builder.Configuration;
|
||||||
|
var connStr = config.GetConnectionString("Default") ??
|
||||||
|
throw new InvalidOperationException("Connection string 'Default' is missing in the configuration.");
|
||||||
|
|
||||||
|
#region Service configuration
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddFinalizeDocumentJob(builder.Configuration);
|
|
||||||
builder.Services.AddHostedService<Worker>();
|
builder.Services.AddHostedService<Worker>();
|
||||||
|
builder.Services.AddDistributedSqlServerCache(options =>
|
||||||
|
{
|
||||||
|
options.ConnectionString = connStr;
|
||||||
|
options.SchemaName = "dbo";
|
||||||
|
options.TableName = "TBDD_CACHE";
|
||||||
|
});
|
||||||
|
#pragma warning disable CS0618
|
||||||
|
builder.Services.AddFinalizeDocumentJob(config);
|
||||||
|
builder.Services.AddEnvelopeGeneratorInfrastructureServices(
|
||||||
|
opt =>
|
||||||
|
{
|
||||||
|
opt.AddDbTriggerParams(config);
|
||||||
|
opt.AddDbContext((provider, options) =>
|
||||||
|
{
|
||||||
|
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
|
||||||
|
options.UseSqlServer(connStr)
|
||||||
|
.LogTo(log => logger.LogInformation("{log}", log), Microsoft.Extensions.Logging.LogLevel.Trace)
|
||||||
|
.EnableSensitiveDataLogging()
|
||||||
|
.EnableDetailedErrors();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
builder.Services.AddEnvelopeGeneratorServices(config);
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
builder.Services.AddUserManager<EGDbContext>();
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
builder.Services.AddLocalization();
|
||||||
|
#endregion
|
||||||
|
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
@@ -22,10 +72,22 @@ if (app.Environment.IsDevelopment())
|
|||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Localizer
|
||||||
|
var supportedCultureNames = config.GetSection("SupportedCultures").Get<string[]>() ?? ["de-DE", "en-US"];
|
||||||
|
var supportedCultures = supportedCultureNames.Select(cName => new CultureInfo(cName)).ToList();
|
||||||
|
var requestLocalizationOptions = new RequestLocalizationOptions
|
||||||
|
{
|
||||||
|
SupportedCultures = supportedCultures,
|
||||||
|
SupportedUICultures = supportedCultures
|
||||||
|
};
|
||||||
|
requestLocalizationOptions.RequestCultureProviders.Add(new QueryStringRequestCultureProvider());
|
||||||
|
app.UseRequestLocalization(requestLocalizationOptions);
|
||||||
|
#endregion
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -6,13 +6,13 @@ public class Worker : BackgroundService
|
|||||||
{
|
{
|
||||||
private readonly ILogger<Worker> _logger;
|
private readonly ILogger<Worker> _logger;
|
||||||
private readonly int _delayMilliseconds;
|
private readonly int _delayMilliseconds;
|
||||||
private readonly FinalizeDocumentJob _finalizeDocumentJob;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
|
||||||
public Worker(ILogger<Worker> logger, IConfiguration configuration, FinalizeDocumentJob finalizeDocumentJob)
|
public Worker(ILogger<Worker> logger, IConfiguration configuration, IServiceScopeFactory scopeFactory)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_delayMilliseconds = Math.Max(1, configuration.GetValue("Worker:DelayMilliseconds", 1000));
|
_delayMilliseconds = Math.Max(1, configuration.GetValue("Worker:DelayMilliseconds", 1000));
|
||||||
_finalizeDocumentJob = finalizeDocumentJob;
|
_scopeFactory = scopeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
@@ -24,7 +24,10 @@ public class Worker : BackgroundService
|
|||||||
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _finalizeDocumentJob.ExecuteAsync(stoppingToken);
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var finalizeDocumentJob = scope.ServiceProvider.GetRequiredService<FinalizeDocumentJob>();
|
||||||
|
await finalizeDocumentJob.ExecuteAsync(stoppingToken);
|
||||||
|
|
||||||
await Task.Delay(_delayMilliseconds, stoppingToken);
|
await Task.Delay(_delayMilliseconds, stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,18 @@
|
|||||||
"Worker": {
|
"Worker": {
|
||||||
"DelayMilliseconds": 1000
|
"DelayMilliseconds": 1000
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
}
|
"SupportedCultures": [ "de-DE", "en-US" ],
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
|
||||||
|
},
|
||||||
|
"UseWindowsService": false,
|
||||||
|
"UseKestrelConfig": false,
|
||||||
|
"Kestrel": {
|
||||||
|
"Endpoints": {
|
||||||
|
"Http": {
|
||||||
|
"Url": "http://localhost:1111"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -193,13 +193,17 @@ public static class Extensions
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Envelope
|
#region Envelope
|
||||||
public static CreateEnvelopeCommand CreateEnvelopeCommand(this Faker fake, int userId) => new()
|
public static CreateEnvelopeCommand CreateEnvelopeCommand(this Faker fake, int userId)
|
||||||
{
|
{
|
||||||
Message = fake.Lorem.Paragraph(fake.Random.Number(2, 5)),
|
var cmd = new CreateEnvelopeCommand
|
||||||
Title = fake.Lorem.Words(fake.Random.Number(3, 4)).Join(" "),
|
{
|
||||||
UserId = userId,
|
Message = fake.Lorem.Paragraph(fake.Random.Number(2, 5)),
|
||||||
UseSQLExecutor = false
|
Title = fake.Lorem.Words(fake.Random.Number(3, 4)).Join(" "),
|
||||||
};
|
UseSQLExecutor = false
|
||||||
|
};
|
||||||
|
cmd.Authorize(userId);
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
public static List<CreateEnvelopeCommand> CreateEnvelopeCommands(this Faker fake, params int[] userIDs)
|
public static List<CreateEnvelopeCommand> CreateEnvelopeCommands(this Faker fake, params int[] userIDs)
|
||||||
=> Enumerable.Range(0, userIDs.Length)
|
=> Enumerable.Range(0, userIDs.Length)
|
||||||
|
|||||||
308
EnvelopeGenerator.Tests/Application/MediatorExtensionsTests.cs
Normal file
308
EnvelopeGenerator.Tests/Application/MediatorExtensionsTests.cs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Tests.Application;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class MediatorExtensionsTests
|
||||||
|
{
|
||||||
|
#region Stub infrastructure
|
||||||
|
|
||||||
|
private sealed class StubRequest<TResponse> : IRequest<TResponse?> { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal <see cref="ISender"/> stub that returns a pre-configured response for any <see cref="ISender.Send{TResponse}"/> call.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class StubSender : ISender
|
||||||
|
{
|
||||||
|
private readonly object? _response;
|
||||||
|
|
||||||
|
public StubSender(object? response) => _response = response;
|
||||||
|
|
||||||
|
public Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return Task.FromResult((TResponse)_response!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Send<TRequest>(TRequest request, CancellationToken cancellationToken = default) where TRequest : IRequest
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<object?> Send(object request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return Task.FromResult(_response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(IStreamRequest<TResponse> request, CancellationToken cancellationToken = default)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public IAsyncEnumerable<object?> CreateStream(object request, CancellationToken cancellationToken = default)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ISender CreateSender<T>(T? response) => new StubSender(response);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Throw — non-null scalar
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Throw_WithNonNullResponse_ReturnsResponse()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<string>("hello");
|
||||||
|
var request = new StubRequest<string?>();
|
||||||
|
|
||||||
|
var result = await sender.GetOr(request).Throw(() => new InvalidOperationException());
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("hello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Throw — null response
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Throw_WithNullResponse_ThrowsCustomException()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<string>(null);
|
||||||
|
var request = new StubRequest<string?>();
|
||||||
|
|
||||||
|
var ex = Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => sender.GetOr(request).Throw(() => new InvalidOperationException("custom")));
|
||||||
|
|
||||||
|
Assert.That(ex!.Message, Is.EqualTo("custom"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Throw — empty collection
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Throw_WithEmptyList_ThrowsCustomException()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<List<string>>(new List<string>());
|
||||||
|
var request = new StubRequest<List<string>?>();
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<ArgumentException>(
|
||||||
|
() => sender.GetOr(request).Throw(() => new ArgumentException("empty")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Throw — non-empty collection
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Throw_WithNonEmptyList_ReturnsResponse()
|
||||||
|
{
|
||||||
|
var expected = new List<int> { 1, 2 };
|
||||||
|
var sender = CreateSender<List<int>>(expected);
|
||||||
|
var request = new StubRequest<List<int>?>();
|
||||||
|
|
||||||
|
var result = await sender.GetOr(request).Throw(() => new InvalidOperationException());
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Throw — string edge case (string implements IEnumerable)
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Throw_WithEmptyString_ReturnsEmptyString()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<string>("");
|
||||||
|
var request = new StubRequest<string?>();
|
||||||
|
|
||||||
|
var result = await sender.GetOr(request).Throw(() => new InvalidOperationException("should not throw"));
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ThrowNotFound — non-null scalar
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ThrowNotFound_WithNonNullResponse_ReturnsResponse()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<string>("hello");
|
||||||
|
var request = new StubRequest<string?>();
|
||||||
|
|
||||||
|
var result = await sender.GetOr(request).ThrowNotFound();
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("hello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ThrowNotFound_WithExceptionMessage_AndNonNullResponse_ReturnsResponse()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<int>(42);
|
||||||
|
var request = new StubRequest<int?>();
|
||||||
|
|
||||||
|
var result = await sender.GetOr(request).ThrowNotFound("not found");
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo(42));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ThrowNotFound — null response
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ThrowNotFound_WithNullResponse_ThrowsNotFoundException()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<string>(null);
|
||||||
|
var request = new StubRequest<string?>();
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<NotFoundException>(() => sender.GetOr(request).ThrowNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ThrowNotFound_WithNullResponse_AndCustomMessage_ContainsMessage()
|
||||||
|
{
|
||||||
|
const string message = "Entity not found";
|
||||||
|
var sender = CreateSender<string>(null);
|
||||||
|
var request = new StubRequest<string?>();
|
||||||
|
|
||||||
|
var ex = Assert.ThrowsAsync<NotFoundException>(
|
||||||
|
() => sender.GetOr(request).ThrowNotFound(message));
|
||||||
|
|
||||||
|
Assert.That(ex!.Message, Does.Contain(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ThrowNotFound_WithNullResponse_HasDefaultMessageWithTypeName()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<string>(null);
|
||||||
|
var request = new StubRequest<string?>();
|
||||||
|
|
||||||
|
var ex = Assert.ThrowsAsync<NotFoundException>(() => sender.GetOr(request).ThrowNotFound());
|
||||||
|
|
||||||
|
Assert.That(ex!.Message, Does.Contain(nameof(String)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ThrowNotFound — empty collection
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ThrowNotFound_WithEmptyList_ThrowsNotFoundException()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<List<string>>(new List<string>());
|
||||||
|
var request = new StubRequest<List<string>?>();
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<NotFoundException>(() => sender.GetOr(request).ThrowNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ThrowNotFound_WithEmptyArray_ThrowsNotFoundException()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<int[]>(Array.Empty<int>());
|
||||||
|
var request = new StubRequest<int[]?>();
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<NotFoundException>(() => sender.GetOr(request).ThrowNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ThrowNotFound_WithEmptyCollection_AndCustomMessage_ContainsMessage()
|
||||||
|
{
|
||||||
|
const string message = "No items found";
|
||||||
|
var sender = CreateSender<List<int>>(new List<int>());
|
||||||
|
var request = new StubRequest<List<int>?>();
|
||||||
|
|
||||||
|
var ex = Assert.ThrowsAsync<NotFoundException>(
|
||||||
|
() => sender.GetOr(request).ThrowNotFound(message));
|
||||||
|
|
||||||
|
Assert.That(ex!.Message, Does.Contain(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ThrowNotFound — non-empty collection
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ThrowNotFound_WithNonEmptyList_ReturnsResponse()
|
||||||
|
{
|
||||||
|
var expected = new List<string> { "a", "b" };
|
||||||
|
var sender = CreateSender<List<string>>(expected);
|
||||||
|
var request = new StubRequest<List<string>?>();
|
||||||
|
|
||||||
|
var result = await sender.GetOr(request).ThrowNotFound();
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ThrowNotFound_WithNonEmptyArray_ReturnsResponse()
|
||||||
|
{
|
||||||
|
var expected = new[] { 1, 2, 3 };
|
||||||
|
var sender = CreateSender<int[]>(expected);
|
||||||
|
var request = new StubRequest<int[]?>();
|
||||||
|
|
||||||
|
var result = await sender.GetOr(request).ThrowNotFound();
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ThrowInvalidOperation — null response
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ThrowInvalidOperation_WithNullResponse_ThrowsInvalidOperationException()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<string>(null);
|
||||||
|
var request = new StubRequest<string?>();
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => sender.GetOr(request).ThrowInvalidOperation());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ThrowInvalidOperation_WithNullResponse_AndCustomMessage_ContainsMessage()
|
||||||
|
{
|
||||||
|
const string message = "Something went wrong";
|
||||||
|
var sender = CreateSender<string>(null);
|
||||||
|
var request = new StubRequest<string?>();
|
||||||
|
|
||||||
|
var ex = Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => sender.GetOr(request).ThrowInvalidOperation(message));
|
||||||
|
|
||||||
|
Assert.That(ex!.Message, Does.Contain(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region CancellationToken
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Throw_WithCancelledToken_ThrowsOperationCanceledException()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<string>("value");
|
||||||
|
var request = new StubRequest<string?>();
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<OperationCanceledException>(
|
||||||
|
() => sender.GetOr(request, cts.Token).Throw(() => new InvalidOperationException()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ThrowNotFound_WithCancelledToken_ThrowsOperationCanceledException()
|
||||||
|
{
|
||||||
|
var sender = CreateSender<string>("value");
|
||||||
|
var request = new StubRequest<string?>();
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<OperationCanceledException>(
|
||||||
|
() => sender.GetOr(request, cts.Token).ThrowNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user