docs: add XML doc comments across src + Sister Projects section in CLAUDE.md
Bulk CommentChecker pass: fills in <param>/<inheritdoc> tags on public APIs across all 23 src/ projects so the doc-coverage gate is green. Also adds a Sister Projects section to CLAUDE.md pointing at the MxAccess Gateway and OtOpcUa sibling repos, and gitignores local credential captures (*login*.txt) and the wonder-app-vd03 deploy/ artifacts.
This commit is contained in:
@@ -37,6 +37,9 @@ public class ApiKeyValidator
|
||||
/// Validates an API key for a given method.
|
||||
/// Returns (isValid, apiKey, statusCode, errorMessage).
|
||||
/// </summary>
|
||||
/// <param name="apiKeyValue">The API key value from the X-API-Key header.</param>
|
||||
/// <param name="methodName">The name of the method being invoked.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<ApiKeyValidationResult> ValidateAsync(
|
||||
string? apiKeyValue,
|
||||
string methodName,
|
||||
@@ -120,18 +123,46 @@ public class ApiKeyValidator
|
||||
/// </summary>
|
||||
public class ApiKeyValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the API key validation was successful.
|
||||
/// </summary>
|
||||
public bool IsValid { get; private init; }
|
||||
/// <summary>
|
||||
/// The HTTP status code for the validation result.
|
||||
/// </summary>
|
||||
public int StatusCode { get; private init; }
|
||||
/// <summary>
|
||||
/// Error message if validation failed, if any.
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; private init; }
|
||||
/// <summary>
|
||||
/// The validated API key, if successful.
|
||||
/// </summary>
|
||||
public ApiKey? ApiKey { get; private init; }
|
||||
/// <summary>
|
||||
/// The validated API method, if successful.
|
||||
/// </summary>
|
||||
public ApiMethod? Method { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful validation result.
|
||||
/// </summary>
|
||||
/// <param name="apiKey">The validated API key.</param>
|
||||
/// <param name="method">The validated API method.</param>
|
||||
public static ApiKeyValidationResult Valid(ApiKey apiKey, ApiMethod method) =>
|
||||
new() { IsValid = true, StatusCode = 200, ApiKey = apiKey, Method = method };
|
||||
|
||||
/// <summary>
|
||||
/// Creates an unauthorized validation result.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message.</param>
|
||||
public static ApiKeyValidationResult Unauthorized(string message) =>
|
||||
new() { IsValid = false, StatusCode = 401, ErrorMessage = message };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a forbidden validation result.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message.</param>
|
||||
public static ApiKeyValidationResult Forbidden(string message) =>
|
||||
new() { IsValid = false, StatusCode = 403, ErrorMessage = message };
|
||||
}
|
||||
|
||||
@@ -12,19 +12,26 @@ public sealed class CommunicationServiceInstanceRouter : IInstanceRouter
|
||||
{
|
||||
private readonly CommunicationService _communicationService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the router with the central communication service.
|
||||
/// </summary>
|
||||
/// <param name="communicationService">Service used to dispatch routed calls to site clusters.</param>
|
||||
public CommunicationServiceInstanceRouter(CommunicationService communicationService)
|
||||
{
|
||||
_communicationService = communicationService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<RouteToCallResponse> RouteToCallAsync(
|
||||
string siteId, RouteToCallRequest request, CancellationToken cancellationToken) =>
|
||||
_communicationService.RouteToCallAsync(siteId, request, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<RouteToGetAttributesResponse> RouteToGetAttributesAsync(
|
||||
string siteId, RouteToGetAttributesRequest request, CancellationToken cancellationToken) =>
|
||||
_communicationService.RouteToGetAttributesAsync(siteId, request, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<RouteToSetAttributesResponse> RouteToSetAttributesAsync(
|
||||
string siteId, RouteToSetAttributesRequest request, CancellationToken cancellationToken) =>
|
||||
_communicationService.RouteToSetAttributesAsync(siteId, request, cancellationToken);
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace ScadaLink.InboundAPI;
|
||||
/// </summary>
|
||||
public static class EndpointExtensions
|
||||
{
|
||||
/// <summary>Registers the <c>POST /api/{methodName}</c> inbound API endpoint with the active-node gate and body-size filter applied.</summary>
|
||||
/// <param name="endpoints">The route builder to add the endpoint to.</param>
|
||||
public static IEndpointRouteBuilder MapInboundAPI(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
endpoints.MapPost("/api/{methodName}", HandleInboundApiRequest)
|
||||
|
||||
@@ -95,6 +95,7 @@ public static class ForbiddenApiChecker
|
||||
/// Analyses the script source and returns the list of trust-model violations.
|
||||
/// An empty list means the script is acceptable.
|
||||
/// </summary>
|
||||
/// <param name="scriptCode">The C# script source to analyse.</param>
|
||||
public static IReadOnlyList<string> FindViolations(string scriptCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(scriptCode))
|
||||
@@ -130,8 +131,10 @@ public static class ForbiddenApiChecker
|
||||
{
|
||||
private readonly List<string> _violations = new();
|
||||
|
||||
/// <summary>Gets the accumulated list of trust-model violation descriptions.</summary>
|
||||
public IReadOnlyList<string> Violations => _violations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void VisitUsingDirective(UsingDirectiveSyntax node)
|
||||
{
|
||||
if (node.Name is not null && IsForbidden(node.Name.ToString()))
|
||||
@@ -140,6 +143,7 @@ public static class ForbiddenApiChecker
|
||||
base.VisitUsingDirective(node);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void VisitQualifiedName(QualifiedNameSyntax node)
|
||||
{
|
||||
// Check the longest qualified name; do not descend so a single
|
||||
@@ -154,6 +158,7 @@ public static class ForbiddenApiChecker
|
||||
base.VisitQualifiedName(node);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
|
||||
{
|
||||
// Catches fully-qualified expressions such as System.IO.File.Delete(...).
|
||||
@@ -178,6 +183,7 @@ public static class ForbiddenApiChecker
|
||||
base.VisitMemberAccessExpression(node);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void VisitIdentifierName(IdentifierNameSyntax node)
|
||||
{
|
||||
// InboundAPI-015: 'dynamic' widens late-bound member access that the
|
||||
|
||||
@@ -11,12 +11,24 @@ namespace ScadaLink.InboundAPI;
|
||||
/// </summary>
|
||||
public interface IInstanceRouter
|
||||
{
|
||||
/// <summary>Routes a script call request to the specified site.</summary>
|
||||
/// <param name="siteId">Target site identifier.</param>
|
||||
/// <param name="request">The call request to route.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the routed call.</param>
|
||||
Task<RouteToCallResponse> RouteToCallAsync(
|
||||
string siteId, RouteToCallRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Routes a batch attribute read request to the specified site.</summary>
|
||||
/// <param name="siteId">Target site identifier.</param>
|
||||
/// <param name="request">The get-attributes request to route.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the routed call.</param>
|
||||
Task<RouteToGetAttributesResponse> RouteToGetAttributesAsync(
|
||||
string siteId, RouteToGetAttributesRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Routes a batch attribute write request to the specified site.</summary>
|
||||
/// <param name="siteId">Target site identifier.</param>
|
||||
/// <param name="request">The set-attributes request to route.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the routed call.</param>
|
||||
Task<RouteToSetAttributesResponse> RouteToSetAttributesAsync(
|
||||
string siteId, RouteToSetAttributesRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ public sealed class InboundApiEndpointFilter : IEndpointFilter
|
||||
private readonly ILogger<InboundApiEndpointFilter> _logger;
|
||||
private readonly InboundApiOptions _options;
|
||||
|
||||
/// <summary>Initializes a new <see cref="InboundApiEndpointFilter"/> with the given logger and options.</summary>
|
||||
/// <param name="logger">Logger for request rejection diagnostics.</param>
|
||||
/// <param name="options">Inbound API options including the maximum request body size.</param>
|
||||
public InboundApiEndpointFilter(
|
||||
ILogger<InboundApiEndpointFilter> logger,
|
||||
IOptions<InboundApiOptions> options)
|
||||
@@ -34,6 +37,9 @@ public sealed class InboundApiEndpointFilter : IEndpointFilter
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
/// <summary>Applies active-node gating and request body size checks before delegating to the next filter or handler.</summary>
|
||||
/// <param name="context">The endpoint filter invocation context containing the HTTP context and arguments.</param>
|
||||
/// <param name="next">The next filter or endpoint handler in the pipeline.</param>
|
||||
public async ValueTask<object?> InvokeAsync(
|
||||
EndpointFilterInvocationContext context,
|
||||
EndpointFilterDelegate next)
|
||||
|
||||
@@ -7,6 +7,7 @@ public class InboundApiOptions
|
||||
/// </summary>
|
||||
public const long DefaultMaxRequestBodyBytes = 1L * 1024 * 1024; // 1 MiB
|
||||
|
||||
/// <summary>Default timeout for inbound API method execution before the request is cancelled.</summary>
|
||||
public TimeSpan DefaultMethodTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -31,6 +31,11 @@ public class InboundScriptExecutor
|
||||
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the InboundScriptExecutor.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="serviceProvider">Service provider for dependency resolution.</param>
|
||||
public InboundScriptExecutor(ILogger<InboundScriptExecutor> logger, IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -40,6 +45,8 @@ public class InboundScriptExecutor
|
||||
/// <summary>
|
||||
/// Registers a compiled script handler for a method name.
|
||||
/// </summary>
|
||||
/// <param name="methodName">The method name to register.</param>
|
||||
/// <param name="handler">The compiled handler function.</param>
|
||||
public void RegisterHandler(string methodName, Func<InboundScriptContext, Task<object?>> handler)
|
||||
{
|
||||
_scriptHandlers[methodName] = handler;
|
||||
@@ -48,6 +55,7 @@ public class InboundScriptExecutor
|
||||
/// <summary>
|
||||
/// Removes a compiled script handler for a method name.
|
||||
/// </summary>
|
||||
/// <param name="methodName">The method name to remove.</param>
|
||||
public void RemoveHandler(string methodName)
|
||||
{
|
||||
_scriptHandlers.TryRemove(methodName, out _);
|
||||
@@ -57,6 +65,8 @@ public class InboundScriptExecutor
|
||||
/// Compiles and registers a single API method script. Returns <c>false</c> if the
|
||||
/// script is empty, fails Roslyn compilation, or violates the script trust model.
|
||||
/// </summary>
|
||||
/// <param name="method">The API method to compile and register.</param>
|
||||
/// <returns>True if successfully compiled and registered, false otherwise.</returns>
|
||||
public bool CompileAndRegister(ApiMethod method)
|
||||
{
|
||||
var handler = Compile(method);
|
||||
@@ -157,6 +167,11 @@ public class InboundScriptExecutor
|
||||
/// <summary>
|
||||
/// Executes the script for the given method with the provided context.
|
||||
/// </summary>
|
||||
/// <param name="method">The API method containing the script to execute.</param>
|
||||
/// <param name="parameters">Input parameters for the script.</param>
|
||||
/// <param name="route">Route helper for cross-site routing.</param>
|
||||
/// <param name="timeout">Timeout duration for script execution.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <param name="parentExecutionId">
|
||||
/// Audit Log #23 (ParentExecutionId): the inbound API request's per-request
|
||||
/// <c>ExecutionId</c> (minted early by <c>AuditWriteMiddleware</c> and stashed
|
||||
@@ -166,6 +181,7 @@ public class InboundScriptExecutor
|
||||
/// script execution points back at this inbound request. Null when the script
|
||||
/// runs outside an inbound API request flow.
|
||||
/// </param>
|
||||
/// <returns>The result of executing the script.</returns>
|
||||
public async Task<InboundScriptResult> ExecuteAsync(
|
||||
ApiMethod method,
|
||||
IReadOnlyDictionary<string, object?> parameters,
|
||||
@@ -270,10 +286,27 @@ public class InboundScriptExecutor
|
||||
/// </summary>
|
||||
public class InboundScriptContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The script parameters.
|
||||
/// </summary>
|
||||
public ScriptParameters Parameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The route helper for cross-site routing.
|
||||
/// </summary>
|
||||
public RouteHelper Route { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The cancellation token for script execution.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the InboundScriptContext.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The input parameters for the script.</param>
|
||||
/// <param name="route">The route helper for cross-site routing.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public InboundScriptContext(
|
||||
IReadOnlyDictionary<string, object?> parameters,
|
||||
RouteHelper route,
|
||||
|
||||
@@ -90,6 +90,13 @@ public sealed class AuditWriteMiddleware
|
||||
private readonly ILogger<AuditWriteMiddleware> _logger;
|
||||
private readonly IOptionsMonitor<AuditLogOptions> _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the middleware with its required dependencies.
|
||||
/// </summary>
|
||||
/// <param name="next">The next middleware in the pipeline.</param>
|
||||
/// <param name="auditWriter">Central audit writer used to persist inbound API audit events.</param>
|
||||
/// <param name="logger">Logger for this middleware.</param>
|
||||
/// <param name="options">Live-reloadable audit log options, read per-request.</param>
|
||||
public AuditWriteMiddleware(
|
||||
RequestDelegate next,
|
||||
ICentralAuditWriter auditWriter,
|
||||
@@ -102,6 +109,10 @@ public sealed class AuditWriteMiddleware
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the middleware: captures the request/response bodies and writes an inbound API audit event.
|
||||
/// </summary>
|
||||
/// <param name="ctx">The current HTTP context.</param>
|
||||
public async Task InvokeAsync(HttpContext ctx)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
@@ -468,6 +479,11 @@ public sealed class AuditWriteMiddleware
|
||||
private readonly MemoryStream _captured;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the capturing stream wrapping an inner sink.
|
||||
/// </summary>
|
||||
/// <param name="inner">The underlying response stream to forward writes to.</param>
|
||||
/// <param name="capBytes">Maximum number of bytes to capture for the audit copy.</param>
|
||||
public CapturedResponseStream(Stream inner, int capBytes)
|
||||
{
|
||||
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||
@@ -477,33 +493,44 @@ public sealed class AuditWriteMiddleware
|
||||
_captured = new MemoryStream();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanRead => false;
|
||||
/// <inheritdoc />
|
||||
public override bool CanSeek => false;
|
||||
/// <inheritdoc />
|
||||
public override bool CanWrite => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override long Length =>
|
||||
throw new NotSupportedException("CapturedResponseStream is write-only.");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException("CapturedResponseStream is write-only.");
|
||||
set => throw new NotSupportedException("CapturedResponseStream is write-only.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Flush() => _inner.Flush();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task FlushAsync(CancellationToken cancellationToken) =>
|
||||
_inner.FlushAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Read(byte[] buffer, int offset, int count) =>
|
||||
throw new NotSupportedException("CapturedResponseStream is write-only.");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override long Seek(long offset, SeekOrigin origin) =>
|
||||
throw new NotSupportedException("CapturedResponseStream is write-only.");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetLength(long value) =>
|
||||
throw new NotSupportedException("CapturedResponseStream is write-only.");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
// Forward to the real sink FIRST — the client must never miss
|
||||
@@ -512,12 +539,14 @@ public sealed class AuditWriteMiddleware
|
||||
CaptureBytes(buffer.AsSpan(offset, count));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
_inner.Write(buffer);
|
||||
CaptureBytes(buffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task WriteAsync(
|
||||
byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -526,6 +555,7 @@ public sealed class AuditWriteMiddleware
|
||||
CaptureBytes(buffer.AsSpan(offset, count));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -574,6 +604,7 @@ public sealed class AuditWriteMiddleware
|
||||
return (string.IsNullOrEmpty(content) ? null : content, truncated);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
|
||||
@@ -18,6 +18,7 @@ public static class AuditWriteMiddlewareExtensions
|
||||
/// must be registered in DI (typically via <c>AddAuditLog</c>) before this
|
||||
/// middleware runs.
|
||||
/// </summary>
|
||||
/// <param name="app">The application builder to add the middleware to.</param>
|
||||
public static IApplicationBuilder UseAuditWriteMiddleware(this IApplicationBuilder app)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(app);
|
||||
|
||||
@@ -13,6 +13,8 @@ public static class ParameterValidator
|
||||
/// Validates the request body against the method's parameter definitions.
|
||||
/// Returns deserialized parameters or an error message.
|
||||
/// </summary>
|
||||
/// <param name="body">The parsed JSON request body; null or undefined if no body was supplied.</param>
|
||||
/// <param name="parameterDefinitions">JSON-serialized list of <see cref="ScadaLink.Commons.Types.InboundApi.ParameterDefinition"/>; null or empty means no parameters are defined.</param>
|
||||
public static ParameterValidationResult Validate(
|
||||
JsonElement? body,
|
||||
string? parameterDefinitions)
|
||||
@@ -148,13 +150,24 @@ public static class ParameterValidator
|
||||
/// </summary>
|
||||
public class ParameterValidationResult
|
||||
{
|
||||
/// <summary>Gets a value indicating whether validation succeeded.</summary>
|
||||
public bool IsValid { get; private init; }
|
||||
/// <summary>Gets the error message when <see cref="IsValid"/> is false; null on success.</summary>
|
||||
public string? ErrorMessage { get; private init; }
|
||||
/// <summary>Gets the validated and type-coerced parameter values keyed by parameter name.</summary>
|
||||
public IReadOnlyDictionary<string, object?> Parameters { get; private init; } = new Dictionary<string, object?>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful validation result with the given parameters.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The validated and coerced parameter values.</param>
|
||||
public static ParameterValidationResult Valid(Dictionary<string, object?> parameters) =>
|
||||
new() { IsValid = true, Parameters = parameters };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed validation result with the given error message.
|
||||
/// </summary>
|
||||
/// <param name="message">Description of the validation failure.</param>
|
||||
public static ParameterValidationResult Invalid(string message) =>
|
||||
new() { IsValid = false, ErrorMessage = message };
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ public static class ReturnValueValidator
|
||||
/// definition. Returns <see cref="ReturnValidationResult.Valid"/> when no
|
||||
/// definition is configured or the result conforms to it.
|
||||
/// </summary>
|
||||
/// <param name="resultJson">The JSON-serialized script return value to validate.</param>
|
||||
/// <param name="returnDefinition">JSON-serialized list of <see cref="ReturnFieldDefinition"/> entries, or null/empty to skip validation.</param>
|
||||
public static ReturnValidationResult Validate(string? resultJson, string? returnDefinition)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(returnDefinition))
|
||||
@@ -125,7 +127,9 @@ public static class ReturnValueValidator
|
||||
/// </summary>
|
||||
public sealed class ReturnFieldDefinition
|
||||
{
|
||||
/// <summary>Field name as it must appear in the script return object.</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>Expected JSON type of this field (e.g., "string", "integer", "boolean", "object", "list").</summary>
|
||||
public string Type { get; set; } = "String";
|
||||
}
|
||||
|
||||
@@ -134,11 +138,16 @@ public sealed class ReturnFieldDefinition
|
||||
/// </summary>
|
||||
public sealed class ReturnValidationResult
|
||||
{
|
||||
/// <summary>True when the return value conforms to the declared return definition.</summary>
|
||||
public bool IsValid { get; private init; }
|
||||
/// <summary>Human-readable error message when <see cref="IsValid"/> is false; empty otherwise.</summary>
|
||||
public string ErrorMessage { get; private init; } = string.Empty;
|
||||
|
||||
/// <summary>Returns a successful validation result.</summary>
|
||||
public static ReturnValidationResult Valid() => new() { IsValid = true };
|
||||
|
||||
/// <summary>Returns a failed validation result with the specified error message.</summary>
|
||||
/// <param name="message">Human-readable description of the validation failure.</param>
|
||||
public static ReturnValidationResult Invalid(string message) =>
|
||||
new() { IsValid = false, ErrorMessage = message };
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ public class RouteHelper
|
||||
private readonly CancellationToken _deadlineToken;
|
||||
private readonly Guid? _parentExecutionId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="RouteHelper"/> with no deadline token and no parent execution id.
|
||||
/// </summary>
|
||||
/// <param name="instanceLocator">Service to resolve the site id for a given instance code.</param>
|
||||
/// <param name="instanceRouter">Service to route cross-site calls to the resolved site.</param>
|
||||
public RouteHelper(
|
||||
IInstanceLocator instanceLocator,
|
||||
IInstanceRouter instanceRouter)
|
||||
@@ -47,6 +52,7 @@ public class RouteHelper
|
||||
/// context so the method timeout actually covers routed calls, as the design doc
|
||||
/// requires.
|
||||
/// </summary>
|
||||
/// <param name="deadlineToken">The executing method's timeout cancellation token to inherit for routed calls.</param>
|
||||
public RouteHelper WithDeadline(CancellationToken deadlineToken) =>
|
||||
new(_instanceLocator, _instanceRouter, deadlineToken, _parentExecutionId);
|
||||
|
||||
@@ -59,12 +65,14 @@ public class RouteHelper
|
||||
/// parent. <see cref="InboundScriptExecutor"/> calls this when it builds the
|
||||
/// script context.
|
||||
/// </summary>
|
||||
/// <param name="parentExecutionId">The inbound request's execution id to stamp as the spawning parent on routed calls, or null for non-routed runs.</param>
|
||||
public RouteHelper WithParentExecutionId(Guid? parentExecutionId) =>
|
||||
new(_instanceLocator, _instanceRouter, _deadlineToken, parentExecutionId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a route target for the specified instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceCode">The unique code of the instance to route calls to.</param>
|
||||
public RouteTarget To(string instanceCode)
|
||||
{
|
||||
return new RouteTarget(
|
||||
@@ -83,6 +91,14 @@ public class RouteTarget
|
||||
private readonly CancellationToken _deadlineToken;
|
||||
private readonly Guid? _parentExecutionId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="RouteTarget"/> for the specified instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceCode">The unique code of the target instance.</param>
|
||||
/// <param name="instanceLocator">Service to resolve the site id for the instance.</param>
|
||||
/// <param name="instanceRouter">Service to route cross-site calls.</param>
|
||||
/// <param name="deadlineToken">Cancellation token representing the method-level deadline.</param>
|
||||
/// <param name="parentExecutionId">Optional parent execution id for audit correlation on routed calls.</param>
|
||||
internal RouteTarget(
|
||||
string instanceCode,
|
||||
IInstanceLocator instanceLocator,
|
||||
@@ -106,6 +122,9 @@ public class RouteTarget
|
||||
/// routed call inherits the executing method's timeout, so the call is bounded by
|
||||
/// the method-level deadline with no token argument.
|
||||
/// </summary>
|
||||
/// <param name="scriptName">Name of the script to call on the remote instance.</param>
|
||||
/// <param name="parameters">Optional parameters passed to the script; may be a dictionary or anonymous object.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token; defaults to the method deadline when not supplied.</param>
|
||||
public async Task<object?> Call(
|
||||
string scriptName,
|
||||
object? parameters = null,
|
||||
@@ -137,6 +156,8 @@ public class RouteTarget
|
||||
/// <summary>
|
||||
/// Gets a single attribute value from the remote instance.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">Name of the attribute to read.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token; defaults to the method deadline.</param>
|
||||
public async Task<object?> GetAttribute(
|
||||
string attributeName,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -148,6 +169,8 @@ public class RouteTarget
|
||||
/// <summary>
|
||||
/// Gets multiple attribute values from the remote instance (batch read).
|
||||
/// </summary>
|
||||
/// <param name="attributeNames">Names of the attributes to read.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token; defaults to the method deadline.</param>
|
||||
public async Task<IReadOnlyDictionary<string, object?>> GetAttributes(
|
||||
IEnumerable<string> attributeNames,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -173,6 +196,9 @@ public class RouteTarget
|
||||
/// <summary>
|
||||
/// Sets a single attribute value on the remote instance.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">Name of the attribute to write.</param>
|
||||
/// <param name="value">Value to set on the attribute.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token; defaults to the method deadline.</param>
|
||||
public async Task SetAttribute(
|
||||
string attributeName,
|
||||
string value,
|
||||
@@ -186,6 +212,8 @@ public class RouteTarget
|
||||
/// <summary>
|
||||
/// Sets multiple attribute values on the remote instance (batch write).
|
||||
/// </summary>
|
||||
/// <param name="attributeValues">Map of attribute names to values to write.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token; defaults to the method deadline.</param>
|
||||
public async Task SetAttributes(
|
||||
IReadOnlyDictionary<string, string> attributeValues,
|
||||
CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -6,6 +6,10 @@ namespace ScadaLink.InboundAPI;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers all inbound API services (API key validator, script executor, route helper, and endpoint filter).
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to register into.</param>
|
||||
public static IServiceCollection AddInboundAPI(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ApiKeyValidator>();
|
||||
|
||||
Reference in New Issue
Block a user