feat: Implement workflow and form management endpoints

- Added endpoints for managing form definitions including:
  - GetFormDefinitionById
  - GetFormDefinitionList
  - PublishFormDefinition
  - SubmitFormData
  - UpdateFormDefinition

- Added endpoints for managing workflow definitions including:
  - CreateWorkflowDefinition
  - DeleteWorkflowDefinition
  - DisableWorkflowDefinition
  - GetWorkflowDefinitionById
  - GetWorkflowDefinitionList
  - PublishWorkflowDefinition
  - UpdateWorkflowDefinition

- Added endpoints for managing workflow instances including:
  - GetWorkflowInstanceById
  - GetWorkflowInstanceList
  - MonitorWorkflowInstances
  - ResumeWorkflowInstance
  - StartWorkflowInstance
  - SuspendWorkflowInstance
  - WithdrawWorkflowInstance

- Added endpoints for managing workflow tasks including:
  - ApproveTask
  - DelegateTask
  - GetCcTasks
  - GetHistoryTasks
  - GetOverdueTasks
  - GetPendingTasks
  - GetTaskById
  - RejectTask
  - TransferTask
  - UrgeTask

- Introduced middleware for handling API responses and global exceptions.
- Configured application settings for database connection and JWT authentication.
This commit is contained in:
向宁 2026-05-17 22:51:37 +08:00
parent f26a20a875
commit f49e0ea1e4
43 changed files with 1664 additions and 14 deletions

View File

@ -0,0 +1,17 @@
using FastEndpoints.Security;
using Workflow.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
namespace Workflow.Api.Configuration;
public static class JwtAuthConfiguration
{
public static void AddJwtAuthentication(this IServiceCollection services, IConfiguration configuration)
{
var jwtConfig = configuration.GetSection("Jwt");
services.AddJWTBearerAuth(jwtConfig["SigningKey"]!, bearerEvents: null);
services.AddAuthorizationBuilder();
}
}

View File

@ -0,0 +1,38 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Form.DTOs;
using Workflow.Application.Form.FormDefinition.Commands;
namespace Workflow.Api.Endpoints.Form;
public class CreateFormDefinitionEndpoint : Endpoint<CreateFormDefinitionRequest, FormDefinitionDto>
{
private readonly IMediator _mediator;
public CreateFormDefinitionEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/forms");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Create a new form definition";
});
}
public override async Task HandleAsync(CreateFormDefinitionRequest req, CancellationToken ct)
{
var command = new CreateFormDefinitionCommand(req.Name, req.Code, req.Description, req.Fields);
var result = await _mediator.Send(command, ct);
await SendAsync(result, 200, ct);
}
}
public class CreateFormDefinitionRequest
{
public string Name { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
public string? Description { get; set; }
public List<FormFieldDto> Fields { get; set; } = new();
}

View File

@ -0,0 +1,34 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Form.FormDefinition.Commands;
namespace Workflow.Api.Endpoints.Form;
public class DeleteFormDefinitionEndpoint : Endpoint<DeleteFormDefinitionRequest>
{
private readonly IMediator _mediator;
public DeleteFormDefinitionEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Delete("/forms/{Id}");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Delete a form definition (soft delete)";
});
}
public override async Task HandleAsync(DeleteFormDefinitionRequest req, CancellationToken ct)
{
var command = new DeleteFormDefinitionCommand(req.Id);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class DeleteFormDefinitionRequest
{
public Guid Id { get; set; }
}

View File

@ -0,0 +1,35 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Form.DTOs;
using Workflow.Application.Form.FormData.Queries;
namespace Workflow.Api.Endpoints.Form;
public class GetFormDataByInstanceEndpoint : Endpoint<GetFormDataByInstanceRequest, FormDataDto?>
{
private readonly IMediator _mediator;
public GetFormDataByInstanceEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/forms/data/{InstanceId}");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get form data by workflow instance id";
});
}
public override async Task HandleAsync(GetFormDataByInstanceRequest req, CancellationToken ct)
{
var query = new GetFormDataByInstanceQuery(req.InstanceId);
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}
public class GetFormDataByInstanceRequest
{
public Guid InstanceId { get; set; }
}

View File

@ -0,0 +1,35 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Form.DTOs;
using Workflow.Application.Form.FormDefinition.Queries;
namespace Workflow.Api.Endpoints.Form;
public class GetFormDefinitionByIdEndpoint : Endpoint<GetFormDefinitionByIdRequest, FormDefinitionDetailDto>
{
private readonly IMediator _mediator;
public GetFormDefinitionByIdEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/forms/{Id}");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get form definition detail by id (includes fields)";
});
}
public override async Task HandleAsync(GetFormDefinitionByIdRequest req, CancellationToken ct)
{
var query = new GetFormDefinitionByIdQuery(req.Id);
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}
public class GetFormDefinitionByIdRequest
{
public Guid Id { get; set; }
}

View File

@ -0,0 +1,38 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Form.DTOs;
using Workflow.Application.Form.FormDefinition.Queries;
using Workflow.Domain.Enums;
namespace Workflow.Api.Endpoints.Form;
public class GetFormDefinitionListEndpoint : Endpoint<GetFormDefinitionListRequest, PagedResult<FormDefinitionDto>>
{
private readonly IMediator _mediator;
public GetFormDefinitionListEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/forms");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get paginated list of form definitions";
});
}
public override async Task HandleAsync(GetFormDefinitionListRequest req, CancellationToken ct)
{
var query = new GetFormDefinitionListQuery(req.PageIndex, req.PageSize, req.Status);
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}
public class GetFormDefinitionListRequest
{
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 20;
public FormStatus? Status { get; set; }
}

View File

@ -0,0 +1,34 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Form.FormDefinition.Commands;
namespace Workflow.Api.Endpoints.Form;
public class PublishFormDefinitionEndpoint : Endpoint<PublishFormDefinitionRequest>
{
private readonly IMediator _mediator;
public PublishFormDefinitionEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/forms/{Id}/publish");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Publish a form definition";
});
}
public override async Task HandleAsync(PublishFormDefinitionRequest req, CancellationToken ct)
{
var command = new PublishFormDefinitionCommand(req.Id);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class PublishFormDefinitionRequest
{
public Guid Id { get; set; }
}

View File

@ -0,0 +1,36 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Form.FormData.Commands;
namespace Workflow.Api.Endpoints.Form;
public class SubmitFormDataEndpoint : Endpoint<SubmitFormDataRequest, Guid>
{
private readonly IMediator _mediator;
public SubmitFormDataEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/forms/data");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Submit form data for a workflow instance";
});
}
public override async Task HandleAsync(SubmitFormDataRequest req, CancellationToken ct)
{
var command = new SubmitFormDataCommand(req.FormDefinitionId, req.InstanceId, req.DataJson);
var result = await _mediator.Send(command, ct);
await SendAsync(result, 200, ct);
}
}
public class SubmitFormDataRequest
{
public Guid FormDefinitionId { get; set; }
public Guid InstanceId { get; set; }
public string DataJson { get; set; } = string.Empty;
}

View File

@ -0,0 +1,38 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Form.DTOs;
using Workflow.Application.Form.FormDefinition.Commands;
namespace Workflow.Api.Endpoints.Form;
public class UpdateFormDefinitionEndpoint : Endpoint<UpdateFormDefinitionRequest, FormDefinitionDto>
{
private readonly IMediator _mediator;
public UpdateFormDefinitionEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Put("/forms/{Id}");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Update a form definition";
});
}
public override async Task HandleAsync(UpdateFormDefinitionRequest req, CancellationToken ct)
{
var command = new UpdateFormDefinitionCommand(req.Id, req.Name, req.Description, req.Fields);
var result = await _mediator.Send(command, ct);
await SendAsync(result, 200, ct);
}
}
public class UpdateFormDefinitionRequest
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public List<FormFieldDto> Fields { get; set; } = new();
}

View File

@ -0,0 +1,37 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowDefinitions.Commands;
using Workflow.Application.Features.WorkflowDefinitions.DTOs;
namespace Workflow.Api.Endpoints.WorkflowDefinition;
public class CreateWorkflowDefinitionEndpoint : Endpoint<CreateWorkflowDefinitionRequest, WorkflowDefinitionDto>
{
private readonly IMediator _mediator;
public CreateWorkflowDefinitionEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-definitions");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Create a new workflow definition";
});
}
public override async Task HandleAsync(CreateWorkflowDefinitionRequest req, CancellationToken ct)
{
var command = new CreateWorkflowDefinitionCommand(req.Name, req.Code, req.Description);
var result = await _mediator.Send(command, ct);
await SendAsync(result, 200, ct);
}
}
public class CreateWorkflowDefinitionRequest
{
public string Name { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
public string? Description { get; set; }
}

View File

@ -0,0 +1,34 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowDefinitions.Commands;
namespace Workflow.Api.Endpoints.WorkflowDefinition;
public class DeleteWorkflowDefinitionEndpoint : Endpoint<DeleteWorkflowDefinitionRequest>
{
private readonly IMediator _mediator;
public DeleteWorkflowDefinitionEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Delete("/workflow-definitions/{Id}");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Delete a workflow definition (soft delete)";
});
}
public override async Task HandleAsync(DeleteWorkflowDefinitionRequest req, CancellationToken ct)
{
var command = new DeleteWorkflowDefinitionCommand(req.Id);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class DeleteWorkflowDefinitionRequest
{
public Guid Id { get; set; }
}

View File

@ -0,0 +1,34 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowDefinitions.Commands;
namespace Workflow.Api.Endpoints.WorkflowDefinition;
public class DisableWorkflowDefinitionEndpoint : Endpoint<DisableWorkflowDefinitionRequest>
{
private readonly IMediator _mediator;
public DisableWorkflowDefinitionEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-definitions/{Id}/disable");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Disable a workflow definition";
});
}
public override async Task HandleAsync(DisableWorkflowDefinitionRequest req, CancellationToken ct)
{
var command = new DisableWorkflowDefinitionCommand(req.Id);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class DisableWorkflowDefinitionRequest
{
public Guid Id { get; set; }
}

View File

@ -0,0 +1,35 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowDefinitions.DTOs;
using Workflow.Application.Features.WorkflowDefinitions.Queries;
namespace Workflow.Api.Endpoints.WorkflowDefinition;
public class GetWorkflowDefinitionByIdEndpoint : Endpoint<GetWorkflowDefinitionByIdRequest, WorkflowDefinitionDetailDto>
{
private readonly IMediator _mediator;
public GetWorkflowDefinitionByIdEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/workflow-definitions/{Id}");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get workflow definition detail by id (includes nodes and edges)";
});
}
public override async Task HandleAsync(GetWorkflowDefinitionByIdRequest req, CancellationToken ct)
{
var query = new GetWorkflowDefinitionByIdQuery(req.Id);
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}
public class GetWorkflowDefinitionByIdRequest
{
public Guid Id { get; set; }
}

View File

@ -0,0 +1,39 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Common;
using Workflow.Application.Features.WorkflowDefinitions.DTOs;
using Workflow.Application.Features.WorkflowDefinitions.Queries;
using Workflow.Domain.Enums;
namespace Workflow.Api.Endpoints.WorkflowDefinition;
public class GetWorkflowDefinitionListEndpoint : Endpoint<GetWorkflowDefinitionListRequest, PagedResult<WorkflowDefinitionDto>>
{
private readonly IMediator _mediator;
public GetWorkflowDefinitionListEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/workflow-definitions");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get paginated list of workflow definitions";
});
}
public override async Task HandleAsync(GetWorkflowDefinitionListRequest req, CancellationToken ct)
{
var query = new GetWorkflowDefinitionListQuery(req.PageIndex, req.PageSize, req.Status);
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}
public class GetWorkflowDefinitionListRequest
{
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 20;
public DefinitionStatus? Status { get; set; }
}

View File

@ -0,0 +1,34 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowDefinitions.Commands;
namespace Workflow.Api.Endpoints.WorkflowDefinition;
public class PublishWorkflowDefinitionEndpoint : Endpoint<PublishWorkflowDefinitionRequest>
{
private readonly IMediator _mediator;
public PublishWorkflowDefinitionEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-definitions/{Id}/publish");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Publish a workflow definition";
});
}
public override async Task HandleAsync(PublishWorkflowDefinitionRequest req, CancellationToken ct)
{
var command = new PublishWorkflowDefinitionCommand(req.Id);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class PublishWorkflowDefinitionRequest
{
public Guid Id { get; set; }
}

View File

@ -0,0 +1,38 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowDefinitions.Commands;
using Workflow.Application.Features.WorkflowDefinitions.DTOs;
namespace Workflow.Api.Endpoints.WorkflowDefinition;
public class UpdateWorkflowDefinitionEndpoint : Endpoint<UpdateWorkflowDefinitionRequest, WorkflowDefinitionDto>
{
private readonly IMediator _mediator;
public UpdateWorkflowDefinitionEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Put("/workflow-definitions/{Id}");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Update a workflow definition";
});
}
public override async Task HandleAsync(UpdateWorkflowDefinitionRequest req, CancellationToken ct)
{
var command = new UpdateWorkflowDefinitionCommand(req.Id, req.Name, req.Description, req.DefinitionJson);
var result = await _mediator.Send(command, ct);
await SendAsync(result, 200, ct);
}
}
public class UpdateWorkflowDefinitionRequest
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public string? DefinitionJson { get; set; }
}

View File

@ -0,0 +1,35 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowInstances.DTOs;
using Workflow.Application.Features.WorkflowInstances.Queries;
namespace Workflow.Api.Endpoints.WorkflowInstance;
public class GetWorkflowInstanceByIdEndpoint : Endpoint<GetWorkflowInstanceByIdRequest, WorkflowInstanceDetailDto>
{
private readonly IMediator _mediator;
public GetWorkflowInstanceByIdEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/workflow-instances/{Id}");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get workflow instance detail by id (includes tokens and tasks)";
});
}
public override async Task HandleAsync(GetWorkflowInstanceByIdRequest req, CancellationToken ct)
{
var query = new GetWorkflowInstanceByIdQuery(req.Id);
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}
public class GetWorkflowInstanceByIdRequest
{
public Guid Id { get; set; }
}

View File

@ -0,0 +1,40 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Common;
using Workflow.Application.Features.WorkflowInstances.DTOs;
using Workflow.Application.Features.WorkflowInstances.Queries;
using Workflow.Domain.Enums;
namespace Workflow.Api.Endpoints.WorkflowInstance;
public class GetWorkflowInstanceListEndpoint : Endpoint<GetWorkflowInstanceListRequest, PagedResult<WorkflowInstanceDto>>
{
private readonly IMediator _mediator;
public GetWorkflowInstanceListEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/workflow-instances");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get paginated list of workflow instances";
});
}
public override async Task HandleAsync(GetWorkflowInstanceListRequest req, CancellationToken ct)
{
var query = new GetWorkflowInstanceListQuery(req.PageIndex, req.PageSize, req.Status, req.InitiatorId);
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}
public class GetWorkflowInstanceListRequest
{
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 20;
public InstanceStatus? Status { get; set; }
public Guid? InitiatorId { get; set; }
}

View File

@ -0,0 +1,30 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowInstances.DTOs;
using Workflow.Application.Features.WorkflowInstances.Queries;
namespace Workflow.Api.Endpoints.WorkflowInstance;
public class MonitorWorkflowInstancesEndpoint : EndpointWithoutRequest<WorkflowMonitorDto>
{
private readonly IMediator _mediator;
public MonitorWorkflowInstancesEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/workflow-instances/monitor");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get workflow monitoring statistics";
});
}
public override async Task HandleAsync(CancellationToken ct)
{
var query = new MonitorWorkflowInstancesQuery();
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}

View File

@ -0,0 +1,34 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowInstances.Commands;
namespace Workflow.Api.Endpoints.WorkflowInstance;
public class ResumeWorkflowInstanceEndpoint : Endpoint<ResumeWorkflowInstanceRequest>
{
private readonly IMediator _mediator;
public ResumeWorkflowInstanceEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-instances/{Id}/resume");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Resume a suspended workflow instance";
});
}
public override async Task HandleAsync(ResumeWorkflowInstanceRequest req, CancellationToken ct)
{
var command = new ResumeWorkflowInstanceCommand(req.Id);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class ResumeWorkflowInstanceRequest
{
public Guid Id { get; set; }
}

View File

@ -0,0 +1,36 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowInstances.Commands;
namespace Workflow.Api.Endpoints.WorkflowInstance;
public class StartWorkflowInstanceEndpoint : Endpoint<StartWorkflowInstanceRequest, Guid>
{
private readonly IMediator _mediator;
public StartWorkflowInstanceEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-instances");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Start a new workflow instance";
});
}
public override async Task HandleAsync(StartWorkflowInstanceRequest req, CancellationToken ct)
{
var command = new StartWorkflowInstanceCommand(req.DefinitionCode, req.Title, req.Variables);
var result = await _mediator.Send(command, ct);
await SendAsync(result, 200, ct);
}
}
public class StartWorkflowInstanceRequest
{
public string DefinitionCode { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? Variables { get; set; }
}

View File

@ -0,0 +1,34 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowInstances.Commands;
namespace Workflow.Api.Endpoints.WorkflowInstance;
public class SuspendWorkflowInstanceEndpoint : Endpoint<SuspendWorkflowInstanceRequest>
{
private readonly IMediator _mediator;
public SuspendWorkflowInstanceEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-instances/{Id}/suspend");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Suspend a running workflow instance";
});
}
public override async Task HandleAsync(SuspendWorkflowInstanceRequest req, CancellationToken ct)
{
var command = new SuspendWorkflowInstanceCommand(req.Id);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class SuspendWorkflowInstanceRequest
{
public Guid Id { get; set; }
}

View File

@ -0,0 +1,35 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowInstances.Commands;
namespace Workflow.Api.Endpoints.WorkflowInstance;
public class WithdrawWorkflowInstanceEndpoint : Endpoint<WithdrawWorkflowInstanceRequest>
{
private readonly IMediator _mediator;
public WithdrawWorkflowInstanceEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-instances/{Id}/withdraw");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Withdraw a workflow instance (initiator only)";
});
}
public override async Task HandleAsync(WithdrawWorkflowInstanceRequest req, CancellationToken ct)
{
var command = new WithdrawWorkflowInstanceCommand(req.Id, req.UserId);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class WithdrawWorkflowInstanceRequest
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
}

View File

@ -0,0 +1,36 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowTasks.Commands;
namespace Workflow.Api.Endpoints.WorkflowTask;
public class ApproveTaskEndpoint : Endpoint<ApproveTaskRequest>
{
private readonly IMediator _mediator;
public ApproveTaskEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-tasks/{Id}/approve");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Approve a pending task";
});
}
public override async Task HandleAsync(ApproveTaskRequest req, CancellationToken ct)
{
var command = new ApproveTaskCommand(req.Id, req.UserId, req.Comment);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class ApproveTaskRequest
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string? Comment { get; set; }
}

View File

@ -0,0 +1,36 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowTasks.Commands;
namespace Workflow.Api.Endpoints.WorkflowTask;
public class DelegateTaskEndpoint : Endpoint<DelegateTaskRequest>
{
private readonly IMediator _mediator;
public DelegateTaskEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-tasks/{Id}/delegate");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Delegate a task to another user";
});
}
public override async Task HandleAsync(DelegateTaskRequest req, CancellationToken ct)
{
var command = new DelegateTaskCommand(req.Id, req.FromUserId, req.ToUserId);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class DelegateTaskRequest
{
public Guid Id { get; set; }
public Guid FromUserId { get; set; }
public Guid ToUserId { get; set; }
}

View File

@ -0,0 +1,38 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Common;
using Workflow.Application.Features.WorkflowTasks.DTOs;
using Workflow.Application.Features.WorkflowTasks.Queries;
namespace Workflow.Api.Endpoints.WorkflowTask;
public class GetCcTasksEndpoint : Endpoint<GetCcTasksRequest, PagedResult<WorkflowTaskListItemDto>>
{
private readonly IMediator _mediator;
public GetCcTasksEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/workflow-tasks/cc");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get CC (carbon copy) tasks for a user";
});
}
public override async Task HandleAsync(GetCcTasksRequest req, CancellationToken ct)
{
var query = new GetCcTasksQuery(req.UserId, req.PageIndex, req.PageSize);
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}
public class GetCcTasksRequest
{
public Guid UserId { get; set; }
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 20;
}

View File

@ -0,0 +1,38 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Common;
using Workflow.Application.Features.WorkflowTasks.DTOs;
using Workflow.Application.Features.WorkflowTasks.Queries;
namespace Workflow.Api.Endpoints.WorkflowTask;
public class GetHistoryTasksEndpoint : Endpoint<GetHistoryTasksRequest, PagedResult<WorkflowTaskListItemDto>>
{
private readonly IMediator _mediator;
public GetHistoryTasksEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/workflow-tasks/history");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get completed (approved/rejected) tasks for a user";
});
}
public override async Task HandleAsync(GetHistoryTasksRequest req, CancellationToken ct)
{
var query = new GetHistoryTasksQuery(req.UserId, req.PageIndex, req.PageSize);
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}
public class GetHistoryTasksRequest
{
public Guid UserId { get; set; }
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 20;
}

View File

@ -0,0 +1,38 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Common;
using Workflow.Application.Features.WorkflowTasks.DTOs;
using Workflow.Application.Features.WorkflowTasks.Queries;
namespace Workflow.Api.Endpoints.WorkflowTask;
public class GetOverdueTasksEndpoint : Endpoint<GetOverdueTasksRequest, PagedResult<WorkflowTaskListItemDto>>
{
private readonly IMediator _mediator;
public GetOverdueTasksEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/workflow-tasks/overdue");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get overdue tasks (past due date, still pending)";
});
}
public override async Task HandleAsync(GetOverdueTasksRequest req, CancellationToken ct)
{
var query = new GetOverdueTasksQuery(req.UserId, req.PageIndex, req.PageSize);
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}
public class GetOverdueTasksRequest
{
public Guid? UserId { get; set; }
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 20;
}

View File

@ -0,0 +1,38 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Common;
using Workflow.Application.Features.WorkflowTasks.DTOs;
using Workflow.Application.Features.WorkflowTasks.Queries;
namespace Workflow.Api.Endpoints.WorkflowTask;
public class GetPendingTasksEndpoint : Endpoint<GetPendingTasksRequest, PagedResult<WorkflowTaskListItemDto>>
{
private readonly IMediator _mediator;
public GetPendingTasksEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/workflow-tasks/pending");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get pending tasks for a user";
});
}
public override async Task HandleAsync(GetPendingTasksRequest req, CancellationToken ct)
{
var query = new GetPendingTasksQuery(req.UserId, req.PageIndex, req.PageSize);
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}
public class GetPendingTasksRequest
{
public Guid UserId { get; set; }
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 20;
}

View File

@ -0,0 +1,35 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowTasks.DTOs;
using Workflow.Application.Features.WorkflowTasks.Queries;
namespace Workflow.Api.Endpoints.WorkflowTask;
public class GetTaskByIdEndpoint : Endpoint<GetTaskByIdRequest, WorkflowTaskListItemDto>
{
private readonly IMediator _mediator;
public GetTaskByIdEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Get("/workflow-tasks/{Id}");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Get task detail by id";
});
}
public override async Task HandleAsync(GetTaskByIdRequest req, CancellationToken ct)
{
var query = new GetTaskByIdQuery(req.Id);
var result = await _mediator.Send(query, ct);
await SendAsync(result, 200, ct);
}
}
public class GetTaskByIdRequest
{
public Guid Id { get; set; }
}

View File

@ -0,0 +1,36 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowTasks.Commands;
namespace Workflow.Api.Endpoints.WorkflowTask;
public class RejectTaskEndpoint : Endpoint<RejectTaskRequest>
{
private readonly IMediator _mediator;
public RejectTaskEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-tasks/{Id}/reject");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Reject a pending task";
});
}
public override async Task HandleAsync(RejectTaskRequest req, CancellationToken ct)
{
var command = new RejectTaskCommand(req.Id, req.UserId, req.Comment);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class RejectTaskRequest
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string? Comment { get; set; }
}

View File

@ -0,0 +1,36 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowTasks.Commands;
namespace Workflow.Api.Endpoints.WorkflowTask;
public class TransferTaskEndpoint : Endpoint<TransferTaskRequest>
{
private readonly IMediator _mediator;
public TransferTaskEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-tasks/{Id}/transfer");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Transfer a task to another user";
});
}
public override async Task HandleAsync(TransferTaskRequest req, CancellationToken ct)
{
var command = new TransferTaskCommand(req.Id, req.FromUserId, req.ToUserId);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class TransferTaskRequest
{
public Guid Id { get; set; }
public Guid FromUserId { get; set; }
public Guid ToUserId { get; set; }
}

View File

@ -0,0 +1,35 @@
using FastEndpoints;
using MediatR;
using Workflow.Application.Features.WorkflowTasks.Commands;
namespace Workflow.Api.Endpoints.WorkflowTask;
public class UrgeTaskEndpoint : Endpoint<UrgeTaskRequest>
{
private readonly IMediator _mediator;
public UrgeTaskEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-tasks/{Id}/urge");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Urge a pending task (send notification to assignee)";
});
}
public override async Task HandleAsync(UrgeTaskRequest req, CancellationToken ct)
{
var command = new UrgeTaskCommand(req.Id, req.UserId);
await _mediator.Send(command, ct);
await SendOkAsync(ct);
}
}
public class UrgeTaskRequest
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
}

View File

@ -0,0 +1,71 @@
using System.Text.Json;
namespace Workflow.Api.Middleware;
public class ApiResponseMiddleware
{
private readonly RequestDelegate _next;
public ApiResponseMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var originalBodyStream = context.Response.Body;
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
await _next(context);
context.Response.Body = originalBodyStream;
responseBody.Seek(0, SeekOrigin.Begin);
var responseText = await new StreamReader(responseBody).ReadToEndAsync();
// Skip wrapping for non-JSON responses, error responses, or Swagger
if (context.Response.StatusCode >= 400 ||
context.Response.ContentType?.Contains("application/json") != true ||
context.Request.Path.StartsWithSegments("/swagger"))
{
responseBody.Seek(0, SeekOrigin.Begin);
await responseBody.CopyToAsync(originalBodyStream);
return;
}
// Parse the original response and wrap it in the standard envelope
object? data;
if (string.IsNullOrWhiteSpace(responseText))
{
data = null;
}
else
{
try
{
data = JsonSerializer.Deserialize<JsonElement>(responseText);
}
catch
{
data = responseText;
}
}
var wrappedResponse = new
{
code = 0,
data,
message = "ok"
};
var json = JsonSerializer.Serialize(wrappedResponse, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
context.Response.ContentLength = System.Text.Encoding.UTF8.GetByteCount(json);
await context.Response.WriteAsync(json);
}
}

View File

@ -0,0 +1,67 @@
using System.Net;
using System.Text.Json;
using Workflow.Domain.Exceptions;
namespace Workflow.Api.Middleware;
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var (statusCode, message) = exception switch
{
BusinessException ex => ((int)HttpStatusCode.BadRequest, ex.Message),
NotFoundException ex => ((int)HttpStatusCode.NotFound, ex.Message),
UnauthorizedException ex => ((int)HttpStatusCode.Unauthorized, ex.Message),
InvalidStateTransitionException ex => ((int)HttpStatusCode.BadRequest, ex.Message),
_ => ((int)HttpStatusCode.InternalServerError, "An unexpected error occurred.")
};
if (statusCode == (int)HttpStatusCode.InternalServerError)
{
_logger.LogError(exception, "Unhandled exception: {Message}", exception.Message);
}
else
{
_logger.LogWarning(exception, "Business exception: {Message}", exception.Message);
}
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
var response = new
{
code = statusCode,
message,
data = (object?)null
};
var json = JsonSerializer.Serialize(response, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
await context.Response.WriteAsync(json);
}
}

View File

@ -0,0 +1,59 @@
using System.Text.Json;
using FastEndpoints;
using FastEndpoints.Swagger;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Workflow.Api.Configuration;
using Workflow.Api.Middleware;
using Workflow.Application.Form.FormDefinition.Commands;
using Workflow.Infrastructure.Persistence;
var builder = WebApplication.CreateBuilder(args);
// DbContext
builder.Services.AddDbContext<WorkflowDbContext>(options =>
{
options.UseNpgsql(builder.Configuration.GetConnectionString("Default"), npgsql =>
{
npgsql.MigrationsAssembly(typeof(WorkflowDbContext).Assembly.FullName);
});
});
// MediatR
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateFormDefinitionCommand).Assembly));
// FastEndpoints
builder.Services.AddFastEndpoints();
builder.Services.SwaggerDocument();
// JWT Authentication
builder.Services.AddJwtAuthentication(builder.Configuration);
// CORS
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins("http://localhost:5173", "http://localhost:5174", "http://localhost:3000", "http://localhost:5666")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
var app = builder.Build();
// Middleware pipeline (order matters)
app.UseCors();
app.UseMiddleware<GlobalExceptionMiddleware>();
app.UseMiddleware<ApiResponseMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.UseFastEndpoints(config =>
{
config.Endpoints.RoutePrefix = "api";
config.Serializer.Options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
app.UseSwaggerGen();
app.Run();

View File

@ -46,24 +46,208 @@ TemporaryDependencyNodeTargetIdentifier=net10.0
/optimize-
/out:obj/Debug/net10.0/Workflow.Api.dll
/refout:obj/Debug/net10.0/refint/Workflow.Api.dll
/target:library
/target:exe
/warnaserror+
/utf8output
/deterministic+
/langversion:14.0
/features:use-roslyn-tokenizer=true
/warnaserror+:NU1605,SYSLIB0011
[sourceFiles]
Configuration/JwtAuthConfiguration.cs
Endpoints/Form/
CreateFormDefinitionEndpoint.cs
DeleteFormDefinitionEndpoint.cs
GetFormDataByInstanceEndpoint.cs
GetFormDefinitionByIdEndpoint.cs
GetFormDefinitionListEndpoint.cs
PublishFormDefinitionEndpoint.cs
SubmitFormDataEndpoint.cs
UpdateFormDefinitionEndpoint.cs
Endpoints/WorkflowDefinition/
CreateWorkflowDefinitionEndpoint.cs
DeleteWorkflowDefinitionEndpoint.cs
DisableWorkflowDefinitionEndpoint.cs
GetWorkflowDefinitionByIdEndpoint.cs
GetWorkflowDefinitionListEndpoint.cs
PublishWorkflowDefinitionEndpoint.cs
UpdateWorkflowDefinitionEndpoint.cs
Endpoints/WorkflowInstance/
GetWorkflowInstanceByIdEndpoint.cs
GetWorkflowInstanceListEndpoint.cs
MonitorWorkflowInstancesEndpoint.cs
ResumeWorkflowInstanceEndpoint.cs
StartWorkflowInstanceEndpoint.cs
SuspendWorkflowInstanceEndpoint.cs
WithdrawWorkflowInstanceEndpoint.cs
Endpoints/WorkflowTask/
ApproveTaskEndpoint.cs
DelegateTaskEndpoint.cs
GetCcTasksEndpoint.cs
GetHistoryTasksEndpoint.cs
GetOverdueTasksEndpoint.cs
GetPendingTasksEndpoint.cs
GetTaskByIdEndpoint.cs
RejectTaskEndpoint.cs
TransferTaskEndpoint.cs
UrgeTaskEndpoint.cs
Middleware/
ApiResponseMiddleware.cs
GlobalExceptionMiddleware.cs
obj/Debug/net10.0/
.NETCoreApp,Version=v10.0.AssemblyAttributes.cs
SwaggerExportPathInitializer.g.cs
Workflow.Api.AssemblyInfo.cs
Workflow.Api.GlobalUsings.g.cs
Program.cs
[metadataReferences]
../
Workflow.Application/obj/Debug/net10.0/ref/Workflow.Application.dll
Workflow.Domain/obj/Debug/net10.0/ref/Workflow.Domain.dll
Workflow.Infrastructure/obj/Debug/net10.0/ref/Workflow.Infrastructure.dll
<DOTNET>/packs/Microsoft.AspNetCore.App.Ref/10.0.7/ref/net10.0/
Microsoft.AspNetCore.Antiforgery.dll
Microsoft.AspNetCore.Authentication.Abstractions.dll
Microsoft.AspNetCore.Authentication.BearerToken.dll
Microsoft.AspNetCore.Authentication.Cookies.dll
Microsoft.AspNetCore.Authentication.Core.dll
Microsoft.AspNetCore.Authentication.dll
Microsoft.AspNetCore.Authentication.OAuth.dll
Microsoft.AspNetCore.Authorization.dll
Microsoft.AspNetCore.Authorization.Policy.dll
Microsoft.AspNetCore.Components.Authorization.dll
Microsoft.AspNetCore.Components.dll
Microsoft.AspNetCore.Components.Endpoints.dll
Microsoft.AspNetCore.Components.Forms.dll
Microsoft.AspNetCore.Components.Server.dll
Microsoft.AspNetCore.Components.Web.dll
Microsoft.AspNetCore.Connections.Abstractions.dll
Microsoft.AspNetCore.CookiePolicy.dll
Microsoft.AspNetCore.Cors.dll
Microsoft.AspNetCore.Cryptography.Internal.dll
Microsoft.AspNetCore.Cryptography.KeyDerivation.dll
Microsoft.AspNetCore.DataProtection.Abstractions.dll
Microsoft.AspNetCore.DataProtection.dll
Microsoft.AspNetCore.DataProtection.Extensions.dll
Microsoft.AspNetCore.Diagnostics.Abstractions.dll
Microsoft.AspNetCore.Diagnostics.dll
Microsoft.AspNetCore.Diagnostics.HealthChecks.dll
Microsoft.AspNetCore.dll
Microsoft.AspNetCore.HostFiltering.dll
Microsoft.AspNetCore.Hosting.Abstractions.dll
Microsoft.AspNetCore.Hosting.dll
Microsoft.AspNetCore.Hosting.Server.Abstractions.dll
Microsoft.AspNetCore.Html.Abstractions.dll
Microsoft.AspNetCore.Http.Abstractions.dll
Microsoft.AspNetCore.Http.Connections.Common.dll
Microsoft.AspNetCore.Http.Connections.dll
Microsoft.AspNetCore.Http.dll
Microsoft.AspNetCore.Http.Extensions.dll
Microsoft.AspNetCore.Http.Features.dll
Microsoft.AspNetCore.Http.Results.dll
Microsoft.AspNetCore.HttpLogging.dll
Microsoft.AspNetCore.HttpOverrides.dll
Microsoft.AspNetCore.HttpsPolicy.dll
Microsoft.AspNetCore.Identity.dll
Microsoft.AspNetCore.Localization.dll
Microsoft.AspNetCore.Localization.Routing.dll
Microsoft.AspNetCore.Metadata.dll
Microsoft.AspNetCore.Mvc.Abstractions.dll
Microsoft.AspNetCore.Mvc.ApiExplorer.dll
Microsoft.AspNetCore.Mvc.Core.dll
Microsoft.AspNetCore.Mvc.Cors.dll
Microsoft.AspNetCore.Mvc.DataAnnotations.dll
Microsoft.AspNetCore.Mvc.dll
Microsoft.AspNetCore.Mvc.Formatters.Json.dll
Microsoft.AspNetCore.Mvc.Formatters.Xml.dll
Microsoft.AspNetCore.Mvc.Localization.dll
Microsoft.AspNetCore.Mvc.Razor.dll
Microsoft.AspNetCore.Mvc.RazorPages.dll
Microsoft.AspNetCore.Mvc.TagHelpers.dll
Microsoft.AspNetCore.Mvc.ViewFeatures.dll
Microsoft.AspNetCore.OutputCaching.dll
Microsoft.AspNetCore.RateLimiting.dll
Microsoft.AspNetCore.Razor.dll
Microsoft.AspNetCore.Razor.Runtime.dll
Microsoft.AspNetCore.RequestDecompression.dll
Microsoft.AspNetCore.ResponseCaching.Abstractions.dll
Microsoft.AspNetCore.ResponseCaching.dll
Microsoft.AspNetCore.ResponseCompression.dll
Microsoft.AspNetCore.Rewrite.dll
Microsoft.AspNetCore.Routing.Abstractions.dll
Microsoft.AspNetCore.Routing.dll
Microsoft.AspNetCore.Server.HttpSys.dll
Microsoft.AspNetCore.Server.IIS.dll
Microsoft.AspNetCore.Server.IISIntegration.dll
Microsoft.AspNetCore.Server.Kestrel.Core.dll
Microsoft.AspNetCore.Server.Kestrel.dll
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.dll
Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.dll
Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.dll
Microsoft.AspNetCore.Session.dll
Microsoft.AspNetCore.SignalR.Common.dll
Microsoft.AspNetCore.SignalR.Core.dll
Microsoft.AspNetCore.SignalR.dll
Microsoft.AspNetCore.SignalR.Protocols.Json.dll
Microsoft.AspNetCore.StaticAssets.dll
Microsoft.AspNetCore.StaticFiles.dll
Microsoft.AspNetCore.WebSockets.dll
Microsoft.AspNetCore.WebUtilities.dll
Microsoft.Extensions.Caching.Abstractions.dll
Microsoft.Extensions.Caching.Memory.dll
Microsoft.Extensions.Configuration.Abstractions.dll
Microsoft.Extensions.Configuration.Binder.dll
Microsoft.Extensions.Configuration.CommandLine.dll
Microsoft.Extensions.Configuration.dll
Microsoft.Extensions.Configuration.EnvironmentVariables.dll
Microsoft.Extensions.Configuration.FileExtensions.dll
Microsoft.Extensions.Configuration.Ini.dll
Microsoft.Extensions.Configuration.Json.dll
Microsoft.Extensions.Configuration.KeyPerFile.dll
Microsoft.Extensions.Configuration.UserSecrets.dll
Microsoft.Extensions.Configuration.Xml.dll
Microsoft.Extensions.DependencyInjection.Abstractions.dll
Microsoft.Extensions.DependencyInjection.dll
Microsoft.Extensions.Diagnostics.Abstractions.dll
Microsoft.Extensions.Diagnostics.dll
Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.dll
Microsoft.Extensions.Diagnostics.HealthChecks.dll
Microsoft.Extensions.Features.dll
Microsoft.Extensions.FileProviders.Abstractions.dll
Microsoft.Extensions.FileProviders.Composite.dll
Microsoft.Extensions.FileProviders.Embedded.dll
Microsoft.Extensions.FileProviders.Physical.dll
Microsoft.Extensions.FileSystemGlobbing.dll
Microsoft.Extensions.Hosting.Abstractions.dll
Microsoft.Extensions.Hosting.dll
Microsoft.Extensions.Http.dll
Microsoft.Extensions.Identity.Core.dll
Microsoft.Extensions.Identity.Stores.dll
Microsoft.Extensions.Localization.Abstractions.dll
Microsoft.Extensions.Localization.dll
Microsoft.Extensions.Logging.Abstractions.dll
Microsoft.Extensions.Logging.Configuration.dll
Microsoft.Extensions.Logging.Console.dll
Microsoft.Extensions.Logging.Debug.dll
Microsoft.Extensions.Logging.dll
Microsoft.Extensions.Logging.EventLog.dll
Microsoft.Extensions.Logging.EventSource.dll
Microsoft.Extensions.Logging.TraceSource.dll
Microsoft.Extensions.ObjectPool.dll
Microsoft.Extensions.Options.ConfigurationExtensions.dll
Microsoft.Extensions.Options.DataAnnotations.dll
Microsoft.Extensions.Options.dll
Microsoft.Extensions.Primitives.dll
Microsoft.Extensions.Validation.dll
Microsoft.Extensions.WebEncoders.dll
Microsoft.JSInterop.dll
Microsoft.Net.Http.Headers.dll
System.Diagnostics.EventLog.dll
System.Formats.Cbor.dll
System.Security.Cryptography.Xml.dll
System.Threading.RateLimiting.dll
<DOTNET>/packs/Microsoft.NETCore.App.Ref/10.0.7/ref/net10.0/
Microsoft.CSharp.dll
Microsoft.VisualBasic.Core.dll
@ -232,8 +416,74 @@ obj/Debug/net10.0/
System.Xml.XPath.dll
System.Xml.XPath.XDocument.dll
WindowsBase.dll
<NUGET>/
efcore.namingconventions/10.0.1/lib/net10.0/EFCore.NamingConventions.dll
fastendpoints.attributes/8.1.0/lib/net10.0/FastEndpoints.Attributes.dll
fastendpoints.core/8.1.0/lib/net10.0/FastEndpoints.Core.dll
fastendpoints.jobqueues/8.1.0/lib/net10.0/FastEndpoints.JobQueues.dll
fastendpoints.messaging.core/8.1.0/lib/net10.0/FastEndpoints.Messaging.Core.dll
fastendpoints.messaging/8.1.0/lib/net10.0/FastEndpoints.Messaging.dll
fastendpoints.security/8.1.0/lib/net10.0/FastEndpoints.Security.dll
fastendpoints.swagger/8.1.0/lib/net10.0/FastEndpoints.Swagger.dll
fastendpoints/8.1.0/lib/net10.0/FastEndpoints.dll
fluentvalidation/12.1.1/lib/net8.0/FluentValidation.dll
humanizer.core/2.14.1/lib/net6.0/Humanizer.dll
mediatr.contracts/2.0.1/lib/netstandard2.0/MediatR.Contracts.dll
mediatr/14.1.0/lib/net10.0/MediatR.dll
microsoft.aspnetcore.authentication.jwtbearer/10.0.5/lib/net10.0/Microsoft.AspNetCore.Authentication.JwtBearer.dll
microsoft.build.framework/18.0.2/ref/net10.0/Microsoft.Build.Framework.dll
microsoft.codeanalysis.common/5.0.0/lib/net9.0/Microsoft.CodeAnalysis.dll
microsoft.codeanalysis.csharp.workspaces/5.0.0/lib/net9.0/Microsoft.CodeAnalysis.CSharp.Workspaces.dll
microsoft.codeanalysis.csharp/5.0.0/lib/net9.0/Microsoft.CodeAnalysis.CSharp.dll
microsoft.codeanalysis.workspaces.common/5.0.0/lib/net9.0/Microsoft.CodeAnalysis.Workspaces.dll
microsoft.codeanalysis.workspaces.msbuild/5.0.0/lib/net9.0/
Microsoft.CodeAnalysis.ExternalAccess.RazorCompiler.dll
Microsoft.CodeAnalysis.Workspaces.MSBuild.dll
microsoft.entityframeworkcore.abstractions/10.0.7/lib/net10.0/Microsoft.EntityFrameworkCore.Abstractions.dll
microsoft.entityframeworkcore.design/10.0.7/lib/net10.0/Microsoft.EntityFrameworkCore.Design.dll
microsoft.entityframeworkcore.relational/10.0.7/lib/net10.0/Microsoft.EntityFrameworkCore.Relational.dll
microsoft.entityframeworkcore/10.0.7/lib/net10.0/Microsoft.EntityFrameworkCore.dll
microsoft.extensions.dependencymodel/10.0.7/lib/net10.0/Microsoft.Extensions.DependencyModel.dll
microsoft.identitymodel.abstractions/8.14.0/lib/net9.0/Microsoft.IdentityModel.Abstractions.dll
microsoft.identitymodel.jsonwebtokens/8.14.0/lib/net9.0/Microsoft.IdentityModel.JsonWebTokens.dll
microsoft.identitymodel.logging/8.14.0/lib/net9.0/Microsoft.IdentityModel.Logging.dll
microsoft.identitymodel.protocols.openidconnect/8.0.1/lib/net9.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll
microsoft.identitymodel.protocols/8.0.1/lib/net9.0/Microsoft.IdentityModel.Protocols.dll
microsoft.identitymodel.tokens/8.14.0/lib/net9.0/Microsoft.IdentityModel.Tokens.dll
microsoft.visualstudio.solutionpersistence/1.0.52/lib/net8.0/Microsoft.VisualStudio.SolutionPersistence.dll
mono.texttemplating/3.0.0/lib/net6.0/Mono.TextTemplating.dll
namotion.reflection/3.4.3/lib/net8.0/Namotion.Reflection.dll
newtonsoft.json/13.0.3/lib/net6.0/Newtonsoft.Json.dll
njsonschema.annotations/11.5.2/lib/netstandard2.0/NJsonSchema.Annotations.dll
njsonschema.newtonsoftjson/11.5.2/lib/net8.0/NJsonSchema.NewtonsoftJson.dll
njsonschema.yaml/11.5.2/lib/net8.0/NJsonSchema.Yaml.dll
njsonschema/11.5.2/lib/net8.0/NJsonSchema.dll
npgsql.entityframeworkcore.postgresql/10.0.1/lib/net10.0/Npgsql.EntityFrameworkCore.PostgreSQL.dll
npgsql/10.0.2/lib/net10.0/Npgsql.dll
nswag.annotations/14.6.3/lib/netstandard2.0/NSwag.Annotations.dll
nswag.aspnetcore/14.6.3/lib/net10.0/NSwag.AspNetCore.dll
nswag.core.yaml/14.6.3/lib/net8.0/NSwag.Core.Yaml.dll
nswag.core/14.6.3/lib/net8.0/NSwag.Core.dll
nswag.generation.aspnetcore/14.6.3/lib/net10.0/NSwag.Generation.AspNetCore.dll
nswag.generation/14.6.3/lib/net8.0/NSwag.Generation.dll
system.codedom/6.0.0/lib/net6.0/System.CodeDom.dll
system.composition.attributedmodel/9.0.0/lib/net9.0/System.Composition.AttributedModel.dll
system.composition.convention/9.0.0/lib/net9.0/System.Composition.Convention.dll
system.composition.hosting/9.0.0/lib/net9.0/System.Composition.Hosting.dll
system.composition.runtime/9.0.0/lib/net9.0/System.Composition.Runtime.dll
system.composition.typedparts/9.0.0/lib/net9.0/System.Composition.TypedParts.dll
system.identitymodel.tokens.jwt/8.0.1/lib/net9.0/System.IdentityModel.Tokens.Jwt.dll
yamldotnet/16.3.0/lib/net8.0/YamlDotNet.dll
[analyzerReferences]
<DOTNET>/packs/Microsoft.AspNetCore.App.Ref/10.0.7/analyzers/dotnet/cs/
Microsoft.AspNetCore.App.Analyzers.dll
Microsoft.AspNetCore.App.CodeFixes.dll
Microsoft.AspNetCore.App.SourceGenerators.dll
Microsoft.AspNetCore.Components.Analyzers.dll
Microsoft.Extensions.Logging.Generators.dll
Microsoft.Extensions.Options.SourceGeneration.dll
Microsoft.Extensions.Validation.ValidationsGenerator.dll
<DOTNET>/packs/Microsoft.NETCore.App.Ref/10.0.7/analyzers/dotnet/cs/
Microsoft.Interop.ComInterfaceGenerator.dll
Microsoft.Interop.JavaScript.JSImportGenerator.dll
@ -241,9 +491,20 @@ obj/Debug/net10.0/
Microsoft.Interop.SourceGeneration.dll
System.Text.Json.SourceGeneration.dll
System.Text.RegularExpressions.Generator.dll
<DOTNET>/sdk/10.0.203/Sdks/Microsoft.NET.Sdk.Razor/source-generators/
Microsoft.AspNetCore.Razor.Utilities.Shared.dll
Microsoft.CodeAnalysis.Razor.Compiler.dll
Microsoft.Extensions.ObjectPool.dll
<DOTNET>/sdk/10.0.203/Sdks/Microsoft.NET.Sdk.Web/analyzers/cs/
Microsoft.AspNetCore.Analyzers.dll
Microsoft.AspNetCore.Mvc.Analyzers.dll
<DOTNET>/sdk/10.0.203/Sdks/Microsoft.NET.Sdk/analyzers/
Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll
Microsoft.CodeAnalysis.NetAnalyzers.dll
<NUGET>/microsoft.codeanalysis.analyzers/3.11.0/analyzers/dotnet/cs/
Microsoft.CodeAnalysis.Analyzers.dll
Microsoft.CodeAnalysis.CSharp.Analyzers.dll
<NUGET>/microsoft.entityframeworkcore.analyzers/10.0.7/analyzers/dotnet/cs/Microsoft.EntityFrameworkCore.Analyzers.dll
[analyzerConfigFiles]
<DOTNET>/sdk/10.0.203/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig

View File

@ -0,0 +1,17 @@
{
"ConnectionStrings": {
"Default": "Host=localhost;Port=5432;Database=workflow;Username=postgres;Password=postgres"
},
"Jwt": {
"Issuer": "workflow-api",
"Audience": "workflow-api",
"SigningKey": "workflow-engine-secret-key-at-least-32-characters-long"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -1,8 +1,11 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Workflow.Application.Features.WorkflowInstances.DTOs;
using Workflow.Domain.Enums;
using Workflow.Infrastructure.Persistence;
using TaskStatus = Workflow.Domain.Enums.TaskStatus;
namespace Workflow.Application.Features.WorkflowInstances.Queries;
public record MonitorWorkflowInstancesQuery : IRequest<WorkflowMonitorDto>;

View File

@ -3,6 +3,8 @@ using Workflow.Domain.Enums;
using Workflow.Domain.Exceptions;
using Workflow.Infrastructure.Persistence;
using TaskStatus = Workflow.Domain.Enums.TaskStatus;
namespace Workflow.Application.Features.WorkflowTasks.Commands;
public record UrgeTaskCommand(

View File

@ -5,6 +5,8 @@ using Workflow.Application.Features.WorkflowTasks.DTOs;
using Workflow.Domain.Enums;
using Workflow.Infrastructure.Persistence;
using TaskStatus = Workflow.Domain.Enums.TaskStatus;
namespace Workflow.Application.Features.WorkflowTasks.Queries;
public record GetOverdueTasksQuery(

View File

@ -8,6 +8,7 @@ using Workflow.Domain.Enums;
using Workflow.Domain.Exceptions;
using Workflow.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Xunit;
public class ProcessEngineTests
{
@ -86,9 +87,9 @@ public class ProcessEngineTests
.ToListAsync();
tasks.Should().HaveCount(1);
tasks[0].AssigneeId.Should().Be("user-001");
tasks[0].AssigneeRole.Should().Be("user-001");
tasks[0].Type.Should().Be(TaskType.Approval);
tasks[0].Status.Should().Be(Enums.TaskStatus.Pending);
tasks[0].Status.Should().Be(TaskStatus.Pending);
}
[Fact]
@ -110,7 +111,7 @@ public class ProcessEngineTests
tasks.Should().HaveCount(1);
tasks[0].AssigneeRole.Should().Be("manager");
tasks[0].Type.Should().Be(TaskType.Approval);
tasks[0].Status.Should().Be(Enums.TaskStatus.Pending);
tasks[0].Status.Should().Be(TaskStatus.Pending);
}
[Fact]
@ -131,10 +132,10 @@ public class ProcessEngineTests
Id = Guid.NewGuid(),
InstanceId = instance.Id,
TokenId = token.Id,
NodeId = approvalNode.Id.ToString(),
AssigneeId = "user-001",
NodeId = approvalNode.Id,
AssigneeRole = "user-001",
Type = TaskType.Approval,
Status = Enums.TaskStatus.Pending,
Status = TaskStatus.Pending,
};
_dbContext.WorkflowTasks.Add(task);
await _dbContext.SaveChangesAsync();
@ -167,10 +168,10 @@ public class ProcessEngineTests
Id = Guid.NewGuid(),
InstanceId = instance.Id,
TokenId = token.Id,
NodeId = approvalNode.Id.ToString(),
AssigneeId = "user-001",
NodeId = approvalNode.Id,
AssigneeRole = "user-001",
Type = TaskType.Approval,
Status = Enums.TaskStatus.Pending,
Status = TaskStatus.Pending,
};
_dbContext.WorkflowTasks.Add(task);
await _dbContext.SaveChangesAsync();
@ -203,10 +204,10 @@ public class ProcessEngineTests
Id = Guid.NewGuid(),
InstanceId = instance.Id,
TokenId = token.Id,
NodeId = approvalNode.Id.ToString(),
AssigneeId = "user-001",
NodeId = approvalNode.Id,
AssigneeRole = "user-001",
Type = TaskType.Approval,
Status = Enums.TaskStatus.Pending,
Status = TaskStatus.Pending,
};
_dbContext.WorkflowTasks.Add(task);
await _dbContext.SaveChangesAsync();
@ -533,7 +534,7 @@ public class ProcessEngineTests
{
var subDefId = Guid.NewGuid();
var subProcessNode = CreateNode(NodeType.SubProcess, "sub-1");
subProcessNode.Config = $"{{ "definitionId": "{subDefId}" }}";
subProcessNode.Config = $"{{ \"definitionId\": \"{subDefId}\" }}";
var definition = CreateDefinition(subProcessNode);
var instance = CreateInstance(definition);

View File

@ -57,6 +57,7 @@ TemporaryDependencyNodeTargetIdentifier=net10.0
<NUGET>/microsoft.net.test.sdk/18.5.1/build/net8.0/Microsoft.NET.Test.Sdk.Program.cs
@folderNames=..,..,..,..,..,.nuget,packages,microsoft.net.test.sdk,18.5.1,build,net8.0
Condition/ConditionEvaluatorTests.cs
Engine/ProcessEngineTests.cs
obj/Debug/net10.0/
.NETCoreApp,Version=v10.0.AssemblyAttributes.cs
Workflow.Tests.AssemblyInfo.cs