Add workflow and document APIs, middleware, and Swagger
Introduced `CloseWorkflowRequest` for closing workflows. Added `DocumentsController` for document downloads and `WorkflowsController` for workflow retrieval and closing. Implemented `ExceptionHandlingMiddleware` for global exception handling with structured API responses. Enhanced Swagger configuration with metadata and production support. Added `Scalar.AspNetCore` for API reference mapping. Updated project dependencies and improved API documentation.
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
namespace DigitalData.TaskFlow.API.Contracts.Workflows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the request payload for closing a workflow.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CloseWorkflowRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the selected closing method identifier.
|
||||||
|
/// </summary>
|
||||||
|
public required int ClosingMethodId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an optional user comment.
|
||||||
|
/// </summary>
|
||||||
|
public string? Comment { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an optional rejection reason identifier.
|
||||||
|
/// </summary>
|
||||||
|
public int? RejectionReasonId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using DigitalData.TaskFlow.Application.Features.Documents.Queries.GetDocument;
|
||||||
|
using DigitalData.TaskFlow.Domain.Common;
|
||||||
|
using DigitalData.TaskFlow.Domain.Documents;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace DigitalData.TaskFlow.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides document binary download endpoints.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/documents")]
|
||||||
|
public sealed class DocumentsController(ISender sender) : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns binary document content by document identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Document identifier.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Binary stream when available.</returns>
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiEnvelope<object>), StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(typeof(ApiEnvelope<object>), StatusCodes.Status500InternalServerError)]
|
||||||
|
public async Task<IActionResult> GetAsync([FromRoute] string id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var response = await sender.Send(new GetDocumentQuery(id), cancellationToken);
|
||||||
|
var data = response.Data;
|
||||||
|
|
||||||
|
if (data is null)
|
||||||
|
{
|
||||||
|
return NotFound(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return File(data.Content, data.ContentType, data.FileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using DigitalData.TaskFlow.API.Contracts.Workflows;
|
||||||
|
using DigitalData.TaskFlow.Application.Features.Workflows.Commands.CloseWorkflow;
|
||||||
|
using DigitalData.TaskFlow.Application.Features.Workflows.Queries.GetWorkflows;
|
||||||
|
using DigitalData.TaskFlow.Domain.Closing;
|
||||||
|
using DigitalData.TaskFlow.Domain.Common;
|
||||||
|
using DigitalData.TaskFlow.Domain.Workflows;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace DigitalData.TaskFlow.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides workflow read and close endpoints.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/workflows")]
|
||||||
|
public sealed class WorkflowsController(ISender sender) : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns open workflows for a WISAG user identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="wisagId">WISAG user identifier.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Workflow response envelope grouped by source.</returns>
|
||||||
|
[HttpGet("{wisagId:int}")]
|
||||||
|
[ProducesResponseType(typeof(ApiEnvelope<WorkflowSourcesResponse>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiEnvelope<object>), StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(typeof(ApiEnvelope<object>), StatusCodes.Status500InternalServerError)]
|
||||||
|
public async Task<ActionResult<ApiEnvelope<WorkflowSourcesResponse>>> GetAsync(
|
||||||
|
[FromRoute] int wisagId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var response = await sender.Send(new GetWorkflowsQuery(wisagId), cancellationToken);
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a closing decision to a workflow process.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="wisagId">WISAG user identifier.</param>
|
||||||
|
/// <param name="source">Source system name.</param>
|
||||||
|
/// <param name="processId">Process identifier.</param>
|
||||||
|
/// <param name="request">Closing request payload.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Close result envelope.</returns>
|
||||||
|
[HttpPost("{wisagId:int}/{source}/{processId:int}/closing")]
|
||||||
|
[ProducesResponseType(typeof(ApiEnvelope<CloseWorkflowResult>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiEnvelope<object>), StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(typeof(ApiEnvelope<object>), StatusCodes.Status500InternalServerError)]
|
||||||
|
public async Task<ActionResult<ApiEnvelope<CloseWorkflowResult>>> CloseAsync(
|
||||||
|
[FromRoute] int wisagId,
|
||||||
|
[FromRoute] string source,
|
||||||
|
[FromRoute] int processId,
|
||||||
|
[FromBody] CloseWorkflowRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var command = new CloseWorkflowCommand(
|
||||||
|
wisagId,
|
||||||
|
source,
|
||||||
|
processId,
|
||||||
|
request.ClosingMethodId,
|
||||||
|
request.Comment,
|
||||||
|
request.RejectionReasonId);
|
||||||
|
|
||||||
|
var response = await sender.Send(command, cancellationToken);
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
|
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
|
||||||
|
<PackageReference Include="Microsoft.OpenApi" Version="3.10.2" />
|
||||||
|
<PackageReference Include="Scalar.AspNetCore" Version="2.17.14" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.3.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using DigitalData.TaskFlow.Application.Common.Factories;
|
||||||
|
using DigitalData.TaskFlow.Domain.Common;
|
||||||
|
using DigitalData.TaskFlow.Domain.Enums;
|
||||||
|
using FluentValidation;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace DigitalData.TaskFlow.API.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles exceptions globally and returns ticket-compatible API envelopes.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the middleware.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">HTTP context.</param>
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
}
|
||||||
|
catch (ValidationException validationException)
|
||||||
|
{
|
||||||
|
logger.LogWarning(validationException, "Validation failed.");
|
||||||
|
|
||||||
|
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
|
||||||
|
var message = validationException.Errors.FirstOrDefault()?.ErrorMessage
|
||||||
|
?? "Ungultiger Request";
|
||||||
|
|
||||||
|
var response = ApiEnvelopeFactory.Create<object?>(null, MetaCode.InvalidRequest, message);
|
||||||
|
await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions));
|
||||||
|
}
|
||||||
|
catch (NotSupportedException notSupportedException)
|
||||||
|
{
|
||||||
|
logger.LogWarning(notSupportedException, "Operation not supported yet.");
|
||||||
|
|
||||||
|
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
|
||||||
|
var response = ApiEnvelopeFactory.Create<object?>(null, MetaCode.InternalError, notSupportedException.Message);
|
||||||
|
await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions));
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(exception, "Unhandled exception.");
|
||||||
|
|
||||||
|
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
|
||||||
|
var response = ApiEnvelopeFactory.Create<object?>(null, MetaCode.InternalError);
|
||||||
|
await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
using DigitalData.TaskFlow.Application;
|
using DigitalData.TaskFlow.Application;
|
||||||
|
using DigitalData.TaskFlow.API.Middleware;
|
||||||
|
using DigitalData.TaskFlow.API.Options;
|
||||||
using DigitalData.TaskFlow.Infrastructure;
|
using DigitalData.TaskFlow.Infrastructure;
|
||||||
|
using Microsoft.OpenApi;
|
||||||
|
using Scalar.AspNetCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
var config = builder.Configuration;
|
var config = builder.Configuration;
|
||||||
@@ -11,15 +15,46 @@ builder.Services.AddInfrastructureServices(config);
|
|||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen(options =>
|
||||||
|
{
|
||||||
|
options.SwaggerDoc("v1", new OpenApiInfo
|
||||||
|
{
|
||||||
|
Title = "DigitalData TaskFlow API",
|
||||||
|
Version = "v1",
|
||||||
|
Description = "Integration API for taskFLOW workflow and document operations.",
|
||||||
|
Contact = new OpenApiContact
|
||||||
|
{
|
||||||
|
Name = "Digital Data GmbH",
|
||||||
|
Url = new Uri("https://digitaldata.works/"),
|
||||||
|
Email = "info-flow@digitaldata.works"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services
|
||||||
|
.AddOptions<SwaggerOptions>()
|
||||||
|
.Bind(config.GetSection(SwaggerOptions.SectionName));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
var swaggerOptions = app.Services
|
||||||
|
.GetRequiredService<Microsoft.Extensions.Options.IOptions<SwaggerOptions>>()
|
||||||
|
.Value;
|
||||||
|
|
||||||
|
var enableSwagger = app.Environment.IsDevelopment() || swaggerOptions.EnabledInProduction;
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (app.Environment.IsDevelopment())
|
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||||
|
|
||||||
|
if (enableSwagger)
|
||||||
{
|
{
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
|
app.MapScalarApiReference(options =>
|
||||||
|
{
|
||||||
|
options.Title = "DigitalData TaskFlow API";
|
||||||
|
options.Theme = ScalarTheme.BluePlanet;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|||||||
Reference in New Issue
Block a user