Compare commits
46 Commits
8624742eb3
...
4ed080f58a
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ed080f58a | |||
| bf98efd3a3 | |||
| 459cc1cb9a | |||
| cd53d7fbae | |||
| b3ce5ad28a | |||
| d7c2796d79 | |||
| d27c3f3fc7 | |||
| bf02cc80d1 | |||
| 6de45e3feb | |||
| dd33d74863 | |||
| 6eebd10dc1 | |||
| 1a0da4140b | |||
| 94561fe014 | |||
| 4e107d928e | |||
| 3e6c2ea12b | |||
| 606a6727f4 | |||
| 1e21d133ae | |||
| 6e68083a8d | |||
| b67da5434e | |||
| a7f4677ad1 | |||
| edf2468a33 | |||
| 60e5adbf1a | |||
| dc408e7794 | |||
| c34a87771d | |||
| 6041d57948 | |||
| a99f2d55b2 | |||
| edfbfd8e6c | |||
| 23ccd44bd6 | |||
| 2c005c35fb | |||
| 771eb80b9e | |||
| 1cb9ce18b4 | |||
| 5b16d19541 | |||
| b5b1f53e21 | |||
| 81aca90c39 | |||
| 047f8fc258 | |||
| 4b0208ca56 | |||
| 0e62011f92 | |||
| 3998c9ce0b | |||
| 269578194a | |||
| 6a3e0b7d50 | |||
| 50e092d9e2 | |||
| 1000be0c89 | |||
| 4d593e8a8e | |||
| d239d43c1c | |||
| 586b99ddc7 | |||
| 06af6dd43c |
@ -1,42 +0,0 @@
|
|||||||
using MediatR;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using ReC.Application.RecActions.Commands;
|
|
||||||
|
|
||||||
namespace ReC.API.Controllers;
|
|
||||||
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
[ApiController]
|
|
||||||
public class ActionController(IMediator mediator) : ControllerBase
|
|
||||||
{
|
|
||||||
[HttpPost("{profileId}")]
|
|
||||||
public async Task<IActionResult> Invoke([FromRoute] int profileId)
|
|
||||||
{
|
|
||||||
await mediator.InvokeBatchRecAction(profileId);
|
|
||||||
return Accepted();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> CreateAction([FromBody] CreateRecActionCommand command)
|
|
||||||
{
|
|
||||||
await mediator.Send(command);
|
|
||||||
|
|
||||||
return CreatedAtAction(nameof(CreateAction), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("fake")]
|
|
||||||
public async Task<IActionResult> CreateFakeAction(
|
|
||||||
string? endpointUri = null,
|
|
||||||
string type = "GET",
|
|
||||||
string bodyQuery = "SELECT NULL AS REQUEST_BODY;")
|
|
||||||
{
|
|
||||||
await mediator.Send(new CreateRecActionCommand()
|
|
||||||
{
|
|
||||||
ProfileId = 2,
|
|
||||||
EndpointUri = endpointUri,
|
|
||||||
Type = type,
|
|
||||||
BodyQuery = bodyQuery
|
|
||||||
});
|
|
||||||
|
|
||||||
return CreatedAtAction(nameof(CreateFakeAction), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
101
src/ReC.API/Controllers/RecActionController.cs
Normal file
101
src/ReC.API/Controllers/RecActionController.cs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using ReC.API.Models;
|
||||||
|
using ReC.Application.RecActions.Commands;
|
||||||
|
using ReC.Application.RecActions.Queries;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace ReC.API.Controllers;
|
||||||
|
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class RecActionController(IMediator mediator) : ControllerBase
|
||||||
|
{
|
||||||
|
private const long FakeProfileId = 2;
|
||||||
|
|
||||||
|
[HttpPost("invoke/{profileId}")]
|
||||||
|
public async Task<IActionResult> Invoke([FromRoute] int profileId, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
await mediator.InvokeBatchRecAction(profileId, cancel);
|
||||||
|
return Accepted();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("invoke/fake")]
|
||||||
|
public async Task<IActionResult> Invoke(CancellationToken cancel)
|
||||||
|
{
|
||||||
|
await mediator.InvokeBatchRecAction(FakeProfileId, cancel);
|
||||||
|
return Accepted();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region CRUD
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> Get([FromQuery] long profileId, CancellationToken cancel) => Ok(await mediator.Send(new ReadRecActionQuery()
|
||||||
|
{
|
||||||
|
ProfileId = profileId
|
||||||
|
}, cancel));
|
||||||
|
|
||||||
|
[HttpGet("fake")]
|
||||||
|
public async Task<IActionResult> Get(CancellationToken cancel) => Ok(await mediator.Send(new ReadRecActionQuery()
|
||||||
|
{
|
||||||
|
ProfileId = FakeProfileId
|
||||||
|
}, cancel));
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateAction([FromBody] CreateRecActionCommand command, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
await mediator.Send(command, cancel);
|
||||||
|
|
||||||
|
return CreatedAtAction(nameof(CreateAction), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("fake")]
|
||||||
|
public async Task<IActionResult> CreateFakeAction(
|
||||||
|
CancellationToken cancel,
|
||||||
|
[FromBody] FakeRequest? request = null,
|
||||||
|
[FromQuery] string endpointUri = "https://jsonplaceholder.typicode.com/posts",
|
||||||
|
[FromQuery] string? endpointPath = "1",
|
||||||
|
[FromQuery] string type = "GET")
|
||||||
|
{
|
||||||
|
if (endpointPath is not null)
|
||||||
|
endpointUri = new Uri(new Uri(endpointUri.TrimEnd('/') + "/"), endpointPath.TrimStart('/')).ToString();
|
||||||
|
|
||||||
|
var bodyJson = request?.Body is not null ? JsonSerializer.Serialize(request.Body, options: new() { WriteIndented = false }) : null;
|
||||||
|
var headerJson = request?.Header is not null ? JsonSerializer.Serialize(request.Header, options: new() { WriteIndented = false }) : null;
|
||||||
|
|
||||||
|
await mediator.Send(new CreateRecActionCommand()
|
||||||
|
{
|
||||||
|
ProfileId = FakeProfileId,
|
||||||
|
EndpointUri = endpointUri,
|
||||||
|
Type = type,
|
||||||
|
BodyQuery = $@"SELECT '{bodyJson ?? "NULL"}' AS REQUEST_BODY;",
|
||||||
|
HeaderQuery = headerJson is not null ? $@"SELECT '{headerJson}' AS REQUEST_HEADER;" : null,
|
||||||
|
Active = true,
|
||||||
|
EndpointAuthId = 4
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
return CreatedAtAction(nameof(CreateFakeAction), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
public async Task<IActionResult> Delete([FromQuery] int profileId, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
await mediator.Send(new DeleteRecActionsCommand()
|
||||||
|
{
|
||||||
|
ProfileId = FakeProfileId
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("fake")]
|
||||||
|
public async Task<IActionResult> Delete(CancellationToken cancel)
|
||||||
|
{
|
||||||
|
await mediator.Send(new DeleteRecActionsCommand()
|
||||||
|
{
|
||||||
|
ProfileId = FakeProfileId
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
#endregion CRUD
|
||||||
|
}
|
||||||
123
src/ReC.API/Middleware/ExceptionHandlingMiddleware.cs
Normal file
123
src/ReC.API/Middleware/ExceptionHandlingMiddleware.cs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using FluentValidation;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using ReC.Application.Common.Exceptions;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace ReC.API.Middleware;
|
||||||
|
|
||||||
|
//TODO: Fix and use DigitalData.Core.Exceptions.Middleware
|
||||||
|
/// <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 details.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use DigitalData.Core.Exceptions.Middleware")]
|
||||||
|
public class ExceptionHandlingMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ExceptionHandlingMiddleware"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="next">The next middleware in the request pipeline.</param>
|
||||||
|
/// <param name="logger">The logger instance for logging exceptions.</param>
|
||||||
|
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = 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";
|
||||||
|
|
||||||
|
ValidationProblemDetails? details = null;
|
||||||
|
|
||||||
|
switch (exception)
|
||||||
|
{
|
||||||
|
case BadRequestException badRequestEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||||
|
details = new()
|
||||||
|
{
|
||||||
|
Title = "Bad Request",
|
||||||
|
Detail = badRequestEx.Message
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ValidationException validationEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||||
|
|
||||||
|
var errors = validationEx.Errors
|
||||||
|
.GroupBy(e => e.PropertyName, e => e.ErrorMessage)
|
||||||
|
.ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
|
||||||
|
|
||||||
|
details = new ValidationProblemDetails()
|
||||||
|
{
|
||||||
|
Title = "Validation failed",
|
||||||
|
Errors = validationEx.Errors
|
||||||
|
.GroupBy(e => e.PropertyName, e => e.ErrorMessage)
|
||||||
|
.ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray()),
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotFoundException notFoundEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||||
|
details = new()
|
||||||
|
{
|
||||||
|
Title = "Not Found",
|
||||||
|
Detail = notFoundEx.Message
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DataIntegrityException dataIntegrityEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.Conflict;
|
||||||
|
details = new()
|
||||||
|
{
|
||||||
|
Title = "Data Integrity Violation",
|
||||||
|
Detail = dataIntegrityEx.Message
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.LogError(exception, "Unhandled exception occurred.");
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
|
details = new()
|
||||||
|
{
|
||||||
|
Title = "Internal Server Error",
|
||||||
|
Detail = "An unexpected error occurred. Please try again later."
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details is not null)
|
||||||
|
await context.Response.WriteAsJsonAsync(details);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/ReC.API/Models/FakeRequest.cs
Normal file
8
src/ReC.API/Models/FakeRequest.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace ReC.API.Models;
|
||||||
|
|
||||||
|
public class FakeRequest
|
||||||
|
{
|
||||||
|
public Dictionary<string, object>? Body { get; init; }
|
||||||
|
|
||||||
|
public Dictionary<string, object>? Header { get; init; }
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using ReC.Infrastructure;
|
using ReC.API.Middleware;
|
||||||
using ReC.Application;
|
using ReC.Application;
|
||||||
|
using ReC.Infrastructure;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@ -15,11 +16,16 @@ builder.Services.AddRecServices(options =>
|
|||||||
|
|
||||||
builder.Services.AddRecInfrastructure(options =>
|
builder.Services.AddRecInfrastructure(options =>
|
||||||
{
|
{
|
||||||
options.ConfigureDbContext((dbContextOpt) =>
|
options.ConfigureDbContext((provider, opt) =>
|
||||||
{
|
{
|
||||||
var connectionString = builder.Configuration.GetConnectionString("Default")
|
var cnnStr = builder.Configuration.GetConnectionString("Default")
|
||||||
?? throw new InvalidOperationException("Connection string is not found.");
|
?? throw new InvalidOperationException("Connection string is not found.");
|
||||||
dbContextOpt.UseSqlServer(connectionString);
|
|
||||||
|
var logger = provider.GetRequiredService<ILogger<RecDbContext>>();
|
||||||
|
opt.UseSqlServer(cnnStr)
|
||||||
|
.LogTo(log => logger.LogInformation("{log}", log), LogLevel.Trace)
|
||||||
|
.EnableSensitiveDataLogging()
|
||||||
|
.EnableDetailedErrors();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -30,6 +36,10 @@ builder.Services.AddSwaggerGen();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
#pragma warning disable CS0618
|
||||||
|
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,8 +5,11 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Default": "Server=SDD-VMP04-SQL19\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
|
||||||
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"MediatRLicense": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzg0ODUxMjAwIiwiaWF0IjoiMTc1MzM2MjQ5MSIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazB5NmV3MmQ4YTk4Mzg3aDJnbTRuOWswIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.ZqsFG7kv_-xGfxS6ACk3i0iuNiVUXX2AvPI8iAcZ6-z2170lGv__aO32tWpQccD9LCv5931lBNLWSblKS0MT3gOt-5he2TEftwiSQGFwoIBgtOHWsNRMinUrg2trceSp3IhyS3UaMwnxZDrCvx4-0O-kpOzVpizeHUAZNr5U7oSCWO34bpKdae6grtM5e3f93Z1vs7BW_iPgItd-aLvPwApbaG9VhmBTKlQ7b4Jh64y7UXJ9mKP7Qb_Oa97oEg0oY5DPHOWTZWeE1EzORgVr2qkK2DELSHuZ_EIUhODojkClPNAKtvEl_qEjpq0HZCIvGwfCCRlKlSkQqIeZdFkiXg",
|
"LuckyPennySoftwareLicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzg0ODUxMjAwIiwiaWF0IjoiMTc1MzM2MjQ5MSIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazB5NmV3MmQ4YTk4Mzg3aDJnbTRuOWswIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.ZqsFG7kv_-xGfxS6ACk3i0iuNiVUXX2AvPI8iAcZ6-z2170lGv__aO32tWpQccD9LCv5931lBNLWSblKS0MT3gOt-5he2TEftwiSQGFwoIBgtOHWsNRMinUrg2trceSp3IhyS3UaMwnxZDrCvx4-0O-kpOzVpizeHUAZNr5U7oSCWO34bpKdae6grtM5e3f93Z1vs7BW_iPgItd-aLvPwApbaG9VhmBTKlQ7b4Jh64y7UXJ9mKP7Qb_Oa97oEg0oY5DPHOWTZWeE1EzORgVr2qkK2DELSHuZ_EIUhODojkClPNAKtvEl_qEjpq0HZCIvGwfCCRlKlSkQqIeZdFkiXg",
|
||||||
"RecAction": {
|
"RecAction": {
|
||||||
"MaxConcurrentInvocations": 5
|
"MaxConcurrentInvocations": 5
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,11 +5,12 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace ReC.Application.Common.Behaviors;
|
namespace ReC.Application.Common.Behaviors;
|
||||||
|
|
||||||
public class BodyQueryBehavior<TRecAction>(IRecDbContext dbContext) : IPipelineBehavior<TRecAction, Unit>
|
public class BodyQueryBehavior<TRequest, TResponse>(IRecDbContext dbContext) : IPipelineBehavior<TRequest, TResponse>
|
||||||
where TRecAction : RecActionDto
|
where TRequest : RecActionDto
|
||||||
|
where TResponse : notnull
|
||||||
{
|
{
|
||||||
public async Task<Unit> Handle(TRecAction action, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
|
public async Task<TResponse> Handle(TRequest action, RequestHandlerDelegate<TResponse> next, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
if (action.BodyQuery is null)
|
if (action.BodyQuery is null)
|
||||||
return await next(cancel);
|
return await next(cancel);
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,11 @@ using System.Text.Json;
|
|||||||
|
|
||||||
namespace ReC.Application.Common.Behaviors;
|
namespace ReC.Application.Common.Behaviors;
|
||||||
|
|
||||||
public class HeaderQueryBehavior<TRecAction>(IRecDbContext dbContext, ILogger<HeaderQueryBehavior<TRecAction>>? logger = null) : IPipelineBehavior<TRecAction, Unit>
|
public class HeaderQueryBehavior<TRequest, TResponse>(IRecDbContext dbContext, ILogger<HeaderQueryBehavior<TRequest, TResponse>>? logger = null) : IPipelineBehavior<TRequest, TResponse>
|
||||||
where TRecAction : RecActionDto
|
where TRequest : RecActionDto
|
||||||
|
where TResponse : notnull
|
||||||
{
|
{
|
||||||
public async Task<Unit> Handle(TRecAction action, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
|
public async Task<TResponse> Handle(TRequest action, RequestHandlerDelegate<TResponse> next, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
if (action.HeaderQuery is null)
|
if (action.HeaderQuery is null)
|
||||||
return await next(cancel);
|
return await next(cancel);
|
||||||
|
|||||||
30
src/ReC.Application/Common/Behaviors/ValidationBehavior.cs
Normal file
30
src/ReC.Application/Common/Behaviors/ValidationBehavior.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ReC.Application.Common.Behaviors
|
||||||
|
{
|
||||||
|
public class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators) : IPipelineBehavior<TRequest, TResponse>
|
||||||
|
where TRequest : notnull
|
||||||
|
{
|
||||||
|
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
if (validators.Any())
|
||||||
|
{
|
||||||
|
var context = new ValidationContext<TRequest>(request);
|
||||||
|
|
||||||
|
var validationResults = await Task.WhenAll(
|
||||||
|
validators.Select(v =>
|
||||||
|
v.ValidateAsync(context, cancel)));
|
||||||
|
|
||||||
|
var failures = validationResults
|
||||||
|
.SelectMany(r => r.Errors)
|
||||||
|
.Where(f => f != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (failures.Count != 0)
|
||||||
|
throw new ValidationException(failures);
|
||||||
|
}
|
||||||
|
return await next(cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ using ReC.Domain.Entities;
|
|||||||
|
|
||||||
namespace ReC.Application.Common.Dto;
|
namespace ReC.Application.Common.Dto;
|
||||||
|
|
||||||
public class DtoMappingProfile : Profile
|
public class DtoMappingProfile : AutoMapper.Profile
|
||||||
{
|
{
|
||||||
public DtoMappingProfile()
|
public DtoMappingProfile()
|
||||||
{
|
{
|
||||||
|
|||||||
20
src/ReC.Application/Common/Dto/OutResDto.cs
Normal file
20
src/ReC.Application/Common/Dto/OutResDto.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
namespace ReC.Application.Common.Dto;
|
||||||
|
|
||||||
|
public record OutResDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public long ActionId { get; set; }
|
||||||
|
|
||||||
|
public string? Header { get; set; }
|
||||||
|
|
||||||
|
public string? Body { get; set; }
|
||||||
|
|
||||||
|
public string AddedWho { get; set; } = null!;
|
||||||
|
|
||||||
|
public DateTime AddedWhen { get; set; }
|
||||||
|
|
||||||
|
public string? ChangedWho { get; set; }
|
||||||
|
|
||||||
|
public DateTime? ChangedWhen { get; set; }
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using ReC.Application.Common.Behaviors;
|
using ReC.Application.Common.Behaviors;
|
||||||
using ReC.Application.Common.Options;
|
using ReC.Application.Common.Options;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
namespace ReC.Application;
|
namespace ReC.Application;
|
||||||
|
|
||||||
@ -17,6 +19,8 @@ public static class DependencyInjection
|
|||||||
|
|
||||||
configOpt.ApplyConfigurations(services);
|
configOpt.ApplyConfigurations(services);
|
||||||
|
|
||||||
|
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
services.AddAutoMapper(cfg =>
|
services.AddAutoMapper(cfg =>
|
||||||
{
|
{
|
||||||
cfg.AddMaps(Assembly.GetExecutingAssembly());
|
cfg.AddMaps(Assembly.GetExecutingAssembly());
|
||||||
@ -26,7 +30,8 @@ public static class DependencyInjection
|
|||||||
services.AddMediatR(cfg =>
|
services.AddMediatR(cfg =>
|
||||||
{
|
{
|
||||||
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
cfg.AddOpenBehaviors([typeof(BodyQueryBehavior<>), typeof(HeaderQueryBehavior<>)]);
|
cfg.AddOpenBehaviors([typeof(BodyQueryBehavior<,>), typeof(HeaderQueryBehavior<,>)]);
|
||||||
|
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
||||||
cfg.LicenseKey = configOpt.LuckyPennySoftwareLicenseKey;
|
cfg.LicenseKey = configOpt.LuckyPennySoftwareLicenseKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,14 @@ using ReC.Domain.Entities;
|
|||||||
|
|
||||||
namespace ReC.Application.Endpoints;
|
namespace ReC.Application.Endpoints;
|
||||||
|
|
||||||
|
// TODO: update to inject AddedWho from the current host/user contex
|
||||||
public class MappingProfile : AutoMapper.Profile
|
public class MappingProfile : AutoMapper.Profile
|
||||||
{
|
{
|
||||||
public MappingProfile()
|
public MappingProfile()
|
||||||
{
|
{
|
||||||
CreateMap<ObtainEndpointCommand, Endpoint>();
|
CreateMap<ObtainEndpointCommand, Endpoint>()
|
||||||
|
.ForMember(e => e.Active, exp => exp.MapFrom(cmd => true))
|
||||||
|
.ForMember(e => e.AddedWhen, exp => exp.MapFrom(cmd => DateTime.UtcNow))
|
||||||
|
.ForMember(e => e.AddedWho, exp => exp.MapFrom(cmd => "ReC.API"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using ReC.Domain.Entities;
|
||||||
|
|
||||||
namespace ReC.Application.OutResults.Commands;
|
namespace ReC.Application.OutResults.Commands;
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ public class CreateOutResCommand : IRequest
|
|||||||
public string? AddedWho { get; set; }
|
public string? AddedWho { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateOutResCommandHandler(IRepository<CreateOutResCommand> repo) : IRequestHandler<CreateOutResCommand>
|
public class CreateOutResCommandHandler(IRepository<OutRes> repo) : IRequestHandler<CreateOutResCommand>
|
||||||
{
|
{
|
||||||
public Task Handle(CreateOutResCommand request, CancellationToken cancel)
|
public Task Handle(CreateOutResCommand request, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
using AutoMapper;
|
using ReC.Application.OutResults.Commands;
|
||||||
using ReC.Application.OutResults.Commands;
|
|
||||||
using ReC.Domain.Entities;
|
using ReC.Domain.Entities;
|
||||||
|
|
||||||
namespace ReC.Application.OutResults;
|
namespace ReC.Application.OutResults;
|
||||||
|
|
||||||
public class MappingProfiles : Profile
|
// TODO: update to inject AddedWho from the current host/user contex
|
||||||
|
public class MappingProfiles : AutoMapper.Profile
|
||||||
{
|
{
|
||||||
public MappingProfiles()
|
public MappingProfiles()
|
||||||
{
|
{
|
||||||
CreateMap<CreateOutResCommand, OutRes>();
|
CreateMap<CreateOutResCommand, OutRes>()
|
||||||
|
.ForMember(e => e.AddedWhen, exp => exp.MapFrom(cmd => DateTime.UtcNow))
|
||||||
|
.ForMember(e => e.AddedWho, exp => exp.MapFrom(cmd => "ReC.API"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
namespace ReC.Application.OutResults.Queries;
|
||||||
|
|
||||||
|
public class ReadOutResQuery
|
||||||
|
{
|
||||||
|
public long? ProfileId { get; set; }
|
||||||
|
|
||||||
|
public long? ActionId { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace ReC.Application.OutResults.Queries;
|
||||||
|
|
||||||
|
public class ReadOutResQueryValidator : AbstractValidator<ReadOutResQuery>
|
||||||
|
{
|
||||||
|
public ReadOutResQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x)
|
||||||
|
.Must(x => x.ActionId.HasValue || x.ProfileId.HasValue)
|
||||||
|
.WithMessage("At least one of ActionId or ProfileId must be provided.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,8 @@
|
|||||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.5.0" />
|
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.5.0" />
|
||||||
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
|
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
|
||||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||||
|
<PackageReference Include="FluentValidation" Version="12.1.0" />
|
||||||
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.0" />
|
||||||
<PackageReference Include="MediatR" Version="13.1.0" />
|
<PackageReference Include="MediatR" Version="13.1.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.11" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.11" />
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
using DigitalData.Core.Exceptions;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using ReC.Application.Endpoints.Commands;
|
using ReC.Application.Endpoints.Commands;
|
||||||
|
using ReC.Domain.Entities;
|
||||||
|
|
||||||
namespace ReC.Application.RecActions.Commands;
|
namespace ReC.Application.RecActions.Commands;
|
||||||
|
|
||||||
@ -19,9 +21,13 @@ public record CreateRecActionCommand : IRequest
|
|||||||
public string? HeaderQuery { get; init; }
|
public string? HeaderQuery { get; init; }
|
||||||
|
|
||||||
public string BodyQuery { get; init; } = null!;
|
public string BodyQuery { get; init; } = null!;
|
||||||
|
|
||||||
|
public byte Sequence { get; set; } = 1;
|
||||||
|
|
||||||
|
public long? EndpointAuthId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateRecActionCommandHandler(ISender sender) : IRequestHandler<CreateRecActionCommand>
|
public class CreateRecActionCommandHandler(ISender sender, IRepository<RecAction> repo) : IRequestHandler<CreateRecActionCommand>
|
||||||
{
|
{
|
||||||
public async Task Handle(CreateRecActionCommand request, CancellationToken cancel)
|
public async Task Handle(CreateRecActionCommand request, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
@ -33,5 +39,7 @@ public class CreateRecActionCommandHandler(ISender sender) : IRequestHandler<Cre
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw new BadRequestException("Either EndpointId or EndpointUri must be provided.");
|
throw new BadRequestException("Either EndpointId or EndpointUri must be provided.");
|
||||||
|
|
||||||
|
await repo.CreateAsync(request, cancel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using ReC.Domain.Entities;
|
using ReC.Domain.Entities;
|
||||||
|
|
||||||
namespace ReC.Application.RecActions.Commands;
|
namespace ReC.Application.RecActions.Commands;
|
||||||
@ -11,8 +13,12 @@ public class DeleteRecActionsCommand : IRequest
|
|||||||
|
|
||||||
public class DeleteRecActionsCommandHandler(IRepository<RecAction> repo) : IRequestHandler<DeleteRecActionsCommand>
|
public class DeleteRecActionsCommandHandler(IRepository<RecAction> repo) : IRequestHandler<DeleteRecActionsCommand>
|
||||||
{
|
{
|
||||||
public Task Handle(DeleteRecActionsCommand request, CancellationToken cancel)
|
public async Task Handle(DeleteRecActionsCommand request, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
return repo.DeleteAsync(act => act.ProfileId == request.ProfileId, cancel);
|
// TODO: update DeleteAsync (in Core) to return number of deleted records
|
||||||
|
if (!await repo.Where(act => act.ProfileId == request.ProfileId).AnyAsync(cancel))
|
||||||
|
throw new NotFoundException();
|
||||||
|
|
||||||
|
await repo.DeleteAsync(act => act.ProfileId == request.ProfileId, cancel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8,8 +8,8 @@ public record InvokeBatchRecActionsCommand : ReadRecActionQueryBase, IRequest;
|
|||||||
|
|
||||||
public static class InvokeBatchRecActionsCommandExtensions
|
public static class InvokeBatchRecActionsCommandExtensions
|
||||||
{
|
{
|
||||||
public static Task InvokeBatchRecAction(this ISender sender, int profileId)
|
public static Task InvokeBatchRecAction(this ISender sender, long profileId, CancellationToken cancel = default)
|
||||||
=> sender.Send(new InvokeBatchRecActionsCommand { ProfileId = profileId });
|
=> sender.Send(new InvokeBatchRecActionsCommand { ProfileId = profileId }, cancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class InvokeRecActionsCommandHandler(ISender sender, IHttpClientFactory clientFactory, ILogger<InvokeRecActionsCommandHandler>? logger = null) : IRequestHandler<InvokeBatchRecActionsCommand>
|
public class InvokeRecActionsCommandHandler(ISender sender, IHttpClientFactory clientFactory, ILogger<InvokeRecActionsCommandHandler>? logger = null) : IRequestHandler<InvokeBatchRecActionsCommand>
|
||||||
|
|||||||
@ -3,10 +3,14 @@ using ReC.Domain.Entities;
|
|||||||
|
|
||||||
namespace ReC.Application.RecActions;
|
namespace ReC.Application.RecActions;
|
||||||
|
|
||||||
|
// TODO: update to inject AddedWho from the current host/user contex
|
||||||
public class MappingProfile : AutoMapper.Profile
|
public class MappingProfile : AutoMapper.Profile
|
||||||
{
|
{
|
||||||
public MappingProfile()
|
public MappingProfile()
|
||||||
{
|
{
|
||||||
CreateMap<CreateRecActionCommand, RecAction>();
|
CreateMap<CreateRecActionCommand, RecAction>()
|
||||||
|
.ForMember(e => e.Active, exp => exp.MapFrom(cmd => true))
|
||||||
|
.ForMember(e => e.AddedWhen, exp => exp.MapFrom(cmd => DateTime.UtcNow))
|
||||||
|
.ForMember(e => e.AddedWho, exp => exp.MapFrom(cmd => "ReC.API"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -10,12 +10,17 @@ namespace ReC.Application.RecActions.Queries;
|
|||||||
|
|
||||||
public record ReadRecActionQueryBase
|
public record ReadRecActionQueryBase
|
||||||
{
|
{
|
||||||
public int ProfileId { get; init; }
|
public long ProfileId { get; init; }
|
||||||
|
|
||||||
public ReadRecActionQuery ToReadQuery() => new(this);
|
public ReadRecActionQuery ToReadQuery() => new(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ReadRecActionQuery(ReadRecActionQueryBase Root) : ReadRecActionQueryBase(Root), IRequest<IEnumerable<RecActionDto>>;
|
public record ReadRecActionQuery : ReadRecActionQueryBase, IRequest<IEnumerable<RecActionDto>>
|
||||||
|
{
|
||||||
|
public ReadRecActionQuery(ReadRecActionQueryBase root) : base(root) { }
|
||||||
|
|
||||||
|
public ReadRecActionQuery() { }
|
||||||
|
}
|
||||||
|
|
||||||
public class ReadRecActionQueryHandler(IRepository<RecActionView> repo, IMapper mapper) : IRequestHandler<ReadRecActionQuery, IEnumerable<RecActionDto>>
|
public class ReadRecActionQueryHandler(IRepository<RecActionView> repo, IMapper mapper) : IRequestHandler<ReadRecActionQuery, IEnumerable<RecActionDto>>
|
||||||
{
|
{
|
||||||
@ -23,7 +28,7 @@ public class ReadRecActionQueryHandler(IRepository<RecActionView> repo, IMapper
|
|||||||
{
|
{
|
||||||
var actions = await repo.Where(x => x.ProfileId == request.ProfileId).ToListAsync(cancel);
|
var actions = await repo.Where(x => x.ProfileId == request.ProfileId).ToListAsync(cancel);
|
||||||
|
|
||||||
if(actions.Count != 0)
|
if(actions.Count == 0)
|
||||||
throw new NotFoundException($"No actions found for the profile {request.ProfileId}.");
|
throw new NotFoundException($"No actions found for the profile {request.ProfileId}.");
|
||||||
|
|
||||||
return mapper.Map<IEnumerable<RecActionDto>>(actions);
|
return mapper.Map<IEnumerable<RecActionDto>>(actions);
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace ReC.Domain.Entities;
|
namespace ReC.Domain.Entities;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace ReC.Domain.Entities;
|
namespace ReC.Domain.Entities;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace ReC.Domain.Entities;
|
namespace ReC.Domain.Entities;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace ReC.Domain.Entities;
|
namespace ReC.Domain.Entities;
|
||||||
@ -10,7 +9,7 @@ public class Profile
|
|||||||
[Key]
|
[Key]
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
[Column("GUID")]
|
[Column("GUID")]
|
||||||
public short? Id { get; set; }
|
public long Id { get; set; }
|
||||||
|
|
||||||
[Column("ACTIVE")]
|
[Column("ACTIVE")]
|
||||||
public bool? Active { get; set; }
|
public bool? Active { get; set; }
|
||||||
|
|||||||
@ -31,9 +31,9 @@ public static class DependencyInjection
|
|||||||
|
|
||||||
public class ConfigurationOptions
|
public class ConfigurationOptions
|
||||||
{
|
{
|
||||||
internal Action<DbContextOptionsBuilder>? DbContextOptionsAction { get; private set; }
|
internal Action<IServiceProvider, DbContextOptionsBuilder>? DbContextOptionsAction { get; private set; }
|
||||||
|
|
||||||
public ConfigurationOptions ConfigureDbContext(Action<DbContextOptionsBuilder> optionsAction)
|
public ConfigurationOptions ConfigureDbContext(Action<IServiceProvider, DbContextOptionsBuilder> optionsAction)
|
||||||
{
|
{
|
||||||
DbContextOptionsAction = optionsAction;
|
DbContextOptionsAction = optionsAction;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user