Compare commits

...

26 Commits

Author SHA1 Message Date
6f5409f528 Implement HeaderQueryBehavior with database support
Updated `HeaderQueryBehavior` to include dependency injection
for `IRecDbContext` and `ILogger`, enabling database interaction
and logging. Replaced the placeholder `NotImplementedException`
in the `Handle` method with functionality to process `HeaderQuery`
SQL results, deserialize JSON headers, and assign them to the
`Headers` property of the action. Added error handling and
logging for deserialization failures. Updated constructor and
method signatures to support new dependencies and asynchronous
behavior. Added necessary `using` directives for new features.
2025-11-27 17:16:13 +01:00
03498275d7 Implement BodyQueryBehavior and add SQL Server support
Updated BodyQueryBehavior to process BodyQuery using the
IRecDbContext dependency. Added logic to execute SQL queries
via FromSqlRaw and assign results to the action's Body property.
Removed placeholder NotImplementedException.

Added Microsoft.EntityFrameworkCore.SqlServer package to
ReC.Application.csproj to enable SQL Server database operations.
2025-11-27 17:05:12 +01:00
f8581deaf9 Refactor DbContext and add IRecDbContext interface
Refactored `DependencyInjection` to use a generic `TRecDbContext`
for flexibility and added scoped registration for `IRecDbContext`.
Updated `RecDbContext` to implement the new `IRecDbContext`
interface, ensuring adherence to the application-layer contract.

Introduced the `IRecDbContext` interface in the application layer,
defining `DbSet` properties for key entities and a `SaveChangesAsync`
method. Updated `ReC.Infrastructure.csproj` to reference the
application project for interface access.
2025-11-27 16:51:49 +01:00
8ea7d47868 Add DbSets and configure keyless entity in RecDbContext
Added `HeaderQueryResults` and `BodyQueryResults` DbSet
properties to the `RecDbContext` class for interacting
with `HeaderQueryResult` and `BodyQueryResult` entities.

Overrode the `OnModelCreating` method to configure the
`RecAction` entity as keyless using
`modelBuilder.Entity<RecAction>().HasNoKey()`.
2025-11-27 16:40:33 +01:00
2bbfd96d62 Add keyless query result entities for headers and bodies
Added `HeaderQueryResult` and `BodyQueryResult` classes to represent
query results for headers and bodies, respectively. These classes
map their properties to database columns (`REQUEST_HEADER` and
`REQUEST_BODY`) using the `[Column]` attribute. Both properties
are nullable and immutable.

Updated `RecDbContext` to configure these entities as keyless
using the `HasNoKey()` method in the `OnModelCreating` method,
indicating they are used for read-only queries.
2025-11-27 16:37:30 +01:00
4f364e3eb2 Refactor behaviors to Common namespace
Moved `BodyQueryBehavior` and `HeaderQueryBehavior` from
`ReC.Application.RecActions.Behaviors` to
`ReC.Application.Common.Behaviors` to reflect their broader
applicability. Updated `DependencyInjection.cs` to use the new
namespace. This reorganization improves code structure and
maintainability.
2025-11-27 15:54:24 +01:00
0c4d145e20 Add HeaderQueryBehavior and update DI configuration
Added `HeaderQueryBehavior<TRecAction>` to handle header query
processing in the MediatR pipeline. Updated the `DependencyInjection`
class to register `HeaderQueryBehavior<>` alongside
`BodyQueryBehavior<>` in the MediatR configuration. The new behavior
currently throws a `NotImplementedException` as its logic is pending
implementation.
2025-11-27 15:47:12 +01:00
1757c0e055 Add BodyQueryBehavior and register in MediatR pipeline
Introduced the `BodyQueryBehavior<TRecAction>` class, which implements the `IPipelineBehavior` interface to handle actions of type `RecActionDto`. The behavior is currently unimplemented and throws a `NotImplementedException`.

Updated the `DependencyInjection` class to register `BodyQueryBehavior<>` as an open generic pipeline behavior in MediatR. Added necessary `using` directives in both `DependencyInjection.cs` and `BodyQueryBehavior.cs` to support the new behavior.
2025-11-27 15:46:07 +01:00
b1df50bc32 Enhance RecActionDto with HTTP and query handling
Added new properties to `RecActionDto`:
- `Headers` to store HTTP headers as a dictionary.
- `BodyQuery` to represent a query related to the body.
- `Body` to store the body content.
- `PostprocessingQuery` to represent a query for postprocessing.

These changes improve the class's ability to handle HTTP requests and related operations, providing greater flexibility for preprocessing and postprocessing tasks.
2025-11-27 15:30:54 +01:00
a66a70fab3 Refactor HTTP request handling in InvokeRecActionCommand
Replaced the `reqMsg` variable with `httpReq` to simplify the
creation and usage of `HttpRequestMessage`. The `request.RestType`
is now used directly to create `httpReq`, which is passed to
`http.SendAsync`. This change reduces redundancy and improves
code clarity.
2025-11-27 14:54:28 +01:00
a84b5531b7 Add ToHttpRequestMessage and refactor HTTP handling
Introduced a new extension method `ToHttpRequestMessage` in
`HttpExtensions` to simplify `HttpRequestMessage` creation
with URI validation. Added `System.Diagnostics.CodeAnalysis`
for `StringSyntaxAttribute` support.

Refactored `InvokeRecActionCommandHandler` to use the new
`ToHttpRequestMessage` method, improving readability and
encapsulation. Renamed `msg` to `reqMsg` for clarity.
2025-11-27 14:51:43 +01:00
21e3171e11 Refactor ToHttpMethod to use a dictionary for mappings
Replaced the switch statement in the ToHttpMethod method with
a case-insensitive dictionary for mapping HTTP method strings
to HttpMethod objects. Introduced a private static dictionary
to store predefined HTTP methods, including the addition of
the CONNECT method. Improved performance and maintainability
by leveraging dictionary lookups for faster and cleaner code.
2025-11-27 14:47:41 +01:00
bbfff226de Add ToEndpointUriBuilder method to RecActionDto
Introduce the ToEndpointUriBuilder() method in the RecActionDto
class to simplify the creation of a UriBuilder object. The method
configures the UriBuilder based on the EndpointUri and ProfileType
properties, setting the port to -1 and the scheme to ProfileType
if provided. This enhances the flexibility and usability of the
RecActionDto class for endpoint URI construction.
2025-11-27 14:37:35 +01:00
7a4885c86a Refactor HTTP method handling and cleanup
Refactored HTTP method handling by introducing a `ToHttpMethod`
extension method in `HttpExtensions.cs` to convert string
representations of HTTP methods to `HttpMethod` objects.
Replaced manual `HttpMethod` instantiation with the new
extension method in `InvokeRecActionCommandHandler` for
improved readability and reusability.

Removed unused `using` directives from `HttpExtensions.cs`
and cleaned up the `namespace` declaration. Added a `using`
directive for `ReC.Application.Common` in
`InvokeRecActionCommand.cs` to support the new extension method.
2025-11-27 11:45:15 +01:00
a46d97467d Ensure proper disposal and add HttpExtensions class
Updated `InvokeRecActionCommand.cs` to use `using` statements for `HttpClient`, `HttpRequestMessage`, and `HttpResponseMessage` to prevent resource leaks.

Added a new `HttpExtensions.cs` file with a static `HttpExtensions` class as a placeholder for future HTTP-related extension methods. Included necessary `using` directives for potential LINQ, collections, and asynchronous programming.
2025-11-27 11:32:14 +01:00
2f3a685e7d Refactor Rec Actions handling with MediatR support
Refactored the handling of "Rec Actions" by introducing the `InvokeRecActionCommandHandler` class and leveraging the MediatR library for command handling. Simplified the logic in `InvokeBatchRecActionsCommand.cs` by delegating HTTP request handling to the `sender.Send` method, which now uses `InvokeRecActionCommand`.

Updated `InvokeRecActionCommand` to implement the `IRequest` interface, enabling MediatR pipeline compatibility. Added `InvokeRecActionCommandExtensions` for converting `RecActionDto` to `InvokeRecActionCommand`, improving readability.

Centralized HTTP request logic in the new `InvokeRecActionCommandHandler`, which validates `RestType`, sends HTTP requests, and logs warnings for invalid actions. Removed unused `using` directives and added necessary ones for MediatR and logging.

This refactoring improves modularity, testability, and maintainability by separating concerns and streamlining the code structure.
2025-11-27 11:26:46 +01:00
d1e8f619f5 Refactor RecActionDto and add InvokeRecActionCommand
Converted RecActionDto from class to record for immutability
and value-based equality. Added nullable properties
`ActionId` and `ProfileId` to RecActionDto.

Introduced InvokeRecActionCommand.cs, which includes:
- A new InvokeRecActionCommand record inheriting from RecActionDto.
- Constructors for initializing InvokeRecActionCommand.
- An extension method `ToInvokeCommand` for converting
  RecActionDto to InvokeRecActionCommand.
2025-11-27 11:20:41 +01:00
0ec913b95e Refactor to support batch action invocation
Replaced `InvokeRecAction` with `InvokeBatchRecAction` in `ActionController` to transition from single-action to batch-action invocation.

Removed `InvokeRecActionCommand.cs`, which previously handled individual action invocations. Introduced `InvokeBatchRecActionsCommand.cs` to handle batch processing with similar concurrency control logic using a semaphore.

This redesign improves scalability and aligns with updated business requirements while maintaining compatibility with the existing application structure.
2025-11-27 11:05:21 +01:00
cb851a4ec6 Refactor RecAction commands and queries to use records
Refactored `InvokeRecActionCommand` and `ReadRecActionQuery`
to leverage C# `record` types for immutability and improved
data structure clarity. Introduced `ReadRecActionQueryBase`
as a shared base record to separate common properties and
logic. Updated `InvokeRecActionCommandHandler` to use the
new `ToReadQuery` method for compatibility. These changes
enhance code maintainability, reusability, and separation
of concerns while preserving existing functionality.
2025-11-27 10:56:21 +01:00
cd8f757c43 Add RecAction configuration support
Introduced a new `RecAction` configuration section in `appsettings.json` with a `MaxConcurrentInvocations` property. Updated `Program.cs` to use the `Configuration` object to pass the `RecAction` settings to the `AddRecServices` method. Replaced the `MediatRLicense` value in `appsettings.json` with no functional changes.
2025-11-27 10:12:44 +01:00
8aa43db909 Refactor and enhance DependencyInjection class
Organized the `DependencyInjection` class into regions for better code structure: `Required Services`, `Configuration`, `LuckyPennySoftwareLicenseKey`, and `ConfigureRecActions`.

Added `_requiredServices` dictionary to track the configuration status of required services. Updated `LuckyPennySoftwareLicenseKey` to include a null check before marking it as configured.

Moved `_configActions` to the `Configuration` region and added `ApplyConfigurations` to process queued configuration actions.

Enhanced `ConfigureRecActions` to prevent duplicate configurations by throwing an `InvalidOperationException` if already configured.

These changes improve code readability, maintainability, and robustness.
2025-11-27 10:09:20 +01:00
2a9b52ebe1 Ensure required services are configured in DI setup
Added `EnsureRequiredServices` to validate required services
in `ConfigurationOptions`, throwing an exception if any are
missing. Introduced `_requiredServices` dictionary to track
configuration status for `ConfigureRecActions` and
`LuckyPennySoftwareLicenseKey`. Updated property and methods
to mark services as configured. Integrated validation into
`AddRecServices` to enforce proper setup.
2025-11-27 09:58:49 +01:00
988d48581b Add configuration options and license key support
Updated `DependencyInjection.cs` to include support for
`ConfigurationOptions`, enabling queued configuration actions
and the ability to configure `RecActionOptions` via delegates
or `IConfiguration`. Added `LuckyPennySoftwareLicenseKey`
property to `ConfigurationOptions` and integrated it into
`AddAutoMapper` setup. Introduced `ApplyConfigurations`
method to process queued actions. Updated `using` directives
to include necessary namespaces.
2025-11-27 09:31:46 +01:00
46ef5e0d02 Add RecActionOptions class for concurrency configuration
A new `RecActionOptions` class was introduced in the
`ReC.Application.Common.Options` namespace. This class includes
a `MaxConcurrentInvocations` property with a default value of 5,
intended to configure the maximum number of concurrent invocations
allowed. This addition helps centralize and manage concurrency
settings in the application.
2025-11-27 09:20:13 +01:00
5e4f113145 Refactor namespaces for DTO classes
Updated `DtoMappingProfile` and `RecActionDto` to use the
`ReC.Application.Common.Dto` namespace instead of
`ReC.Application.Dto`. Adjusted `ReadRecActionQuery` to reflect
this namespace change. No functional changes were introduced.
2025-11-27 09:16:42 +01:00
3e9edcd8af Add error handling and logging to Rec action invocation
Enhanced the `InvokeRecActionCommandHandler` class by adding
a `catch` block to handle exceptions during HTTP request and
response processing. Logged exception details, including
`ProfileId` and `ActionId`, using `logger?.LogError` for
better observability. Ensured the `finally` block releasing
the semaphore remains unaffected.
2025-11-27 09:15:36 +01:00
20 changed files with 327 additions and 47 deletions

View File

@ -11,7 +11,7 @@ public class ActionController(IMediator mediator) : ControllerBase
[HttpPost("{profileId}")]
public async Task<IActionResult> Invoke([FromRoute] int profileId)
{
await mediator.InvokeRecAction(profileId);
await mediator.InvokeBatchRecAction(profileId);
return Accepted();
}
}

View File

@ -4,10 +4,13 @@ using ReC.Application;
var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
// Add services to the container.
builder.Services.AddRecServices(options =>
{
options.LuckyPennySoftwareLicenseKey = builder.Configuration["LuckyPennySoftwareLicenseKey"];
options.ConfigureRecActions(config.GetSection("RecAction"));
});
builder.Services.AddRecInfrastructure(options =>

View File

@ -6,5 +6,8 @@
}
},
"AllowedHosts": "*",
"MediatRLicense": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzg0ODUxMjAwIiwiaWF0IjoiMTc1MzM2MjQ5MSIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazB5NmV3MmQ4YTk4Mzg3aDJnbTRuOWswIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.ZqsFG7kv_-xGfxS6ACk3i0iuNiVUXX2AvPI8iAcZ6-z2170lGv__aO32tWpQccD9LCv5931lBNLWSblKS0MT3gOt-5he2TEftwiSQGFwoIBgtOHWsNRMinUrg2trceSp3IhyS3UaMwnxZDrCvx4-0O-kpOzVpizeHUAZNr5U7oSCWO34bpKdae6grtM5e3f93Z1vs7BW_iPgItd-aLvPwApbaG9VhmBTKlQ7b4Jh64y7UXJ9mKP7Qb_Oa97oEg0oY5DPHOWTZWeE1EzORgVr2qkK2DELSHuZ_EIUhODojkClPNAKtvEl_qEjpq0HZCIvGwfCCRlKlSkQqIeZdFkiXg"
"MediatRLicense": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzg0ODUxMjAwIiwiaWF0IjoiMTc1MzM2MjQ5MSIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazB5NmV3MmQ4YTk4Mzg3aDJnbTRuOWswIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.ZqsFG7kv_-xGfxS6ACk3i0iuNiVUXX2AvPI8iAcZ6-z2170lGv__aO32tWpQccD9LCv5931lBNLWSblKS0MT3gOt-5he2TEftwiSQGFwoIBgtOHWsNRMinUrg2trceSp3IhyS3UaMwnxZDrCvx4-0O-kpOzVpizeHUAZNr5U7oSCWO34bpKdae6grtM5e3f93Z1vs7BW_iPgItd-aLvPwApbaG9VhmBTKlQ7b4Jh64y7UXJ9mKP7Qb_Oa97oEg0oY5DPHOWTZWeE1EzORgVr2qkK2DELSHuZ_EIUhODojkClPNAKtvEl_qEjpq0HZCIvGwfCCRlKlSkQqIeZdFkiXg",
"RecAction": {
"MaxConcurrentInvocations": 5
}
}

View File

@ -0,0 +1,21 @@
using MediatR;
using ReC.Application.Common.Dto;
using ReC.Application.Common.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace ReC.Application.Common.Behaviors;
public class BodyQueryBehavior<TRecAction>(IRecDbContext dbContext) : IPipelineBehavior<TRecAction, Unit>
where TRecAction : RecActionDto
{
public async Task<Unit> Handle(TRecAction action, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
{
if (action.BodyQuery is null)
return await next(cancel);
var result = await dbContext.BodyQueryResults.FromSqlRaw(action.BodyQuery).FirstOrDefaultAsync(cancel);
action.Body = result?.RawBody;
return await next(cancel);
}
}

View File

@ -0,0 +1,35 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ReC.Application.Common.Dto;
using ReC.Application.Common.Interfaces;
using System.Text.Json;
namespace ReC.Application.Common.Behaviors;
public class HeaderQueryBehavior<TRecAction>(IRecDbContext dbContext, ILogger<HeaderQueryBehavior<TRecAction>>? logger = null) : IPipelineBehavior<TRecAction, Unit>
where TRecAction : RecActionDto
{
public async Task<Unit> Handle(TRecAction action, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
{
if (action.HeaderQuery is null)
return await next(cancel);
var result = await dbContext.HeaderQueryResults.FromSqlRaw(action.HeaderQuery).FirstOrDefaultAsync(cancel);
if(result?.RawHeader is null)
return await next(cancel);
var headerDict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(result.RawHeader);
if(headerDict is null)
{
logger?.LogWarning("Failed to deserialize header query result: {RawHeader}. Profile ID: {ProfileId}, Action ID: {ActionId}", result.RawHeader, action.ProfileId, action.ActionId);
return await next(cancel);
}
action.Headers = headerDict.ToDictionary(header => header.Key, kvp => kvp.Value.ToString());
return await next(cancel);
}
}

View File

@ -1,7 +1,7 @@
using AutoMapper;
using ReC.Domain.Entities;
namespace ReC.Application.Dto;
namespace ReC.Application.Common.Dto;
public class DtoMappingProfile : Profile
{

View File

@ -1,6 +1,6 @@
namespace ReC.Application.Dto;
namespace ReC.Application.Common.Dto;
public class RecActionDto
public record RecActionDto
{
public long? ActionId { get; init; }
@ -54,7 +54,23 @@ public class RecActionDto
public string? HeaderQuery { get; init; }
public Dictionary<string, string>? Headers { get; set; }
public string? BodyQuery { get; init; }
public string? Body { get; set; }
public string? PostprocessingQuery { get; init; }
public UriBuilder ToEndpointUriBuilder()
{
var builder = EndpointUri is null ? new UriBuilder() : new UriBuilder(EndpointUri);
builder.Port = -1;
if (ProfileType is not null)
builder.Scheme = ProfileType;
return builder;
}
}

View File

@ -0,0 +1,26 @@
using System.Diagnostics.CodeAnalysis;
namespace ReC.Application.Common;
public static class HttpExtensions
{
private static readonly Dictionary<string, HttpMethod> _methods = new(StringComparer.OrdinalIgnoreCase)
{
["GET"] = HttpMethod.Get,
["POST"] = HttpMethod.Post,
["PUT"] = HttpMethod.Put,
["DELETE"] = HttpMethod.Delete,
["PATCH"] = HttpMethod.Patch,
["HEAD"] = HttpMethod.Head,
["OPTIONS"] = HttpMethod.Options,
["TRACE"] = HttpMethod.Trace,
["CONNECT"] = HttpMethod.Connect
};
public static HttpMethod ToHttpMethod(this string method) => _methods.TryGetValue(method, out var httpMethod)
? httpMethod
: new HttpMethod(method);
public static HttpRequestMessage ToHttpRequestMessage(this HttpMethod method, [StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri)
=> new(method, requestUri);
}

View File

@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;
using ReC.Domain.Entities;
namespace ReC.Application.Common.Interfaces;
public interface IRecDbContext
{
public DbSet<EndpointParam> EndpointParams { get; }
public DbSet<RecAction> Actions { get; }
public DbSet<OutRes> OutRes { get; }
public DbSet<HeaderQueryResult> HeaderQueryResults { get; }
public DbSet<BodyQueryResult> BodyQueryResults { get; }
public Task<int> SaveChangesAsync(CancellationToken cancel = default);
}

View File

@ -0,0 +1,6 @@
namespace ReC.Application.Common.Options;
public class RecActionOptions
{
public int MaxConcurrentInvocations { get; set; } = 5;
}

View File

@ -1,4 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ReC.Application.Common.Behaviors;
using ReC.Application.Common.Options;
using System.Reflection;
namespace ReC.Application;
@ -10,6 +13,10 @@ public static class DependencyInjection
var configOpt = new ConfigurationOptions();
options.Invoke(configOpt);
configOpt.EnsureRequiredServices();
configOpt.ApplyConfigurations(services);
services.AddAutoMapper(cfg =>
{
cfg.AddMaps(Assembly.GetExecutingAssembly());
@ -19,6 +26,7 @@ public static class DependencyInjection
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
cfg.AddOpenBehaviors([typeof(BodyQueryBehavior<>), typeof(HeaderQueryBehavior<>)]);
cfg.LicenseKey = configOpt.LuckyPennySoftwareLicenseKey;
});
@ -29,6 +37,74 @@ public static class DependencyInjection
public class ConfigurationOptions
{
public string? LuckyPennySoftwareLicenseKey { get; set; }
#region Required Services
private readonly Dictionary<string, bool> _requiredServices = new()
{
{ nameof(ConfigureRecActions), false },
{ nameof(LuckyPennySoftwareLicenseKey), false }
};
internal void EnsureRequiredServices()
{
var missingServices = _requiredServices
.Where(kvp => !kvp.Value)
.Select(kvp => kvp.Key.Replace("Configure", string.Empty));
if (missingServices.Any())
throw new InvalidOperationException($"The following required services were not configured: {string.Join(", ", missingServices)}");
}
#endregion Required Services
#region Configuration
private readonly Queue<Action<IServiceCollection>> _configActions = new();
internal void ApplyConfigurations(IServiceCollection services)
{
while (_configActions.Count > 0)
{
var action = _configActions.Dequeue();
action(services);
}
}
#endregion Configuration
#region LuckyPennySoftwareLicenseKey
private string? _luckyPennySoftwareLicenseKey;
public string? LuckyPennySoftwareLicenseKey
{
get => _luckyPennySoftwareLicenseKey;
set
{
_luckyPennySoftwareLicenseKey = value;
if (value is not null)
_requiredServices[nameof(LuckyPennySoftwareLicenseKey)] = true;
}
}
#endregion LuckyPennySoftwareLicenseKey
#region ConfigureRecActions
public ConfigurationOptions ConfigureRecActions(Action<RecActionOptions> configure)
{
_configActions.Enqueue(services => services.Configure(configure));
if(_requiredServices[nameof(ConfigureRecActions)])
throw new InvalidOperationException("RecActionOptions have already been configured.");
_requiredServices[nameof(ConfigureRecActions)] = true;
return this;
}
public ConfigurationOptions ConfigureRecActions(IConfiguration configuration)
{
_configActions.Enqueue(services => services.Configure<RecActionOptions>(configuration));
if (_requiredServices[nameof(ConfigureRecActions)])
throw new InvalidOperationException("RecActionOptions have already been configured.");
_requiredServices[nameof(ConfigureRecActions)] = true;
return this;
}
#endregion ConfigureRecActions
}
}

View File

@ -13,6 +13,7 @@
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
<PackageReference Include="MediatR" Version="13.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
</ItemGroup>

View File

@ -0,0 +1,49 @@
using MediatR;
using Microsoft.Extensions.Logging;
using ReC.Application.RecActions.Queries;
namespace ReC.Application.RecActions.Commands;
public record InvokeBatchRecActionsCommand : ReadRecActionQueryBase, IRequest;
public static class InvokeBatchRecActionsCommandExtensions
{
public static Task InvokeBatchRecAction(this ISender sender, int profileId)
=> sender.Send(new InvokeBatchRecActionsCommand { ProfileId = profileId });
}
public class InvokeRecActionsCommandHandler(ISender sender, 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 http = clientFactory.CreateClient();
using var semaphore = new SemaphoreSlim(5);
var tasks = actions.Select(async action =>
{
await semaphore.WaitAsync(cancel);
try
{
await sender.Send(action.ToInvokeCommand(), cancel);
}
catch(Exception ex)
{
logger?.LogError(
ex,
"Error invoking Rec action. ProfileId: {ProfileId}, ActionId: {ActionId}",
action.ProfileId,
action.ActionId
);
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);
}
}

View File

@ -1,57 +1,47 @@
using MediatR;
using Microsoft.Extensions.Logging;
using ReC.Application.RecActions.Queries;
using ReC.Application.Common;
using ReC.Application.Common.Dto;
namespace ReC.Application.RecActions.Commands;
public class InvokeRecActionCommand : ReadRecActionQuery, IRequest
public record InvokeRecActionCommand : RecActionDto, IRequest
{
public InvokeRecActionCommand(RecActionDto root) : base(root) { }
public InvokeRecActionCommand() { }
}
public static class InvokeRecActionCommandExtensions
{
public static Task InvokeRecAction(this ISender sender, int profileId)
=> sender.Send(new InvokeRecActionCommand { ProfileId = profileId });
public static InvokeRecActionCommand ToInvokeCommand(this RecActionDto dto) => new(dto);
}
public class InvokeRecActionCommandHandler(ISender sender, IHttpClientFactory clientFactory, ILogger<InvokeRecActionCommandHandler>? logger = null) : IRequestHandler<InvokeRecActionCommand>
public class InvokeRecActionCommandHandler(
IHttpClientFactory clientFactory,
ILogger<InvokeRecActionsCommandHandler>? logger = null
) : IRequestHandler<InvokeRecActionCommand>
{
public async Task Handle(InvokeRecActionCommand request, CancellationToken cancel)
{
var actions = await sender.Send(request as ReadRecActionQuery, cancel);
using var http = clientFactory.CreateClient();
var http = clientFactory.CreateClient();
using var semaphore = new SemaphoreSlim(5);
var tasks = actions.Select(async action =>
if (request.RestType is null)
{
await semaphore.WaitAsync(cancel);
try
{
if (action.RestType is null)
{
logger?.LogWarning(
"Rec action could not be invoked because the RestType value is null. ProfileId: {ProfileId}, ActionId: {ActionId}",
action.ProfileId,
action.ActionId
);
return;
}
logger?.LogWarning(
"Rec action could not be invoked because the RestType value is null. ProfileId: {ProfileId}, ActionId: {ActionId}",
request.ProfileId,
request.ActionId
);
return;
}
var method = new HttpMethod(action.RestType.ToUpper());
var msg = new HttpRequestMessage(method, action.EndpointUri);
using var httpReq = request.RestType
.ToHttpMethod()
.ToHttpRequestMessage(request.EndpointUri);
var response = await http.SendAsync(msg, cancel);
var body = await response.Content.ReadAsStringAsync(cancel);
var headers = response.Headers.ToDictionary();
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);
using var response = await http.SendAsync(httpReq, cancel);
var body = await response.Content.ReadAsStringAsync(cancel);
var headers = response.Headers.ToDictionary();
}
}

View File

@ -1,18 +1,22 @@
using MediatR;
using ReC.Application.Dto;
using DigitalData.Core.Abstraction.Application.Repository;
using ReC.Domain.Entities;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using DigitalData.Core.Exceptions;
using ReC.Application.Common.Dto;
namespace ReC.Application.RecActions.Queries;
public class ReadRecActionQuery : IRequest<IEnumerable<RecActionDto>>
public record ReadRecActionQueryBase
{
public int ProfileId { get; init; }
public ReadRecActionQuery ToReadQuery() => new(this);
}
public record ReadRecActionQuery(ReadRecActionQueryBase Root) : ReadRecActionQueryBase(Root), IRequest<IEnumerable<RecActionDto>>;
public class ReadRecActionQueryHandler(IRepository<RecAction> repo, IMapper mapper) : IRequestHandler<ReadRecActionQuery, IEnumerable<RecActionDto>>
{
public async Task<IEnumerable<RecActionDto>> Handle(ReadRecActionQuery request, CancellationToken cancel)

View File

@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace ReC.Domain.Entities;
public class BodyQueryResult
{
[Column("REQUEST_BODY")]
public string? RawBody { get; init; }
}

View File

@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace ReC.Domain.Entities;
public class HeaderQueryResult
{
[Column("REQUEST_HEADER")]
public string? RawHeader { get; init; }
}

View File

@ -1,6 +1,7 @@
using DigitalData.Core.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ReC.Application.Common.Interfaces;
using ReC.Domain.Entities;
namespace ReC.Infrastructure;
@ -16,7 +17,9 @@ public static class DependencyInjection
if(configOpt.DbContextOptionsAction is null)
throw new InvalidOperationException("DbContextOptionsAction must be configured.");
services.AddDbContext<RecDbContext>(configOpt.DbContextOptionsAction);
services.AddDbContext<TRecDbContext>(configOpt.DbContextOptionsAction);
services.AddScoped<IRecDbContext>(provider => provider.GetRequiredService<TRecDbContext>());
services.AddDbRepository(opt => opt.RegisterFromAssembly<TRecDbContext>(typeof(RecAction).Assembly));

View File

@ -13,6 +13,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ReC.Application\ReC.Application.csproj" />
<ProjectReference Include="..\ReC.Domain\ReC.Domain.csproj" />
</ItemGroup>

View File

@ -1,9 +1,10 @@
using Microsoft.EntityFrameworkCore;
using ReC.Application.Common.Interfaces;
using ReC.Domain.Entities;
namespace ReC.Infrastructure;
public class RecDbContext(DbContextOptions<RecDbContext> options) : DbContext(options)
public class RecDbContext(DbContextOptions<RecDbContext> options) : DbContext(options), IRecDbContext
{
public DbSet<EndpointParam> EndpointParams { get; set; }
@ -11,10 +12,18 @@ public class RecDbContext(DbContextOptions<RecDbContext> options) : DbContext(op
public DbSet<OutRes> OutRes { get; set; }
public DbSet<HeaderQueryResult> HeaderQueryResults { get; set; }
public DbSet<BodyQueryResult> BodyQueryResults { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<RecAction>().HasNoKey();
modelBuilder.Entity<HeaderQueryResult>().HasNoKey();
modelBuilder.Entity<BodyQueryResult>().HasNoKey();
}
}