Compare commits

...

26 Commits

Author SHA1 Message Date
9a12643eb6 Refactor RecActionController and DeleteRecActionsCommand
Updated the `Invoke` method in `RecActionController` to use `cmd` as the route parameter instead of `profileId`, modifying the HTTP POST route to `invoke/{cmd}`.

Refactored the `Delete` method to accept a `DeleteRecActionsCommand` object (`cmd`) instead of `profileId`. Removed the hardcoded `ProfileId` assignment and passed the `cmd` object directly to `mediator.Send`.

Made the `ProfileId` property in `DeleteRecActionsCommand` required by removing its default value, enforcing explicit initialization. This improves validation and ensures flexibility for future enhancements.

These changes enhance the API's clarity, flexibility, and maintainability.
2025-12-04 15:37:06 +01:00
f9c0a8be55 Refactor Get method to accept query object directly
Simplified the `Get` method in `RecActionController` by replacing
the `profileId` parameter with a `ReadRecActionQuery` object.
This eliminates the need to manually construct the query object
within the method, improving code readability and reducing
redundancy.
2025-12-04 15:27:43 +01:00
77fde199e1 Update invoked parameter binding in Get method
The `Get` method in the `RecActionController` class was updated to use the `[FromQuery]` attribute for the `invoked` parameter. This change ensures that the parameter value is explicitly bound from the query string of the HTTP request, improving clarity and alignment with expected usage.
2025-12-04 15:25:50 +01:00
9d15dfe8a5 Add 'invoked' parameter to Get method in controller
Updated the Get method in RecActionController to include a
new optional parameter, `invoked`, with a default value of
`false`. Updated XML documentation to reflect this change.

Modified the `ReadRecActionQuery` initialization to pass the
`invoked` parameter. Removed the previous method signature
that only accepted a `CancellationToken`.
2025-12-04 15:17:38 +01:00
c41c394f48 Refactor query handling for dynamic customization
Updated `InvokeBatchRecActionsCommandExtensions` to filter actions with `Invoked = false` using a lambda in `ToReadQuery`.

Refactored `ReadRecActionQueryBase` to remove the `Invoked` property and updated `ToReadQuery` to accept a delegate for external query modifications.

Moved the `Invoked` property to `ReadRecActionQuery` and added a parameterless constructor.

These changes improve flexibility and enable dynamic query customization.
2025-12-04 15:15:09 +01:00
34d0741ac8 Add Invoked filter to ReadRecActionQuery handler
Introduced a nullable `Invoked` property in `ReadRecActionQueryBase`
to enable conditional filtering of actions based on `Root.OutRes`.
Added a `ToReadQuery` method for easier query conversion.

Refactored `ReadRecActionQueryHandler` to apply dynamic filtering
based on the `Invoked` property:
- `true`: Filters actions with non-null `Root.OutRes`.
- `false`: Filters actions with null `Root.OutRes`.
- `null`: No additional filtering applied.

Replaced hardcoded filtering logic with the new dynamic approach.
2025-12-04 14:50:25 +01:00
0f3fd320b0 Refactor RootAction to Root in RecActionView
Renamed the `RootAction` property to `Root` in the `RecActionView` class to improve clarity and align with naming conventions. Updated the `ReadRecActionQueryHandler` class to reference the renamed property in its LINQ query, ensuring consistency between the data model and query logic.
2025-12-04 14:46:30 +01:00
3b6df031a6 Add filtering for non-null OutRes in ReadRecActionQuery
Enhanced the `Handle` method in `ReadRecActionQueryHandler`
to include an additional filtering condition when querying
the repository. The query now filters actions where
`RootAction.OutRes` is not null, in addition to matching
the `ProfileId`. This ensures that only relevant actions
with a valid `OutRes` are included in the result set.
2025-12-04 14:46:13 +01:00
b00902e461 Add one-to-one relationship between RecAction and OutRes
Introduced a new `OutRes` navigation property in the `RecAction`
class to establish a one-to-one relationship with the `OutRes`
entity. Updated `RecDbContext` to configure this relationship
using the Fluent API. The `OutRes` entity references its
associated `RecAction` via the `Action` navigation property,
with `ActionId` as the foreign key.
2025-12-04 14:42:00 +01:00
74f4d06031 Add RootAction navigation property to RecActionView
Introduced a new nullable `RootAction` property in the `RecActionView` class. This property is decorated with the `[ForeignKey("Id")]` attribute, establishing a foreign key relationship with the `Id` property. This change enables the `RecActionView` entity to reference an optional `RecAction` entity, enhancing the data model and supporting entity relationships.
2025-12-04 14:32:36 +01:00
5ee3ca2d99 Add foreign key relationship to RecActionView
Introduced a `Profile` navigation property in the `RecActionView` class, linked to the `ProfileId` column using the `[ForeignKey]` attribute. The `Profile` property is nullable to support cases where no related `Profile` exists.
2025-12-04 14:20:54 +01:00
a62923c8d6 Enhance OutResController with new features and docs
Added XML documentation to `OutResController` methods for clarity.
Introduced `ResultType` enum to specify result parts (Full, Header, Body).
Enhanced `Get` methods to support fake/test profiles and actions.
Added `[ProducesResponseType(StatusCodes.Status200OK)]` for explicit
response status codes. Updated `Get` overloads for various scenarios.
Utilized `config.GetFakeProfileId()` for dynamic fake profile handling.
2025-12-04 14:07:17 +01:00
9901726f5a Add CancellationToken support to OutResController methods
Updated `Get` methods in `OutResController` to include a
`CancellationToken` parameter, enabling support for request
cancellation during asynchronous operations. Updated all
`mediator.Send` calls to propagate the `CancellationToken`.

Simplified the "fake/{actionId}" route by removing the explicit
assignment of `HttpContext.Response.ContentType` for non-
`ResultType.Full` cases.
2025-12-04 14:01:47 +01:00
b8074cfaf1 Add endpoints for RecActions and improve documentation
Added new endpoints for invoking, retrieving, creating, and deleting
RecActions, including support for both specific and fake/test profiles.
Endpoints include:

- `Invoke` for batch invocation of RecActions.
- `Get` for retrieving RecActions by profile.
- `CreateAction` for creating new RecActions.
- `CreateFakeAction` for creating test RecActions.
- `Delete` for deleting RecActions by profile.

Enhanced all endpoints with XML documentation for clarity and added
`ProducesResponseType` attributes to specify expected HTTP status codes.
2025-12-04 14:00:05 +01:00
dbfae5cdad Refactor error handling in JsonExtensions
Replaced the `catch` block in the `JsonExtensions` class to suppress exceptions silently instead of returning the original string when JSON deserialization fails. The `return str;` statement was moved outside the `try-catch` block, making it the default return value. This change removes explicit handling of invalid JSON inputs and may impact debugging.
2025-12-04 13:54:48 +01:00
d02bebc6e2 Refactor and enhance JSON handling with extensions
Refactored `ConfigExtensions.cs` to move `ConfigurationExtensions`
to the `ReC.API.Extensions` namespace and re-added the
`GetFakeProfileId` method. Introduced `JsonExtensions.cs` with
a `JsonToDynamic` method for recursive JSON deserialization,
simplifying nested JSON handling. Updated `OutResController.cs`
and `RecActionController.cs` to use the new `JsonToDynamic`
method, improving code readability and reducing duplication.
Overall, modularized and cleaned up code for better maintainability.
2025-12-04 13:44:10 +01:00
9165f9d746 Add endpoint for filtered OutRes results by actionId
Introduced a new `HttpGet` endpoint `fake/{actionId}` in the
`OutResController` to support fetching OutRes data filtered
by `actionId` and an optional `resultType` parameter.

Added logic to handle different `resultType` values (`Full`,
`Header`, `Body`) and return the appropriate part of the
result. Updated response `ContentType` for non-`Full` results
and ensured null-safe handling for `Body` and `Header`.

Included a new `ResultType` enum and added necessary `using`
directives for the new functionality.
2025-12-04 13:19:43 +01:00
5a226bfcea Update query to return IEnumerable<OutResDto>
Reordered using directives in DtoMappingProfile.cs.
Added AutoMapper mapping for OutRes to OutResDto.
Modified ReadOutResQuery to return IEnumerable<OutResDto>.
Updated ReadOutResHandler to handle collection results:
- Changed Handle method to return IEnumerable<OutResDto>.
- Updated mapper.Map to map to a collection of OutResDto.
2025-12-04 11:54:38 +01:00
5cefe1457f Refactor to use scoped services in command handler
Updated `InvokeRecActionsCommandHandler` to use `IServiceScopeFactory` for creating scoped services. Replaced direct `IHttpClientFactory` usage with dynamic resolution of `ISender` within a service scope. Improved dependency injection adherence and ensured proper scoping of services. Added `Microsoft.Extensions.DependencyInjection` to `using` directives.
2025-12-04 11:48:22 +01:00
f4390d992a Refactor to use configurable FakeProfileId
Replaced hardcoded FakeProfileId with a dynamic value retrieved
from the configuration using a new GetFakeProfileId extension
method. Updated OutResController and RecActionController to
inject IConfiguration and use the new method. Added a new
HttpGet endpoint in OutResController for fake profile queries.

Modified appsettings.json to include a FakeProfileId setting
with a default value of 2. Introduced ConfigurationExtensions
to centralize configuration access logic, improving code
maintainability and flexibility.
2025-12-04 11:14:37 +01:00
906d99105c Add name to validation rule in ReadOutResQueryValidator
The validation rule in the `ReadOutResQueryValidator` class was updated to include a `WithName("Identifier")` call. This assigns a name ("Identifier") to the rule, improving the clarity and usability of validation error messages, especially in scenarios where multiple rules are applied and need to be distinguished.
2025-12-04 10:17:57 +01:00
3b4671c8e5 Add HttpGet endpoint to OutResController for data retrieval
Added a new asynchronous `Get` method to the `OutResController`
class in the `ReC.API.Controllers` namespace. This method handles
HTTP GET requests, accepts a `ReadOutResQuery` object as a query
parameter, and uses the `mediator` to process the query. The
result is returned in an HTTP 200 OK response. This change
enables retrieval of resources or data based on the provided
query parameters.
2025-12-04 10:14:33 +01:00
534b254d0a Add NotFoundException handling to ReadOutResHandler
Updated `using` directives to include `DigitalData.Core.Exceptions` for custom exception handling. Renamed `dtos` to `resList` for clarity. Added a check to throw `NotFoundException` when no results are found in the query. Updated the return logic to ensure mapping occurs only when results exist. These changes enhance error handling and improve code robustness.
2025-12-04 10:13:46 +01:00
2cd7c035eb Add OutResController with MediatR integration
Introduced a new `OutResController` in the `ReC.API.Controllers`
namespace to handle API requests. The controller uses MediatR
for request handling and is decorated with `[ApiController]`
and `[Route("api/[controller]")]` attributes.

Added a `Get` method to process `ReadOutResQuery` objects
from query parameters and return the result via MediatR.
Included necessary `using` directives for dependencies.
2025-12-04 10:11:20 +01:00
aa34bdd279 Add ProfileId filter and navigation property to OutRes
Enhanced `ReadOutResHandler` to support filtering by `ProfileId`
when querying `OutRes` entities. This was implemented by adding
a condition to filter results based on the `ProfileId` of the
associated `Action`.

Updated the `OutRes` class to include a navigation property
`Action`, annotated with `[ForeignKey]` to link it to the
`ActionId` column. This establishes a relationship between
`OutRes` and `RecAction`, improving data access and maintainability.
2025-12-04 10:06:56 +01:00
2de0b5bfa3 Refactor ReadOutResQuery and add query handler
Refactored `ReadOutResQuery` to a record type implementing
`IRequest<OutResDto>` for MediatR. Introduced `ReadOutResHandler`
to handle the query, leveraging `IRepository` and `IMapper` for
database access and mapping. Added support for filtering by
`ActionId` and asynchronous query execution. Streamlined design
by moving `ProfileId` and `ActionId` to immutable `init`
properties in the record.
2025-12-04 10:03:57 +01:00
15 changed files with 281 additions and 33 deletions

View File

@ -0,0 +1,77 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ReC.API.Extensions;
using ReC.Application.OutResults.Queries;
namespace ReC.API.Controllers;
[Route("api/[controller]")]
[ApiController]
public class OutResController(IMediator mediator, IConfiguration config) : ControllerBase
{
/// <summary>
/// Gets output results based on the provided query parameters.
/// </summary>
/// <param name="query">The query to filter output results.</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>A list of output results matching the query.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Get([FromQuery] ReadOutResQuery query, CancellationToken cancel) => Ok(await mediator.Send(query, cancel));
/// <summary>
/// Gets output results for a fake/test profile.
/// </summary>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>A list of output results for the fake profile.</returns>
[HttpGet("fake")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Get(CancellationToken cancel) => Ok(await mediator.Send(new ReadOutResQuery()
{
ProfileId = config.GetFakeProfileId()
}, cancel));
/// <summary>
/// Gets a specific output result for a fake/test profile and action.
/// </summary>
/// <param name="actionId">The ID of the action to retrieve the result for.</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <param name="resultType">Specifies which part of the result to return (Full, Header, or Body).</param>
/// <returns>The requested output result or a part of it (header/body).</returns>
[HttpGet("fake/{actionId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Get([FromRoute] long actionId, CancellationToken cancel, ResultType resultType = ResultType.Full)
{
var res = (await mediator.Send(new ReadOutResQuery()
{
ProfileId = config.GetFakeProfileId(),
ActionId = actionId
}, cancel)).First();
return resultType switch
{
ResultType.Body => res.Body is null ? Ok(new object { }) : Ok(res.Body.JsonToDynamic()),
ResultType.Header => res.Header is null ? Ok(new object { }) : Ok(res.Header.JsonToDynamic()),
_ => Ok(res),
};
}
}
/// <summary>
/// Defines the type of result to be returned from an output result query.
/// </summary>
public enum ResultType
{
/// <summary>
/// Return the full result object.
/// </summary>
Full,
/// <summary>
/// Return only the header part of the result.
/// </summary>
Header,
/// <summary>
/// Return only the body part of the result.
/// </summary>
Body
}

View File

@ -1,5 +1,6 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ReC.API.Extensions;
using ReC.API.Models;
using ReC.Application.RecActions.Commands;
using ReC.Application.RecActions.Queries;
@ -9,38 +10,68 @@ namespace ReC.API.Controllers;
[Route("api/[controller]")]
[ApiController]
public class RecActionController(IMediator mediator) : ControllerBase
public class RecActionController(IMediator mediator, IConfiguration config) : ControllerBase
{
private const long FakeProfileId = 2;
[HttpPost("invoke/{profileId}")]
/// <summary>
/// Invokes a batch of RecActions for a given profile.
/// </summary>
/// <param name="profileId">The ID of the profile.</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>An HTTP 202 Accepted response indicating the process has been started.</returns>
[HttpPost("invoke/{cmd}")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
public async Task<IActionResult> Invoke([FromRoute] int profileId, CancellationToken cancel)
{
await mediator.InvokeBatchRecAction(profileId, cancel);
return Accepted();
}
/// <summary>
/// Invokes a batch of RecActions for a fake/test profile.
/// </summary>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>An HTTP 202 Accepted response indicating the process has been started.</returns>
[HttpPost("invoke/fake")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
public async Task<IActionResult> Invoke(CancellationToken cancel)
{
await mediator.InvokeBatchRecAction(FakeProfileId, cancel);
await mediator.InvokeBatchRecAction(config.GetFakeProfileId(), cancel);
return Accepted();
}
#region CRUD
/// <summary>
/// Gets all RecActions for a given profile.
/// </summary>
/// <param name="profileId">The ID of the profile.</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>A list of RecActions for the specified profile.</returns>
[HttpGet]
public async Task<IActionResult> Get([FromQuery] long profileId, CancellationToken cancel) => Ok(await mediator.Send(new ReadRecActionQuery()
{
ProfileId = profileId
}, cancel));
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Get([FromQuery] ReadRecActionQuery query, CancellationToken cancel) => Ok(await mediator.Send(query, cancel));
/// <summary>
/// Gets all RecActions for a fake/test profile.
/// </summary>
/// <param name="cancel">A token to cancel the operation.</param>
/// <param name="invoked"></param>
/// <returns>A list of RecActions for the fake profile.</returns>
[HttpGet("fake")]
public async Task<IActionResult> Get(CancellationToken cancel) => Ok(await mediator.Send(new ReadRecActionQuery()
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Get(CancellationToken cancel, [FromQuery] bool invoked = false) => Ok(await mediator.Send(new ReadRecActionQuery()
{
ProfileId = FakeProfileId
ProfileId = config.GetFakeProfileId(),
Invoked = invoked
}, cancel));
/// <summary>
/// Creates a new RecAction.
/// </summary>
/// <param name="command">The command containing the details for the new RecAction.</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>An HTTP 201 Created response.</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> CreateAction([FromBody] CreateRecActionCommand command, CancellationToken cancel)
{
await mediator.Send(command, cancel);
@ -48,7 +79,17 @@ public class RecActionController(IMediator mediator) : ControllerBase
return CreatedAtAction(nameof(CreateAction), null);
}
/// <summary>
/// Creates a new fake RecAction for testing purposes.
/// </summary>
/// <param name="cancel">A token to cancel the operation.</param>
/// <param name="request">The optional request body and header for the fake action.</param>
/// <param name="endpointUri">The target endpoint URI.</param>
/// <param name="endpointPath">The optional path to append to the endpoint URI.</param>
/// <param name="type">The HTTP method type (e.g., GET, POST).</param>
/// <returns>An HTTP 201 Created response.</returns>
[HttpPost("fake")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> CreateFakeAction(
CancellationToken cancel,
[FromBody] FakeRequest? request = null,
@ -64,7 +105,7 @@ public class RecActionController(IMediator mediator) : ControllerBase
await mediator.Send(new CreateRecActionCommand()
{
ProfileId = FakeProfileId,
ProfileId = config.GetFakeProfileId(),
EndpointUri = endpointUri,
Type = type,
BodyQuery = $@"SELECT '{bodyJson ?? "NULL"}' AS REQUEST_BODY;",
@ -76,23 +117,32 @@ public class RecActionController(IMediator mediator) : ControllerBase
return CreatedAtAction(nameof(CreateFakeAction), null);
}
/// <summary>
/// Deletes all RecActions associated with a specific profile.
/// </summary>
/// <param name="cmd"></param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>An HTTP 204 No Content response upon successful deletion.</returns>
[HttpDelete]
public async Task<IActionResult> Delete([FromQuery] int profileId, CancellationToken cancel)
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Delete([FromQuery] DeleteRecActionsCommand cmd, CancellationToken cancel)
{
await mediator.Send(new DeleteRecActionsCommand()
{
ProfileId = FakeProfileId
}, cancel);
await mediator.Send(cmd, cancel);
return NoContent();
}
/// <summary>
/// Deletes all RecActions for a fake/test profile.
/// </summary>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>An HTTP 204 No Content response upon successful deletion.</returns>
[HttpDelete("fake")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Delete(CancellationToken cancel)
{
await mediator.Send(new DeleteRecActionsCommand()
{
ProfileId = FakeProfileId
ProfileId = config.GetFakeProfileId()
}, cancel);
return NoContent();

View File

@ -0,0 +1,6 @@
namespace ReC.API.Extensions;
public static class ConfigurationExtensions
{
public static int GetFakeProfileId(this IConfiguration config) => config.GetValue("FakeProfileId", 2);
}

View File

@ -0,0 +1,53 @@
using System.Text.Json;
public static class JsonExtensions
{
/// <summary>
/// Deserialize JSON string and automatically parse nested JSON strings.
/// </summary>
public static dynamic? JsonToDynamic(this string json)
{
// Deserialize the top-level JSON
var result = JsonSerializer.Deserialize<JsonElement>(json);
// Recursively fix stringified JSON objects
return JsonToDynamic(result);
}
private static dynamic? JsonToDynamic(JsonElement obj)
{
switch (obj.ValueKind)
{
case JsonValueKind.Object:
var dict = new Dictionary<string, dynamic?>();
foreach (var prop in obj.EnumerateObject())
{
dict[prop.Name] = JsonToDynamic(prop.Value);
}
return dict;
case JsonValueKind.Array:
var list = new List<dynamic>();
foreach (var item in obj.EnumerateArray())
{
list.Add(JsonToDynamic(item));
}
return list;
case JsonValueKind.String:
var str = obj.GetString();
// Try to parse string as JSON
if (!string.IsNullOrWhiteSpace(str) && (str.StartsWith('{') || str.StartsWith('[')))
{
try
{
return JsonToDynamic(JsonSerializer.Deserialize<JsonElement>(str));
}
catch { }
}
return str;
default:
return obj.GetRawText();
}
}
}

View File

@ -13,5 +13,6 @@
"RecAction": {
"MaxConcurrentInvocations": 5
},
"AddedWho": "ReC.API"
"AddedWho": "ReC.API",
"FakeProfileId": 2
}

View File

@ -1,5 +1,4 @@
using AutoMapper;
using ReC.Domain.Entities;
using ReC.Domain.Entities;
namespace ReC.Application.Common.Dto;
@ -8,5 +7,6 @@ public class DtoMappingProfile : AutoMapper.Profile
public DtoMappingProfile()
{
CreateMap<RecActionView, RecActionDto>();
CreateMap<OutRes, OutResDto>();
}
}

View File

@ -1,8 +1,37 @@
namespace ReC.Application.OutResults.Queries;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using ReC.Application.Common.Dto;
using ReC.Domain.Entities;
public class ReadOutResQuery
namespace ReC.Application.OutResults.Queries;
public record ReadOutResQuery : IRequest<IEnumerable<OutResDto>>
{
public long? ProfileId { get; set; }
public long? ProfileId { get; init; }
public long? ActionId { get; set; }
public long? ActionId { get; init; }
}
public class ReadOutResHandler(IRepository<OutRes> repo, IMapper mapper) : IRequestHandler<ReadOutResQuery, IEnumerable<OutResDto>>
{
public async Task<IEnumerable<OutResDto>> Handle(ReadOutResQuery request, CancellationToken cancel)
{
var q = repo.Query;
if(request.ActionId is long actionId)
q = q.Where(res => res.ActionId == actionId);
if(request.ProfileId is long profileId)
q = q.Where(res => res.Action!.ProfileId == profileId);
var resList = await q.ToListAsync(cancel);
if (resList.Count == 0)
throw new NotFoundException();
return mapper.Map<IEnumerable<OutResDto>>(resList);
}
}

View File

@ -8,6 +8,7 @@ public class ReadOutResQueryValidator : AbstractValidator<ReadOutResQuery>
{
RuleFor(x => x)
.Must(x => x.ActionId.HasValue || x.ProfileId.HasValue)
.WithMessage("At least one of ActionId or ProfileId must be provided.");
.WithMessage("At least one of ActionId or ProfileId must be provided.")
.WithName("Identifier");
}
}

View File

@ -8,7 +8,7 @@ namespace ReC.Application.RecActions.Commands;
public class DeleteRecActionsCommand : IRequest
{
public long ProfileId { get; init; } = 2;
public required long ProfileId { get; init; }
}
public class DeleteRecActionsCommandHandler(IRepository<RecAction> repo) : IRequestHandler<DeleteRecActionsCommand>

View File

@ -1,4 +1,5 @@
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ReC.Application.RecActions.Queries;
@ -12,11 +13,11 @@ public static class InvokeBatchRecActionsCommandExtensions
=> 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, IServiceScopeFactory scopeFactory, IHttpClientFactory clientFactory, ILogger<InvokeRecActionsCommandHandler>? logger = null) : IRequestHandler<InvokeBatchRecActionsCommand>
{
public async Task Handle(InvokeBatchRecActionsCommand request, CancellationToken cancel)
{
var actions = await sender.Send(request.ToReadQuery(), cancel);
var actions = await sender.Send(request.ToReadQuery(q => q.Invoked = false), cancel);
var http = clientFactory.CreateClient();
@ -27,7 +28,9 @@ public class InvokeRecActionsCommandHandler(ISender sender, IHttpClientFactory c
await semaphore.WaitAsync(cancel);
try
{
await sender.Send(action.ToInvokeCommand(), cancel);
using var scope = scopeFactory.CreateScope();
var sender = scope.ServiceProvider.GetRequiredService<ISender>();
await sender.Send(action.ToInvokeCommand(), cancel);
}
catch(Exception ex)
{

View File

@ -12,13 +12,20 @@ public record ReadRecActionQueryBase
{
public long ProfileId { get; init; }
public ReadRecActionQuery ToReadQuery() => new(this);
public ReadRecActionQuery ToReadQuery(Action<ReadRecActionQuery> modify)
{
ReadRecActionQuery query = new(this);
modify(query);
return query;
}
}
public record ReadRecActionQuery : ReadRecActionQueryBase, IRequest<IEnumerable<RecActionDto>>
{
public ReadRecActionQuery(ReadRecActionQueryBase root) : base(root) { }
public bool? Invoked { get; set; } = null;
public ReadRecActionQuery() { }
}
@ -26,7 +33,12 @@ public class ReadRecActionQueryHandler(IRepository<RecActionView> repo, IMapper
{
public async Task<IEnumerable<RecActionDto>> Handle(ReadRecActionQuery request, CancellationToken cancel)
{
var actions = await repo.Where(x => x.ProfileId == request.ProfileId).ToListAsync(cancel);
var query = repo.Where(act => act.ProfileId == request.ProfileId);
if (request.Invoked is bool invoked)
query = invoked ? query.Where(act => act.Root!.OutRes != null) : query.Where(act => act.Root!.OutRes == null);
var actions = await query.ToListAsync(cancel);
if(actions.Count == 0)
throw new NotFoundException($"No actions found for the profile {request.ProfileId}.");

View File

@ -14,6 +14,9 @@ public class OutRes
[Column("ACTION_ID")]
public long? ActionId { get; set; }
[ForeignKey("ActionId")]
public RecAction? Action { get; set; }
[Column("RESULT_HEADER")]
public string? Header { get; set; }

View File

@ -69,4 +69,6 @@ public class RecAction
[Column("CHANGED_WHEN")]
public DateTime? ChangedWhen { get; set; }
public OutRes? OutRes { get; set; }
}

View File

@ -17,9 +17,15 @@ public class RecActionView
[Column("ACTION_ID")]
public required long Id { get; set; }
[ForeignKey("Id")]
public RecAction? Root { get; set; }
[Column("PROFILE_ID")]
public long? ProfileId { get; set; }
[ForeignKey("ProfileId")]
public Profile? Profile { get; set; }
[Column("PROFILE_NAME")]
[MaxLength(100)]
public string? ProfileName { get; set; }

View File

@ -35,5 +35,10 @@ public class RecDbContext(DbContextOptions<RecDbContext> options) : DbContext(op
modelBuilder.Entity<HeaderQueryResult>().HasNoKey();
modelBuilder.Entity<BodyQueryResult>().HasNoKey();
modelBuilder.Entity<RecAction>()
.HasOne(act => act.OutRes)
.WithOne(res => res.Action)
.HasForeignKey<OutRes>(res => res.ActionId);
}
}