260 Commits

Author SHA1 Message Date
761fd208e5 Bump version to 2.2.1-beta
Updated Version, AssemblyVersion, and FileVersion in ReC.API.csproj from 2.2.0-beta/2.2.0.0 to 2.2.1-beta/2.2.1.0. No other changes made.
2026-04-17 00:34:40 +02:00
d149cbea3a Improve error handling in query behaviors for SQL execution
Wrap ExecuteScalarAsync in try-catch blocks in BodyQueryBehavior and HeaderQueryBehavior. Throw DataIntegrityException with detailed context if SQL execution fails, aiding in diagnosing malformed or problematic stored SQL queries.
2026-04-17 00:34:01 +02:00
bb2dd4d63b Stricter error handling in BodyQuery and HeaderQuery behaviors
Throw DataIntegrityException when body or header queries return
null, no result, or invalid JSON. Remove ILogger and related
logging from HeaderQueryBehavior for simplification. This change
ensures data integrity issues are surfaced immediately.
2026-04-17 00:23:36 +02:00
4bde1d090f Refactor query behaviors to process action collections
Refactored BodyQueryBehavior and HeaderQueryBehavior to operate on collections of RecActionViewDto instead of single instances. Moved SQL execution and property assignment logic into private helper methods using ADO.NET commands. Improved null checks and logging, and updated type constraints to reflect the new usage. Behaviors now return the modified collection after processing.
2026-04-17 00:15:52 +02:00
6681e56afc Refactor entity selection to use EntityType enum
Replaced string-based entity identifiers in CRUD procedure and command classes with a strongly-typed EntityType enum. Updated all relevant handlers and records to use the new enum property, improving type safety and maintainability. Added necessary using directives and updated documentation comments to reflect these changes.
2026-04-16 17:14:29 +02:00
d61f5ce885 Update validators to use IsInEnum for Entity property
Refactored DeleteObjectProcedureValidator, InsertObjectProcedureValidator, and UpdateObjectProcedureValidator to use .IsInEnum() for validating the Entity property. This replaces custom or hardcoded checks with enum-based validation, improving consistency, maintainability, and robustness across all validators.
2026-04-16 17:13:00 +02:00
6374a5c257 Add EntityType enum and support in StoredProcedureBuilder
Introduced the EntityType enum to represent target entities for stored procedure operations, along with XML documentation and mapping comments. Added EntityTypeExtensions with a ToDbString method for consistent DB string conversion. Updated StoredProcedureBuilder to support adding EntityType parameters, improving type safety and maintainability. Minor formatting and using directive adjustments included.
2026-04-16 17:12:19 +02:00
3b4954d387 Remove FakeRequest class and ResultType enum
Deleted FakeRequest.cs and ResultType.cs, removing the FakeRequest class (with Body and Header properties) and the ResultType enum (Full, OnlyHeader, OnlyBody). These types are no longer needed in the codebase.
2026-04-16 14:55:10 +02:00
42d586604e Add UpdateObjectProcedureValidator with validation rules
Introduced UpdateObjectProcedureValidator using FluentValidation to enforce constraints on UpdateObjectProcedure fields, including ID checks and maximum length restrictions on various properties and nested objects.
2026-04-16 14:50:00 +02:00
4088a52196 Add FluentValidation for ReadResultViewQuery filters
Introduced ReadResultViewQueryValidator to enforce that at least one filter (Id, ActionId, ProfileId, or BatchId) is provided. Also validates that numeric IDs are greater than 0 and BatchId is not empty when present.
2026-04-16 14:48:09 +02:00
58b3c8ec95 Add validator for ReadRecActionViewQuery ProfileId
Introduced ReadRecActionViewQueryValidator using FluentValidation to ensure ProfileId, if provided, is greater than 0. Returns a specific validation message if the rule is violated.
2026-04-16 14:46:07 +02:00
68b3eb53c0 Add ReadProfileViewQueryValidator for Id validation
Introduced ReadProfileViewQueryValidator using FluentValidation to ensure the Id property, if provided, is greater than 0. Includes a custom error message for invalid Id values.
2026-04-16 14:39:26 +02:00
0d9489203f Add InsertResultCommandValidator with validation rules
Introduce InsertResultCommandValidator using FluentValidation to enforce required and value constraints on ActionId and References.BatchId properties, including custom error messages.
2026-04-16 14:38:45 +02:00
0a564d8aa8 Add DeleteObjectProcedureValidator with validation rules
Introduced DeleteObjectProcedureValidator using FluentValidation to ensure Start is greater than 0 and End is greater than or equal to Start, with custom error messages for each rule.
2026-04-16 14:36:43 +02:00
f5b2db0296 Update tests to expect ValidationException for BatchId dupes
Refactored InvokeBatchDuplicateGuardTests to expect and assert
FluentValidation's ValidationException instead of the custom
BadRequestException when a duplicate BatchId is submitted.
Assertions and comments were updated accordingly, and the
DigitalData.Core.Exceptions import was removed. Test logic
remains unchanged.
2026-04-16 14:15:21 +02:00
7a22024624 Remove batch result check from rec action invocation
Removed the check that blocked rec action invocation if results
already existed for a batch. Also updated using directives for
exception handling and logging.
2026-04-16 14:14:19 +02:00
c9cd92a55a Add validator for InvokeBatchRecActionViewsCommand
Introduced InvokeBatchRecActionViewsCommandValidator using FluentValidation. This validator ensures BatchId is provided and checks asynchronously via MediatR that no results exist for the given BatchId before allowing the command to proceed. Provides a clear validation message if results are already present.
2026-04-16 14:13:06 +02:00
93adaba322 Update error message to omit result count in exception
Removed the count of existing results from the BadRequestException
message when invoking rec actions for a batch. The error now simply
states that results are already associated with the batch.
2026-04-16 11:28:46 +02:00
c16cb2a1c4 Bump version to 2.2.0-beta in ReC.API.csproj
Updated project version fields in ReC.API.csproj from 2.1.0-beta to 2.2.0-beta, including Version, AssemblyVersion, FileVersion, and InformationalVersion.
2026-04-16 10:55:59 +02:00
c20162e051 Update build package output path to M: drive
Changed DesktopBuildPackageLocation in IISProfile.pubxml to output the build package to the M:\App&Service directory instead of the P: drive. This ensures published packages are stored in the new target location.
2026-04-16 10:53:30 +02:00
70c2f7342d Refactor result view query to support LastBatch retrieval
Replaced Last with LastBatch in ReadResultViewQuery to enable fetching all results from the most recent batch. Updated handler logic and tests accordingly, and added GetLastBatchEntitiesAsync to retrieve entities by latest BatchId.
2026-04-16 10:49:15 +02:00
a10f917084 Add tests for batch duplicate guard in rec action invoke
Added InvokeBatchDuplicateGuardTests to verify that invoking batch rec actions with an existing BatchId throws BadRequestException, while using a new BatchId does not trigger the duplicate guard. Tests use MediatR ISender and real database data for integration coverage. Added necessary using directives.
2026-04-16 10:40:34 +02:00
e1c3f74cd4 Prevent rec action invocation if batch has results
Add validation in InvokeRecActionViewsCommandHandler to check
for existing results before invoking rec actions for a batch.
Throw BadRequestException if results are found to avoid
duplicate processing. Add necessary using statements for
exceptions and queries.
2026-04-16 10:22:31 +02:00
e45aeea2b9 Add BatchId filter to ReadResultViewQuery and handler
Added an optional BatchId property to ReadResultViewQuery to enable filtering by BatchId. Updated ReadResultViewQueryHandler to apply this filter when BatchId is provided. Also adjusted the order of IncludeAction logic for clarity.
2026-04-16 10:20:57 +02:00
38f91aae84 Add AnyResultViewQuery to check for matching ResultView
Introduced AnyResultViewQuery and its handler to determine if any ResultView entity exists matching optional filters (Id, ActionId, ProfileId, BatchId). The handler builds the query dynamically and uses AnyAsync for efficient existence checks.
2026-04-16 10:20:18 +02:00
9bb5c97df6 Require non-null References for batch rec action invoke
Enforce non-nullable References in RecActionController and InvokeBatchRecActionViewsCommand. Update tests to always provide References and add missing using directive. Improves type safety and ensures consistent reference handling.
2026-04-16 10:02:34 +02:00
d8c7499436 Add BatchId and AddedWho to DTO; make BatchId nullable
Added BatchId and AddedWho properties to ResultViewDto. Changed BatchId in ResultView from required to nullable to align with DTO and support optional batch IDs.
2026-04-16 09:58:46 +02:00
6d86760354 Make BatchId in ResultView a required string
Changed the BatchId property in the ResultView class from a nullable string to a required non-nullable string. This ensures that BatchId must always be provided when creating a ResultView instance, improving data integrity.
2026-04-16 09:55:57 +02:00
6b1daf77cb Make References required in InsertResultCommand
Changed the References property in InsertResultCommand from a nullable type to required. References must now always be provided and cannot be null when creating an InsertResultCommand instance. This enforces stricter data integrity for command creation.
2026-04-16 09:55:26 +02:00
d3d5ebac61 Rename InvokeReferencesDto to InvokeReferences project-wide
Replaced all usages of InvokeReferencesDto with InvokeReferences across controllers, commands, and DTOs. This change standardizes the reference type naming by removing the "Dto" suffix, with no changes to the structure or behavior.
2026-04-16 09:51:15 +02:00
b1924f2a4a Make References and BatchId required for RecAction invoke
Updated InvokeRecActionViewCommand and InvokeReferencesDto to require non-null References and BatchId. Updated RecActionApi.InvokeAsync to require InvokeReferences and added an overload accepting batchId for convenience. This enforces stricter input validation and aligns client and backend requirements.
2026-04-16 09:50:52 +02:00
c27ed1e744 Change RecStatus enum type from short to byte
Reduced the underlying type of the RecStatus enum from short to byte to decrease memory usage. No changes were made to the enum values or their definitions.
2026-04-15 17:02:28 +02:00
9eb54565cb Make Info non-nullable in InsertResultCommand
Changed the Info property from short? to short in InsertResultCommand,
making it a required field and ensuring it cannot be null. This enforces
that all InsertResultCommand instances must provide a value for Info.
2026-04-15 16:57:45 +02:00
05dfb6f424 Update pRESULT_STATUS_ID to TinyInt in procedure handlers
Changed SQL parameter type for pRESULT_STATUS_ID from SmallInt to TinyInt in both InsertObjectProcedureHandler and UpdateObjectProcedureHandler to align with database schema and ensure type consistency.
2026-04-15 16:40:16 +02:00
cf6c90ad05 Auto-set SqlDbType.DateTime for DateTime parameters
Automatically assigns SqlDbType.DateTime to parameters when the value is a DateTime and no dbType is specified. This ensures correct SQL type mapping for DateTime values in stored procedures.
2026-04-15 15:31:52 +02:00
4a9c4341c2 Add BatchId to InvokeReferences; clean up property layout
Added BatchId property to InvokeReferences for batch identification.
Refactored Reference3-5 property declarations for clarity and
consistency by removing unnecessary line breaks.
2026-04-15 14:36:36 +02:00
ead12b6095 Add pRESULT_BATCH_ID parameter to update procedure
Added support for the pRESULT_BATCH_ID parameter in the database command, sourcing its value from request.Result.References?.BatchId. This enables batch ID information to be included in update operations when available.
2026-04-15 14:35:56 +02:00
3c7fcb71c0 Add pRESULT_BATCH_ID parameter to insert procedure
Added support for the pRESULT_BATCH_ID parameter in the database command, allowing the batch ID from request.Result?.References?.BatchId to be included when inserting objects. This enables tracking and referencing of batch operations.
2026-04-15 14:35:37 +02:00
0b01b4a658 Add BatchId property to InvokeReferencesDto
Added an optional BatchId property to the InvokeReferencesDto record, enabling support for batch identification alongside existing reference fields.
2026-04-15 14:34:44 +02:00
8d511ec81a Add BatchId property to ResultView class
Added the BatchId property to the ResultView class, mapping it to the "BATCH_ID" column in the database. This property is of type string? and allows tracking of batch identifiers in result records.
2026-04-15 14:33:54 +02:00
685c5abca7 Remove HTTP status formatting/parsing from ResultView
Removed FormatHttpStatusInfo, ParseHttpStatusInfo, and the related Regex from the ResultView class. These methods are no longer needed and have been deleted to simplify the codebase.
2026-04-15 14:33:06 +02:00
b7aea848a9 Add InvokeReferences support to InvokeAsync method
Added InvokeReferences class for passing optional reference values (Reference1–Reference5) when invoking a profile. Updated InvokeAsync to accept and serialize these references in the POST request. Improved XML docs to reflect the new parameter.
2026-04-15 13:57:42 +02:00
e5eb0f19e7 Add optional references to RecAction invoke endpoint
The Invoke action in RecActionController now accepts an optional InvokeReferencesDto from the request body. This enables clients to provide reference values that are passed through to all result records. The method signature, XML documentation, and command dispatch have been updated to support this enhancement.
2026-04-15 13:37:40 +02:00
859ff5902e Include References in InsertResultCommand for all results
Add request.References to InsertResultCommand in both
PreprocessingBehavior and PostprocessingBehavior, ensuring
references are recorded with both success and error results.
2026-04-15 13:33:39 +02:00
42789567f0 Refactor UpdateResultDto to use InvokeReferencesDto
Replaced five separate reference string properties in UpdateResultDto with a single References property of type InvokeReferencesDto for improved structure and maintainability. Added the necessary using directive for the new type.
2026-04-15 13:32:17 +02:00
46eef255ca Add References support to batch rec action commands
Added an optional References property to InvokeBatchRecActionViewsCommand. Updated the handler to pass this property to each individual InvokeRecActionViewCommand. Also included the ReC.Domain.Constants namespace for required types.
2026-04-15 13:31:54 +02:00
aae42949b6 Refactor InsertResultCommand references into DTO
Replaced Reference1–5 string properties in InsertResultCommand with a single References property of type InvokeReferencesDto? for better maintainability. Added necessary using directive for the new DTO.
2026-04-15 13:14:17 +02:00
bdf273c8e1 Add support for references in InvokeRecAction commands
Introduced the InvokeReferencesDto record to encapsulate up to five optional reference strings. Updated InvokeRecActionViewCommand to include an optional References property. Modified InvokeRecActionViewCommandHandler to pass these references to InsertResultCommand, ensuring reference data is associated with command execution and results.
2026-04-15 12:57:13 +02:00
ba8ab28b03 Refactor: group Result reference fields into References obj
Refactored InsertObjectProcedureHandler and UpdateObjectProcedureHandler to access RESULT_REFERENCE1-5 via the new Result.References object instead of individual fields. This improves encapsulation and organizes related reference data within the Result model.
2026-04-15 12:54:51 +02:00
4cc8d22756 Update InsertResultCommand Info property to use integer
Changed the Info property in InsertResultCommand initialization from a string ("200") to an integer (200) to match the expected data type. This ensures type consistency in the test setup.
2026-04-15 11:48:38 +02:00
2a4378eb9a Set Info to numeric HTTP status code in result object
Changed the Info property to store the raw (short) HTTP status code
from response.StatusCode instead of using the formatted output from
ResultView.FormatHttpStatusInfo. This provides a simpler, numeric
representation of the HTTP status in the result.
2026-04-15 11:48:20 +02:00
cb5bbfb722 Change Info to short? and update DB param to pRESULT_INFO_ID
Changed UpdateResultDto.Info from string? to short? for type safety. Updated UpdateObjectProcedureHandler to use pRESULT_INFO_ID with SqlDbType.SmallInt, reflecting the new type.
2026-04-15 11:46:40 +02:00
2736a78d4f Change Info from string to short and update parameter name
Changed the Info property in InsertResultCommand from string? to short?.
Renamed the related parameter in InsertObjectProcedureHandler from pRESULT_INFO to pRESULT_INFO_ID and set its type to SqlDbType.SmallInt to match the new property type.
2026-04-15 11:45:46 +02:00
ddb8b2673e Add support for five new RESULT_REFERENCE parameters
Added pRESULT_REFERENCE1 through pRESULT_REFERENCE5 to the stored procedure call, mapping them to the corresponding Reference1–Reference5 properties in request.Result. This allows passing additional reference data with the procedure execution.
2026-04-14 20:43:43 +02:00
a70aee6e28 Add Reference1-5 properties to UpdateResultDto
Added five optional string properties (Reference1 through Reference5) to UpdateResultDto for storing additional reference information related to update results. This enhances the DTO's flexibility for carrying extra context as needed.
2026-04-14 20:43:01 +02:00
f329543793 Add support for five new RESULT_REFERENCE parameters
Added pRESULT_REFERENCE1 through pRESULT_REFERENCE5 to the procedure handler, mapping them from the corresponding Reference1-5 properties in request.Result. This enables passing additional reference data to the stored procedure.
2026-04-14 20:42:41 +02:00
645891150c Add Reference1-5 properties to InsertResultCommand
Added five optional string properties (Reference1-5) to the InsertResultCommand record to support storing additional reference information with each command instance. This enhances flexibility for passing extra data as needed.
2026-04-14 20:41:53 +02:00
95cb34394c Add ProfileTypeName property to RecActionView
Added the ProfileTypeName string property to RecActionView, mapped to the "PROFILE_TYPE" column, to store the profile type name alongside the existing ProfileType property.
2026-04-14 20:40:16 +02:00
83d6832236 Add InfoId and Reference fields to ResultView model
Added InfoId and Reference1–Reference5 properties to the ResultView class, each mapped to corresponding database columns. These fields enable storage and retrieval of additional result-related information and references.
2026-04-14 20:40:00 +02:00
e816340755 Improve HTTP status info formatting in result views
Replaced the simple status code string with ResultView.FormatHttpStatusInfo for the Info field, providing more descriptive HTTP status information. Added the ReC.Domain.Views namespace import to support this change.
2026-04-02 21:02:32 +02:00
64e8e2a5cc Add HTTP status formatting/parsing to ResultView
Added FormatHttpStatusInfo and ParseHttpStatusInfo methods to ResultView for converting between HttpStatusCode values and "statusCode statusName" strings. Included supporting Regex and necessary using directives.
2026-04-02 21:02:15 +02:00
0edf2626a7 Add Info property to InsertResultCommand in test
The InsertResultProcedure_runs_via_mediator test now sets the Info property to "200" when initializing InsertResultCommand, ensuring the command includes this field during testing.
2026-04-02 20:39:05 +02:00
1d16276a8a Add InfoDetail property to InsertResultCommand
Added a nullable string property InfoDetail to InsertResultCommand to allow storing additional detailed information with each result. No other changes were made.
2026-04-02 20:38:55 +02:00
4eae092031 Update result handling and error status in command handler
Removed unused statusCode variable and now store HTTP status code as a string in the Info field. Changed exception handling to set Status to RecStatus.Error instead of RecStatus.Failed.
2026-04-02 20:38:01 +02:00
ce7fe03525 Add InfoDetail property to ResultView model
Added the InfoDetail property to the ResultView class, mapping it to the RESULT_INFO_DETAIL column in the database. This allows for storing and retrieving additional result detail information.
2026-04-02 20:37:46 +02:00
a93780df5c Add Info, InfoDetail, and Error to UpdateResultDto
Extended UpdateResultDto with Info, InfoDetail, and Error fields. Updated UpdateObjectProcedureHandler to pass these new properties as parameters to the stored procedure, enabling richer result and error reporting in database updates.
2026-04-02 20:37:29 +02:00
d7a2a01421 Add pRESULT_INFO_DETAIL param to InsertObjectProcedureHandler
Added support for the pRESULT_INFO_DETAIL parameter in the database command, mapping it from request.Result?.InfoDetail to enable passing detailed result information to the stored procedure.
2026-04-02 20:36:51 +02:00
329e441ede Standardize InsertResultCommand status and info fields
Updated InsertResultCommand usage to replace QuerySuccess with OK and Failed with Error for status reporting. Changed Info to InfoDetail for both preprocessing and postprocessing behaviors to ensure consistent result and error handling.
2026-04-02 20:36:35 +02:00
1ad7ff3b34 Add InfoDetail property to ResultViewDto
Added a nullable string property InfoDetail with a public setter to the ResultViewDto record for storing additional detailed information. No changes were made to the existing Error property.
2026-04-02 20:36:03 +02:00
bcfbd851bd Simplify RecStatus to OK/Error and update extensions
Refactored the RecStatus enum to only include OK and Error values,
removing all HTTP status code mappings and related documentation.
Updated RecStatusExtensions to map HTTP 2xx codes to OK and all
others to Error, and removed obsolete conversion and success logic.
This clarifies the separation between general operation status and
protocol-specific details.
2026-04-02 20:35:48 +02:00
2e157656a7 Update validator registration assembly in DI setup
Changed validator registration to use the assembly containing InsertObjectProcedureValidator instead of AuthScopedValidator, ensuring the correct set of validators are included in the dependency injection container.
2026-03-30 15:36:18 +02:00
8042a6f898 Update project version to 2.1.0-beta
Bump version numbers in ReC.API.csproj from 2.0.2-beta to 2.1.0-beta, including Version, AssemblyVersion, FileVersion, and InformationalVersion fields. No other changes were made.
2026-03-30 14:53:01 +02:00
f25fc627fe Update AutoMapper to version 16.1.1
Upgraded the AutoMapper NuGet package in ReC.Application.csproj from version 15.1.0 to 16.1.1 to ensure compatibility with the latest features and bug fixes. No other dependencies were changed.
2026-03-30 14:48:17 +02:00
d6af24cd91 Remove PlaceholderResolutionException class
Deleted the PlaceholderResolutionException class and its namespace. This exception was previously used for unresolved placeholders due to missing properties. All related properties and custom messages have been removed.
2026-03-30 14:33:23 +02:00
bb5eac023c Change ReplacePlaceholders to return NULL for unresolved
Previously, ReplacePlaceholders threw PlaceholderResolutionException when a placeholder could not be resolved. Now, unresolved placeholders are replaced with "NULL" instead. All exception references and related tests have been updated to reflect this new behavior. Documentation has also been revised accordingly.
2026-03-30 14:33:13 +02:00
77baf395ce Update ReplacePlaceholders mapping to use single argument
Removed src.Profile from ReplacePlaceholders calls in DtoMappingProfile for PreprocessingQuery and PostprocessingQuery mappings, now passing only src as the argument. This simplifies the mapping logic and aligns with the updated method signature.
2026-03-30 14:32:31 +02:00
6c9eab6df6 Refactor ResultViewDto status and add info/error fields
Replaced StatusCode with a RecStatus Status property in ResultViewDto. Added Info and Error string properties to provide additional result details. Imported ReC.Domain.Constants for RecStatus usage.
2026-03-30 13:21:48 +02:00
c64794755d Remove "EXEC" from stored procedure call in builder
Standardize StoredProcedureBuilder usage by omitting the "EXEC" keyword when specifying the procedure name. This ensures consistent and correct invocation of stored procedures.
2026-03-30 13:21:11 +02:00
de2185bf0a Specify byte as underlying type for ResultType enum
Changed ResultType enum declaration to explicitly use byte as its underlying type for improved memory efficiency and clarity.
2026-03-30 13:20:40 +02:00
fde9735b27 Remove redundant Result property validation in InsertObject
Removed the rule requiring Result to have StatusId, Info, or Error set. Now only ResultActionId is required when Result is present.
2026-03-30 13:20:21 +02:00
0342b9e0c6 Unify error status code as RecStatus.Failed
Renamed RecStatus.QueryFailed to RecStatus.Failed and updated all usages and documentation to reflect its broader purpose as a general failure code for any operation, not just SQL queries. Improved consistency in error handling and status reporting across the codebase.
2026-03-30 11:55:58 +02:00
47698b9046 Rename ToStatus to ToRecStatus for HttpStatusCode conversion
Standardize extension method naming by renaming ToStatus to ToRecStatus for converting HttpStatusCode to RecStatus across the codebase. Updated all usages and related tests for consistency and clarity.
2026-03-30 11:40:26 +02:00
a03d21ebc6 Rename StatusExtensions to RecStatusExtensions for clarity
Renamed the StatusExtensions class and related XML documentation references to RecStatusExtensions to better reflect its association with the RecStatus enum and improve code clarity and consistency.
2026-03-30 11:39:37 +02:00
acff0aca89 Rename Status enum to RecStatus across the codebase
Refactored all usages of the Status enum to RecStatus to improve clarity and prevent naming conflicts with other status enums (e.g., HTTP status codes). Updated command handlers, behaviors, data models, and extension methods to use RecStatus, and adjusted related serialization logic accordingly. This makes the domain-specific status handling more explicit and maintainable.
2026-03-30 11:35:22 +02:00
ce0e53baf6 Update test for InsertResultCommand Status property change
Refactored InsertResultProcedure_runs_via_mediator to use the new Status property (set via HttpStatusCode.OK.ToStatus()) instead of the old StatusId integer. Added necessary using directives to support updated types and methods.
2026-03-30 11:33:42 +02:00
620c0eff22 Add ToStatus extension for HttpStatusCode to Status conversion
Introduced a ToStatus extension method in StatusExtensions to enable direct conversion from HttpStatusCode to Status by casting. This simplifies mapping between HTTP status codes and internal Status values.
2026-03-30 11:33:28 +02:00
68f4486fa1 Make Status required in InsertResultCommand
Changed the Status property in InsertResultCommand from nullable to required, ensuring that a Status value must always be provided when creating an instance. This improves data integrity and prevents accidental omission of the Status field.
2026-03-30 11:33:08 +02:00
2b5e63cb45 Update result status handling in InsertResultCommand
Replaced StatusId with Status property in InsertResultCommand, mapping HTTP status codes to application-specific status enums using ToStatus(). Exception handling now sets Status to QueryFailed. Removed usage of StatusId.
2026-03-30 11:22:57 +02:00
e9e697fa0d Update to use Result.Status instead of StatusId in insert proc
Changed the parameter for "pRESULT_STATUS_ID" from request.Result?.StatusId to request.Result?.Status in InsertObjectProcedureHandler. This aligns with updates to the data model or business logic, ensuring the correct status property is used when inserting objects.
2026-03-30 11:22:27 +02:00
606eccb855 Add Status to InsertResultCommand for query outcome reporting
Enhance PostprocessingBehavior and PreprocessingBehavior to set the Status property on InsertResultCommand. Status is now set to QuerySuccess on successful execution and QueryFailed on exceptions, improving clarity of query execution results.
2026-03-30 11:11:51 +02:00
3146acfa45 Refactor InsertResultCommand to use Status enum
Replaced StatusId (short?) with Status (Status?) in InsertResultCommand, moving from a numeric status identifier to a more descriptive or structured status representation from ReC.Domain.Constants.
2026-03-30 11:11:28 +02:00
f363872e7a Refactor ResultView: replace StatusCode with Status object
Replaced the short? StatusCode property in ResultView with a Status object to provide a more descriptive representation of status information. This change improves code clarity and supports richer status handling.
2026-03-30 10:51:58 +02:00
ed4683323d Change Status enum underlying type to short
Explicitly set the Status enum's underlying type to short instead of the default int to optimize memory usage and clarify intent. No other changes were made.
2026-03-30 10:50:33 +02:00
4aeef10ef7 Add StatusExtensions with HTTP status mapping methods
Added StatusExtensions.cs with extension methods for Status and HttpStatusCode:
- ToHttpStatusCode maps Status to nullable HttpStatusCode if possible.
- IsSuccess checks if a Status or HttpStatusCode represents a successful response.
- Handles both direct Status values and those convertible to HTTP codes.
2026-03-30 10:41:39 +02:00
e04e90d8c6 Add Status enum for HTTP and SQL operation status codes
Introduced Status enum in ReC.Domain.Constants to unify status reporting for both HTTP responses and SQL query execution results. The enum includes all standard HTTP status codes and custom codes for SQL query success/failure, with detailed documentation for each value. This standardizes status handling across web and database operations.
2026-03-30 10:41:09 +02:00
93b5f976d3 Refactor stored procedure SQL construction and execution
Centralize stored procedure SQL generation in StoredProcedureBuilder,
allowing handlers to specify procedure name and return variable.
Removes manual SQL string building from DeleteObjectProcedure and
UpdateObjectProcedure handlers, reducing boilerplate and improving
maintainability.
2026-03-30 09:30:07 +02:00
b66a49f74d Refactor: add StoredProcedureBuilder for SQL calls
Introduce StoredProcedureBuilder to centralize and simplify the construction of SQL stored procedure calls and parameter lists. Refactor DeleteObjectProcedureHandler, InsertObjectProcedureHandler, and UpdateObjectProcedureHandler to use this utility, replacing manual StringBuilder and parameter management. Improves code readability, reduces duplication, and standardizes parameter handling, including output parameters.
2026-03-27 15:11:16 +01:00
70dc52139d Refactor SQL param and command construction in procedures
Refactored InsertObject, UpdateObject, and DeleteObject procedure handlers to dynamically build SQL command strings and parameter lists. Introduced local Add functions to include only non-null parameters, improving code clarity and reducing unnecessary SQL parameter passing. The logic for handling stored procedure results and exceptions remains unchanged.
2026-03-27 14:58:56 +01:00
210ed9be8d Refactor SQL construction with StringBuilder for clarity
Refactored DeleteObjectProcedureHandler, InsertObjectProcedureHandler, and UpdateObjectProcedureHandler to use StringBuilder for building SQL command strings. This improves readability and maintainability without changing the logic or parameters. Added System.Text using directives as needed.
2026-03-27 14:54:27 +01:00
b2544b64e3 Refactor: use named params in SP calls, cleanup usings
Updated Delete, Insert, and Update object procedure handlers to use named parameters in SQL stored procedure calls instead of positional parameters, improving clarity and reducing risk of misalignment. Also removed unused using statements from UpdateObjectProcedure.cs.
2026-03-27 14:21:45 +01:00
0b1e0d25ca Explicitly set SqlDbType.TinyInt for relevant SQL parameters
Updated InsertObjectProcedureHandler and UpdateObjectProcedureHandler to explicitly specify SqlDbType.TinyInt for parameters representing TINYINT columns. Also improved handling of nullable and enum values for these parameters to ensure correct type casting and null handling. This enhances type safety and prevents potential SQL type conversion issues.
2026-03-27 14:19:19 +01:00
c1027abfc6 Specify SqlDbType.SmallInt for relevant SqlParameters
Explicitly set SqlDbType.SmallInt for parameters related to small integer fields in UpdateObjectProcedureHandler. This improves type safety and prevents potential data conversion issues by ensuring correct parameter types are sent to SQL Server.
2026-03-27 13:30:28 +01:00
40c8fa359c Specify SqlDbType.SmallInt for relevant SqlParameters
Explicitly set SqlDbType.SmallInt for parameters related to endpoint params, SQL connection, profile language, and endpoint params group. This ensures correct type handling and prevents potential data conversion issues with the database.
2026-03-27 13:29:43 +01:00
1375f5f0e4 Set @pRESULT_STATUS_ID param type to SmallInt explicitly
Explicitly specify SqlDbType.SmallInt for the @pRESULT_STATUS_ID parameter when calling the stored procedure. This change ensures correct type handling and helps prevent potential SQL type mismatches or conversion errors.
2026-03-27 13:13:36 +01:00
29bc0cf8b5 Relax RESULT validation to allow StatusId, Info, or Error
Previously, RESULT required a non-null StatusId. Now, validation passes if at least one of StatusId, Info, or Error is provided, making the requirements more flexible.
2026-03-27 09:46:43 +01:00
c8b264cef6 Improve null safety in InsertObjectProcedureValidator
Updated validation logic to use null-forgiving operators and added null checks for nested properties in When clauses. This ensures rules are only applied when parent objects are not null, preventing possible null reference errors and improving overall robustness.
2026-03-27 09:40:33 +01:00
078525d85d Refactor RecAction invoke endpoint to use profileId param
Changed the HTTP POST route to accept a profileId instead of a command object, updated XML documentation accordingly, and refactored the method to construct the command internally using the provided profileId before sending it to the mediator. This improves clarity and API usability.
2026-03-27 09:26:13 +01:00
b3dfdd1e5c Update namespaces to Common.Dto for DTO-related files
Refactored PlaceholderExtensions, DtoMappingProfile, and InvokeActionTests
to use the ReC.Application.Common.Dto namespace instead of
ReC.Application.Common.Behaviors.Action. Updated using directives and
namespaces to improve code organization for DTO-related logic.
2026-03-26 16:52:54 +01:00
bd78ada686 Remove all DTO class definitions from application
Deleted the contents of several DTO files, including ConnectionDto, EndpointAuthDto, EndpointDto, EndpointParamDto, OutResDto, ProfileDto, and RecActionDto. These files previously contained record definitions for data transfer objects used throughout the application. All related code, including properties and using directives, has been removed, leaving the files empty.
2026-03-26 15:48:49 +01:00
2b4773a4c0 Remove Root and ActionId from ResultViewDto
Removed the Root (OutResDto?) and ActionId (long?) properties from the ResultViewDto record to simplify its structure and remove unused fields.
2026-03-26 15:48:35 +01:00
ff7d6c99ae Update RecActionViewDto mapping and add Profile property
Refactored DtoMappingProfile to pass both the source object and its Profile to ReplacePlaceholders when mapping queries. Added a nullable Profile property to RecActionViewDto to include full profile details in the DTO.
2026-03-26 15:48:06 +01:00
fa438e70cb Allow ReplacePlaceholders to handle null objects safely
Updated ReplacePlaceholders to accept nullable objects and skip nulls during placeholder resolution, preventing NullReferenceExceptions when nulls are passed in the objects array.
2026-03-26 15:32:00 +01:00
4931d3b8aa Map queries with placeholders replaced in DTOs
Updated RecActionView to RecActionViewDto mapping to replace placeholders in PreprocessingQuery and PostprocessingQuery using ReplacePlaceholders. Added necessary using directive for the extension method.
2026-03-26 15:28:08 +01:00
6aae26bfb6 Update using directive to new Action namespace
Replaced the using directive for InvokeAction with Action in InvokeActionTests.cs to reflect recent namespace reorganization. No other changes were made.
2026-03-26 15:23:30 +01:00
38d8ef6e93 Update namespace in PlaceholderExtensions.cs
Changed namespace from ReC.Application.Common.Behaviors.InvokeAction to ReC.Application.Common.Behaviors.Action for consistency. No other code changes were made.
2026-03-26 14:58:01 +01:00
7bc5428bd4 Refactor: move query behaviors to Action namespace
Updated the namespaces for BodyQueryBehavior and HeaderQueryBehavior from ReC.Application.Common.Behaviors to ReC.Application.Common.Behaviors.Action. Adjusted related imports in DependencyInjection.cs to reflect this change for improved code organization.
2026-03-26 14:40:50 +01:00
c405f369ac Add unit tests for InvokeAction placeholder replacement
Introduce InvokeActionTests.cs with comprehensive tests for:
- Placeholder replacement logic (ReplacePlaceholders) across int, bool, string, DateTime, DateTimeOffset types, multiple placeholders, and various prefix formats.
- Value extraction by column name (GetValueByColumnName), including models with and without [Column] attributes.
- Exception handling for unresolvable placeholders and invalid input scenarios.
These tests ensure robust coverage of both normal and error cases.
2026-03-26 14:37:58 +01:00
c2e073dade Add ReplacePlaceholders for SQL-style string interpolation
Introduce ReplacePlaceholders to PlaceholderExtensions, enabling replacement of {#...#COLUMN_NAME} placeholders in strings with property values from provided objects. Uses a generated regex for matching and converts values to SQL-compatible literals. Throws PlaceholderResolutionException if a column cannot be resolved. Refactored class to partial to support regex generation.
2026-03-26 14:37:29 +01:00
7a11ac3635 Add PlaceholderResolutionException for unresolved placeholders
Introduced PlaceholderResolutionException in the ReC.Application.Common.Exceptions namespace. This exception provides detailed context when a placeholder cannot be resolved due to a missing property with a specific column name, including the placeholder, column name, and input string.
2026-03-26 14:36:35 +01:00
a91e3264b4 Remove Action folder reference from project file
The <Folder Include="Common\Behaviors\Action\" /> entry was removed from ReC.Application.csproj, so the folder is no longer explicitly included in the project structure. No code or project references were affected.
2026-03-26 14:35:39 +01:00
2ae5251550 Rename class and update namespace for placeholder logic
Renamed the ReflectionExtensions class to PlaceholderExtensions and moved it from the ReC.Domain.Extensions namespace to ReC.Application.Common.Behaviors.InvokeAction to better reflect its purpose and location within the project structure.
2026-03-26 13:48:16 +01:00
56730c0d4e Make GetValueByColumnName a generic extension method
Refactored GetValueByColumnName to use a generic type parameter constrained to class types. This enhances type safety and enables better type inference and static analysis when accessing property values by column name.
2026-03-26 13:42:45 +01:00
d8aa032a57 Add ReflectionExtensions for property lookup by column name
Introduced a static ReflectionExtensions class with a GetValueByColumnName extension method to retrieve property values by their [Column] attribute name. Also removed an unused folder reference from the project file.
2026-03-26 13:16:04 +01:00
e96773f3c4 Remove DbModelConfigurationException and related logic
Removed the DbModelConfigurationException class and all its usages from DbModelOptions and EntityOptions. Indexers in these classes no longer throw this exception when configuration is missing. Updated the project file to include the Common\Options\DbModel\ folder.
2026-03-26 10:34:24 +01:00
06e92b588f Remove DbModel configuration and related JSON file
Removed all references to DbModel configuration from Program.cs and RecApplicationTestBase.cs. Deleted appsettings.DbModel.json, eliminating custom entity and column mapping definitions. The application no longer loads or uses DbModel configuration from JSON.
2026-03-26 10:34:05 +01:00
b922cbbb30 Remove ConfigureDbModel from dependency injection setup
Eliminated all references to ConfigureDbModel in DependencyInjection.cs, including related imports, configuration methods, and required service tracking. Dependency injection configuration is otherwise unchanged.
2026-03-26 10:32:24 +01:00
6c56375e3e Refactor RecDbContext to remove dynamic view mapping
Removed dependency on DbModelOptions and IOptions. Entity-to-view mappings are now hardcoded in OnModelCreating with fixed view names and schema. Property-to-column mappings now use EF Core conventions. Simplifies configuration and maintenance, but reduces flexibility for schema changes. Entity relationships remain explicitly configured.
2026-03-26 10:31:48 +01:00
fa9aa23f32 Add EF Core data annotations to view models
Added [Key], [Column], and [Table] attributes to ProfileView, RecActionView, and ResultView classes to explicitly map properties to database columns and views. Expanded model properties for clearer schema alignment and improved maintainability with Entity Framework Core.
2026-03-26 10:30:00 +01:00
b86d0c0f99 Add [Column] mapping attributes to query result classes
Added [Column] attributes to properties in BodyQueryResult, HeaderQueryResult, and InsertObjectResult to explicitly map them to their respective database columns. Also included necessary using directives for DataAnnotations.Schema to support these mappings. This enhances ORM compatibility and ensures correct property-to-column mapping.
2026-03-26 10:28:52 +01:00
b0d89ceba4 Remove properties from RecActionOptions class
Removed AddedWho and UseHttp1ForNtlm properties from RecActionOptions, leaving the class empty. This change cleans up unused configuration options.
2026-03-25 16:13:32 +01:00
11ebfdd21e Refactor DbModelOptions to use indexers for lookups
Replaced GetEntity and GetColumn extension methods with indexers
on DbModelOptions and EntityOptions. Updated all usages to use
the new indexer syntax, improving code clarity and error handling
for missing entity or column configurations.
2026-03-25 16:10:50 +01:00
1467acc4a1 Refactor DbModel options to use generic entity mapping
Replaces strongly-typed view options classes with a generic EntityOptions class and a dictionary-based configuration for entity-to-view and property-to-column mappings. Updates appsettings.DbModel.json to match the new structure. Refactors RecDbContext to use extension methods for mapping configuration. Removes obsolete options classes and simplifies exception handling for missing mappings. This change improves flexibility and maintainability of database view configuration.
2026-03-25 15:25:29 +01:00
e761fbd1ca Add DbModel config to Rec services setup
Added options.ConfigureDbModel to the AddRecServices configuration, enabling Rec services to use settings from the "DbModel" section of the app configuration for enhanced database model customization.
2026-03-25 13:30:12 +01:00
2e83d4a24a Make RecDbContext model mapping fully configurable
Refactored RecDbContext to use DbModelOptions injected via IOptions, replacing hardcoded view and column names with configuration-driven mappings. This enables dynamic control of entity-to-database mapping through appsettings.json, improving flexibility and maintainability.
2026-03-25 13:29:29 +01:00
37ba85d681 Add support for configuring DbModelOptions via DI
Introduce ConfigureDbModel methods to DependencyInjection for setting up DbModelOptions from code or configuration. Update required services tracking and add usage in Program.cs to enable structured DbModelOptions injection.
2026-03-25 13:26:54 +01:00
a46cd08122 Refactor DbModel options to use explicit, typed classes
Replaced the old generic, dictionary-based entity configuration system with a new, strongly-typed options structure under ReC.Application.Common.Options.DbModel. Introduced specific options classes for each major database view and result type, each with clear property mappings and defaults. Added a ViewOptions class for view/schema info. Removed all legacy entity mapping infrastructure, resulting in a more maintainable and type-safe configuration approach.
2026-03-25 12:53:10 +01:00
3d46901af5 Add DbModel view mappings to appsettings.DbModel.json
Added a new "DbModel" section to appsettings.DbModel.json, defining property-to-column mappings for RecActionView, ProfileView, ResultView, HeaderQueryResult, BodyQueryResult, and InsertObjectResult. This centralizes and standardizes database view and query result configurations.
2026-03-25 12:44:12 +01:00
90e8adbd36 Update behavior namespaces to InvokeAction for clarity
Renamed namespaces in PreprocessingBehavior and PostprocessingBehavior from .Action to .InvokeAction, and updated related using directives in DependencyInjection.cs. Added a folder entry for Common\Behaviors\Action\ in the project file for organization.
2026-03-25 11:43:30 +01:00
aef59def7f Add ResultType to InsertResultCommand in all handlers
Added the Type property (ResultType) to every InsertResultCommand sent in PostprocessingBehavior, PreprocessingBehavior, and InvokeRecActionViewCommand. This ensures each result now includes its context (Post, Pre, or Main), both in normal and exception flows.
2026-03-25 11:17:38 +01:00
d7783b6e81 Make InsertObjectProcedure properties nullable, require ResultType
Updated InsertObjectProcedure to make related command properties nullable and removed default initializations. Updated handler to use null-conditional access for these properties. Changed InsertResultCommand.Type to required and updated tests accordingly. Improves null safety and clarifies required fields.
2026-03-25 11:17:21 +01:00
4126f984e4 Expand and refactor OutResDto for richer data support
Refactored OutResDto to include additional properties such as Action, Profile, Status, and Type, and made several fields nullable. This enhances flexibility and allows for more comprehensive and optional data representation in API responses.
2026-03-25 11:06:07 +01:00
0e2328c287 Add support for Result.Info, Error, and Type in insert proc
Extended InsertObjectProcedureHandler to include Result.Info, Result.Error, and Result.Type in both the SQL parameter list and the command string. This ensures these fields are correctly passed to and handled by the stored procedure.
2026-03-25 11:02:56 +01:00
e04e054151 Add optional ResultType to InsertResultCommand
Added a nullable Type property of ResultType to InsertResultCommand for enhanced result categorization. Also included the required using directive for ReC.Domain.Constants.
2026-03-25 11:02:35 +01:00
6082a637fe Add mapping for result type fields in RecDbContext
Added entity property mappings for RESULT_TYPE_ID (Type) and RESULT_TYPE (TypeName) columns in RecDbContext to support storing and retrieving result type information.
2026-03-25 11:00:12 +01:00
93669a6358 Add Type and TypeName to ResultView model
Imported ResultType and added Type (ResultType?) and TypeName (string?) properties to ResultView to support representing result types alongside status information.
2026-03-25 10:59:33 +01:00
fecd9219b4 Add ResultType enum to ReC.Domain.Constants
Introduced a new ResultType enum with values Pre, Main, and Post to represent different result stages. Pre is explicitly set to 1.
2026-03-25 10:58:45 +01:00
bd07b4482c Remove CS0618 warning suppression for ExceptionHandlingMiddleware
Removed #pragma directives that suppressed CS0618 (obsolete API usage) warnings around app.UseMiddleware<ExceptionHandlingMiddleware>(). Obsolete warnings for this middleware will now be shown during compilation.
2026-03-25 10:28:49 +01:00
520aec427b Remove obsolete attribute and add exception using directive
Removed [Obsolete] from ExceptionHandlingMiddleware, indicating it is no longer deprecated. Added using directive for ReC.Application.Common.Exceptions to support updated exception handling.
2026-03-25 10:27:41 +01:00
26b7a82451 Handle RecActionException in middleware with 422 response
Added specific handling for RecActionException in the exception middleware. Now logs a warning with ActionId and ProfileId, returns a 422 Unprocessable Entity status, and provides detailed error information in the response.
2026-03-25 10:26:24 +01:00
08c0d29d84 Improve error handling and logging in batch rec actions
Add ILogger support to InvokeRecActionViewsCommandHandler for enhanced error and warning logging. Log and continue on handled exceptions when appropriate, and rethrow for critical errors. Clean up and reorder using directives. This increases robustness and traceability during batch rec action processing.
2026-03-25 10:25:52 +01:00
405b5f3ab1 Add ActionId and ProfileId properties to RecActionException
Expose ActionId and ProfileId as public properties in the
RecActionException class, allowing external access to these
values from exception instances. Refactored the constructor
to use a block body.
2026-03-25 10:17:24 +01:00
32af65d30c Remove InvokeBatchRecActionViewsCommand extension method
Deleted the InvokeBatchRecActionViewsCommandExtensions class and its InvokeBatchRecActionView extension method for ISender. The command record and handler remain unchanged.
2026-03-25 10:07:15 +01:00
30ccf05c57 Use RecActionException for contextual error handling
Replaced generic exception with RecActionException in PostprocessingBehavior when ErrorAction is Stop, providing ActionId and ProfileId for better error context. Added necessary using statements for the new exception and related namespaces.
2026-03-25 09:58:02 +01:00
a14d5ff112 Improve error handling with RecActionException
Wrap exceptions in PreprocessingBehavior with RecActionException, including ActionId and ProfileId for better context and debugging. This enhances error reporting when ErrorAction is set to Stop.
2026-03-25 09:57:46 +01:00
fec125b0d5 Improve exception context in action error handling
Updated InvokeRecActionViewCommandHandler to throw a RecActionException with action Id and ProfileId when an error occurs and ErrorAction is set to Stop. This change enhances error traceability by providing more contextual information in exceptions.
2026-03-25 09:56:58 +01:00
82ae4c5957 Add RecActionException custom exception class
Introduced RecActionException in ReC.Application.Common.Exceptions to encapsulate errors related to rec actions, including actionId, optional profileId, and inner exception details in the error message.
2026-03-25 09:55:52 +01:00
894b7bb070 Improve error handling and resource cleanup in action handler
Refactored InvokeRecActionViewCommandHandler to enhance error handling by logging exceptions and conditionally rethrowing based on ErrorAction. Simplified NTLM HttpClient/handler management for proper disposal. Improved code structure and formatting for clarity.
2026-03-24 17:02:06 +01:00
a707cce6e4 Refactor error handling in Pre/Postprocessing behaviors
Refactored PreprocessingBehavior and PostprocessingBehavior to send InsertResultCommand with only relevant info or error fields. Now throws exceptions when ErrorAction is Stop, allowing upstream error handling and preventing unnecessary or misleading result data. Improves consistency and clarity in error processing.
2026-03-24 16:19:00 +01:00
2ca85a2372 Refactor action behaviors to use Unit instead of bool
Refactored PreprocessingBehavior and PostprocessingBehavior for InvokeRecActionViewCommand to use MediatR's Unit as the response type instead of bool. Updated method signatures, return values, and DI registration accordingly to align with MediatR conventions for commands without return values.
2026-03-24 16:01:24 +01:00
dbe09cd07b Refactor error handling in RecActionView invocation
Switch to exception-based error handling for invoking RecActionView actions, removing boolean return values and related checks. Update command and handler signatures to use void return type. Clean up unused using directives. This improves clarity and robustness of error management during batch processing.
2026-03-24 16:00:47 +01:00
690dcea7a8 Add PostprocessingBehavior to MediatR pipeline
Registered PostprocessingBehavior for InvokeRecActionViewCommand (with bool result) in the MediatR pipeline via DependencyInjection. This ensures both preprocessing and postprocessing behaviors are executed for this command.
2026-03-24 14:41:41 +01:00
fdae4d26be Add PostprocessingBehavior for action result logging
Introduced PostprocessingBehavior as an IPipelineBehavior for InvokeRecActionViewCommand. This behavior handles post-processing by executing optional queries, serializing results, and logging outcomes via InsertResultCommand. It also manages error handling based on the action's ErrorAction setting, ensuring consistent result tracking after command execution.
2026-03-24 14:40:52 +01:00
2883cf9be4 Add PreprocessingBehavior for InvokeRecActionViewCommand
Register PreprocessingBehavior in the MediatR pipeline specifically for InvokeRecActionViewCommand. Update using directives to reflect the shift from Procedures to Commands and from Common.Behaviors to Common.Behaviors.Action.
2026-03-24 14:15:01 +01:00
9410c5dc0d Refactor PreprocessingBehavior to log results and errors
Refactored PreprocessingBehavior to serialize and log the results and errors of preprocessing SQL queries using InsertResultCommand. Now records both successful and failed executions, while preserving the stop-on-error behavior. Cleaned up and updated using directives.
2026-03-24 14:01:45 +01:00
35e99d9f2a Change ExecuteDynamicSqlAsync to return object list
The ExecuteDynamicSqlAsync method now returns an IEnumerable of
dictionaries representing query result rows, instead of a JSON
string. This allows consumers to work directly with the data
in its native .NET object form.
2026-03-24 13:26:10 +01:00
5fd65e52a3 Pass CancellationToken to ExecuteDynamicSqlAsync
Updated PreprocessingBehavior to include the CancellationToken when calling ExecuteDynamicSqlAsync, enabling query execution to be cancelled if the operation is aborted.
2026-03-24 13:21:40 +01:00
2508a8b986 Add CancellationToken to ExecuteDynamicSqlAsync method
The ExecuteDynamicSqlAsync extension for IRecDbContext now accepts an optional CancellationToken, which is passed to all async database operations to support cancellation. Also added a TODO to move this method to Common.Infrastructure in the future.
2026-03-24 13:21:05 +01:00
daff1477be Refactor PUT endpoints to use id in route and DTO in body
Refactored update (PUT) endpoints in multiple controllers to accept the record id as a route parameter and the update data as a DTO in the request body. Updated method signatures, XML documentation, and imports to align with RESTful conventions and improve API clarity. Controller methods now construct command objects using the id and DTO before sending to MediatR.
2026-03-24 12:07:50 +01:00
dcfa47c68d Update DELETE endpoints to use [FromQuery] parameters
Refactored all relevant controller DELETE actions to accept command/procedure parameters from the query string ([FromQuery]) instead of the request body ([FromBody]). Updated XML documentation and method signatures to reflect this change, including renaming parameters from "procedure" to "command" where appropriate. Also removed the unused IConfiguration dependency from EndpointAuthController.
2026-03-24 11:57:53 +01:00
e691faf620 Rename Result*Procedure to Result*Command for consistency
Refactor all Result-related procedure records, handlers, and usages to use the *Command suffix instead of *Procedure. Update controller actions, handler interfaces, tests, and InsertObjectProcedure accordingly. No functional changes; improves naming consistency across the codebase.
2026-03-24 11:40:28 +01:00
cac33c46df Rename *ActionProcedure classes to *ActionCommand
Renamed InsertActionProcedure, UpdateActionProcedure, and DeleteActionProcedure to their respective *ActionCommand counterparts to align with CQRS conventions. Updated all controller actions, handlers, tests, and related usages accordingly. No changes to business logic or method signatures.
2026-03-24 11:39:55 +01:00
de503cac5b Rename Profile*Procedure types to Profile*Command for clarity
Renamed InsertProfileProcedure, UpdateProfileProcedure, and DeleteProfileProcedure (and their handlers) to use the "Command" suffix for consistency. Updated all usages in controllers, handlers, and tests. No logic changes; only type names were updated for improved clarity.
2026-03-24 11:39:04 +01:00
4999beda3b Refactor endpoint Procedures to Commands for CQRS alignment
Renamed Insert/Update/DeleteEndpointProcedure classes to their
respective Command counterparts to follow CQRS conventions.
Updated controller actions, handlers, InsertObjectProcedure,
and related unit tests to use the new Command types.
No functional changes were made; this is a naming refactor.
2026-03-24 11:38:22 +01:00
5df36d94e0 Rename EndpointParams procedures to commands for CQRS clarity
Renamed Insert/Update/DeleteEndpointParamsProcedure classes and
handlers to use the "Command" suffix (e.g., InsertEndpointParamsCommand)
for consistency with CQRS conventions. Updated all controller actions,
handlers, and tests to use the new command names. This improves
clarity and aligns naming with standard command patterns.
2026-03-24 11:37:30 +01:00
d3d24a0fb6 Refactor: Rename UpdateEndpointAuthProcedure to Command
Renamed UpdateEndpointAuthProcedure to UpdateEndpointAuthCommand across the codebase for improved naming consistency. Updated controller, handler, and tests to use the new command name, aligning with CQRS conventions for state-modifying operations.
2026-03-24 11:36:49 +01:00
a6b0cbaf9d Rename InsertEndpointAuthProcedure to InsertEndpointAuthCommand
Refactored all usages of InsertEndpointAuthProcedure to InsertEndpointAuthCommand, including method signatures, handler interfaces, and test cases, to better align with CQRS naming conventions for write operations.
2026-03-24 11:35:58 +01:00
0162d059da Refactor: Rename DeleteEndpointAuthProcedure to Command
Renamed DeleteEndpointAuthProcedure to DeleteEndpointAuthCommand across the codebase, including controller, handler, and tests, to improve naming consistency and align with CQRS conventions for write operations.
2026-03-24 11:31:33 +01:00
302fee4908 Refactor tests to send commands directly to mediator
Refactored test code to remove ToObjectProcedure usage and send command objects directly to the mediator. Updated update procedure tests to use Data and Id properties. Replaced custom Execute*Procedure methods with sender.Send. Cleaned up unused usings. These changes improve consistency and reflect updates to command structures.
2026-03-24 11:29:15 +01:00
3e10176d98 Add PreprocessingBehavior for action command pipeline
Introduced PreprocessingBehavior implementing MediatR's IPipelineBehavior for InvokeRecActionViewCommand. This behavior executes a preprocessing SQL query if specified, and halts processing if an error occurs and ErrorAction is set to Stop. Added necessary using directives for dependencies.
2026-03-24 11:14:11 +01:00
4f0f99e0f8 Refactor InsertResultProcedure execution method
Replaced sender.ExecuteInsertProcedure with sender.Send for executing InsertResultProcedure. Removed the _options.AddedWho parameter, passing only the procedure object and cancellation token. The construction of InsertResultProcedure remains unchanged.
2026-03-24 11:11:36 +01:00
8fb4b4005c Refactor UpdateObjectProcedure to use DTO properties
Replaced *Procedure properties in UpdateObjectProcedure with corresponding DTO types (e.g., UpdateActionDto, UpdateEndpointDto, etc.) and added the necessary DTO namespace import. This decouples the record from procedure logic, improving separation of concerns and data transfer handling.
2026-03-24 11:11:12 +01:00
b3bb7144ef Refactor IUpdateProcedure to generic with Id and Data props
Changed IUpdateProcedure to a generic interface IUpdateProcedure<T>, adding Id (long) and Data (T) properties for improved type safety and flexibility in update procedures. The interface continues to inherit from IRequest<int>.
2026-03-24 11:10:47 +01:00
eff6350d77 Refactor update procedures to use DTO-based pattern
Refactored Update*Procedure records to encapsulate update data in dedicated DTOs (e.g., UpdateActionDto, UpdateEndpointDto) via a generic Data property. Updated interfaces to be generic and modified handlers to pass only the DTO to UpdateObjectProcedure. This improves maintainability, reduces duplication, and standardizes update logic across entities.
2026-03-24 11:09:46 +01:00
114b5de71d Add Update DTOs for partial entity updates in Procedures
Introduced six new DTO record classes under ReC.Application.Common.Procedures.UpdateProcedure.Dto to support partial (nullable) updates for Action, Endpoint, EndpointAuth, EndpointParams, Profile, and Result entities. These DTOs enable PATCH-like update operations by allowing selective property updates. No existing code was modified.
2026-03-24 11:08:37 +01:00
f786192786 Refactor ResultController to use mediator.Send for commands
Standardize command handling by replacing custom mediator methods
with mediator.Send in ResultController. Update PUT endpoint to
remove id route parameter and rely on payload for identification.
2026-03-24 11:07:42 +01:00
50741bfdd3 Refactor RecActionController to use MediatR Send only
Simplified controller by removing IConfiguration dependency and unused usings. Refactored all endpoints to use MediatR's Send method with command objects, eliminating custom mediator extension methods and route parameters like id. This streamlines command handling and reduces dependencies.
2026-03-24 11:06:20 +01:00
561eafe48c Refactor ProfileController to use MediatR Send method
Replaces custom Execute*Procedure methods with MediatR's Send for insert, update, and delete operations. Removes obsolete using directives. Updates the PUT endpoint to accept the profile ID in the request body instead of the route.
2026-03-24 11:04:41 +01:00
84358ced96 Refactor EndpointsController to use standard MediatR pattern
Removed IConfiguration dependency and replaced custom mediator
extension methods with standard mediator.Send calls for endpoint
commands. Simplified method signatures and removed unused usings.
Updated PUT endpoint to accept UpdateEndpointProcedure directly.
2026-03-24 11:03:28 +01:00
d2e97a2fef Refactor EndpointParamsController to use MediatR.Send
Removed IConfiguration dependency and custom MediatR extension methods from EndpointParamsController. All command handling now uses mediator.Send directly, and route/config parameters have been eliminated from endpoints. Cleaned up unused usings and streamlined controller logic.
2026-03-24 11:02:58 +01:00
d505c8415e Refactor EndpointAuthController to use MediatR Send
Replaced custom procedure execution methods with MediatR's Send method in EndpointAuthController. Removed obsolete using directives and updated the PUT endpoint to no longer require an id route parameter, expecting the id in the payload instead. This streamlines the controller and aligns it with standard MediatR practices.
2026-03-24 10:58:43 +01:00
a590ffd2dc Refactor InsertProfileProcedure to use MediatR handler
Refactored InsertProfileProcedure by removing the ToObjectProcedure method and introducing InsertProfileProcedureHandler, which implements IRequestHandler and delegates insert logic via MediatR's ISender. Updated using directives to include MediatR. This aligns profile insertion with the MediatR request/response pattern.
2026-03-24 10:14:58 +01:00
94da75ce37 Add MediatR handler for DeleteProfileProcedure command
Introduce DeleteProfileProcedureHandler using MediatR's IRequestHandler to process DeleteProfileProcedure requests. The handler sends a DeleteObjectProcedure via ISender, replacing the previous ToObjectProcedure method. Also, add necessary using directives and refactor logic to centralize command handling.
2026-03-24 10:14:10 +01:00
9c1ffd7df8 Refactor UpdateEndpointProcedure and add handler
Refactored UpdateEndpointProcedure to implement IUpdateProcedure directly and removed the ToObjectProcedure method. Introduced UpdateEndpointProcedureHandler using MediatR's ISender for command handling, aligning with MediatR patterns and enabling dependency injection.
2026-03-24 10:13:35 +01:00
e31d034266 Refactor InsertEndpointProcedure; add MediatR handler
Refactored InsertEndpointProcedure to remove the ToObjectProcedure method, making it a simple data record. Introduced InsertEndpointProcedureHandler using MediatR's IRequestHandler to handle insert commands asynchronously. Added necessary MediatR using directives.
2026-03-24 10:13:12 +01:00
649d7eff8c Refactor DeleteEndpointProcedure to use MediatR handler
Replaces the ToObjectProcedure method with a dedicated
DeleteEndpointProcedureHandler implementing IRequestHandler.
The handler uses MediatR's ISender to send DeleteObjectProcedure,
improving separation of concerns and aligning with MediatR
request/response patterns.
2026-03-24 10:12:35 +01:00
401d67de4c Refactor UpdateEndpointParamsProcedure to MediatR pattern
Refactored UpdateEndpointParamsProcedure to include all relevant properties and removed the ToObjectProcedure method. Introduced UpdateEndpointParamsProcedureHandler using MediatR's IRequestHandler, delegating update logic via ISender. This improves code structure and maintainability.
2026-03-24 10:11:55 +01:00
ed94415a33 Refactor InsertEndpointParamsProcedure with MediatR handler
Remove ToObjectProcedure method from InsertEndpointParamsProcedure and introduce InsertEndpointParamsProcedureHandler using MediatR's IRequestHandler. This decouples conversion and command logic, aligning with MediatR patterns and improving separation of concerns.
2026-03-24 10:10:14 +01:00
04513a3d08 Refactor endpoint params delete to use MediatR handler
Replaces ToObjectProcedure with DeleteEndpointParamsProcedureHandler using MediatR's request/handler pattern. Improves separation of concerns by delegating deletion logic to the handler and updates imports and namespace accordingly.
2026-03-24 10:09:37 +01:00
d390c3f7b6 Remove DeleteObjectProcedureExtensions and its methods
Deleted the DeleteObjectProcedureExtensions static class and its ExecuteDeleteProcedure extension methods from DeleteObjectProcedure.cs. These methods previously provided convenience for executing delete procedures via ISender. The core DeleteObjectProcedure record and its handler remain unchanged.
2026-03-24 10:08:45 +01:00
a40f20f6d9 Refactor UpdateEndpointAuthProcedure and add handler
Expanded UpdateEndpointAuthProcedure with new properties and removed the ToObjectProcedure method. Introduced UpdateEndpointAuthProcedureHandler using MediatR's ISender for command handling, improving separation of concerns and enabling dependency injection.
2026-03-24 09:59:21 +01:00
ee1f6a8753 Refactor InsertEndpointAuthProcedure handling
Move insert logic from InsertEndpointAuthProcedure to a new InsertEndpointAuthProcedureHandler using MediatR. Remove ToObjectProcedure method and update usings. The record now serves as a pure data structure.
2026-03-24 09:51:59 +01:00
1e35b6e263 Refactor DeleteEndpointAuthProcedure to use MediatR handler
Refactored DeleteEndpointAuthProcedure by removing the ToObjectProcedure method and introducing DeleteEndpointAuthProcedureHandler, which implements IRequestHandler and delegates deletion via MediatR's ISender. Consolidated using directives and namespace declarations.
2026-03-24 09:51:21 +01:00
e152e9a37a Refactor ChangedWho handling in UpdateObjectProcedure
ChangedWho is now initialized directly and no longer settable via a public method. Removed the ChangedBy method and related extension methods. Added a TODO to move ChangedWho assignment to authentication middleware in the future.
2026-03-24 09:48:37 +01:00
554aaa8b6c Refactor InsertObjectProcedure AddedWho handling
Removed AddedBy method and related extension; AddedWho is now set to a default value internally. Added a TODO to move AddedWho assignment to authentication middleware in the future.
2026-03-24 09:47:24 +01:00
329d156d08 Update IUpdateProcedure to use MediatR IRequest<int>
Refactored IUpdateProcedure to inherit from MediatR's IRequest<int> for integration with the MediatR pipeline. Removed the ToObjectProcedure method and added the necessary using directive for MediatR. This simplifies the interface and standardizes request handling.
2026-03-24 09:44:41 +01:00
246362812a Refactor IInsertProcedure for MediatR compatibility
IInsertProcedure now inherits from IRequest<long> to integrate with MediatR's request/response pattern. Removed the ToObjectProcedure method and added the necessary MediatR using directive.
2026-03-24 09:43:57 +01:00
71430918ac Refactor IDeleteProcedure to use MediatR IRequest<int>
IDeleteProcedure now inherits from MediatR's IRequest<int>, aligning it with MediatR request/response patterns. Removed the ToObjectProcedure method from the interface. Added the MediatR using directive. Note: The namespace and using directive are now on the same line, which may need formatting correction.
2026-03-24 09:43:36 +01:00
7a1705365b Add dynamic SQL execution to IRecDbContext interface
Added Database property to IRecDbContext for database operations. Introduced ExecuteDynamicSqlAsync extension method to run arbitrary SQL and return results as JSON. This enables flexible querying and result serialization.
2026-03-24 09:42:59 +01:00
acf136e689 Refactor UpdateProfileProcedure and add MediatR handler
Refactored UpdateProfileProcedure to implement IUpdateProcedure and expanded its properties. Removed ToObjectProcedure method. Introduced UpdateProfileProcedureHandler using MediatR's ISender for command dispatch. Modernized structure to use record types and handler classes.
2026-03-24 09:41:17 +01:00
6b4897702a Refactor DeleteActionProcedure to use MediatR handler
Refactored DeleteActionProcedure to implement IDeleteProcedure directly and removed the ToObjectProcedure method. Introduced DeleteActionProcedureHandler using MediatR's IRequestHandler to delegate deletion logic via ISender. Updated using directives for MediatR integration.
2026-03-19 23:19:07 +01:00
7d4e082958 Refactor InsertActionProcedure to use MediatR handler
Replaces the ToObjectProcedure method with an InsertActionProcedureHandler that implements IRequestHandler using MediatR. The handler maps InsertActionProcedure to InsertObjectProcedure and sends it via ISender. Also updates using statements to include MediatR.
2026-03-19 23:18:57 +01:00
d1dd021952 Refactor UpdateActionProcedure and add handler class
Expanded UpdateActionProcedure with new properties and removed the ToObjectProcedure method. Introduced UpdateActionProcedureHandler using MediatR to handle update logic and delegate to UpdateObjectProcedure. Centralized update logic in the handler for better maintainability.
2026-03-19 23:18:47 +01:00
5afc1791b0 Refactor DeleteResultProcedure to use MediatR handler
Remove ToObjectProcedure method and add DeleteResultProcedureHandler implementing IRequestHandler. The handler sends DeleteObjectProcedure via MediatR, mapping relevant properties. Also update using directives and namespace.
2026-03-19 23:18:38 +01:00
2ec07d7e96 Refactor InsertResultProcedure to use MediatR handler
Refactored InsertResultProcedure by removing its ToObjectProcedure method and introducing InsertResultProcedureHandler, which implements IRequestHandler and delegates object insertion via MediatR's ISender. This shifts conversion logic from the record to the handler and improves separation of concerns.
2026-03-19 23:18:29 +01:00
cbe4f1ba3c Refactor UpdateResultProcedure to use MediatR handler
Refactored UpdateResultProcedure to implement IUpdateProcedure directly and removed the ToObjectProcedure method. Introduced UpdateResultProcedureHandler using MediatR's IRequestHandler pattern, delegating update logic via ISender. This improves code structure and maintainability.
2026-03-19 23:18:17 +01:00
16155da033 Add Info and Error properties to OutResDto
Added two new nullable string properties, Info and Error, to the OutResDto record to support additional informational and error messages in responses.
2026-03-19 18:51:33 +01:00
0aa1414ea6 Add @pRESULT_INFO and @pRESULT_ERROR SQL parameters
Added two new SQL parameters, @pRESULT_INFO and @pRESULT_ERROR, to InsertObjectProcedure.cs. These are set from request.Result.Info and request.Result.Error, or to DBNull.Value if null, to support additional result data in the procedure.
2026-03-19 18:50:47 +01:00
181a9a83fd Add Info and Error properties to InsertResultProcedure
Added nullable string properties Info and Error to the InsertResultProcedure record to allow storing additional information and error messages related to insert operations.
2026-03-19 18:50:34 +01:00
5e3e12bad8 Add Info and Error properties to entity mapping
Added Info and Error properties to the entity configuration, mapping them to the RESULT_INFO and RESULT_ERROR columns in the database using HasColumnName. This allows the entity to store and retrieve additional result information and error details.
2026-03-19 18:49:28 +01:00
6dcc128ad5 Add Info, Error, AddedWho, and ChangedWho to ResultView
Extended ResultView with four new nullable string properties:
Info and Error for additional details and error messages,
and AddedWho and ChangedWho for tracking user metadata.
2026-03-19 18:49:08 +01:00
754ef88644 Bump version to 2.0.2-beta in ReC.API.csproj
Updated project, assembly, and file versions from 2.0.1-beta/2.0.1.0 to 2.0.2-beta/2.0.2.0 to reflect new release. No other changes were made.
2026-03-16 13:46:10 +01:00
95ece6fdcf Remove unused imports from ResultController.cs
Removed unnecessary using statements for ReC.API.Extensions and ReC.API.Models to clean up the code and improve maintainability. No functional changes were made.
2026-03-16 13:44:38 +01:00
87194df697 Refactor and expand REST action authentication support
Refactored authentication logic for REST actions to use a structured switch block, adding explicit support for ApiKey, BearerToken, JwtBearer, OAuth2, BasicAuth, and NTLM authentication types. Introduced dedicated HttpClient/Handler for NTLM with proper disposal. Improved extensibility, clarity, and resource management. Added IOptions and Options usage for configuration.
2026-03-16 13:43:27 +01:00
b38d53248c Force HTTP/1.1 for NTLM when UseHttp1ForNtlm is enabled
Added logic to set HTTP/1.1 and exact version policy for NTLM
authentication requests when the UseHttp1ForNtlm option is true.
This ensures compatibility with NTLM endpoints requiring HTTP/1.1.
2026-03-16 13:41:04 +01:00
56b604bd35 Add UseHttp1ForNtlm option to RecAction config
Added the boolean UseHttp1ForNtlm property to the RecAction section in appsettings.json and RecActionOptions class. This option defaults to false and allows control over using HTTP/1 for NTLM authentication.
2026-03-16 13:38:45 +01:00
f67579dba9 Move AddedWho config to RecActionOptions and refactor usage
Centralize AddedWho under RecAction in appsettings.json and add it to RecActionOptions. Update InvokeRecActionViewCommandHandler to use IOptions<RecActionOptions> for strongly-typed configuration access, replacing previous config-based retrieval. Removes global AddedWho property for improved maintainability.
2026-03-16 13:32:34 +01:00
636397efb8 Remove MaxConcurrentInvocations from RecAction config
Removed the MaxConcurrentInvocations property from the RecActionOptions class and deleted the corresponding setting from the RecAction section in appsettings.json. This makes RecActionOptions an empty class.
2026-03-16 13:28:50 +01:00
ef4d0767e9 Remove FakeProfileId config and related extension method
Removed the GetFakeProfileId extension method and its class from ConfigExtensions.cs, along with the "FakeProfileId" entry from appsettings.json. Also fixed a trailing comma in appsettings.json to ensure valid JSON syntax.
2026-03-16 13:26:08 +01:00
f15725ade2 Support NTLM password substitution in DEBUG mode
In DEBUG builds, replace "%NTLM_PW%" in NTLM auth password with a value from config. This enables dynamic credential testing without hardcoding passwords.
2026-03-16 13:18:24 +01:00
382eef0089 Enable User Secrets in ReC.API.csproj for dev config
Added <UserSecretsId> to the project file to support ASP.NET Core User Secrets, allowing secure storage of sensitive development configuration data.
2026-03-16 12:28:58 +01:00
5d316e43b9 Bump version to 2.0.1-beta in ReC.API.csproj
Updated <Version>, <AssemblyVersion>, <FileVersion>, and <InformationalVersion> fields in the project file from 2.0.0-beta to 2.0.1-beta to reflect a new pre-release build. No other changes were made.
2026-03-03 08:19:31 +01:00
0c8d7f6b3c Add 'Last' option to ReadResultViewQuery for latest result
Added a Last property to ReadResultViewQuery to allow fetching only the most recent ResultView entity. Updated the handler logic to return either the last result or all results based on this property.
2026-03-02 16:17:21 +01:00
0e7870b556 Refactor: remove HttpExtensions and inline HTTP method mapping
Removed HttpExtensions.cs and its extension methods for RestType-to-HttpMethod conversion. Introduced a private static CreateHttpRequestMessage method in InvokeRecActionViewCommandHandler to handle HTTP method mapping and request creation directly, reducing indirection and simplifying dependencies.
2026-03-02 14:31:38 +01:00
ec119a3045 Add options to include Action/Profile in ResultView queries
Added IncludeAction and IncludeProfile properties to ReadResultViewQuery, allowing callers to control eager loading of related Action and Profile entities. Updated handler to conditionally include these navigation properties based on the new flags for more flexible data retrieval.
2026-03-02 13:57:14 +01:00
776813d05d Simplify StringContent assignment in HTTP request
Refactored code to assign StringContent directly to httpReq.Content without a using block. Disposal of StringContent is now handled by HttpRequestMessage, improving code clarity and resource management.
2026-03-02 13:40:43 +01:00
0a3761921d Refactor RecActionView command invocation, remove extension
Removed the ToInvokeCommand extension method and now construct InvokeRecActionViewCommand instances directly in the batch handler. Also added an unused using directive in InvokeBatchRecActionViewsCommand.cs.
2026-03-02 13:11:50 +01:00
23246d4ebf Remove Invoked filter from ReadRecActionViewQuery
Previously, only uninvoked actions were fetched by applying Invoked = false in ReadRecActionViewQuery. Now, the filter is removed to retrieve all actions for the given ProfileId, regardless of their Invoked status.
2026-03-02 10:44:04 +01:00
27d731a5b0 Bump version to 2.0.0-beta in ReC.API.csproj
Updated <Version>, <AssemblyVersion>, <FileVersion>, and <InformationalVersion> fields from 1.0.3-beta to 2.0.0-beta to reflect a major version change, indicating significant updates or breaking changes in the project.
2026-01-22 10:41:26 +01:00
b8f797f14d Make EndpointAuthType non-nullable with default NoAuth
Changed EndpointAuthType in RecActionViewDto from nullable to non-nullable and set its default value to EndpointAuthType.NoAuth to ensure it always has a valid value.
2026-01-22 10:37:37 +01:00
6feef53733 Update namespaces and usings for Insert procedures
Refactored InsertActionProcedure to the RecActions.Commands namespace and updated its usings to import InsertObjectProcedure. Added missing using for RecActions.Commands in InsertObjectProcedure.cs. These changes improve code organization and clarify command responsibilities.
2026-01-22 10:17:08 +01:00
878e096c57 Refactor TypeId to use RestType enum in InsertActionProcedure
Changed TypeId from byte? to RestType? in InsertActionProcedure for stronger typing. Updated InsertObjectProcedureHandler to cast RestType to byte? when creating SQL parameters. Added using directive for ReC.Domain.Constants to support the new enum type.
2026-01-22 09:55:35 +01:00
2ded140ad5 Rename SqlExceptionNumber to BadRequestSqlExceptionNumber
Clarify intent by renaming SqlExceptionNumber to BadRequestSqlExceptionNumber in both configuration and code. This makes it explicit that these SQL exception numbers are mapped to HTTP 400 Bad Request errors. All relevant usages and settings have been updated accordingly.
2026-01-22 09:33:54 +01:00
e2ca249d13 Add SQL exception 50000 to handled exception numbers
Updated appsettings.json to include 50000 in the SqlExceptionNumber array, allowing the application to recognize and handle SQL exceptions with this number in addition to existing ones. This change can be applied at runtime without requiring a restart.
2026-01-22 09:31:41 +01:00
e782eab62a Refactor handlers to use IOptionsMonitor for SQL options
Refactored Delete, Insert, and Update procedure handlers to inject IOptionsMonitor<SqlExceptionOptions> instead of SqlExceptionOptions, enabling dynamic configuration updates. Updated all references to use CurrentValue. Added necessary using directives and cleaned up redundant usings in InsertObjectProcedure.cs.
2026-01-22 09:21:39 +01:00
d8e08b237d Simplify SqlException config structure in appsettings.json
Removed the "ErrorMessages" wrapper from the SqlException section,
placing "SqlExceptionNumber" directly under "SqlException".
No changes to the exception numbers themselves. Improved
readability and clarity of the configuration.
2026-01-22 02:04:52 +01:00
Developer 02
88cb1dc16a Handle SqlException in UpdateObjectProcedureHandler
Wrap stored procedure execution in try-catch to handle SqlException.
Throw BadRequestException for configured SQL error numbers.
Update constructor to accept SqlExceptionOptions.
Add necessary using directives for new exception handling.
2026-01-22 01:54:07 +01:00
Developer 02
8d6a09213e Simplify BadRequestException error message on insert failure
Replaced the detailed error message for insert failures with only the original exception message from SqlException, removing extra guidance about referenced entities.
2026-01-22 01:53:55 +01:00
Developer 02
54f412ced2 Improve error handling in DeleteObjectProcedureHandler
Refactored DeleteObjectProcedureHandler to inject SqlExceptionOptions and wrap stored procedure execution in a try-catch block. Added logic to throw custom exceptions (DeleteObjectFailedException, BadRequestException) based on result codes and SQL exception numbers, enhancing robustness and configurability of error handling.
2026-01-22 01:53:40 +01:00
Developer 02
51b9c62188 Add SqlException config options and update dependencies
Added ConfigureSqlException methods to DependencyInjection for flexible SQL exception handling configuration. Updated RecApplicationTestBase to use new options. Bumped DigitalData.Core.Exceptions to v1.1.1.
2026-01-22 01:49:59 +01:00
Developer 02
bb5525778d Improve error handling in InsertObjectProcedureHandler
Wrap SQL execution in try-catch and inject SqlExceptionOptions.
Throw BadRequestException with a clear message when insert fails due to missing referenced entities, based on SqlException number. Other SQL exceptions are rethrown. This provides better feedback for foreign key/reference errors.
2026-01-22 01:49:13 +01:00
Developer 02
ee793632df Add SQL exception handling configuration support
Updated Program.cs to configure SQL exception handling using the new "SqlException" section in appsettings.json. Renamed "SqlExceptionTranslator" to "SqlException" and added error message mappings for specific SQL exception numbers to enable custom exception translation.
2026-01-22 01:46:46 +01:00
Developer 02
22bb585f60 Refactor SQL exception handling and config structure
Simplify SQL exception tracking by replacing error message mappings
with a list of relevant error numbers in appsettings.json. Remove
custom error message logic and related classes, introducing
SqlExceptionOptions to hold tracked error codes.
2026-01-22 01:45:50 +01:00
Developer 02
ed7237c8dd Add SqlExceptionTranslator config and options class
Introduce SqlExceptionTranslator section in appsettings.json to map common SQL error codes to user-friendly messages. Add SqlExceptionTranslatorOptions class to manage error code/message mapping and support configuration binding.
2026-01-22 01:13:02 +01:00
Developer 02
f71bcf37e9 Add SqlExceptionTranslator for custom SQL error handling
Introduced SqlExceptionTranslator and ISqlExceptionTranslator to translate SQL exceptions into user-friendly error messages for bad request scenarios. Uses configurable options to identify relevant error numbers and format messages with templates.
2026-01-22 00:34:35 +01:00
Developer 02
c7e366af60 Refactor ResultController ctor, enhance BadRequest logging
Removed IConfiguration from ResultController constructor, now only using IMediator. Added logging for BadRequestException inner exceptions in ExceptionHandlingMiddleware for improved error diagnostics.
2026-01-21 22:24:41 +01:00
86f4e3141e Remove all 'fake' profile endpoints from controllers
Removed API endpoints for invoking, retrieving, and deleting RecActions and Results associated with fake/test profiles in RecActionController and ResultController. Only standard CRUD operations for real profiles are now exposed.
2026-01-21 12:06:19 +01:00
869ba9858f Update ProfileController to use CreatedAtAction in Post
Changed the Post method to return CreatedAtAction, providing a Location header linking to the new profile resource, in line with RESTful best practices.
2026-01-20 16:38:53 +01:00
5dee104377 Handle SQL exceptions in InsertActionProcedure test
Wrap InsertActionProcedure_runs_via_mediator in a try-catch block to handle SQL exceptions for duplicate key and foreign key violations, marking the test as passed with explanatory messages. Also, add EndpointId to the test data and improve assertion clarity. This increases test robustness against common DB constraint issues in integration testing.
2026-01-20 16:22:30 +01:00
36e1d5fad1 Update test assertions to allow any non-default int result
Previously, tests asserted that procedure results were exactly 0.
Now, assertions require only that results are not the default int
value, making the tests more flexible to non-zero outcomes.
2026-01-20 16:10:40 +01:00
304490d661 Add explicit @RC handling in SQL for delete/update procs
Updated DeleteObjectProcedureHandler and UpdateObjectProcedureHandler to declare, initialize, and select the @RC variable in their SQL command strings. This ensures the stored procedure return code is explicitly captured and returned by the query.
2026-01-20 15:39:54 +01:00
126 changed files with 2548 additions and 1633 deletions

View File

@@ -25,7 +25,7 @@ public class CommonController(IMediator mediator) : ControllerBase
}
[HttpDelete]
public async Task<IActionResult> DeleteObject([FromBody] DeleteObjectProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> DeleteObject([FromQuery] DeleteObjectProcedure procedure, CancellationToken cancel)
{
var result = await mediator.Send(procedure, cancel);
return Ok(result);

View File

@@ -1,15 +1,13 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using ReC.Application.EndpointAuth.Commands;
namespace ReC.API.Controllers;
[Route("api/[controller]")]
[ApiController]
public class EndpointAuthController(IMediator mediator, IConfiguration config) : ControllerBase
public class EndpointAuthController(IMediator mediator) : ControllerBase
{
/// <summary>
/// Inserts an endpoint authentication record via the ENDPOINT_AUTH insert procedure.
@@ -19,38 +17,38 @@ public class EndpointAuthController(IMediator mediator, IConfiguration config) :
/// <returns>The created ENDPOINT_AUTH identifier.</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Post([FromBody] InsertEndpointAuthProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Post([FromBody] InsertEndpointAuthCommand procedure, CancellationToken cancel)
{
var id = await mediator.ExecuteInsertProcedure(procedure, config["AddedWho"], cancel);
var id = await mediator.Send(procedure, cancel);
return StatusCode(StatusCodes.Status201Created, id);
}
/// <summary>
/// Updates an endpoint authentication record via the ENDPOINT_AUTH update procedure.
/// </summary>
/// <param name="id">ENDPOINT_AUTH identifier to update.</param>
/// <param name="procedure">UpdateEndpointAuthProcedure payload.</param>
/// <param name="id">The identifier of the ENDPOINT_AUTH record to update.</param>
/// <param name="data">UpdateEndpointAuthProcedure payload.</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>No content on success.</returns>
[HttpPut("{id:long}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Put([FromRoute] long id, [FromBody] UpdateEndpointAuthProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Put([FromRoute] long id, [FromBody] UpdateEndpointAuthDto data, CancellationToken cancel)
{
await mediator.ExecuteUpdateProcedure(procedure, id, cancel: cancel);
await mediator.Send(new UpdateEndpointAuthCommand() { Id = id, Data = data}, cancel);
return NoContent();
}
/// <summary>
/// Deletes endpoint authentication records via the ENDPOINT_AUTH delete procedure for the specified id range.
/// </summary>
/// <param name="procedure">DeleteEndpointAuthProcedure payload (Start, End, Force).</param>
/// <param name="command">DeleteEndpointAuthProcedure payload (Start, End, Force).</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>No content on success.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Delete([FromBody] DeleteEndpointAuthProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Delete([FromQuery] DeleteEndpointAuthCommand command, CancellationToken cancel)
{
await mediator.ExecuteDeleteProcedure(procedure, cancel);
await mediator.Send(command, cancel);
return NoContent();
}
}

View File

@@ -1,15 +1,13 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using ReC.Application.EndpointParams.Commands;
namespace ReC.API.Controllers;
[Route("api/[controller]")]
[ApiController]
public class EndpointParamsController(IMediator mediator, IConfiguration config) : ControllerBase
public class EndpointParamsController(IMediator mediator) : ControllerBase
{
/// <summary>
/// Inserts endpoint parameter records via the ENDPOINT_PARAMS insert procedure.
@@ -19,38 +17,38 @@ public class EndpointParamsController(IMediator mediator, IConfiguration config)
/// <returns>The created ENDPOINT_PARAMS identifier.</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Post([FromBody] InsertEndpointParamsProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Post([FromBody] InsertEndpointParamsCommand procedure, CancellationToken cancel)
{
var id = await mediator.ExecuteInsertProcedure(procedure, config["AddedWho"], cancel);
var id = await mediator.Send(procedure, cancel);
return StatusCode(StatusCodes.Status201Created, id);
}
/// <summary>
/// Updates endpoint parameter records via the ENDPOINT_PARAMS update procedure.
/// </summary>
/// <param name="id">ENDPOINT_PARAMS identifier to update.</param>
/// <param name="procedure">UpdateEndpointParamsProcedure payload.</param>
/// <param name="id">The identifier of the ENDPOINT_PARAMS record to update.</param>
/// <param name="data">UpdateEndpointParamsProcedure payload.</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>No content on success.</returns>
[HttpPut("{id:long}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Put([FromRoute] long id, [FromBody] UpdateEndpointParamsProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Put([FromRoute] long id, [FromBody] UpdateEndpointParamsDto data, CancellationToken cancel)
{
await mediator.ExecuteUpdateProcedure(procedure, id, cancel: cancel);
await mediator.Send(new UpdateEndpointParamsCommand() { Id = id, Data = data }, cancel);
return NoContent();
}
/// <summary>
/// Deletes endpoint parameter records via the ENDPOINT_PARAMS delete procedure for the specified id range.
/// </summary>
/// <param name="procedure">DeleteEndpointParamsProcedure payload (Start, End, Force).</param>
/// <param name="command">DeleteEndpointParamsProcedure payload (Start, End, Force).</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>No content on success.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Delete([FromBody] DeleteEndpointParamsProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Delete([FromQuery] DeleteEndpointParamsCommand command, CancellationToken cancel)
{
await mediator.ExecuteDeleteProcedure(procedure, cancel);
await mediator.Send(command, cancel);
return NoContent();
}
}

View File

@@ -1,15 +1,13 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using ReC.Application.Endpoints.Commands;
namespace ReC.API.Controllers;
[Route("api/[controller]")]
[ApiController]
public class EndpointsController(IMediator mediator, IConfiguration config) : ControllerBase
public class EndpointsController(IMediator mediator) : ControllerBase
{
/// <summary>
/// Inserts an endpoint via the ENDPOINT insert procedure.
@@ -19,38 +17,38 @@ public class EndpointsController(IMediator mediator, IConfiguration config) : Co
/// <returns>The created ENDPOINT identifier.</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Post([FromBody] InsertEndpointProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Post([FromBody] InsertEndpointCommand procedure, CancellationToken cancel)
{
var id = await mediator.ExecuteInsertProcedure(procedure, config["AddedWho"], cancel);
var id = await mediator.Send(procedure, cancel);
return StatusCode(StatusCodes.Status201Created, id);
}
/// <summary>
/// Updates an endpoint via the ENDPOINT update procedure.
/// </summary>
/// <param name="id">ENDPOINT identifier to update.</param>
/// <param name="procedure">UpdateEndpointProcedure payload.</param>
/// <param name="id">The identifier of the ENDPOINT record to update.</param>
/// <param name="data">UpdateEndpointProcedure payload.</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>No content on success.</returns>
[HttpPut("{id:long}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Put([FromRoute] long id, [FromBody] UpdateEndpointProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Put([FromRoute] long id, [FromBody] UpdateEndpointDto data, CancellationToken cancel)
{
await mediator.ExecuteUpdateProcedure(procedure, id, cancel: cancel);
await mediator.Send(new UpdateEndpointCommand() { Id = id, Data = data }, cancel);
return NoContent();
}
/// <summary>
/// Deletes endpoints via the ENDPOINT delete procedure for the specified id range.
/// </summary>
/// <param name="procedure">DeleteEndpointProcedure payload (Start, End, Force).</param>
/// <param name="command">DeleteEndpointProcedure payload (Start, End, Force).</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>No content on success.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Delete([FromBody] DeleteEndpointProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Delete([FromQuery] DeleteEndpointCommand command, CancellationToken cancel)
{
await mediator.ExecuteDeleteProcedure(procedure, cancel);
await mediator.Send(command, cancel);
return NoContent();
}
}
}

View File

@@ -1,8 +1,6 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using ReC.Application.Profile.Commands;
using ReC.Application.Profile.Queries;
@@ -26,38 +24,38 @@ public class ProfileController(IMediator mediator) : ControllerBase
/// <returns>The created profile identifier.</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Post([FromBody] InsertProfileProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Post([FromBody] InsertProfileCommand procedure, CancellationToken cancel)
{
var id = await mediator.ExecuteInsertProcedure(procedure, cancel: cancel);
return StatusCode(StatusCodes.Status201Created, id);
var id = await mediator.Send(procedure, cancel);
return CreatedAtAction(nameof(Get), new { id }, id);
}
/// <summary>
/// Updates a profile via the PROFILE update procedure.
/// </summary>
/// <param name="id">Profile identifier to update.</param>
/// <param name="procedure">UpdateProfileProcedure payload.</param>
/// <param name="id">The identifier of the PROFILE record to update.</param>
/// <param name="data">UpdateProfileProcedure payload.</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>No content on success.</returns>
[HttpPut("{id:long}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Put([FromRoute] long id, [FromBody] UpdateProfileProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Put([FromRoute] long id, [FromBody] UpdateProfileDto data, CancellationToken cancel)
{
await mediator.ExecuteUpdateProcedure(procedure, id, cancel: cancel);
await mediator.Send(new UpdateProfileCommand() { Id = id, Data = data }, cancel);
return NoContent();
}
/// <summary>
/// Deletes profile records via the PROFILE delete procedure for the specified id range.
/// </summary>
/// <param name="procedure">DeleteProfileProcedure payload (Start, End, Force).</param>
/// <param name="command">DeleteProfileProcedure payload (Start, End, Force).</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>No content on success.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Delete([FromBody] DeleteProfileProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Delete([FromQuery] DeleteProfileCommand command, CancellationToken cancel)
{
await mediator.ExecuteDeleteProcedure(procedure, cancel);
await mediator.Send(command, cancel);
return NoContent();
}
}

View File

@@ -1,9 +1,6 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ReC.API.Extensions;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using ReC.Application.RecActions.Commands;
using ReC.Application.RecActions.Queries;
@@ -11,32 +8,24 @@ namespace ReC.API.Controllers;
[Route("api/[controller]")]
[ApiController]
public class RecActionController(IMediator mediator, IConfiguration config) : ControllerBase
public class RecActionController(IMediator mediator) : ControllerBase
{
/// <summary>
/// Invokes a batch of RecActions for a given profile.
/// </summary>
/// <param name="profileId">The ID of the profile.</param>
/// <param name="profileId">The identifier of the profile whose RecActions should be invoked.</param>
/// <param name="references">Optional reference values that are passed through to all result records.</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/{profileId}")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
public async Task<IActionResult> Invoke([FromRoute] int profileId, CancellationToken cancel)
public async Task<IActionResult> Invoke([FromRoute] long profileId, [FromBody] InvokeReferences references, CancellationToken cancel = default)
{
await mediator.InvokeBatchRecActionView(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.InvokeBatchRecActionView(config.GetFakeProfileId(), cancel);
await mediator.Send(new InvokeBatchRecActionViewsCommand
{
ProfileId = profileId,
References = references
}, cancel);
return Accepted();
}
@@ -51,20 +40,6 @@ public class RecActionController(IMediator mediator, IConfiguration config) : Co
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Get([FromQuery] ReadRecActionViewQuery 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")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Get(CancellationToken cancel, [FromQuery] bool invoked = false) => Ok(await mediator.Send(new ReadRecActionViewQuery()
{
ProfileId = config.GetFakeProfileId(),
Invoked = invoked
}, cancel));
/// <summary>
/// Creates a new RecAction.
/// </summary>
@@ -73,9 +48,9 @@ public class RecActionController(IMediator mediator, IConfiguration config) : Co
/// <returns>An HTTP 201 Created response.</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Create([FromBody] InsertActionProcedure command, CancellationToken cancel)
public async Task<IActionResult> Create([FromBody] InsertActionCommand command, CancellationToken cancel)
{
await mediator.ExecuteInsertProcedure(command, config["AddedWho"], cancel);
await mediator.Send(command, cancel);
return StatusCode(StatusCodes.Status201Created);
}
@@ -83,48 +58,29 @@ public class RecActionController(IMediator mediator, IConfiguration config) : Co
/// <summary>
/// Updates a RecAction via the ACTION update procedure.
/// </summary>
/// <param name="id">RecAction identifier to update.</param>
/// <param name="procedure">UpdateActionProcedure payload.</param>
/// <param name="id">The identifier of the ACTION record to update.</param>
/// <param name="data">UpdateActionProcedure payload.</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>No content on success.</returns>
[HttpPut("{id:long}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Update([FromRoute] long id, [FromBody] UpdateActionProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Update([FromRoute] long id, [FromBody] UpdateActionDto data, CancellationToken cancel)
{
await mediator.ExecuteUpdateProcedure(procedure, id, cancel: cancel);
await mediator.Send(new UpdateActionCommand() { Id = id, Data = data }, cancel);
return NoContent();
}
/// <summary>
/// Deletes RecActions via the ACTION delete procedure for the specified id range.
/// </summary>
/// <param name="procedure">DeleteActionProcedure payload (Start, End, Force).</param>
/// <param name="command">DeleteActionProcedure payload (Start, End, Force).</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>An HTTP 204 No Content response upon successful deletion.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Delete([FromBody] DeleteActionProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Delete([FromQuery] DeleteActionCommand command, CancellationToken cancel)
{
await mediator.ExecuteDeleteProcedure(procedure, cancel);
return NoContent();
}
/// <summary>
/// Deletes RecActions for a fake/test profile via the ACTION delete procedure.
/// </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.ExecuteDeleteProcedure(new DeleteActionProcedure
{
Start = config.GetFakeProfileId(),
End = config.GetFakeProfileId(),
Force = false
}, cancel);
await mediator.Send(command, cancel);
return NoContent();
}
#endregion CRUD

View File

@@ -1,10 +1,6 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ReC.API.Extensions;
using ReC.API.Models;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using ReC.Application.Results.Commands;
using ReC.Application.Results.Queries;
@@ -12,7 +8,7 @@ namespace ReC.API.Controllers;
[Route("api/[controller]")]
[ApiController]
public class ResultController(IMediator mediator, IConfiguration config) : ControllerBase
public class ResultController(IMediator mediator) : ControllerBase
{
/// <summary>
/// Gets output results based on the provided query parameters.
@@ -24,43 +20,6 @@ public class ResultController(IMediator mediator, IConfiguration config) : Contr
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Get([FromQuery] ReadResultViewQuery 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 ReadResultViewQuery()
{
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, OnlyHeader, or OnlyBody).</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 ReadResultViewQuery()
{
ProfileId = config.GetFakeProfileId(),
ActionId = actionId
}, cancel)).First();
return resultType switch
{
ResultType.OnlyBody => res.Body is null ? NotFound() : Ok(res.Body.JsonToDynamic()),
ResultType.OnlyHeader => res.Header is null ? NotFound() : Ok(res.Header.JsonToDynamic()),
_ => Ok(res),
};
}
/// <summary>
/// Inserts a RESULT record via the insert procedure.
/// </summary>
@@ -69,58 +28,38 @@ public class ResultController(IMediator mediator, IConfiguration config) : Contr
/// <returns>The created RESULT identifier.</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Post([FromBody] InsertResultProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Post([FromBody] InsertResultCommand procedure, CancellationToken cancel)
{
var id = await mediator.ExecuteInsertProcedure(procedure, cancel: cancel);
var id = await mediator.Send(procedure, cancel);
return CreatedAtAction(nameof(Get), new { actionId = procedure.ActionId }, new { id, procedure.ActionId });
}
/// <summary>
/// Updates a RESULT record via the update procedure.
/// </summary>
/// <param name="id">RESULT identifier to update.</param>
/// <param name="procedure">UpdateResultProcedure payload.</param>
/// <param name="id">The identifier of the RESULT record to update.</param>
/// <param name="data">UpdateResultProcedure payload.</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>No content on success.</returns>
[HttpPut("{id:long}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Put([FromRoute] long id, [FromBody] UpdateResultProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Put([FromRoute] long id, [FromBody] UpdateResultDto data, CancellationToken cancel)
{
await mediator.ExecuteUpdateProcedure(procedure, id, cancel: cancel);
await mediator.Send(new UpdateResultCommand() { Id = id, Data = data }, cancel);
return NoContent();
}
/// <summary>
/// Deletes RESULT records via the delete procedure for the specified id range.
/// </summary>
/// <param name="procedure">DeleteResultProcedure payload (Start, End, Force).</param>
/// <param name="command">DeleteResultProcedure payload (Start, End, Force).</param>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>No content on success.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Delete([FromBody] DeleteResultProcedure procedure, CancellationToken cancel)
public async Task<IActionResult> Delete([FromQuery] DeleteResultCommand command, CancellationToken cancel)
{
await mediator.ExecuteDeleteProcedure(procedure, cancel);
return NoContent();
}
/// <summary>
/// Deletes RESULT records for a fake/test profile via the delete procedure.
/// </summary>
/// <param name="cancel">A token to cancel the operation.</param>
/// <returns>No content on success.</returns>
[HttpDelete("fake")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Delete(CancellationToken cancel)
{
await mediator.ExecuteDeleteProcedure(new DeleteResultProcedure
{
Start = config.GetFakeProfileId(),
End = config.GetFakeProfileId(),
Force = false
}, cancel);
await mediator.Send(command, cancel);
return NoContent();
}
}

View File

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

View File

@@ -7,13 +7,11 @@ 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;
@@ -63,6 +61,13 @@ public class ExceptionHandlingMiddleware
switch (exception)
{
case BadRequestException badRequestEx:
if (badRequestEx.InnerException is not null)
{
logger.LogError(
badRequestEx.InnerException,
"BadRequestException inner exception captured.");
}
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
details = new()
{
@@ -157,6 +162,22 @@ public class ExceptionHandlingMiddleware
};
break;
case RecActionException recActionEx:
logger.LogWarning(
recActionEx,
"Rec action failed. ActionId: {ActionId}, ProfileId: {ProfileId}",
recActionEx.ActionId,
recActionEx.ProfileId);
context.Response.StatusCode = (int)HttpStatusCode.UnprocessableEntity;
details = new()
{
Title = "Rec Action Failed",
Detail = recActionEx.InnerException?.Message
?? "An error occurred while executing the rec action. Check the logs for more details."
};
break;
default:
logger.LogError(exception, "Unhandled exception occurred.");
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

View File

@@ -1,8 +0,0 @@
namespace ReC.API.Models;
public class FakeRequest
{
public Dictionary<string, object>? Body { get; init; }
public Dictionary<string, object>? Header { get; init; }
}

View File

@@ -1,17 +0,0 @@
namespace ReC.API.Models;
public enum ResultType
{
/// <summary>
/// Return the full result object.
/// </summary>
Full,
/// <summary>
/// Return only the header part of the result.
/// </summary>
OnlyHeader,
/// <summary>
/// Return only the body part of the result.
/// </summary>
OnlyBody
}

View File

@@ -37,6 +37,7 @@ try
{
options.LuckyPennySoftwareLicenseKey = builder.Configuration["LuckyPennySoftwareLicenseKey"];
options.ConfigureRecActions(config.GetSection("RecAction"));
options.ConfigureSqlException(config.GetSection("SqlException"));
});
builder.Services.AddRecInfrastructure(options =>
@@ -69,9 +70,7 @@ try
var app = builder.Build();
#pragma warning disable CS0618
app.UseMiddleware<ExceptionHandlingMiddleware>();
#pragma warning restore CS0618
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment() || config.GetValue<bool>("UseSwagger"))

View File

@@ -9,7 +9,7 @@
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<ExcludeApp_Data>false</ExcludeApp_Data>
<ProjectGuid>420218ad-3c27-4003-9a84-36c92352f175</ProjectGuid>
<DesktopBuildPackageLocation>P:\Install .Net\0 DD - Smart UP\ReC\API\$(Version)\Rec.API.zip</DesktopBuildPackageLocation>
<DesktopBuildPackageLocation>M:\App&amp;Service\0 DD - Smart UP\ReC\API\$(Version)\Rec.API.zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>Rec.API</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>

View File

@@ -10,13 +10,14 @@
<Product>ReC.API</Product>
<PackageIcon>Assets\icon.ico</PackageIcon>
<PackageTags>digital data rest-caller rec api</PackageTags>
<Version>1.0.3-beta</Version>
<AssemblyVersion>1.0.3.0</AssemblyVersion>
<FileVersion>1.0.3.0</FileVersion>
<InformationalVersion>1.0.3-beta</InformationalVersion>
<Version>2.2.1-beta</Version>
<AssemblyVersion>2.2.1.0</AssemblyVersion>
<FileVersion>2.2.1.0</FileVersion>
<InformationalVersion>2.2.0-beta</InformationalVersion>
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
<UserSecretsId>cf893b96-c71a-4a96-a6a7-40004249e1a3</UserSecretsId>
</PropertyGroup>
<ItemGroup>

View File

@@ -6,8 +6,12 @@
"AllowedHosts": "*",
"LuckyPennySoftwareLicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzg0ODUxMjAwIiwiaWF0IjoiMTc1MzM2MjQ5MSIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazB5NmV3MmQ4YTk4Mzg3aDJnbTRuOWswIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.ZqsFG7kv_-xGfxS6ACk3i0iuNiVUXX2AvPI8iAcZ6-z2170lGv__aO32tWpQccD9LCv5931lBNLWSblKS0MT3gOt-5he2TEftwiSQGFwoIBgtOHWsNRMinUrg2trceSp3IhyS3UaMwnxZDrCvx4-0O-kpOzVpizeHUAZNr5U7oSCWO34bpKdae6grtM5e3f93Z1vs7BW_iPgItd-aLvPwApbaG9VhmBTKlQ7b4Jh64y7UXJ9mKP7Qb_Oa97oEg0oY5DPHOWTZWeE1EzORgVr2qkK2DELSHuZ_EIUhODojkClPNAKtvEl_qEjpq0HZCIvGwfCCRlKlSkQqIeZdFkiXg",
"RecAction": {
"MaxConcurrentInvocations": 5
"AddedWho": "ReC.API",
"UseHttp1ForNtlm": false
},
"AddedWho": "ReC.API",
"FakeProfileId": 2
// Bad request SqlException numbers numbers can be updated at runtime; no restart required.
"SqlException": {
// https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlexception.number
"BadRequestSqlExceptionNumbers": [ 515, 547, 2601, 2627, 50000 ]
}
}

View File

@@ -0,0 +1,54 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using ReC.Application.Common.Dto;
using ReC.Application.Common.Exceptions;
using ReC.Application.Common.Interfaces;
namespace ReC.Application.Common.Behaviors.Action;
public class BodyQueryBehavior<TRequest, TResponse>(IRecDbContext dbContext) : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
where TResponse : IEnumerable<RecActionViewDto>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancel)
{
var actions = await next(cancel);
foreach (var action in actions)
await SetBody(action, cancel);
return actions;
}
private async Task SetBody(RecActionViewDto action, CancellationToken cancel)
{
if (action.BodyQuery is not string bodyQuery)
return;
await using var command = dbContext.Database.GetDbConnection().CreateCommand();
command.CommandText = bodyQuery;
await dbContext.Database.OpenConnectionAsync(cancel);
try
{
object? scalar;
try
{
scalar = await command.ExecuteScalarAsync(cancel);
}
catch (Exception ex)
{
throw new DataIntegrityException(
$"Body query execution failed. The stored SQL may be malformed. ActionId: {action.Id}, ProfileId: {action.ProfileId}, Error: {ex.Message}");
}
action.Body = scalar as string
?? throw new DataIntegrityException(
$"Body query returned no result or a null value. ActionId: {action.Id}, ProfileId: {action.ProfileId}");
}
finally
{
await dbContext.Database.CloseConnectionAsync();
}
}
}

View File

@@ -0,0 +1,61 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using ReC.Application.Common.Dto;
using ReC.Application.Common.Exceptions;
using ReC.Application.Common.Interfaces;
using System.Text.Json;
namespace ReC.Application.Common.Behaviors.Action;
public class HeaderQueryBehavior<TRequest, TResponse>(IRecDbContext dbContext) : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
where TResponse : IEnumerable<RecActionViewDto>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancel)
{
var actions = await next(cancel);
foreach (var action in actions)
await SetHeader(action, cancel);
return actions;
}
private async Task SetHeader(RecActionViewDto action, CancellationToken cancel)
{
if (action.HeaderQuery is not string headerQuery)
return;
await using var command = dbContext.Database.GetDbConnection().CreateCommand();
command.CommandText = headerQuery;
await dbContext.Database.OpenConnectionAsync(cancel);
try
{
object? scalar;
try
{
scalar = await command.ExecuteScalarAsync(cancel);
}
catch (Exception ex)
{
throw new DataIntegrityException(
$"Header query execution failed. The stored SQL may be malformed. ActionId: {action.Id}, ProfileId: {action.ProfileId}, Error: {ex.Message}");
}
if (scalar is not string rawHeader)
throw new DataIntegrityException(
$"Header query returned no result or a null value. ActionId: {action.Id}, ProfileId: {action.ProfileId}");
var headerDict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(rawHeader)
?? throw new DataIntegrityException(
$"Header query returned invalid JSON. ActionId: {action.Id}, ProfileId: {action.ProfileId}");
action.Headers = headerDict.ToDictionary(header => header.Key, kvp => kvp.Value.ToString());
}
finally
{
await dbContext.Database.CloseConnectionAsync();
}
}
}

View File

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

View File

@@ -1,43 +0,0 @@
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<TRequest, TResponse>(IRecDbContext dbContext, ILogger<HeaderQueryBehavior<TRequest, TResponse>>? logger = null) : IPipelineBehavior<TRequest, TResponse>
where TRequest : RecActionViewDto
where TResponse : notnull
{
public async Task<TResponse> Handle(TRequest action, RequestHandlerDelegate<TResponse> next, CancellationToken cancel)
{
if (action.HeaderQuery is null)
return await next(cancel);
var result = await dbContext.HeaderQueryResults.FromSqlRaw(action.HeaderQuery).SingleOrDefaultAsync(cancel);
if (result?.RawHeader is null)
{
logger?.LogWarning("Header query did not return a result or returned a null REQUEST_HEADER. Profile ID: {ProfileId}, Action ID: {Id}", action.ProfileId, action.Id);
return await next(cancel);
}
var headerDict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(result.RawHeader);
if(headerDict is null)
{
logger?.LogWarning(
"Header JSON deserialization returned null. RawHeader: {RawHeader}, ProfileId: {ProfileId}, Id: {Id}",
result.RawHeader, action.ProfileId, action.Id);
return await next(cancel);
}
action.Headers = headerDict.ToDictionary(header => header.Key, kvp => kvp.Value.ToString());
return await next(cancel);
}
}

View File

@@ -0,0 +1,53 @@
using MediatR;
using ReC.Application.Common.Exceptions;
using ReC.Application.Common.Interfaces;
using ReC.Application.RecActions.Commands;
using ReC.Application.Results.Commands;
using ReC.Domain.Constants;
using System.Text.Json;
namespace ReC.Application.Common.Behaviors.InvokeAction;
public class PostprocessingBehavior(IRecDbContext context, ISender sender) : IPipelineBehavior<InvokeRecActionViewCommand, Unit>
{
public async Task<Unit> Handle(InvokeRecActionViewCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
{
await next(cancel);
try
{
if (request.Action.PostprocessingQuery is string query)
{
var result = await context.ExecuteDynamicSqlAsync(query, cancel);
var info = JsonSerializer.Serialize(result);
await sender.Send(new InsertResultCommand()
{
Status = RecStatus.OK,
ActionId = request.Action.Id,
InfoDetail = info,
Type = ResultType.Post,
References = request.References
}, cancel);
}
}
catch (Exception ex)
{
var error = ex.ToString();
await sender.Send(new InsertResultCommand()
{
Status = RecStatus.Error,
ActionId = request.Action.Id,
Error = error,
Type = ResultType.Post,
References = request.References
}, cancel);
if (request.Action.ErrorAction == ErrorAction.Stop)
throw new RecActionException(request.Action.Id, request.Action.ProfileId, ex);
}
return Unit.Value;
}
}

View File

@@ -0,0 +1,48 @@
using MediatR;
using ReC.Application.Common.Exceptions;
using ReC.Application.Common.Interfaces;
using ReC.Application.RecActions.Commands;
using ReC.Application.Results.Commands;
using ReC.Domain.Constants;
using System.Text.Json;
namespace ReC.Application.Common.Behaviors.InvokeAction;
public class PreprocessingBehavior(IRecDbContext context, ISender sender) : IPipelineBehavior<InvokeRecActionViewCommand, Unit>
{
public async Task<Unit> Handle(InvokeRecActionViewCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
{
try
{
if (request.Action.PreprocessingQuery is string query)
{
var result = await context.ExecuteDynamicSqlAsync(query, cancel);
await sender.Send(new InsertResultCommand()
{
Status = RecStatus.OK,
ActionId = request.Action.Id,
InfoDetail = JsonSerializer.Serialize(result),
Type = ResultType.Pre,
References = request.References
}, cancel);
}
}
catch (Exception ex)
{
await sender.Send(new InsertResultCommand()
{
Status = RecStatus.Error,
ActionId = request.Action.Id,
Error = ex.ToString(),
Type = ResultType.Pre,
References = request.References
}, cancel);
if (request.Action.ErrorAction == ErrorAction.Stop)
throw new RecActionException(request.Action.Id, request.Action.ProfileId, ex);
}
return await next(cancel);
}
}

View File

@@ -1,32 +0,0 @@
namespace ReC.Application.Common.Dto;
public record ConnectionDto
{
public short? Id { get; set; }
public string? Bezeichnung { get; set; }
public string? SqlProvider { get; set; }
public string? Server { get; set; }
public string? Datenbank { get; set; }
public string? Username { get; set; }
public string? Password { get; set; }
public string? Bemerkung { get; set; }
public bool? Aktiv { get; set; }
public string? ErstelltWer { get; set; }
public DateTime? ErstelltWann { get; set; }
public string? GeandertWer { get; set; }
public DateTime? GeaendertWann { get; set; }
public bool? SysConnection { get; set; }
}

View File

@@ -1,4 +1,4 @@
using ReC.Domain.Views;
using ReC.Domain.Views;
namespace ReC.Application.Common.Dto;
@@ -6,7 +6,12 @@ public class DtoMappingProfile : AutoMapper.Profile
{
public DtoMappingProfile()
{
CreateMap<RecActionView, RecActionViewDto>();
CreateMap<RecActionView, RecActionViewDto>()
.ForMember(dest => dest.PreprocessingQuery, opt => opt.MapFrom((src, _) =>
src.PreprocessingQuery?.ReplacePlaceholders(src)))
.ForMember(dest => dest.PostprocessingQuery, opt => opt.MapFrom((src, _) =>
src.PostprocessingQuery?.ReplacePlaceholders(src)));
CreateMap<ResultView, ResultViewDto>();
CreateMap<ProfileView, ProfileViewDto>();
}

View File

@@ -1,39 +0,0 @@
using ReC.Domain.Constants;
using System.ComponentModel.DataAnnotations.Schema;
namespace ReC.Application.Common.Dto;
public record EndpointAuthDto
{
public long? Id { get; set; }
public bool? Active { get; set; }
public string? Description { get; set; }
public EndpointAuthType? Type { get; set; }
public string? ApiKey { get; set; }
public string? ApiValue { get; set; }
public ApiKeyLocation? ApiKeyAddTo { get; set; }
public string? Token { get; set; }
public string? Username { get; set; }
public string? Password { get; set; }
public string? Domain { get; set; }
public string? Workstation { get; set; }
public string? AddedWho { get; set; }
public DateTime? AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -1,22 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace ReC.Application.Common.Dto;
public record EndpointDto
{
public long Id { get; set; }
public bool? Active { get; set; }
public string? Description { get; set; }
public string? Uri { get; set; }
public string? AddedWho { get; set; }
public DateTime? AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -1,33 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace ReC.Application.Common.Dto;
/// <summary>
/// Represents the TBREC_CFG_ENDPOINT_PARAMS table.
/// All properties are nullable to provide flexibility on the database side,
/// preventing breaking changes if columns are altered to be nullable in production.
/// </summary>
public record EndpointParamDto
{
public long? Id { get; set; }
public bool? Active { get; set; }
public string? Description { get; set; }
public short? GroupId { get; set; }
public byte? Sequence { get; set; }
public string? Key { get; set; }
public string? Value { get; set; }
public string? AddedWho { get; set; }
public DateTime? AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -1,20 +0,0 @@
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; }
}

View File

@@ -0,0 +1,60 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using System.Text.RegularExpressions;
namespace ReC.Application.Common.Dto;
public static partial class PlaceholderExtensions
{
[GeneratedRegex(@"\{#[^#]+#[^}]+\}")]
private static partial Regex PlaceholderRegex();
/// <summary>
/// Replaces placeholders in the format <c>{#ANY_STRING#COLUMN_NAME}</c> with the corresponding
/// property value resolved via <see cref="GetValueByColumnName{T}"/> from the provided objects.
/// Values are converted to SQL-compatible string representations.
/// If a placeholder cannot be resolved, it is replaced with <c>NULL</c>.
/// </summary>
public static string ReplacePlaceholders(this string str, params object?[] objects)
{
return PlaceholderRegex().Replace(str, match =>
{
var placeholder = match.Value;
var inner = placeholder[2..^1]; // remove {# and }
var lastHash = inner.LastIndexOf('#');
var columnName = inner[(lastHash + 1)..];
foreach (var obj in objects)
{
if (obj is null)
continue;
var value = obj.GetValueByColumnName(columnName);
if (value is not null)
return ToSqlLiteral(value);
}
return "NULL";
});
}
private static string ToSqlLiteral(object value) => value switch
{
bool b => b ? "TRUE" : "FALSE",
DateTime dt => dt.ToString("yyyy-MM-dd HH:mm:ss"),
DateTimeOffset dto => dto.ToString("yyyy-MM-dd HH:mm:ss zzz"),
_ => value.ToString() ?? string.Empty
};
/// <summary>
/// Gets the value of a property by its column name defined in <see cref="ColumnAttribute"/>.
/// Returns <c>null</c> if no property with the given column name exists.
/// </summary>
public static object? GetValueByColumnName<T>(this T obj, string columnName) where T : class
{
var property = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(p => p.GetCustomAttribute<ColumnAttribute>()?.Name == columnName);
return property?.GetValue(obj);
}
}

View File

@@ -1,30 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace ReC.Application.Common.Dto;
public record ProfileDto
{
public long Id { get; set; }
public bool? Active { get; set; }
public string? Type { get; set; }
public string? Mandantor { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public string? LogLevel { get; set; }
public string? Language { get; set; }
public string? AddedWho { get; set; }
public DateTime? AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -1,53 +0,0 @@
using ReC.Domain.Constants;
using System.ComponentModel.DataAnnotations.Schema;
namespace ReC.Application.Common.Dto;
public record RecActionDto
{
public long? Id { get; set; }
public long? ProfileId { get; set; }
public ProfileDto? Profile { get; set; }
public bool? Active { get; set; }
public byte? Sequence { get; set; }
public long? EndpointId { get; set; }
public EndpointDto? Endpoint { get; set; }
public long? EndpointAuthId { get; set; }
public EndpointAuthDto? EndpointAuth { get; set; }
public short? EndpointParamsId { get; set; }
public short? SqlConnectionId { get; set; }
public ConnectionDto? SqlConnection { get; set; }
public string? Type { get; set; }
public string? PreprocessingQuery { get; set; }
public string? HeaderQuery { get; set; }
public string? BodyQuery { get; set; }
public string? PostprocessingQuery { get; set; }
public ErrorAction? ErrorAction { get; set; }
public string? AddedWho { get; set; }
public DateTime? AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
public OutResDto? OutRes { get; set; }
}

View File

@@ -8,6 +8,8 @@ public record RecActionViewDto
public long? ProfileId { get; init; }
public ProfileViewDto? Profile { get; init; }
public string? ProfileName { get; init; }
public ProfileType? ProfileType { get; init; }
@@ -20,7 +22,7 @@ public record RecActionViewDto
public long? EndpointAuthId { get; init; }
public EndpointAuthType? EndpointAuthType { get; init; }
public EndpointAuthType EndpointAuthType { get; init; } = EndpointAuthType.NoAuth;
public string? EndpointAuthTypeName { get; init; }

View File

@@ -1,11 +1,11 @@
namespace ReC.Application.Common.Dto;
using ReC.Domain.Constants;
namespace ReC.Application.Common.Dto;
public record ResultViewDto
{
public long Id { get; init; }
public OutResDto? Root { get; init; }
public long? ActionId { get; init; }
public RecActionViewDto? Action { get; init; }
@@ -16,7 +16,7 @@ public record ResultViewDto
public string? ProfileName { get; init; }
public short? StatusCode { get; init; }
public RecStatus Status { get; set; }
public string? StatusName { get; init; }
@@ -24,6 +24,14 @@ public record ResultViewDto
public string? Body { get; init; }
public string? Info { get; set; }
public string? InfoDetail { get; set; }
public string? Error { get; set; }
public string? BatchId { get; set; }
public string? AddedWho { get; init; }
public DateTime? AddedWhen { get; init; }

View File

@@ -0,0 +1,9 @@
namespace ReC.Application.Common.Exceptions;
public class RecActionException(long actionId, long? profileId, Exception innerException)
: Exception($"Rec action failed. ActionId: {actionId}, ProfileId: {profileId}", innerException)
{
public long ActionId { get; } = actionId;
public long? ProfileId { get; } = profileId;
}

View File

@@ -1,29 +0,0 @@
using ReC.Domain.Constants;
using System.Diagnostics.CodeAnalysis;
namespace ReC.Application.Common;
public static class HttpExtensions
{
private static readonly Dictionary<RestType, HttpMethod> _methods = new()
{
[RestType.Get] = HttpMethod.Get,
[RestType.Post] = HttpMethod.Post,
[RestType.Put] = HttpMethod.Put,
[RestType.Delete] = HttpMethod.Delete,
[RestType.Patch] = HttpMethod.Patch,
[RestType.Head] = HttpMethod.Head,
[RestType.Options] = HttpMethod.Options,
[RestType.Trace] = HttpMethod.Trace,
[RestType.Connect] = HttpMethod.Connect
};
public static HttpMethod ToHttpMethod(this RestType method) => !method.IsValid()
? throw new ArgumentOutOfRangeException(nameof(method), $"The RestType value '{method}' is not valid.")
: _methods.TryGetValue(method, out var httpMethod)
? httpMethod
: new HttpMethod(method.ToHttpMethodName());
public static HttpRequestMessage ToHttpRequestMessage(this HttpMethod method, [StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri)
=> new(method, requestUri);
}

View File

@@ -1,6 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using ReC.Domain.QueryOutput;
using ReC.Domain.Views;
using System.Text.Json;
namespace ReC.Application.Common.Interfaces;
@@ -21,4 +23,39 @@ public interface IRecDbContext
#endregion DbSets
public Task<int> SaveChangesAsync(CancellationToken cancel = default);
public DatabaseFacade Database { get; }
}
public static class RecDbContextSaveExtensions
{
//TODO: Once it is finalized, move it to Common.Infrastructure
public static async Task<IEnumerable<Dictionary<string, object?>>> ExecuteDynamicSqlAsync(this IRecDbContext context, string sql, CancellationToken cancel = default)
{
var result = new List<Dictionary<string, object?>>();
using var command = context.Database.GetDbConnection().CreateCommand();
command.CommandText = sql;
await context.Database.OpenConnectionAsync(cancel);
using var reader = await command.ExecuteReaderAsync(cancel);
while (await reader.ReadAsync(cancel))
{
var row = new Dictionary<string, object?>();
for (int i = 0; i < reader.FieldCount; i++)
{
var columnName = reader.GetName(i);
var value = reader.IsDBNull(i) ? null : reader.GetValue(i);
row[columnName] = value;
}
result.Add(row);
}
return result;
}
}

View File

@@ -2,5 +2,5 @@
public class RecActionOptions
{
public int MaxConcurrentInvocations { get; set; } = 5;
}
public bool UseHttp1ForNtlm { get; set; } = false;
}

View File

@@ -0,0 +1,6 @@
namespace ReC.Application.Common.Options;
public class SqlExceptionOptions
{
public HashSet<int> BadRequestSqlExceptionNumbers { get; set; } = [];
}

View File

@@ -1,16 +1,19 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using MediatR;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
using ReC.Application.Common.Exceptions;
using ReC.Application.Common.Options;
namespace ReC.Application.Common.Procedures.DeleteProcedure;
public record DeleteObjectProcedure : IRequest<int>
{
/// <summary>
/// Target entity: ACTION, ENDPOINT, ENDPOINT_AUTH, ENDPOINT_PARAMS, PROFILE, RESULT
/// Target entity for the delete operation.
/// </summary>
public string Entity { get; set; } = null!;
public required EntityType Entity { get; set; }
/// <summary>
/// Start GUID/ID (inclusive)
@@ -28,50 +31,33 @@ public record DeleteObjectProcedure : IRequest<int>
public bool Force { get; set; }
}
public static class DeleteObjectProcedureExtensions
{
public static Task<int> ExecuteDeleteProcedure(this ISender sender, IDeleteProcedure procedure, CancellationToken cancel = default)
{
return sender.Send(procedure.ToObjectProcedure(), cancel);
}
public static Task<int> ExecuteDeleteProcedure(this ISender sender, string entity, long start, long end = 0, bool force = false, CancellationToken cancel = default)
{
return sender.Send(new DeleteObjectProcedure
{
Entity = entity,
Start = start,
End = end,
Force = force
}, cancel);
}
}
public class DeleteObjectProcedureHandler(IRepository repo) : IRequestHandler<DeleteObjectProcedure, int>
public class DeleteObjectProcedureHandler(IRepository repo, IOptionsMonitor<SqlExceptionOptions> sqlExOpt) : IRequestHandler<DeleteObjectProcedure, int>
{
public async Task<int> Handle(DeleteObjectProcedure request, CancellationToken cancel)
{
var parameters = new[]
{
new SqlParameter("@pENTITY", request.Entity ?? (object)DBNull.Value),
new SqlParameter("@pSTART", request.Start.ToString()),
new SqlParameter("@pEND", request.End.ToString()),
new SqlParameter("@pFORCE", (object?)request.Force ?? DBNull.Value)
};
var sp = new StoredProcedureBuilder("[dbo].[PRREC_DELETE_OBJECT]", "RC")
.Add("pENTITY", request.Entity)
.Add("pSTART", request.Start.ToString())
.Add("pEND", request.End.ToString())
.Add("pFORCE", request.Force);
var result = await repo.ExecuteQueryRawAsync(
"EXEC @RC = [dbo].[PRREC_DELETE_OBJECT] " +
"@pENTITY, @pSTART, @pEND, @pFORCE; " +
"SELECT @RC;",
parameters,
cancel);
// The stored procedure returns 0 on success, error codes > 0 on failure
if (result > 0)
try
{
throw new DeleteObjectFailedException(request, $"DeleteObject stored procedure failed with error code: {result}");
var result = await repo.ExecuteQueryRawAsync(sp.BuildSql(), sp.BuildParameters(), cancel);
if (result > 0)
{
throw new DeleteObjectFailedException(request, $"DeleteObject stored procedure failed with error code: {result}");
}
return result;
}
catch (SqlException ex)
{
if (sqlExOpt.CurrentValue.BadRequestSqlExceptionNumbers.Contains(ex.Number))
throw new BadRequestException(ex.Message, ex);
else
throw;
}
return result;
}
}

View File

@@ -1,6 +1,7 @@
using MediatR;
namespace ReC.Application.Common.Procedures.DeleteProcedure;
public interface IDeleteProcedure
public interface IDeleteProcedure : IRequest<int>
{
public DeleteObjectProcedure ToObjectProcedure();
}

View File

@@ -0,0 +1,51 @@
namespace ReC.Application.Common.Procedures;
/// <summary>
/// Represents the target entity type for stored procedure operations (Insert, Update, Delete).
/// </summary>
public enum EntityType
{
/// <summary>
/// A scheduled or configured action within a profile that invokes an endpoint (maps to TBREC_CFG_ACTION).
/// </summary>
Action,
/// <summary>
/// A REST endpoint URI configuration (maps to TBREC_CFG_ENDPOINT).
/// </summary>
Endpoint,
/// <summary>
/// Authentication credentials for an endpoint such as API key, Bearer token, or NTLM (maps to TBREC_CFG_ENDPOINT_AUTH).
/// </summary>
EndpointAuth,
/// <summary>
/// Key-value parameters attached to an endpoint (maps to TBREC_CFG_ENDPOINT_PARAMS).
/// </summary>
EndpointParams,
/// <summary>
/// A profile that groups one or more actions and defines execution settings (maps to TBREC_CFG_PROFILE).
/// </summary>
Profile,
/// <summary>
/// The outcome of an action invocation including HTTP status, headers, body, and error details (maps to TBREC_OUT_RESULT).
/// </summary>
Result
}
public static class EntityTypeExtensions
{
public static string ToDbString(this EntityType entityType) => entityType switch
{
EntityType.Action => "ACTION",
EntityType.Endpoint => "ENDPOINT",
EntityType.EndpointAuth => "ENDPOINT_AUTH",
EntityType.EndpointParams => "ENDPOINT_PARAMS",
EntityType.Profile => "PROFILE",
EntityType.Result => "RESULT",
_ => throw new ArgumentOutOfRangeException(nameof(entityType), $"Not expected entity type value: {entityType}")
};
}

View File

@@ -1,6 +1,7 @@
namespace ReC.Application.Common.Procedures.InsertProcedure;
using MediatR;
public interface IInsertProcedure
namespace ReC.Application.Common.Procedures.InsertProcedure;
public interface IInsertProcedure : IRequest<long>
{
public InsertObjectProcedure ToObjectProcedure(string? addedWho = null);
}

View File

@@ -1,134 +1,120 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using MediatR;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
using ReC.Application.Common.Exceptions;
using ReC.Application.Common.Options;
using ReC.Application.EndpointAuth.Commands;
using ReC.Application.EndpointParams.Commands;
using ReC.Application.Endpoints.Commands;
using ReC.Application.Results.Commands;
using ReC.Application.Profile.Commands;
using ReC.Application.RecActions.Commands;
using ReC.Application.Results.Commands;
using System.Data;
namespace ReC.Application.Common.Procedures.InsertProcedure;
public record InsertObjectProcedure : IRequest<long>
{
/// <summary>
/// Target entity: ACTION, ENDPOINT, ENDPOINT_AUTH, ENDPOINT_PARAMS, PROFILE, RESULT
/// Target entity for the insert operation.
/// </summary>
public string Entity { get; set; } = null!;
public required EntityType Entity { get; set; }
internal string? AddedWho { get; private set; }
//TODO: update to set in authentication middleware or similar, and remove from procedure properties
internal string? AddedWho { get; private set; } = "ReC.API";
public InsertObjectProcedure AddedBy(string? addedWho = null)
{
AddedWho = addedWho ?? "ReC.API";
return this;
}
public InsertActionProcedure Action { get; set; } = new();
public InsertEndpointProcedure Endpoint { get; set; } = new();
public InsertEndpointAuthProcedure EndpointAuth { get; set; } = new();
public InsertProfileProcedure Profile { get; set; } = new();
public InsertResultProcedure Result { get; set; } = new();
public InsertEndpointParamsProcedure EndpointParams { get; set; } = new();
public InsertActionCommand? Action { get; set; }
public InsertEndpointCommand? Endpoint { get; set; }
public InsertEndpointAuthCommand? EndpointAuth { get; set; }
public InsertProfileCommand? Profile { get; set; }
public InsertResultCommand? Result { get; set; }
public InsertEndpointParamsCommand? EndpointParams { get; set; }
}
public static class InsertObjectProcedureExtensions
{
public static Task<long> ExecuteInsertProcedure(this ISender sender, IInsertProcedure procedure, string? addedWho = null, CancellationToken cancel = default)
{
return sender.Send(procedure.ToObjectProcedure(addedWho ?? "Rec.API"), cancel);
}
}
public class InsertObjectProcedureHandler(IRepository repo) : IRequestHandler<InsertObjectProcedure, long>
public class InsertObjectProcedureHandler(IRepository repo, IOptionsMonitor<SqlExceptionOptions> sqlExOpt) : IRequestHandler<InsertObjectProcedure, long>
{
public async Task<long> Handle(InsertObjectProcedure request, CancellationToken cancel)
{
var parameters = new[]
var sp = new StoredProcedureBuilder("[dbo].[PRREC_INSERT_OBJECT]")
.Add("pENTITY", request.Entity)
.Add("pADDED_WHO", request.AddedWho)
.Add("pADDED_WHEN", DateTime.UtcNow)
.Add("pACTION_PROFILE_ID", request.Action?.ProfileId)
.Add("pACTION_ACTIVE", request.Action?.Active)
.Add("pACTION_SEQUENCE", request.Action?.Sequence, SqlDbType.TinyInt)
.Add("pACTION_ENDPOINT_ID", request.Action?.EndpointId)
.Add("pACTION_ENDPOINT_AUTH_ID", request.Action?.EndpointAuthId)
.Add("pACTION_ENDPOINT_PARAMS_ID", request.Action?.EndpointParamsId, SqlDbType.SmallInt)
.Add("pACTION_SQL_CONNECTION_ID", request.Action?.SqlConnectionId, SqlDbType.SmallInt)
.Add("pACTION_TYPE_ID", request.Action?.TypeId is not null ? (byte)request.Action.TypeId : null, SqlDbType.TinyInt)
.Add("pACTION_PRE_SQL", request.Action?.PreSql)
.Add("pACTION_HEADER_SQL", request.Action?.HeaderSql)
.Add("pACTION_BODY_SQL", request.Action?.BodySql)
.Add("pACTION_POST_SQL", request.Action?.PostSql)
.Add("pACTION_ERROR_ACTION_ID", request.Action?.ErrorActionId, SqlDbType.TinyInt)
.Add("pENDPOINT_ACTIVE", request.Endpoint?.Active)
.Add("pENDPOINT_DESCRIPTION", request.Endpoint?.Description)
.Add("pENDPOINT_URI", request.Endpoint?.Uri)
.Add("pENDPOINT_AUTH_ACTIVE", request.EndpointAuth?.Active)
.Add("pENDPOINT_AUTH_DESCRIPTION", request.EndpointAuth?.Description)
.Add("pENDPOINT_AUTH_TYPE_ID", request.EndpointAuth?.TypeId, SqlDbType.TinyInt)
.Add("pENDPOINT_AUTH_API_KEY", request.EndpointAuth?.ApiKey)
.Add("pENDPOINT_AUTH_API_VALUE", request.EndpointAuth?.ApiValue)
.Add("pENDPOINT_AUTH_API_KEY_ADD_TO_ID", request.EndpointAuth?.ApiKeyAddToId)
.Add("pENDPOINT_AUTH_TOKEN", request.EndpointAuth?.Token)
.Add("pENDPOINT_AUTH_USERNAME", request.EndpointAuth?.Username)
.Add("pENDPOINT_AUTH_PASSWORD", request.EndpointAuth?.Password)
.Add("pENDPOINT_AUTH_DOMAIN", request.EndpointAuth?.Domain)
.Add("pENDPOINT_AUTH_WORKSTATION", request.EndpointAuth?.Workstation)
.Add("pPROFILE_ACTIVE", request.Profile?.Active)
.Add("pPROFILE_TYPE_ID", request.Profile?.TypeId, SqlDbType.TinyInt)
.Add("pPROFILE_MANDANTOR", request.Profile?.Mandantor)
.Add("pPROFILE_NAME", request.Profile?.Name)
.Add("pPROFILE_DESCRIPTION", request.Profile?.Description)
.Add("pPROFILE_LOG_LEVEL_ID", request.Profile?.LogLevelId, SqlDbType.TinyInt)
.Add("pPROFILE_LANGUAGE_ID", request.Profile?.LanguageId, SqlDbType.SmallInt)
.Add("pRESULT_ACTION_ID", request.Result?.ActionId)
.Add("pRESULT_STATUS_ID", request.Result?.Status, SqlDbType.TinyInt)
.Add("pRESULT_TYPE_ID", request.Result?.Type is not null ? (byte)request.Result.Type : null, SqlDbType.TinyInt)
.Add("pRESULT_HEADER", request.Result?.Header)
.Add("pRESULT_BODY", request.Result?.Body)
.Add("pRESULT_INFO_ID", request.Result?.Info, SqlDbType.SmallInt)
.Add("pRESULT_INFO_DETAIL", request.Result?.InfoDetail)
.Add("pRESULT_ERROR", request.Result?.Error)
.Add("pRESULT_BATCH_ID", request.Result?.References?.BatchId)
.Add("pRESULT_REFERENCE1", request.Result?.References?.Reference1)
.Add("pRESULT_REFERENCE2", request.Result?.References?.Reference2)
.Add("pRESULT_REFERENCE3", request.Result?.References?.Reference3)
.Add("pRESULT_REFERENCE4", request.Result?.References?.Reference4)
.Add("pRESULT_REFERENCE5", request.Result?.References?.Reference5)
.Add("pENDPOINT_PARAMS_ACTIVE", request.EndpointParams?.Active)
.Add("pENDPOINT_PARAMS_DESCRIPTION", request.EndpointParams?.Description)
.Add("pENDPOINT_PARAMS_GROUP_ID", request.EndpointParams?.GroupId, SqlDbType.SmallInt)
.Add("pENDPOINT_PARAMS_SEQUENCE", request.EndpointParams?.Sequence, SqlDbType.TinyInt)
.Add("pENDPOINT_PARAMS_KEY", request.EndpointParams?.Key)
.Add("pENDPOINT_PARAMS_VALUE", request.EndpointParams?.Value)
.AddOutput("oGUID", SqlDbType.BigInt);
try
{
new SqlParameter("@pENTITY", request.Entity ?? (object)DBNull.Value),
await repo.ExecuteQueryRawAsync(sp.BuildSql(), sp.BuildParameters(), cancel);
}
catch (SqlException ex)
{
if (sqlExOpt.CurrentValue.BadRequestSqlExceptionNumbers.Contains(ex.Number))
throw new BadRequestException(ex.Message, ex);
else
throw;
}
new SqlParameter("@pADDED_WHO", (object?)request.AddedWho ?? DBNull.Value),
new SqlParameter("@pADDED_WHEN", (object?)DateTime.UtcNow ?? DBNull.Value),
var guidParam = sp.GetParameter("oGUID");
new SqlParameter("@pACTION_PROFILE_ID", (object?)request.Action.ProfileId ?? DBNull.Value),
new SqlParameter("@pACTION_ACTIVE", (object?)request.Action.Active ?? DBNull.Value),
new SqlParameter("@pACTION_SEQUENCE", (object?)request.Action.Sequence ?? DBNull.Value),
new SqlParameter("@pACTION_ENDPOINT_ID", (object?)request.Action.EndpointId ?? DBNull.Value),
new SqlParameter("@pACTION_ENDPOINT_AUTH_ID", (object?)request.Action.EndpointAuthId ?? DBNull.Value),
new SqlParameter("@pACTION_ENDPOINT_PARAMS_ID", (object?)request.Action.EndpointParamsId ?? DBNull.Value),
new SqlParameter("@pACTION_SQL_CONNECTION_ID", (object?)request.Action.SqlConnectionId ?? DBNull.Value),
new SqlParameter("@pACTION_TYPE_ID", (object?)request.Action.TypeId ?? DBNull.Value),
new SqlParameter("@pACTION_PRE_SQL", (object?)request.Action.PreSql ?? DBNull.Value),
new SqlParameter("@pACTION_HEADER_SQL", (object?)request.Action.HeaderSql ?? DBNull.Value),
new SqlParameter("@pACTION_BODY_SQL", (object?)request.Action.BodySql ?? DBNull.Value),
new SqlParameter("@pACTION_POST_SQL", (object?)request.Action.PostSql ?? DBNull.Value),
new SqlParameter("@pACTION_ERROR_ACTION_ID", (object?)request.Action.ErrorActionId ?? DBNull.Value),
new SqlParameter("@pENDPOINT_ACTIVE", (object?)request.Endpoint.Active ?? DBNull.Value),
new SqlParameter("@pENDPOINT_DESCRIPTION", (object?)request.Endpoint.Description ?? DBNull.Value),
new SqlParameter("@pENDPOINT_URI", (object?)request.Endpoint.Uri ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_ACTIVE", (object?)request.EndpointAuth.Active ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_DESCRIPTION", (object?)request.EndpointAuth.Description ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_TYPE_ID", (object?)request.EndpointAuth.TypeId ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_API_KEY", (object?)request.EndpointAuth.ApiKey ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_API_VALUE", (object?)request.EndpointAuth.ApiValue ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_API_KEY_ADD_TO_ID", (object?)request.EndpointAuth.ApiKeyAddToId ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_TOKEN", (object?)request.EndpointAuth.Token ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_USERNAME", (object?)request.EndpointAuth.Username ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_PASSWORD", (object?)request.EndpointAuth.Password ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_DOMAIN", (object?)request.EndpointAuth.Domain ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_WORKSTATION", (object?)request.EndpointAuth.Workstation ?? DBNull.Value),
new SqlParameter("@pPROFILE_ACTIVE", (object?)request.Profile.Active ?? DBNull.Value),
new SqlParameter("@pPROFILE_TYPE_ID", (object?)request.Profile.TypeId ?? DBNull.Value),
new SqlParameter("@pPROFILE_MANDANTOR", (object?)request.Profile.Mandantor ?? DBNull.Value),
new SqlParameter("@pPROFILE_NAME", (object?)request.Profile.Name ?? DBNull.Value),
new SqlParameter("@pPROFILE_DESCRIPTION", (object?)request.Profile.Description ?? DBNull.Value),
new SqlParameter("@pPROFILE_LOG_LEVEL_ID", (object?)request.Profile.LogLevelId ?? DBNull.Value),
new SqlParameter("@pPROFILE_LANGUAGE_ID", (object?)request.Profile.LanguageId ?? DBNull.Value),
new SqlParameter("@pRESULT_ACTION_ID", (object?)request.Result.ActionId ?? DBNull.Value),
new SqlParameter("@pRESULT_STATUS_ID", (object?)request.Result.StatusId ?? DBNull.Value),
new SqlParameter("@pRESULT_HEADER", (object?)request.Result.Header ?? DBNull.Value),
new SqlParameter("@pRESULT_BODY", (object?)request.Result.Body ?? DBNull.Value),
new SqlParameter("@pENDPOINT_PARAMS_ACTIVE", (object?)request.EndpointParams.Active ?? DBNull.Value),
new SqlParameter("@pENDPOINT_PARAMS_DESCRIPTION", (object?)request.EndpointParams.Description ?? DBNull.Value),
new SqlParameter("@pENDPOINT_PARAMS_GROUP_ID", (object?)request.EndpointParams.GroupId ?? DBNull.Value),
new SqlParameter("@pENDPOINT_PARAMS_SEQUENCE", (object?)request.EndpointParams.Sequence ?? DBNull.Value),
new SqlParameter("@pENDPOINT_PARAMS_KEY", (object?)request.EndpointParams.Key ?? DBNull.Value),
new SqlParameter("@pENDPOINT_PARAMS_VALUE", (object?)request.EndpointParams.Value ?? DBNull.Value),
new SqlParameter
{
ParameterName = "@oGUID",
SqlDbType = System.Data.SqlDbType.BigInt,
Direction = System.Data.ParameterDirection.Output
}
};
await repo.ExecuteQueryRawAsync(
"EXEC [dbo].[PRREC_INSERT_OBJECT] " +
"@pENTITY, @pADDED_WHO, @pADDED_WHEN, " +
"@pACTION_PROFILE_ID, @pACTION_ACTIVE, @pACTION_SEQUENCE, @pACTION_ENDPOINT_ID, @pACTION_ENDPOINT_AUTH_ID, @pACTION_ENDPOINT_PARAMS_ID, @pACTION_SQL_CONNECTION_ID, @pACTION_TYPE_ID, @pACTION_PRE_SQL, @pACTION_HEADER_SQL, @pACTION_BODY_SQL, @pACTION_POST_SQL, @pACTION_ERROR_ACTION_ID, " +
"@pENDPOINT_ACTIVE, @pENDPOINT_DESCRIPTION, @pENDPOINT_URI, " +
"@pENDPOINT_AUTH_ACTIVE, @pENDPOINT_AUTH_DESCRIPTION, @pENDPOINT_AUTH_TYPE_ID, @pENDPOINT_AUTH_API_KEY, @pENDPOINT_AUTH_API_VALUE, @pENDPOINT_AUTH_API_KEY_ADD_TO_ID, @pENDPOINT_AUTH_TOKEN, @pENDPOINT_AUTH_USERNAME, @pENDPOINT_AUTH_PASSWORD, @pENDPOINT_AUTH_DOMAIN, @pENDPOINT_AUTH_WORKSTATION, " +
"@pPROFILE_ACTIVE, @pPROFILE_TYPE_ID, @pPROFILE_MANDANTOR, @pPROFILE_NAME, @pPROFILE_DESCRIPTION, @pPROFILE_LOG_LEVEL_ID, @pPROFILE_LANGUAGE_ID, " +
"@pRESULT_ACTION_ID, @pRESULT_STATUS_ID, @pRESULT_HEADER, @pRESULT_BODY, " +
"@pENDPOINT_PARAMS_ACTIVE, @pENDPOINT_PARAMS_DESCRIPTION, @pENDPOINT_PARAMS_GROUP_ID, @pENDPOINT_PARAMS_SEQUENCE, @pENDPOINT_PARAMS_KEY, @pENDPOINT_PARAMS_VALUE, " +
"@oGUID OUTPUT",
parameters,
cancel);
var guidParam = parameters.Last();
if (guidParam.Value != DBNull.Value)
if (guidParam.Value is long longValue)
if (guidParam?.Value != DBNull.Value)
if (guidParam!.Value is long longValue)
return longValue;
else if (long.TryParse(guidParam.Value.ToString(), out var guid))
else if (long.TryParse(guidParam.Value?.ToString(), out var guid))
return guid;
throw new InsertObjectFailedException(request, "InsertObject stored procedure did not return a valid identifier.");

View File

@@ -0,0 +1,81 @@
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using System.Data;
using System.Text;
namespace ReC.Application.Common.Procedures;
internal sealed class StoredProcedureBuilder(string procedureName, string? returnVariable = null)
{
private readonly StringBuilder _execSql = returnVariable is not null
? new StringBuilder($"EXEC @{returnVariable} = {procedureName}")
: new StringBuilder($"EXEC {procedureName}");
private readonly List<SqlParameter> _parameters = [];
private char _separator = ' ';
public StoredProcedureBuilder Add(string name, object? value, SqlDbType? dbType = null)
{
if (value is null) return this;
_execSql.AppendLine($"{_separator}@{name} = @{name}");
_separator = ',';
if (!dbType.HasValue && value is DateTime)
dbType = SqlDbType.DateTime;
if (dbType.HasValue)
_parameters.Add(new SqlParameter($"@{name}", dbType.Value) { Value = value });
else
_parameters.Add(new SqlParameter($"@{name}", value));
return this;
}
public StoredProcedureBuilder Add(string name, EntityType entityType)
{
var entityTypeStr = entityType switch
{
EntityType.Action => "ACTION",
EntityType.Endpoint => "ENDPOINT",
EntityType.EndpointAuth => "ENDPOINT_AUTH",
EntityType.EndpointParams => "ENDPOINT_PARAMS",
EntityType.Profile => "PROFILE",
EntityType.Result => "RESULT",
_ => throw new ArgumentOutOfRangeException(nameof(entityType), $"Not expected entity type value: {entityType}")
};
return Add(name, entityTypeStr);
}
public StoredProcedureBuilder AddOutput(string name, SqlDbType dbType)
{
_execSql.AppendLine($"{_separator}@{name} = @{name} OUTPUT");
_separator = ',';
_parameters.Add(new SqlParameter
{
ParameterName = $"@{name}",
SqlDbType = dbType,
Direction = ParameterDirection.Output
});
return this;
}
public string BuildSql()
{
if (returnVariable is null)
return _execSql.ToString();
return new StringBuilder()
.AppendLine($"DECLARE @{returnVariable} SMALLINT = 0;")
.Append(_execSql).AppendLine(";")
.AppendLine($"SELECT @{returnVariable};")
.ToString();
}
public SqlParameter[] BuildParameters() => [.. _parameters];
public SqlParameter? GetParameter(string name) =>
_parameters.Find(p => p.ParameterName == $"@{name}");
}

View File

@@ -1,6 +1,6 @@
namespace ReC.Application.Common.Procedures.InsertProcedure;
namespace ReC.Application.Common.Procedures.UpdateProcedure.Dto;
public record InsertActionProcedure : IInsertProcedure
public record UpdateActionDto
{
public long? ProfileId { get; set; }
public bool? Active { get; set; }
@@ -15,13 +15,4 @@ public record InsertActionProcedure : IInsertProcedure
public string? BodySql { get; set; }
public string? PostSql { get; set; }
public byte? ErrorActionId { get; set; }
public InsertObjectProcedure ToObjectProcedure(string? addedWho = null)
{
return new InsertObjectProcedure
{
Entity = "ACTION",
Action = this
}.AddedBy(addedWho);
}
}

View File

@@ -1,8 +1,6 @@
using ReC.Application.Common.Procedures.InsertProcedure;
namespace ReC.Application.Common.Procedures.UpdateProcedure.Dto;
namespace ReC.Application.EndpointAuth.Commands;
public record InsertEndpointAuthProcedure : IInsertProcedure
public record UpdateEndpointAuthDto
{
public bool? Active { get; set; }
public string? Description { get; set; }
@@ -15,13 +13,4 @@ public record InsertEndpointAuthProcedure : IInsertProcedure
public string? Password { get; set; }
public string? Domain { get; set; }
public string? Workstation { get; set; }
public InsertObjectProcedure ToObjectProcedure(string? addedWho = null)
{
return new InsertObjectProcedure
{
Entity = "ENDPOINT_AUTH",
EndpointAuth = this
}.AddedBy(addedWho);
}
}

View File

@@ -0,0 +1,8 @@
namespace ReC.Application.Common.Procedures.UpdateProcedure.Dto;
public record UpdateEndpointDto
{
public bool? Active { get; set; }
public string? Description { get; set; }
public string? Uri { get; set; }
}

View File

@@ -0,0 +1,11 @@
namespace ReC.Application.Common.Procedures.UpdateProcedure.Dto;
public record UpdateEndpointParamsDto
{
public bool? Active { get; set; }
public string? Description { get; set; }
public short? GroupId { get; set; }
public byte? Sequence { get; set; }
public string? Key { get; set; }
public string? Value { get; set; }
}

View File

@@ -0,0 +1,15 @@
namespace ReC.Application.Common.Procedures.UpdateProcedure.Dto;
public record UpdateProfileDto
{
public bool? Active { get; set; }
public byte? TypeId { get; set; }
public string? Mandantor { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public byte? LogLevelId { get; set; }
public short? LanguageId { get; set; }
public DateTime? FirstRun { get; set; }
public DateTime? LastRun { get; set; }
public string? LastResult { get; set; }
}

View File

@@ -0,0 +1,15 @@
using ReC.Application.RecActions.Commands;
namespace ReC.Application.Common.Procedures.UpdateProcedure.Dto;
public record UpdateResultDto
{
public long? ActionId { get; set; }
public short? StatusId { get; set; }
public string? Header { get; set; }
public string? Body { get; set; }
public short? Info { get; set; }
public string? InfoDetail { get; set; }
public string? Error { get; set; }
public InvokeReferences? References { get; set; }
}

View File

@@ -1,6 +1,10 @@
using MediatR;
namespace ReC.Application.Common.Procedures.UpdateProcedure;
public interface IUpdateProcedure
public interface IUpdateProcedure<T> : IRequest<int>
{
public UpdateObjectProcedure ToObjectProcedure(long id, string? changedWho = null);
}
public long Id { get; set; }
public T Data { get; set; }
}

View File

@@ -1,137 +1,121 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using MediatR;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
using ReC.Application.Common.Exceptions;
using ReC.Application.EndpointAuth.Commands;
using ReC.Application.EndpointParams.Commands;
using ReC.Application.Endpoints.Commands;
using ReC.Application.Results.Commands;
using ReC.Application.Profile.Commands;
using ReC.Application.RecActions.Commands;
using ReC.Application.Common.Options;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using System.Data;
namespace ReC.Application.Common.Procedures.UpdateProcedure;
public record UpdateObjectProcedure : IRequest<int>
{
/// <summary>
/// Target entity: ACTION, ENDPOINT, ENDPOINT_AUTH, ENDPOINT_PARAMS, PROFILE, RESULT
/// Target entity for the update operation.
/// </summary>
public string Entity { get; set; } = null!;
public required EntityType Entity { get; set; }
/// <summary>
/// Target GUID to update (required)
/// </summary>
public long Id { get; set; }
internal string? ChangedWho { get; private set; }
//TODO: update to set in authentication middleware or similar, and remove from procedure properties
internal string? ChangedWho { get; private set; } = "ReC.API";
public UpdateObjectProcedure ChangedBy(string? changedWho = null)
{
ChangedWho = changedWho ?? "ReC.API";
return this;
}
public UpdateActionProcedure Action { get; set; } = new();
public UpdateEndpointProcedure Endpoint { get; set; } = new();
public UpdateEndpointAuthProcedure EndpointAuth { get; set; } = new();
public UpdateProfileProcedure Profile { get; set; } = new();
public UpdateResultProcedure Result { get; set; } = new();
public UpdateEndpointParamsProcedure EndpointParams { get; set; } = new();
public UpdateActionDto Action { get; set; } = new();
public UpdateEndpointDto Endpoint { get; set; } = new();
public UpdateEndpointAuthDto EndpointAuth { get; set; } = new();
public UpdateProfileDto Profile { get; set; } = new();
public UpdateResultDto Result { get; set; } = new();
public UpdateEndpointParamsDto EndpointParams { get; set; } = new();
}
public static class UpdateObjectProcedureExtensions
{
public static Task<int> ExecuteUpdateProcedure(this ISender sender, IUpdateProcedure procedure, long id, string? changedWho = null, CancellationToken cancel = default)
{
return sender.Send(procedure.ToObjectProcedure(id, changedWho ?? "ReC.API"), cancel);
}
}
public class UpdateObjectProcedureHandler(IRepository repo) : IRequestHandler<UpdateObjectProcedure, int>
public class UpdateObjectProcedureHandler(IRepository repo, IOptionsMonitor<SqlExceptionOptions> sqlExOpt) : IRequestHandler<UpdateObjectProcedure, int>
{
public async Task<int> Handle(UpdateObjectProcedure request, CancellationToken cancel)
{
var parameters = new[]
var sp = new StoredProcedureBuilder("[dbo].[PRREC_UPDATE_OBJECT]", "RC")
.Add("pENTITY", request.Entity)
.Add("pGUID", request.Id)
.Add("pCHANGED_WHO", request.ChangedWho)
.Add("pCHANGED_WHEN", DateTime.UtcNow)
.Add("pACTION_PROFILE_ID", request.Action.ProfileId)
.Add("pACTION_ACTIVE", request.Action.Active)
.Add("pACTION_SEQUENCE", request.Action.Sequence, SqlDbType.TinyInt)
.Add("pACTION_ENDPOINT_ID", request.Action.EndpointId)
.Add("pACTION_ENDPOINT_AUTH_ID", request.Action.EndpointAuthId)
.Add("pACTION_ENDPOINT_PARAMS_ID", request.Action.EndpointParamsId, SqlDbType.SmallInt)
.Add("pACTION_SQL_CONNECTION_ID", request.Action.SqlConnectionId, SqlDbType.SmallInt)
.Add("pACTION_TYPE_ID", request.Action.TypeId, SqlDbType.TinyInt)
.Add("pACTION_PRE_SQL", request.Action.PreSql)
.Add("pACTION_HEADER_SQL", request.Action.HeaderSql)
.Add("pACTION_BODY_SQL", request.Action.BodySql)
.Add("pACTION_POST_SQL", request.Action.PostSql)
.Add("pACTION_ERROR_ACTION_ID", request.Action.ErrorActionId, SqlDbType.TinyInt)
.Add("pENDPOINT_ACTIVE", request.Endpoint.Active)
.Add("pENDPOINT_DESCRIPTION", request.Endpoint.Description)
.Add("pENDPOINT_URI", request.Endpoint.Uri)
.Add("pENDPOINT_AUTH_ACTIVE", request.EndpointAuth.Active)
.Add("pENDPOINT_AUTH_DESCRIPTION", request.EndpointAuth.Description)
.Add("pENDPOINT_AUTH_TYPE_ID", request.EndpointAuth.TypeId, SqlDbType.TinyInt)
.Add("pENDPOINT_AUTH_API_KEY", request.EndpointAuth.ApiKey)
.Add("pENDPOINT_AUTH_API_VALUE", request.EndpointAuth.ApiValue)
.Add("pENDPOINT_AUTH_API_KEY_ADD_TO_ID", request.EndpointAuth.ApiKeyAddToId)
.Add("pENDPOINT_AUTH_TOKEN", request.EndpointAuth.Token)
.Add("pENDPOINT_AUTH_USERNAME", request.EndpointAuth.Username)
.Add("pENDPOINT_AUTH_PASSWORD", request.EndpointAuth.Password)
.Add("pENDPOINT_AUTH_DOMAIN", request.EndpointAuth.Domain)
.Add("pENDPOINT_AUTH_WORKSTATION", request.EndpointAuth.Workstation)
.Add("pENDPOINT_PARAMS_ACTIVE", request.EndpointParams.Active)
.Add("pENDPOINT_PARAMS_DESCRIPTION", request.EndpointParams.Description)
.Add("pENDPOINT_PARAMS_GROUP_ID", request.EndpointParams.GroupId, SqlDbType.SmallInt)
.Add("pENDPOINT_PARAMS_SEQUENCE", request.EndpointParams.Sequence, SqlDbType.TinyInt)
.Add("pENDPOINT_PARAMS_KEY", request.EndpointParams.Key)
.Add("pENDPOINT_PARAMS_VALUE", request.EndpointParams.Value)
.Add("pPROFILE_ACTIVE", request.Profile.Active)
.Add("pPROFILE_TYPE_ID", request.Profile.TypeId, SqlDbType.TinyInt)
.Add("pPROFILE_MANDANTOR", request.Profile.Mandantor)
.Add("pPROFILE_NAME", request.Profile.Name)
.Add("pPROFILE_DESCRIPTION", request.Profile.Description)
.Add("pPROFILE_LOG_LEVEL_ID", request.Profile.LogLevelId, SqlDbType.TinyInt)
.Add("pPROFILE_LANGUAGE_ID", request.Profile.LanguageId, SqlDbType.SmallInt)
.Add("pPROFILE_FIRST_RUN", request.Profile.FirstRun)
.Add("pPROFILE_LAST_RUN", request.Profile.LastRun)
.Add("pPROFILE_LAST_RESULT", request.Profile.LastResult)
.Add("pRESULT_ACTION_ID", request.Result.ActionId)
.Add("pRESULT_STATUS_ID", request.Result.StatusId, SqlDbType.TinyInt)
.Add("pRESULT_HEADER", request.Result.Header)
.Add("pRESULT_BODY", request.Result.Body)
.Add("pRESULT_INFO_ID", request.Result.Info, SqlDbType.SmallInt)
.Add("pRESULT_INFO_DETAIL", request.Result.InfoDetail)
.Add("pRESULT_ERROR", request.Result.Error)
.Add("pRESULT_BATCH_ID", request.Result.References?.BatchId)
.Add("pRESULT_REFERENCE1", request.Result.References?.Reference1)
.Add("pRESULT_REFERENCE2", request.Result.References?.Reference2)
.Add("pRESULT_REFERENCE3", request.Result.References?.Reference3)
.Add("pRESULT_REFERENCE4", request.Result.References?.Reference4)
.Add("pRESULT_REFERENCE5", request.Result.References?.Reference5);
try
{
new SqlParameter("@pENTITY", request.Entity ?? (object)DBNull.Value),
new SqlParameter("@pGUID", (object?)request.Id ?? DBNull.Value),
var result = await repo.ExecuteQueryRawAsync(sp.BuildSql(), sp.BuildParameters(), cancel);
new SqlParameter("@pCHANGED_WHO", (object?)request.ChangedWho ?? DBNull.Value),
new SqlParameter("@pCHANGED_WHEN", (object?)DateTime.UtcNow ?? DBNull.Value),
if (result > 0)
{
throw new UpdateObjectFailedException(request, $"UpdateObject stored procedure failed with error code: {result}");
}
new SqlParameter("@pACTION_PROFILE_ID", (object?)request.Action.ProfileId ?? DBNull.Value),
new SqlParameter("@pACTION_ACTIVE", (object?)request.Action.Active ?? DBNull.Value),
new SqlParameter("@pACTION_SEQUENCE", (object?)request.Action.Sequence ?? DBNull.Value),
new SqlParameter("@pACTION_ENDPOINT_ID", (object?)request.Action.EndpointId ?? DBNull.Value),
new SqlParameter("@pACTION_ENDPOINT_AUTH_ID", (object?)request.Action.EndpointAuthId ?? DBNull.Value),
new SqlParameter("@pACTION_ENDPOINT_PARAMS_ID", (object?)request.Action.EndpointParamsId ?? DBNull.Value),
new SqlParameter("@pACTION_SQL_CONNECTION_ID", (object?)request.Action.SqlConnectionId ?? DBNull.Value),
new SqlParameter("@pACTION_TYPE_ID", (object?)request.Action.TypeId ?? DBNull.Value),
new SqlParameter("@pACTION_PRE_SQL", (object?)request.Action.PreSql ?? DBNull.Value),
new SqlParameter("@pACTION_HEADER_SQL", (object?)request.Action.HeaderSql ?? DBNull.Value),
new SqlParameter("@pACTION_BODY_SQL", (object?)request.Action.BodySql ?? DBNull.Value),
new SqlParameter("@pACTION_POST_SQL", (object?)request.Action.PostSql ?? DBNull.Value),
new SqlParameter("@pACTION_ERROR_ACTION_ID", (object?)request.Action.ErrorActionId ?? DBNull.Value),
new SqlParameter("@pENDPOINT_ACTIVE", (object?)request.Endpoint.Active ?? DBNull.Value),
new SqlParameter("@pENDPOINT_DESCRIPTION", (object?)request.Endpoint.Description ?? DBNull.Value),
new SqlParameter("@pENDPOINT_URI", (object?)request.Endpoint.Uri ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_ACTIVE", (object?)request.EndpointAuth.Active ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_DESCRIPTION", (object?)request.EndpointAuth.Description ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_TYPE_ID", (object?)request.EndpointAuth.TypeId ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_API_KEY", (object?)request.EndpointAuth.ApiKey ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_API_VALUE", (object?)request.EndpointAuth.ApiValue ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_API_KEY_ADD_TO_ID", (object?)request.EndpointAuth.ApiKeyAddToId ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_TOKEN", (object?)request.EndpointAuth.Token ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_USERNAME", (object?)request.EndpointAuth.Username ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_PASSWORD", (object?)request.EndpointAuth.Password ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_DOMAIN", (object?)request.EndpointAuth.Domain ?? DBNull.Value),
new SqlParameter("@pENDPOINT_AUTH_WORKSTATION", (object?)request.EndpointAuth.Workstation ?? DBNull.Value),
new SqlParameter("@pENDPOINT_PARAMS_ACTIVE", (object?)request.EndpointParams.Active ?? DBNull.Value),
new SqlParameter("@pENDPOINT_PARAMS_DESCRIPTION", (object?)request.EndpointParams.Description ?? DBNull.Value),
new SqlParameter("@pENDPOINT_PARAMS_GROUP_ID", (object?)request.EndpointParams.GroupId ?? DBNull.Value),
new SqlParameter("@pENDPOINT_PARAMS_SEQUENCE", (object?)request.EndpointParams.Sequence ?? DBNull.Value),
new SqlParameter("@pENDPOINT_PARAMS_KEY", (object?)request.EndpointParams.Key ?? DBNull.Value),
new SqlParameter("@pENDPOINT_PARAMS_VALUE", (object?)request.EndpointParams.Value ?? DBNull.Value),
new SqlParameter("@pPROFILE_ACTIVE", (object?)request.Profile.Active ?? DBNull.Value),
new SqlParameter("@pPROFILE_TYPE_ID", (object?)request.Profile.TypeId ?? DBNull.Value),
new SqlParameter("@pPROFILE_MANDANTOR", (object?)request.Profile.Mandantor ?? DBNull.Value),
new SqlParameter("@pPROFILE_NAME", (object?)request.Profile.Name ?? DBNull.Value),
new SqlParameter("@pPROFILE_DESCRIPTION", (object?)request.Profile.Description ?? DBNull.Value),
new SqlParameter("@pPROFILE_LOG_LEVEL_ID", (object?)request.Profile.LogLevelId ?? DBNull.Value),
new SqlParameter("@pPROFILE_LANGUAGE_ID", (object?)request.Profile.LanguageId ?? DBNull.Value),
new SqlParameter("@pPROFILE_FIRST_RUN", (object?)request.Profile.FirstRun ?? DBNull.Value),
new SqlParameter("@pPROFILE_LAST_RUN", (object?)request.Profile.LastRun ?? DBNull.Value),
new SqlParameter("@pPROFILE_LAST_RESULT", (object?)request.Profile.LastResult ?? DBNull.Value),
new SqlParameter("@pRESULT_ACTION_ID", (object?)request.Result.ActionId ?? DBNull.Value),
new SqlParameter("@pRESULT_STATUS_ID", (object?)request.Result.StatusId ?? DBNull.Value),
new SqlParameter("@pRESULT_HEADER", (object?)request.Result.Header ?? DBNull.Value),
new SqlParameter("@pRESULT_BODY", (object?)request.Result.Body ?? DBNull.Value)
};
var result = await repo.ExecuteQueryRawAsync(
"EXEC @RC = [dbo].[PRREC_UPDATE_OBJECT] " +
"@pENTITY, @pGUID, @pCHANGED_WHO, @pCHANGED_WHEN, " +
"@pACTION_PROFILE_ID, @pACTION_ACTIVE, @pACTION_SEQUENCE, @pACTION_ENDPOINT_ID, @pACTION_ENDPOINT_AUTH_ID, @pACTION_ENDPOINT_PARAMS_ID, @pACTION_SQL_CONNECTION_ID, @pACTION_TYPE_ID, @pACTION_PRE_SQL, @pACTION_HEADER_SQL, @pACTION_BODY_SQL, @pACTION_POST_SQL, @pACTION_ERROR_ACTION_ID, " +
"@pENDPOINT_ACTIVE, @pENDPOINT_DESCRIPTION, @pENDPOINT_URI, " +
"@pENDPOINT_AUTH_ACTIVE, @pENDPOINT_AUTH_DESCRIPTION, @pENDPOINT_AUTH_TYPE_ID, @pENDPOINT_AUTH_API_KEY, @pENDPOINT_AUTH_API_VALUE, @pENDPOINT_AUTH_API_KEY_ADD_TO_ID, @pENDPOINT_AUTH_TOKEN, @pENDPOINT_AUTH_USERNAME, @pENDPOINT_AUTH_PASSWORD, @pENDPOINT_AUTH_DOMAIN, @pENDPOINT_AUTH_WORKSTATION, " +
"@pENDPOINT_PARAMS_ACTIVE, @pENDPOINT_PARAMS_DESCRIPTION, @pENDPOINT_PARAMS_GROUP_ID, @pENDPOINT_PARAMS_SEQUENCE, @pENDPOINT_PARAMS_KEY, @pENDPOINT_PARAMS_VALUE, " +
"@pPROFILE_ACTIVE, @pPROFILE_TYPE_ID, @pPROFILE_MANDANTOR, @pPROFILE_NAME, @pPROFILE_DESCRIPTION, @pPROFILE_LOG_LEVEL_ID, @pPROFILE_LANGUAGE_ID, @pPROFILE_FIRST_RUN, @pPROFILE_LAST_RUN, @pPROFILE_LAST_RESULT, " +
"@pRESULT_ACTION_ID, @pRESULT_STATUS_ID, @pRESULT_HEADER, @pRESULT_BODY; " +
"SELECT @RC;",
parameters,
cancel);
// The stored procedure returns 0 on success, error codes > 0 on failure
if (result > 0)
{
throw new UpdateObjectFailedException(request, $"UpdateObject stored procedure failed with error code: {result}");
return result;
}
catch (SqlException ex)
{
if (sqlExOpt.CurrentValue.BadRequestSqlExceptionNumbers.Contains(ex.Number))
throw new BadRequestException(ex.Message, ex);
else
throw;
}
return result;
}
}

View File

@@ -0,0 +1,22 @@
using FluentValidation;
using ReC.Application.Common.Procedures.DeleteProcedure;
namespace ReC.Application.Common.Validations;
public class DeleteObjectProcedureValidator : AbstractValidator<DeleteObjectProcedure>
{
public DeleteObjectProcedureValidator()
{
RuleFor(x => x.Entity)
.IsInEnum()
.WithMessage("ENTITY must be a valid EntityType value.");
RuleFor(x => x.Start)
.GreaterThan(0)
.WithMessage("Start GUID/ID must be greater than 0.");
RuleFor(x => x.End)
.GreaterThanOrEqualTo(x => x.Start)
.WithMessage("End GUID/ID must be greater than or equal to Start GUID/ID.");
}
}

View File

@@ -7,66 +7,60 @@ public class InsertObjectProcedureValidator : AbstractValidator<InsertObjectProc
{
public InsertObjectProcedureValidator()
{
// ENTITY must be one of the allowed values
RuleFor(x => x.Entity)
.NotEmpty()
.Must(e => e is "ACTION" or "ENDPOINT" or "ENDPOINT_AUTH" or "ENDPOINT_PARAMS" or "PROFILE" or "RESULT")
.WithMessage("ENTITY must be one of: ACTION, ENDPOINT, ENDPOINT_AUTH, ENDPOINT_PARAMS, PROFILE, RESULT.");
.IsInEnum()
.WithMessage("ENTITY must be a valid EntityType value.");
// ACTION validation
When(x => x.Entity == "ACTION", () =>
When(x => x.Action != null, () =>
{
RuleFor(x => x.Action.ProfileId)
RuleFor(x => x.Action!.ProfileId)
.NotNull()
.WithMessage("ACTION requires ActionProfileId (maps to @pACTION_PROFILE_ID).");
RuleFor(x => x.Action.EndpointId)
RuleFor(x => x.Action!.EndpointId)
.NotNull()
.WithMessage("ACTION requires ActionEndpointId (maps to @pACTION_ENDPOINT_ID).");
});
// ENDPOINT validation
When(x => x.Entity == "ENDPOINT", () =>
When(x => x.Endpoint != null, () =>
{
RuleFor(x => x.Endpoint.Uri)
RuleFor(x => x.Endpoint!.Uri)
.NotEmpty()
.WithMessage("ENDPOINT requires EndpointUri (maps to @pENDPOINT_URI).")
.MaximumLength(2000);
});
// PROFILE validation
When(x => x.Entity == "PROFILE", () =>
When(x => x.Profile != null, () =>
{
RuleFor(x => x.Profile.Name)
RuleFor(x => x.Profile!.Name)
.NotEmpty()
.WithMessage("PROFILE requires ProfileName (maps to @pPROFILE_NAME).")
.MaximumLength(50);
RuleFor(x => x.Profile.Mandantor)
RuleFor(x => x.Profile!.Mandantor)
.MaximumLength(50)
.When(x => x.Profile.Mandantor != null);
.When(x => x.Profile!.Mandantor != null);
RuleFor(x => x.Profile.Description)
RuleFor(x => x.Profile!.Description)
.MaximumLength(250)
.When(x => x.Profile.Description != null);
.When(x => x.Profile!.Description != null);
});
// RESULT validation
When(x => x.Entity == "RESULT", () =>
When(x => x.Result != null, () =>
{
RuleFor(x => x.Result.ActionId)
RuleFor(x => x.Result!.ActionId)
.NotNull()
.WithMessage("RESULT requires ResultActionId (maps to @pRESULT_ACTION_ID).");
RuleFor(x => x.Result.StatusId)
.NotNull()
.WithMessage("RESULT requires ResultStatusId (maps to @pRESULT_STATUS_ID).");
});
// ENDPOINT_PARAMS validation
When(x => x.Entity == "ENDPOINT_PARAMS", () =>
When(x => x.EndpointParams != null, () =>
{
RuleFor(x => x.EndpointParams.GroupId)
RuleFor(x => x.EndpointParams!.GroupId)
.NotNull()
.WithMessage("ENDPOINT_PARAMS requires EndpointParamsGroupId (maps to @pENDPOINT_PARAMS_GROUP_ID).");
});
@@ -76,12 +70,12 @@ public class InsertObjectProcedureValidator : AbstractValidator<InsertObjectProc
.MaximumLength(50)
.When(x => x.AddedWho != null);
RuleFor(x => x.Endpoint.Description)
RuleFor(x => x.Endpoint!.Description)
.MaximumLength(250)
.When(x => x.Endpoint.Description != null);
.When(x => x.Endpoint is { Description: not null });
RuleFor(x => x.EndpointAuth.Description)
RuleFor(x => x.EndpointAuth!.Description)
.MaximumLength(250)
.When(x => x.EndpointAuth.Description != null);
.When(x => x.EndpointAuth is { Description: not null });
}
}

View File

@@ -0,0 +1,21 @@
using FluentValidation;
using ReC.Application.Results.Commands;
namespace ReC.Application.Common.Validations;
public class InsertResultCommandValidator : AbstractValidator<InsertResultCommand>
{
public InsertResultCommandValidator()
{
RuleFor(x => x.ActionId)
.NotNull()
.WithMessage("ActionId is required.")
.GreaterThan(0L)
.When(x => x.ActionId.HasValue)
.WithMessage("ActionId must be greater than 0.");
RuleFor(x => x.References.BatchId)
.NotEmpty()
.WithMessage("BatchId is required.");
}
}

View File

@@ -0,0 +1,22 @@
using FluentValidation;
using MediatR;
using ReC.Application.RecActions.Commands;
using ReC.Application.Results.Queries;
namespace ReC.Application.Common.Validations;
public class InvokeBatchRecActionViewsCommandValidator : AbstractValidator<InvokeBatchRecActionViewsCommand>
{
public InvokeBatchRecActionViewsCommandValidator(ISender sender)
{
RuleFor(x => x.References.BatchId)
.NotEmpty()
.WithMessage("BatchId is required.")
.MustAsync(async (batchId, cancel) =>
{
var any = await sender.Send(new AnyResultViewQuery(BatchId: batchId), cancel);
return !any;
})
.WithMessage(x => $"Cannot invoke rec actions for batch '{x.References.BatchId}' because there are already results associated with it.");
}
}

View File

@@ -0,0 +1,15 @@
using FluentValidation;
using ReC.Application.Profile.Queries;
namespace ReC.Application.Common.Validations;
public class ReadProfileViewQueryValidator : AbstractValidator<ReadProfileViewQuery>
{
public ReadProfileViewQueryValidator()
{
RuleFor(x => x.Id)
.GreaterThan(0)
.When(x => x.Id.HasValue)
.WithMessage("Id must be greater than 0.");
}
}

View File

@@ -0,0 +1,15 @@
using FluentValidation;
using ReC.Application.RecActions.Queries;
namespace ReC.Application.Common.Validations;
public class ReadRecActionViewQueryValidator : AbstractValidator<ReadRecActionViewQuery>
{
public ReadRecActionViewQueryValidator()
{
RuleFor(x => x.ProfileId)
.GreaterThan(0)
.When(x => x.ProfileId.HasValue)
.WithMessage("ProfileId must be greater than 0.");
}
}

View File

@@ -0,0 +1,34 @@
using FluentValidation;
using ReC.Application.Results.Queries;
namespace ReC.Application.Common.Validations;
public class ReadResultViewQueryValidator : AbstractValidator<ReadResultViewQuery>
{
public ReadResultViewQueryValidator()
{
RuleFor(x => x)
.Must(x => x.Id.HasValue || x.ActionId.HasValue || x.ProfileId.HasValue || x.BatchId is not null)
.WithMessage("At least one filter (Id, ActionId, ProfileId or BatchId) must be provided.");
RuleFor(x => x.Id)
.GreaterThan(0)
.When(x => x.Id.HasValue)
.WithMessage("Id must be greater than 0.");
RuleFor(x => x.ActionId)
.GreaterThan(0)
.When(x => x.ActionId.HasValue)
.WithMessage("ActionId must be greater than 0.");
RuleFor(x => x.ProfileId)
.GreaterThan(0)
.When(x => x.ProfileId.HasValue)
.WithMessage("ProfileId must be greater than 0.");
RuleFor(x => x.BatchId)
.NotEmpty()
.When(x => x.BatchId is not null)
.WithMessage("BatchId must not be empty when provided.");
}
}

View File

@@ -0,0 +1,52 @@
using FluentValidation;
using ReC.Application.Common.Procedures.UpdateProcedure;
namespace ReC.Application.Common.Validations;
public class UpdateObjectProcedureValidator : AbstractValidator<UpdateObjectProcedure>
{
public UpdateObjectProcedureValidator()
{
RuleFor(x => x.Entity)
.IsInEnum()
.WithMessage("ENTITY must be a valid EntityType value.");
RuleFor(x => x.Id)
.GreaterThan(0)
.WithMessage("Target GUID/ID must be greater than 0.");
RuleFor(x => x.ChangedWho)
.MaximumLength(50)
.When(x => x.ChangedWho != null);
When(x => x.Endpoint is { Description: not null }, () =>
{
RuleFor(x => x.Endpoint.Description)
.MaximumLength(250);
});
When(x => x.EndpointAuth is { Description: not null }, () =>
{
RuleFor(x => x.EndpointAuth.Description)
.MaximumLength(250);
});
When(x => x.Profile is { Name: not null }, () =>
{
RuleFor(x => x.Profile.Name)
.MaximumLength(50);
});
When(x => x.Profile is { Mandantor: not null }, () =>
{
RuleFor(x => x.Profile.Mandantor)
.MaximumLength(50);
});
When(x => x.Profile is { Description: not null }, () =>
{
RuleFor(x => x.Profile.Description)
.MaximumLength(250);
});
}
}

View File

@@ -3,8 +3,11 @@ using MediatR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ReC.Application.Common.Behaviors;
using ReC.Application.Common.Behaviors.Action;
using ReC.Application.Common.Behaviors.InvokeAction;
using ReC.Application.Common.Constants;
using ReC.Application.Common.Options;
using ReC.Application.RecActions.Commands;
using System.Reflection;
namespace ReC.Application;
@@ -33,6 +36,8 @@ public static class DependencyInjection
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
cfg.AddOpenBehaviors([typeof(BodyQueryBehavior<,>), typeof(HeaderQueryBehavior<,>)]);
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<InvokeRecActionViewCommand, Unit>), typeof(PostprocessingBehavior));
cfg.AddBehavior(typeof(IPipelineBehavior<InvokeRecActionViewCommand, Unit>), typeof(PreprocessingBehavior));
cfg.LicenseKey = configOpt.LuckyPennySoftwareLicenseKey;
});
@@ -116,5 +121,23 @@ public static class DependencyInjection
return this;
}
#endregion ConfigureRecActions
#region ConfigureSqlException
public ConfigurationOptions ConfigureSqlException(Action<SqlExceptionOptions> configure)
{
_configActions.Enqueue(services => services.Configure(configure));
return this;
}
public ConfigurationOptions ConfigureSqlException(IConfiguration configuration)
{
_configActions.Enqueue(services =>
{
services.Configure<SqlExceptionOptions>(configuration);
});
return this;
}
#endregion ConfigureSqlException
}
}

View File

@@ -0,0 +1,37 @@
using MediatR;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures;
namespace ReC.Application.EndpointAuth.Commands;
public record DeleteEndpointAuthCommand : IDeleteProcedure
{
/// <summary>
/// Start GUID/ID (inclusive)
/// </summary>
public long Start { get; set; }
/// <summary>
/// End GUID/ID (inclusive). If 0, will be set to Start value.
/// </summary>
public long End { get; set; }
/// <summary>
/// If true, delete even if dependent ACTION data exists
/// </summary>
public bool Force { get; set; }
}
public class DeleteEndpointAuthProcedureHandler(ISender sender) : IRequestHandler<DeleteEndpointAuthCommand, int>
{
public async Task<int> Handle(DeleteEndpointAuthCommand request, CancellationToken cancel)
{
return await sender.Send(new DeleteObjectProcedure
{
Entity = EntityType.EndpointAuth,
Start = request.Start,
End = request.End,
Force = request.Force
}, cancel);
}
}

View File

@@ -1,32 +0,0 @@
using ReC.Application.Common.Procedures.DeleteProcedure;
namespace ReC.Application.EndpointAuth.Commands;
public record DeleteEndpointAuthProcedure : IDeleteProcedure
{
/// <summary>
/// Start GUID/ID (inclusive)
/// </summary>
public long Start { get; set; }
/// <summary>
/// End GUID/ID (inclusive). If 0, will be set to Start value.
/// </summary>
public long End { get; set; }
/// <summary>
/// If true, delete even if dependent ACTION data exists
/// </summary>
public bool Force { get; set; }
public DeleteObjectProcedure ToObjectProcedure()
{
return new DeleteObjectProcedure
{
Entity = "ENDPOINT_AUTH",
Start = Start,
End = End,
Force = Force
};
}
}

View File

@@ -0,0 +1,32 @@
using MediatR;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Common.Procedures;
namespace ReC.Application.EndpointAuth.Commands;
public record InsertEndpointAuthCommand : IInsertProcedure
{
public bool? Active { get; set; }
public string? Description { get; set; }
public byte? TypeId { get; set; }
public string? ApiKey { get; set; }
public string? ApiValue { get; set; }
public bool? ApiKeyAddToId { get; set; }
public string? Token { get; set; }
public string? Username { get; set; }
public string? Password { get; set; }
public string? Domain { get; set; }
public string? Workstation { get; set; }
}
public class InsertEndpointAuthProcedureHandler(ISender sender) : IRequestHandler<InsertEndpointAuthCommand, long>
{
public async Task<long> Handle(InsertEndpointAuthCommand request, CancellationToken cancel)
{
return await sender.Send(new InsertObjectProcedure
{
Entity = EntityType.EndpointAuth,
EndpointAuth = request
}, cancel);
}
}

View File

@@ -0,0 +1,26 @@
using MediatR;
using ReC.Application.Common.Procedures.UpdateProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using ReC.Application.Common.Procedures;
namespace ReC.Application.EndpointAuth.Commands;
public record UpdateEndpointAuthCommand : IUpdateProcedure<UpdateEndpointAuthDto>
{
public long Id { get; set; }
public UpdateEndpointAuthDto Data { get; set; } = null!;
}
public class UpdateEndpointAuthProcedureHandler(ISender sender) : IRequestHandler<UpdateEndpointAuthCommand, int>
{
public async Task<int> Handle(UpdateEndpointAuthCommand request, CancellationToken cancel)
{
return await sender.Send(new UpdateObjectProcedure
{
Entity = EntityType.EndpointAuth,
Id = request.Id,
EndpointAuth = request.Data
}, cancel);
}
}

View File

@@ -1,28 +0,0 @@
using ReC.Application.Common.Procedures.UpdateProcedure;
namespace ReC.Application.EndpointAuth.Commands;
public record UpdateEndpointAuthProcedure : IUpdateProcedure
{
public bool? Active { get; set; }
public string? Description { get; set; }
public byte? TypeId { get; set; }
public string? ApiKey { get; set; }
public string? ApiValue { get; set; }
public bool? ApiKeyAddToId { get; set; }
public string? Token { get; set; }
public string? Username { get; set; }
public string? Password { get; set; }
public string? Domain { get; set; }
public string? Workstation { get; set; }
public UpdateObjectProcedure ToObjectProcedure(long id, string? changedWho = null)
{
return new UpdateObjectProcedure
{
Entity = "ENDPOINT_AUTH",
Id = id,
EndpointAuth = this
}.ChangedBy(changedWho);
}
}

View File

@@ -0,0 +1,37 @@
using MediatR;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures;
namespace ReC.Application.EndpointParams.Commands;
public record DeleteEndpointParamsCommand : IDeleteProcedure
{
/// <summary>
/// Start GUID/ID (inclusive)
/// </summary>
public long Start { get; set; }
/// <summary>
/// End GUID/ID (inclusive). If 0, will be set to Start value.
/// </summary>
public long End { get; set; }
/// <summary>
/// If true, delete even if dependent ACTION data exists
/// </summary>
public bool Force { get; set; }
}
public class DeleteEndpointParamsProcedureHandler(ISender sender) : IRequestHandler<DeleteEndpointParamsCommand, int>
{
public async Task<int> Handle(DeleteEndpointParamsCommand request, CancellationToken cancel)
{
return await sender.Send(new DeleteObjectProcedure
{
Entity = EntityType.EndpointParams,
Start = request.Start,
End = request.End,
Force = request.Force
}, cancel);
}
}

View File

@@ -1,32 +0,0 @@
using ReC.Application.Common.Procedures.DeleteProcedure;
namespace ReC.Application.EndpointParams.Commands;
public record DeleteEndpointParamsProcedure : IDeleteProcedure
{
/// <summary>
/// Start GUID/ID (inclusive)
/// </summary>
public long Start { get; set; }
/// <summary>
/// End GUID/ID (inclusive). If 0, will be set to Start value.
/// </summary>
public long End { get; set; }
/// <summary>
/// If true, delete even if dependent ACTION data exists
/// </summary>
public bool Force { get; set; }
public DeleteObjectProcedure ToObjectProcedure()
{
return new DeleteObjectProcedure
{
Entity = "ENDPOINT_PARAMS",
Start = Start,
End = End,
Force = Force
};
}
}

View File

@@ -0,0 +1,27 @@
using MediatR;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Common.Procedures;
namespace ReC.Application.EndpointParams.Commands;
public record InsertEndpointParamsCommand : IInsertProcedure
{
public bool? Active { get; set; }
public string? Description { get; set; }
public short? GroupId { get; set; }
public byte? Sequence { get; set; }
public string? Key { get; set; }
public string? Value { get; set; }
}
public class InsertEndpointParamsProcedureHandler(ISender sender) : IRequestHandler<InsertEndpointParamsCommand, long>
{
public async Task<long> Handle(InsertEndpointParamsCommand request, CancellationToken cancel)
{
return await sender.Send(new InsertObjectProcedure
{
Entity = EntityType.EndpointParams,
EndpointParams = request
}, cancel);
}
}

View File

@@ -1,22 +0,0 @@
using ReC.Application.Common.Procedures.InsertProcedure;
namespace ReC.Application.EndpointParams.Commands;
public record InsertEndpointParamsProcedure : IInsertProcedure
{
public bool? Active { get; set; }
public string? Description { get; set; }
public short? GroupId { get; set; }
public byte? Sequence { get; set; }
public string? Key { get; set; }
public string? Value { get; set; }
public InsertObjectProcedure ToObjectProcedure(string? addedWho = null)
{
return new InsertObjectProcedure
{
Entity = "ENDPOINT_PARAMS",
EndpointParams = this
}.AddedBy(addedWho);
}
}

View File

@@ -0,0 +1,26 @@
using MediatR;
using ReC.Application.Common.Procedures.UpdateProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using ReC.Application.Common.Procedures;
namespace ReC.Application.EndpointParams.Commands;
public record UpdateEndpointParamsCommand : IUpdateProcedure<UpdateEndpointParamsDto>
{
public long Id { get; set; }
public UpdateEndpointParamsDto Data { get; set; } = null!;
}
public class UpdateEndpointParamsProcedureHandler(ISender sender) : IRequestHandler<UpdateEndpointParamsCommand, int>
{
public async Task<int> Handle(UpdateEndpointParamsCommand request, CancellationToken cancel)
{
return await sender.Send(new UpdateObjectProcedure
{
Entity = EntityType.EndpointParams,
Id = request.Id,
EndpointParams = request.Data
}, cancel);
}
}

View File

@@ -1,23 +0,0 @@
using ReC.Application.Common.Procedures.UpdateProcedure;
namespace ReC.Application.EndpointParams.Commands;
public record UpdateEndpointParamsProcedure : IUpdateProcedure
{
public bool? Active { get; set; }
public string? Description { get; set; }
public short? GroupId { get; set; }
public byte? Sequence { get; set; }
public string? Key { get; set; }
public string? Value { get; set; }
public UpdateObjectProcedure ToObjectProcedure(long id, string? changedWho = null)
{
return new UpdateObjectProcedure
{
Entity = "ENDPOINT_PARAMS",
Id = id,
EndpointParams = this
}.ChangedBy(changedWho);
}
}

View File

@@ -0,0 +1,37 @@
using MediatR;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures;
namespace ReC.Application.Endpoints.Commands;
public record DeleteEndpointCommand : IDeleteProcedure
{
/// <summary>
/// Start GUID/ID (inclusive)
/// </summary>
public long Start { get; set; }
/// <summary>
/// End GUID/ID (inclusive). If 0, will be set to Start value.
/// </summary>
public long End { get; set; }
/// <summary>
/// If true, delete even if dependent ACTION data exists
/// </summary>
public bool Force { get; set; }
}
public class DeleteEndpointProcedureHandler(ISender sender) : IRequestHandler<DeleteEndpointCommand, int>
{
public async Task<int> Handle(DeleteEndpointCommand request, CancellationToken cancel)
{
return await sender.Send(new DeleteObjectProcedure
{
Entity = EntityType.Endpoint,
Start = request.Start,
End = request.End,
Force = request.Force
}, cancel);
}
}

View File

@@ -1,32 +0,0 @@
using ReC.Application.Common.Procedures.DeleteProcedure;
namespace ReC.Application.Endpoints.Commands;
public record DeleteEndpointProcedure : IDeleteProcedure
{
/// <summary>
/// Start GUID/ID (inclusive)
/// </summary>
public long Start { get; set; }
/// <summary>
/// End GUID/ID (inclusive). If 0, will be set to Start value.
/// </summary>
public long End { get; set; }
/// <summary>
/// If true, delete even if dependent ACTION data exists
/// </summary>
public bool Force { get; set; }
public DeleteObjectProcedure ToObjectProcedure()
{
return new DeleteObjectProcedure
{
Entity = "ENDPOINT",
Start = Start,
End = End,
Force = Force
};
}
}

View File

@@ -0,0 +1,24 @@
using MediatR;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Common.Procedures;
namespace ReC.Application.Endpoints.Commands;
public record InsertEndpointCommand : IInsertProcedure
{
public bool? Active { get; set; }
public string? Description { get; set; }
public string? Uri { get; set; }
}
public class InsertEndpointProcedureHandler(ISender sender) : IRequestHandler<InsertEndpointCommand, long>
{
public async Task<long> Handle(InsertEndpointCommand request, CancellationToken cancel)
{
return await sender.Send(new InsertObjectProcedure
{
Entity = EntityType.Endpoint,
Endpoint = request
}, cancel);
}
}

View File

@@ -1,19 +0,0 @@
using ReC.Application.Common.Procedures.InsertProcedure;
namespace ReC.Application.Endpoints.Commands;
public record InsertEndpointProcedure : IInsertProcedure
{
public bool? Active { get; set; }
public string? Description { get; set; }
public string? Uri { get; set; }
public InsertObjectProcedure ToObjectProcedure(string? addedWho = null)
{
return new InsertObjectProcedure
{
Entity = "ENDPOINT",
Endpoint = this
}.AddedBy(addedWho);
}
}

View File

@@ -0,0 +1,26 @@
using MediatR;
using ReC.Application.Common.Procedures.UpdateProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using ReC.Application.Common.Procedures;
namespace ReC.Application.Endpoints.Commands;
public record UpdateEndpointCommand : IUpdateProcedure<UpdateEndpointDto>
{
public long Id { get; set; }
public UpdateEndpointDto Data { get; set; } = null!;
}
public class UpdateEndpointProcedureHandler(ISender sender) : IRequestHandler<UpdateEndpointCommand, int>
{
public async Task<int> Handle(UpdateEndpointCommand request, CancellationToken cancel)
{
return await sender.Send(new UpdateObjectProcedure
{
Entity = EntityType.Endpoint,
Id = request.Id,
Endpoint = request.Data
}, cancel);
}
}

View File

@@ -1,20 +0,0 @@
using ReC.Application.Common.Procedures.UpdateProcedure;
namespace ReC.Application.Endpoints.Commands;
public record UpdateEndpointProcedure : IUpdateProcedure
{
public bool? Active { get; set; }
public string? Description { get; set; }
public string? Uri { get; set; }
public UpdateObjectProcedure ToObjectProcedure(long id, string? changedWho = null)
{
return new UpdateObjectProcedure
{
Entity = "ENDPOINT",
Id = id,
Endpoint = this
}.ChangedBy(changedWho);
}
}

View File

@@ -0,0 +1,37 @@
using MediatR;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures;
namespace ReC.Application.Profile.Commands;
public record DeleteProfileCommand : IDeleteProcedure
{
/// <summary>
/// Start GUID/ID (inclusive)
/// </summary>
public long Start { get; set; }
/// <summary>
/// End GUID/ID (inclusive). If 0, will be set to Start value.
/// </summary>
public long End { get; set; }
/// <summary>
/// If true, delete even if dependent ACTION data exists
/// </summary>
public bool Force { get; set; }
}
public class DeleteProfileProcedureHandler(ISender sender) : IRequestHandler<DeleteProfileCommand, int>
{
public async Task<int> Handle(DeleteProfileCommand request, CancellationToken cancel)
{
return await sender.Send(new DeleteObjectProcedure
{
Entity = EntityType.Profile,
Start = request.Start,
End = request.End,
Force = request.Force
}, cancel);
}
}

View File

@@ -1,32 +0,0 @@
using ReC.Application.Common.Procedures.DeleteProcedure;
namespace ReC.Application.Profile.Commands;
public record DeleteProfileProcedure : IDeleteProcedure
{
/// <summary>
/// Start GUID/ID (inclusive)
/// </summary>
public long Start { get; set; }
/// <summary>
/// End GUID/ID (inclusive). If 0, will be set to Start value.
/// </summary>
public long End { get; set; }
/// <summary>
/// If true, delete even if dependent ACTION data exists
/// </summary>
public bool Force { get; set; }
public DeleteObjectProcedure ToObjectProcedure()
{
return new DeleteObjectProcedure
{
Entity = "PROFILE",
Start = Start,
End = End,
Force = Force
};
}
}

View File

@@ -0,0 +1,28 @@
using MediatR;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Common.Procedures;
namespace ReC.Application.Profile.Commands;
public record InsertProfileCommand : IInsertProcedure
{
public bool? Active { get; set; }
public byte? TypeId { get; set; }
public string? Mandantor { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public byte? LogLevelId { get; set; }
public short? LanguageId { get; set; }
}
public class InsertProfileProcedureHandler(ISender sender) : IRequestHandler<InsertProfileCommand, long>
{
public async Task<long> Handle(InsertProfileCommand request, CancellationToken cancel)
{
return await sender.Send(new InsertObjectProcedure
{
Entity = EntityType.Profile,
Profile = request
}, cancel);
}
}

View File

@@ -1,23 +0,0 @@
using ReC.Application.Common.Procedures.InsertProcedure;
namespace ReC.Application.Profile.Commands;
public record InsertProfileProcedure : IInsertProcedure
{
public bool? Active { get; set; }
public byte? TypeId { get; set; }
public string? Mandantor { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public byte? LogLevelId { get; set; }
public short? LanguageId { get; set; }
public InsertObjectProcedure ToObjectProcedure(string? addedWho = null)
{
return new InsertObjectProcedure
{
Entity = "PROFILE",
Profile = this
}.AddedBy(addedWho);
}
}

View File

@@ -0,0 +1,26 @@
using MediatR;
using ReC.Application.Common.Procedures.UpdateProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using ReC.Application.Common.Procedures;
namespace ReC.Application.Profile.Commands;
public record UpdateProfileCommand : IUpdateProcedure<UpdateProfileDto>
{
public long Id { get; set; }
public UpdateProfileDto Data { get; set; } = null!;
}
public class UpdateProfileProcedureHandler(ISender sender) : IRequestHandler<UpdateProfileCommand, int>
{
public async Task<int> Handle(UpdateProfileCommand request, CancellationToken cancel)
{
return await sender.Send(new UpdateObjectProcedure
{
Entity = EntityType.Profile,
Id = request.Id,
Profile = request.Data
}, cancel);
}
}

View File

@@ -1,27 +0,0 @@
using ReC.Application.Common.Procedures.UpdateProcedure;
namespace ReC.Application.Profile.Commands;
public record UpdateProfileProcedure : IUpdateProcedure
{
public bool? Active { get; set; }
public byte? TypeId { get; set; }
public string? Mandantor { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public byte? LogLevelId { get; set; }
public short? LanguageId { get; set; }
public DateTime? FirstRun { get; set; }
public DateTime? LastRun { get; set; }
public string? LastResult { get; set; }
public UpdateObjectProcedure ToObjectProcedure(long id, string? changedWho = null)
{
return new UpdateObjectProcedure
{
Entity = "PROFILE",
Id = id,
Profile = this
}.ChangedBy(changedWho);
}
}

View File

@@ -7,10 +7,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="15.1.0" />
<PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.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.1" />
<PackageReference Include="FluentValidation" Version="12.1.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.0" />
<PackageReference Include="MediatR" Version="13.1.0" />

View File

@@ -0,0 +1,37 @@
using MediatR;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures;
namespace ReC.Application.RecActions.Commands;
public record DeleteActionCommand : IDeleteProcedure
{
/// <summary>
/// Start GUID/ID (inclusive)
/// </summary>
public long Start { get; set; }
/// <summary>
/// End GUID/ID (inclusive). If 0, will be set to Start value.
/// </summary>
public long End { get; set; }
/// <summary>
/// If true, delete even if dependent RESULT data exists
/// </summary>
public bool Force { get; set; }
}
public class DeleteActionProcedureHandler(ISender sender) : IRequestHandler<DeleteActionCommand, int>
{
public async Task<int> Handle(DeleteActionCommand request, CancellationToken cancel)
{
return await sender.Send(new DeleteObjectProcedure
{
Entity = EntityType.Action,
Start = request.Start,
End = request.End,
Force = request.Force
}, cancel);
}
}

View File

@@ -1,32 +0,0 @@
using ReC.Application.Common.Procedures.DeleteProcedure;
namespace ReC.Application.RecActions.Commands;
public record DeleteActionProcedure : IDeleteProcedure
{
/// <summary>
/// Start GUID/ID (inclusive)
/// </summary>
public long Start { get; set; }
/// <summary>
/// End GUID/ID (inclusive). If 0, will be set to Start value.
/// </summary>
public long End { get; set; }
/// <summary>
/// If true, delete even if dependent RESULT data exists
/// </summary>
public bool Force { get; set; }
public DeleteObjectProcedure ToObjectProcedure()
{
return new DeleteObjectProcedure
{
Entity = "ACTION",
Start = Start,
End = End,
Force = Force
};
}
}

View File

@@ -1,8 +1,11 @@
using ReC.Application.Common.Procedures.UpdateProcedure;
using MediatR;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Domain.Constants;
using ReC.Application.Common.Procedures;
namespace ReC.Application.RecActions.Commands;
public record UpdateActionProcedure : IUpdateProcedure
public record InsertActionCommand : IInsertProcedure
{
public long? ProfileId { get; set; }
public bool? Active { get; set; }
@@ -11,20 +14,22 @@ public record UpdateActionProcedure : IUpdateProcedure
public long? EndpointAuthId { get; set; }
public short? EndpointParamsId { get; set; }
public short? SqlConnectionId { get; set; }
public byte? TypeId { get; set; }
public RestType? TypeId { get; set; }
public string? PreSql { get; set; }
public string? HeaderSql { get; set; }
public string? BodySql { get; set; }
public string? PostSql { get; set; }
public byte? ErrorActionId { get; set; }
public UpdateObjectProcedure ToObjectProcedure(long id, string? changedWho = null)
{
return new UpdateObjectProcedure
{
Entity = "ACTION",
Id = id,
Action = this
}.ChangedBy(changedWho);
}
}
public class InsertActionProcedureHandler(ISender sender) : IRequestHandler<InsertActionCommand, long>
{
public async Task<long> Handle(InsertActionCommand request, CancellationToken cancel)
{
return await sender.Send(new InsertObjectProcedure
{
Entity = EntityType.Action,
Action = request
}, cancel);
}
}

View File

@@ -1,4 +1,6 @@
using MediatR;
using Microsoft.Extensions.Logging;
using ReC.Application.Common.Exceptions;
using ReC.Application.RecActions.Queries;
using ReC.Domain.Constants;
@@ -7,31 +9,49 @@ namespace ReC.Application.RecActions.Commands;
public record InvokeBatchRecActionViewsCommand : IRequest
{
public long ProfileId { get; init; }
public required InvokeReferences References { get; init; }
}
public static class InvokeBatchRecActionViewsCommandExtensions
{
public static Task InvokeBatchRecActionView(this ISender sender, long profileId, CancellationToken cancel = default)
=> sender.Send(new InvokeBatchRecActionViewsCommand { ProfileId = profileId }, cancel);
}
public class InvokeRecActionViewsCommandHandler(ISender sender) : IRequestHandler<InvokeBatchRecActionViewsCommand>
public class InvokeRecActionViewsCommandHandler(ISender sender, ILogger<InvokeRecActionViewsCommandHandler>? logger = null) : IRequestHandler<InvokeBatchRecActionViewsCommand>
{
public async Task Handle(InvokeBatchRecActionViewsCommand request, CancellationToken cancel)
{
var actions = await sender.Send(new ReadRecActionViewQuery() { ProfileId = request.ProfileId, Invoked = false }, cancel);
var actions = await sender.Send(new ReadRecActionViewQuery() { ProfileId = request.ProfileId }, cancel);
foreach (var action in actions)
{
var ok = await sender.Send(action.ToInvokeCommand(), cancel);
if (!ok)
try
{
await sender.Send(new InvokeRecActionViewCommand()
{
Action = action,
References = request.References
}, cancel);
}
catch (RecActionException ex)
{
switch (action.ErrorAction)
{
case ErrorAction.Continue:
logger?.LogWarning(ex, "Rec action failed but continuing. ActionId: {ActionId}, ProfileId: {ProfileId}", ex.ActionId, ex.ProfileId);
break;
default:
return;
// Rethrow the exception to stop processing further actions
throw;
}
}
catch (Exception ex)
{
switch (action.ErrorAction)
{
case ErrorAction.Continue:
logger?.LogError(ex, "Unexpected error during rec action. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
break;
default:
// Rethrow the exception to stop processing further actions
throw;
}
}
}
}
}

View File

@@ -1,12 +1,13 @@
using MediatR;
using MediatR;
using Microsoft.Extensions.Configuration;
using ReC.Application.Common;
using Microsoft.Extensions.Options;
using ReC.Application.Common.Constants;
using ReC.Application.Common.Dto;
using ReC.Application.Common.Exceptions;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Common.Options;
using ReC.Application.Results.Commands;
using ReC.Domain.Constants;
using ReC.Domain.Views;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
@@ -14,134 +15,193 @@ using System.Text.Json;
namespace ReC.Application.RecActions.Commands;
public record InvokeRecActionViewCommand : IRequest<bool>
public record InvokeRecActionViewCommand : IRequest
{
public RecActionViewDto Action { get; set; } = null!;
public required InvokeReferences References { get; set; }
}
public static class InvokeRecActionViewCommandExtensions
public record InvokeReferences
{
public static InvokeRecActionViewCommand ToInvokeCommand(this RecActionViewDto dto) => new() { Action = dto };
public required string BatchId { get; init; }
public string? Reference1 { get; init; }
public string? Reference2 { get; init; }
public string? Reference3 { get; init; }
public string? Reference4 { get; init; }
public string? Reference5 { get; init; }
}
public class InvokeRecActionViewCommandHandler(
IOptions<RecActionOptions> options,
ISender sender,
IHttpClientFactory clientFactory,
IConfiguration? config = null
) : IRequestHandler<InvokeRecActionViewCommand, bool>
) : IRequestHandler<InvokeRecActionViewCommand>
{
public async Task<bool> Handle(InvokeRecActionViewCommand request, CancellationToken cancel)
private readonly RecActionOptions _options = options.Value;
public async Task Handle(InvokeRecActionViewCommand request, CancellationToken cancel)
{
var action = request.Action;
HttpClient? ntlmClient = null;
using var http = clientFactory.CreateClient(Http.ClientName);
if (action.RestType is not RestType restType)
throw new DataIntegrityException(
$"Rec action could not be invoked because the RestType value is null. " +
$"ProfileId: {action.ProfileId}, " +
$"Id: {action.Id}"
);
using var httpReq = restType
.ToHttpMethod()
.ToHttpRequestMessage(action.EndpointUri);
if (action.Body is not null)
try
{
using var reqBody = new StringContent(action.Body);
httpReq.Content = reqBody;
}
if (action.Headers is not null)
foreach (var header in action.Headers)
httpReq.Headers.Add(header.Key, header.Value);
switch (action.EndpointAuthType)
{
case EndpointAuthType.NoAuth:
break;
case EndpointAuthType.ApiKey:
if (action.EndpointAuthApiKey is string apiKey && action.EndpointAuthApiValue is string apiValue)
{
switch (action.EndpointAuthApiKeyAddTo)
{
case ApiKeyLocation.Header:
httpReq.Headers.Add(apiKey, apiValue);
break;
case ApiKeyLocation.Query:
var uriBuilder = new UriBuilder(httpReq.RequestUri!);
var query = System.Web.HttpUtility.ParseQueryString(uriBuilder.Query);
query[apiKey] = apiValue;
uriBuilder.Query = query.ToString();
httpReq.RequestUri = uriBuilder.Uri;
break;
default:
throw new DataIntegrityException(
$"The API key location '{action.EndpointAuthApiKeyAddTo}' is not supported. " +
$"ProfileId: {action.ProfileId}, " +
$"Id: {action.Id}"
);
}
}
break;
case EndpointAuthType.BearerToken:
case EndpointAuthType.JwtBearer:
case EndpointAuthType.OAuth2:
if (action.EndpointAuthToken is string authToken)
httpReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
break;
case EndpointAuthType.BasicAuth:
if (action.EndpointAuthUsername is string authUsername && action.EndpointAuthPassword is string authPassword)
{
var basicAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{authUsername}:{authPassword}"));
httpReq.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuth);
}
break;
case EndpointAuthType.NtlmAuth:
if (!string.IsNullOrWhiteSpace(action.EndpointAuthUsername))
{
var credentials = new NetworkCredential(
action.EndpointAuthUsername,
action.EndpointAuthPassword,
action.EndpointAuthDomain);
var credentialCache = new CredentialCache { { httpReq.RequestUri!, "NTLM", credentials } };
httpReq.Options.Set(new HttpRequestOptionsKey<CredentialCache>("Credentials"), credentialCache);
}
break;
case EndpointAuthType.DigestAuth:
case EndpointAuthType.OAuth1:
case EndpointAuthType.AwsSignature:
// These authentication methods require more complex implementations,
// often involving multi-step handshakes or specialized libraries.
// They are left as placeholders for future implementation.
default:
throw new NotImplementedException(
$"The authentication type '{action.EndpointAuthType}' is not supported yet. " +
if (action.RestType is not RestType restType)
throw new DataIntegrityException(
$"Rec action could not be invoked because the RestType value is null. " +
$"ProfileId: {action.ProfileId}, " +
$"Id: {action.Id}"
);
using var httpReq = CreateHttpRequestMessage(restType, action.EndpointUri);
if (action.Body is not null)
httpReq.Content = new StringContent(action.Body);
if (action.Headers is not null)
foreach (var header in action.Headers)
httpReq.Headers.Add(header.Key, header.Value);
switch (action.EndpointAuthType)
{
case EndpointAuthType.NoAuth:
break;
case EndpointAuthType.ApiKey:
if (action.EndpointAuthApiKey is string apiKey && action.EndpointAuthApiValue is string apiValue)
{
switch (action.EndpointAuthApiKeyAddTo)
{
case ApiKeyLocation.Header:
httpReq.Headers.Add(apiKey, apiValue);
break;
case ApiKeyLocation.Query:
var uriBuilder = new UriBuilder(httpReq.RequestUri!);
var query = System.Web.HttpUtility.ParseQueryString(uriBuilder.Query);
query[apiKey] = apiValue;
uriBuilder.Query = query.ToString();
httpReq.RequestUri = uriBuilder.Uri;
break;
default:
throw new DataIntegrityException(
$"The API key location '{action.EndpointAuthApiKeyAddTo}' is not supported. " +
$"ProfileId: {action.ProfileId}, " +
$"Id: {action.Id}"
);
}
}
break;
case EndpointAuthType.BearerToken:
case EndpointAuthType.JwtBearer:
case EndpointAuthType.OAuth2:
if (action.EndpointAuthToken is string authToken)
httpReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
break;
case EndpointAuthType.BasicAuth:
if (action.EndpointAuthUsername is string authUsername && action.EndpointAuthPassword is string authPassword)
{
var basicAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{authUsername}:{authPassword}"));
httpReq.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuth);
}
break;
case EndpointAuthType.NtlmAuth:
if (!string.IsNullOrWhiteSpace(action.EndpointAuthUsername))
{
if (_options.UseHttp1ForNtlm)
{
httpReq.Version = HttpVersion.Version11;
httpReq.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
}
var endpointAuthPassword = action.EndpointAuthPassword
#if DEBUG
?.Replace("%NTLM_PW%", config?.GetValue<string>("%NTLM_PW%"))
#endif
;
var credentials = new NetworkCredential(
action.EndpointAuthUsername,
endpointAuthPassword,
action.EndpointAuthDomain);
var credentialCache = new CredentialCache { { httpReq.RequestUri!, "NTLM", credentials } };
var ntlmHandler = new HttpClientHandler
{
Credentials = credentialCache,
UseDefaultCredentials = false
};
ntlmClient = new HttpClient(ntlmHandler, disposeHandler: true);
}
break;
case EndpointAuthType.DigestAuth:
case EndpointAuthType.OAuth1:
case EndpointAuthType.AwsSignature:
// These authentication methods require more complex implementations,
// often involving multi-step handshakes or specialized libraries.
// They are left as placeholders for future implementation.
default:
throw new NotImplementedException(
$"The authentication type '{action.EndpointAuthType}' is not supported yet. " +
$"ProfileId: {action.ProfileId}, " +
$"Id: {action.Id}"
);
}
var http = ntlmClient ?? clientFactory.CreateClient(Http.ClientName);
using var response = await http.SendAsync(httpReq, cancel);
var resBody = await response.Content.ReadAsStringAsync(cancel);
var resHeaders = response.Headers.ToDictionary();
await sender.Send(new InsertResultCommand()
{
Status = response.StatusCode.ToRecStatus(),
ActionId = action.Id,
Header = JsonSerializer.Serialize(resHeaders, options: new() { WriteIndented = false }),
Body = resBody,
Info = (short)response.StatusCode,
Type = ResultType.Main,
References = request.References
}, cancel);
}
using var response = await http.SendAsync(httpReq, cancel);
var resBody = await response.Content.ReadAsStringAsync(cancel);
var resHeaders = response.Headers.ToDictionary();
var statusCode = (short)response.StatusCode;
await sender.ExecuteInsertProcedure(new InsertResultProcedure()
catch(Exception ex)
{
StatusId = statusCode,
ActionId = action.Id,
Header = JsonSerializer.Serialize(resHeaders, options: new() { WriteIndented = false }),
Body = resBody
}, config?["AddedWho"], cancel);
await sender.Send(new InsertResultCommand()
{
Status = RecStatus.Error,
ActionId = action.Id,
Error = ex.ToString(),
Type = ResultType.Main,
References = request.References
}, cancel);
return response.IsSuccessStatusCode;
if (action.ErrorAction == ErrorAction.Stop)
throw new RecActionException(action.Id, action.ProfileId, ex);
}
finally
{
ntlmClient?.Dispose();
}
}
private static HttpRequestMessage CreateHttpRequestMessage(RestType restType, string? endpointUri)
{
var method = restType switch
{
RestType.Get => HttpMethod.Get,
RestType.Post => HttpMethod.Post,
RestType.Put => HttpMethod.Put,
RestType.Delete => HttpMethod.Delete,
RestType.Patch => HttpMethod.Patch,
RestType.Head => HttpMethod.Head,
RestType.Options => HttpMethod.Options,
RestType.Trace => HttpMethod.Trace,
RestType.Connect => HttpMethod.Connect,
RestType.None => throw new ArgumentOutOfRangeException(nameof(restType), $"The RestType value '{restType}' is not valid."),
_ => new HttpMethod(restType.ToString().ToUpperInvariant())
};
return new HttpRequestMessage(method, endpointUri);
}
}

View File

@@ -0,0 +1,26 @@
using MediatR;
using ReC.Application.Common.Procedures.UpdateProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using ReC.Application.Common.Procedures;
namespace ReC.Application.RecActions.Commands;
public record UpdateActionCommand : IUpdateProcedure<UpdateActionDto>
{
public long Id { get; set; }
public UpdateActionDto Data { get; set; } = null!;
}
public class UpdateActionProcedureHandler(ISender sender) : IRequestHandler<UpdateActionCommand, int>
{
public async Task<int> Handle(UpdateActionCommand request, CancellationToken cancel)
{
return await sender.Send(new UpdateObjectProcedure
{
Entity = EntityType.Action,
Id = request.Id,
Action = request.Data
}, cancel);
}
}

View File

@@ -1,8 +1,10 @@
using MediatR;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures;
namespace ReC.Application.Results.Commands;
public record DeleteResultProcedure : IDeleteProcedure
public record DeleteResultCommand : IDeleteProcedure
{
/// <summary>
/// Start GUID/ID (inclusive)
@@ -18,15 +20,18 @@ public record DeleteResultProcedure : IDeleteProcedure
/// Force parameter (not used for RESULT entity as it has no dependencies)
/// </summary>
public bool Force { get; set; }
public DeleteObjectProcedure ToObjectProcedure()
{
return new DeleteObjectProcedure
{
Entity = "RESULT",
Start = Start,
End = End,
Force = Force
};
}
}
public class DeleteResultProcedureHandler(ISender sender) : IRequestHandler<DeleteResultCommand, int>
{
public async Task<int> Handle(DeleteResultCommand request, CancellationToken cancel)
{
return await sender.Send(new DeleteObjectProcedure
{
Entity = EntityType.Result,
Start = request.Start,
End = request.End,
Force = request.Force
}, cancel);
}
}

View File

@@ -0,0 +1,32 @@
using MediatR;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.RecActions.Commands;
using ReC.Domain.Constants;
using ReC.Application.Common.Procedures;
namespace ReC.Application.Results.Commands;
public record InsertResultCommand : IInsertProcedure
{
public long? ActionId { get; set; }
public required RecStatus Status { get; set; }
public string? Header { get; set; }
public string? Body { get; set; }
public short Info { get; set; }
public string? InfoDetail { get; set; }
public string? Error { get; set; }
public required ResultType Type { get; set; }
public required InvokeReferences References { get; set; }
}
public class InsertResultProcedureHandler(ISender sender) : IRequestHandler<InsertResultCommand, long>
{
public async Task<long> Handle(InsertResultCommand request, CancellationToken cancel)
{
return await sender.Send(new InsertObjectProcedure
{
Entity = EntityType.Result,
Result = request
}, cancel);
}
}

View File

@@ -1,20 +0,0 @@
using ReC.Application.Common.Procedures.InsertProcedure;
namespace ReC.Application.Results.Commands;
public record InsertResultProcedure : IInsertProcedure
{
public long? ActionId { get; set; }
public short? StatusId { get; set; }
public string? Header { get; set; }
public string? Body { get; set; }
public InsertObjectProcedure ToObjectProcedure(string? addedWho = null)
{
return new InsertObjectProcedure
{
Entity = "RESULT",
Result = this
}.AddedBy(addedWho ?? "Rec.API");
}
}

View File

@@ -0,0 +1,26 @@
using MediatR;
using ReC.Application.Common.Procedures.UpdateProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
using ReC.Application.Common.Procedures;
namespace ReC.Application.Results.Commands;
public record UpdateResultCommand : IUpdateProcedure<UpdateResultDto>
{
public long Id { get; set; }
public UpdateResultDto Data { get; set; } = null!;
}
public class UpdateResultProcedureHandler(ISender sender) : IRequestHandler<UpdateResultCommand, int>
{
public async Task<int> Handle(UpdateResultCommand request, CancellationToken cancel)
{
return await sender.Send(new UpdateObjectProcedure
{
Entity = EntityType.Result,
Id = request.Id,
Result = request.Data
}, cancel);
}
}

View File

@@ -1,21 +0,0 @@
using ReC.Application.Common.Procedures.UpdateProcedure;
namespace ReC.Application.Results.Commands;
public record UpdateResultProcedure : IUpdateProcedure
{
public long? ActionId { get; set; }
public short? StatusId { get; set; }
public string? Header { get; set; }
public string? Body { get; set; }
public UpdateObjectProcedure ToObjectProcedure(long id, string? changedWho = null)
{
return new UpdateObjectProcedure
{
Entity = "RESULT",
Id = id,
Result = this
}.ChangedBy(changedWho);
}
}

View File

@@ -0,0 +1,35 @@
using DigitalData.Core.Abstraction.Application.Repository;
using MediatR;
using Microsoft.EntityFrameworkCore;
using ReC.Domain.Views;
namespace ReC.Application.Results.Queries;
public record AnyResultViewQuery(
long? Id = null,
long? ActionId = null,
long? ProfileId = null,
string? BatchId = null
) : IRequest<bool>;
public class AnyResultViewQueryHandler(IRepository<ResultView> repo) : IRequestHandler<AnyResultViewQuery, bool>
{
public Task<bool> Handle(AnyResultViewQuery request, CancellationToken cancel)
{
var q = repo.Query;
if(request.Id is long id)
q = q.Where(rv => rv.Id == id);
if(request.ActionId is long actionId)
q = q.Where(rv => rv.ActionId == actionId);
if(request.ProfileId is long profileId)
q = q.Where(rv => rv.ProfileId == profileId);
if(request.BatchId is string batchId)
q = q.Where(rv => rv.BatchId == batchId);
return q.AnyAsync(cancel);
}
}

View File

@@ -16,6 +16,14 @@ public record ReadResultViewQuery : IRequest<IEnumerable<ResultViewDto>>
public long? ActionId { get; init; } = null;
public long? ProfileId { get; init; } = null;
public string? BatchId { get; init; } = null;
public bool IncludeAction { get; init; } = true;
public bool IncludeProfile { get; init; } = false;
public bool LastBatch { get; init; } = false;
}
public class ReadResultViewQueryHandler(IRepository<ResultView> repo, IMapper mapper) : IRequestHandler<ReadResultViewQuery, IEnumerable<ResultViewDto>>
@@ -33,7 +41,18 @@ public class ReadResultViewQueryHandler(IRepository<ResultView> repo, IMapper ma
if(request.ProfileId is long profileId)
q = q.Where(rv => rv.ProfileId == profileId);
var entities = await q.ToListAsync(cancel);
if(request.BatchId is string batchId)
q = q.Where(rv => rv.BatchId == batchId);
if (request.IncludeAction)
q = q.Include(rv => rv.Action);
if(request.IncludeProfile)
q = q.Include(rv => rv.Profile);
var entities = request.LastBatch
? await GetLastBatchEntitiesAsync(q, cancel)
: await q.ToListAsync(cancel);
if (entities.Count == 0)
throw new NotFoundException($"No result views found for the given criteria. Criteria: {
@@ -46,4 +65,20 @@ public class ReadResultViewQueryHandler(IRepository<ResultView> repo, IMapper ma
return mapper.Map<IEnumerable<ResultViewDto>>(entities);
}
private static async Task<List<ResultView>> GetLastBatchEntitiesAsync(IQueryable<ResultView> q, CancellationToken cancel)
{
var lastBatchId = await q
.Where(rv => rv.BatchId != null)
.OrderByDescending(rv => rv.AddedWhen)
.Select(rv => rv.BatchId)
.FirstOrDefaultAsync(cancel);
if (lastBatchId is null)
return [];
return await q
.Where(rv => rv.BatchId == lastBatchId)
.ToListAsync(cancel);
}
}

View File

@@ -0,0 +1,50 @@
namespace ReC.Client.Api
{
/// <summary>
/// Optional reference values that are passed through to all result records when invoking a profile.
/// </summary>
public class InvokeReferences
{
/// <summary>Batch identifier.</summary>
public string
#if NET
?
#endif
BatchId { get; set; }
/// <summary>Reference value 1.</summary>
public string
#if NET
?
#endif
Reference1 { get; set; }
/// <summary>Reference value 2.</summary>
public string
#if NET
?
#endif
Reference2 { get; set; }
/// <summary>Reference value 3.</summary>
public string
#if NET
?
#endif
Reference3 { get; set; }
/// <summary>Reference value 4.</summary>
public string
#if NET
?
#endif
Reference4 { get; set; }
/// <summary>Reference value 5.</summary>
public string
#if NET
?
#endif
Reference5 { get; set; }
}
}

View File

@@ -21,14 +21,28 @@ namespace ReC.Client.Api
/// Invokes a ReC action for the specified profile.
/// </summary>
/// <param name="profileId">The profile identifier.</param>
/// <param name="references">Optional reference values to pass through to all result records.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns><see langword="true"/> if the request succeeds; otherwise, <see langword="false"/>.</returns>
public async Task<bool> InvokeAsync(int profileId, CancellationToken cancellationToken = default)
public async Task<bool> InvokeAsync(int profileId, InvokeReferences references, CancellationToken cancellationToken = default)
{
var resp = await Http.PostAsync($"{ResourcePath}/invoke/{profileId}", content: null, cancellationToken);
var content = references != null ? ReCClientHelpers.ToJsonContent(references) : null;
var resp = await Http.PostAsync($"{ResourcePath}/invoke/{profileId}", content, cancellationToken);
return resp.IsSuccessStatusCode;
}
/// <summary>
/// Invokes a ReC action for the specified profile.
/// </summary>
/// <param name="profileId">The profile identifier.</param>
/// <param name="batchId">Batch identifier.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns><see langword="true"/> if the request succeeds; otherwise, <see langword="false"/>.</returns>
public Task<bool> InvokeAsync(int profileId, string batchId, CancellationToken cancellationToken = default)
{
return InvokeAsync(profileId, new InvokeReferences() { BatchId = batchId }, cancellationToken);
}
/// <summary>
/// Retrieves Rec actions.
/// </summary>

View File

@@ -0,0 +1,23 @@
namespace ReC.Domain.Constants;
/// <summary>
/// Represents the general outcome of an operation, independent of any specific technology or protocol.
/// <para>
/// Technology-specific details (e.g., HTTP status codes) are stored separately
/// in the <c>RESULT_INFO</c> and <c>RESULT_INFO_DETAIL</c> fields.
/// </para>
/// </summary>
/// <seealso cref="RecStatusExtensions"/>
public enum RecStatus : byte
{
/// <summary>
/// Indicates that the operation completed successfully (value 0).
/// </summary>
OK = 0,
/// <summary>
/// Indicates that the operation failed (value 1).
/// When set, the <c>RESULT_ERROR</c> field should contain the error details.
/// </summary>
Error = 1
}

Some files were not shown because too many files have changed in this diff Show More