DRN.Framework.Utils 0.10.0-preview002

Prefix Reserved
This is a prerelease version of DRN.Framework.Utils.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package DRN.Framework.Utils --version 0.10.0-preview002
                    
NuGet\Install-Package DRN.Framework.Utils -Version 0.10.0-preview002
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DRN.Framework.Utils" Version="0.10.0-preview002" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DRN.Framework.Utils" Version="0.10.0-preview002" />
                    
Directory.Packages.props
<PackageReference Include="DRN.Framework.Utils" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DRN.Framework.Utils --version 0.10.0-preview002
                    
#r "nuget: DRN.Framework.Utils, 0.10.0-preview002"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DRN.Framework.Utils@0.10.0-preview002
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DRN.Framework.Utils&version=0.10.0-preview002&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=DRN.Framework.Utils&version=0.10.0-preview002&prerelease
                    
Install as a Cake Tool

master develop Quality Gate Status

Security Rating Maintainability Rating Reliability Rating Vulnerabilities Bugs Lines of Code Coverage

DRN.Framework.Utils

Core utilities package providing attribute-based dependency injection, configuration management, scoped logging, ambient context, and essential extensions.

Overview

  • Register public, concrete services with DI attributes and AddServicesWithAttributes(); bind settings with [Config] and IAppSettings.
  • Collect operation data with IScopedLog, coordinate root and keyed cancellation, and access initialized request scopes through ScopeContext.
  • Generate and validate source-known IDs, filter by creation time, and paginate by internal ID order.
  • Encode, hash, encrypt, merge JSON, validate JPEGs, resolve app data paths, and pack bits with the data utilities.
  • Use HTTP factories and response converters with explicit failure and disposal contracts; evaluate configured claims and MFA evidence with authentication helpers.

Table of Contents


QuickStart: Beginner

This console example registers the calling assembly and resolves a scoped service. It uses Development settings for the example; see Configuration for deployed applications.

using DRN.Framework.Utils.DependencyInjection;
using DRN.Framework.Utils.DependencyInjection.Attributes;
using DRN.Framework.Utils.Settings;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddSingleton<IAppSettings>(_ => AppSettings.Development());
services.AddServicesWithAttributes();

using var provider = services.BuildServiceProvider();
await provider.ValidateServicesAddedByAttributesAsync();
using var scope = provider.CreateScope();
Console.WriteLine(scope.ServiceProvider.GetRequiredService<IGreetingService>().Greet("World"));

public interface IGreetingService { string Greet(string name); }

[Scoped<IGreetingService>]
public class GreetingService : IGreetingService
{
    public string Greet(string name) => $"Hello, {name}!";
}

In an MVC application, inject IGreetingService into the controller and call Greet from the action.

using Microsoft.AspNetCore.Mvc;

public class HomeController(IGreetingService greetingService) : Controller
{
    public IActionResult Index() => Ok(greetingService.Greet("World"));
}

QuickStart: Advanced

Add these types to the same assembly to combine configuration binding and scoped logging. The example checks a timeout and records an amount; it does not call a payment provider.

using DRN.Framework.SharedKernel;
using DRN.Framework.Utils.DependencyInjection.Attributes;
using DRN.Framework.Utils.Logging;
using DRN.Framework.Utils.Settings;

[Config]
public class PaymentSettings
{
    public string ApiKey { get; set; } = string.Empty;
    public int TimeoutSeconds { get; set; } = 30;
}

public interface IPaymentService { PaymentResult Process(decimal amount); }
public sealed record PaymentResult(bool Success);

[Scoped<IPaymentService>]
public class PaymentService(IAppSettings settings, IScopedLog log, PaymentSettings config) : IPaymentService
{
    public PaymentResult Process(decimal amount)
    {
        using var duration = log.Measure("PaymentProcessing");
        log.Add("Amount", amount);
        log.Add("Environment", settings.Environment.ToString());
        log.AddToActions("Processing payment");

        if (config.TimeoutSeconds < 10)
            throw ExceptionFor.Configuration("Timeout too short");

        return new PaymentResult(Success: true);
    }
}

[Config] binds the PaymentSettings section by class name. Supply that section through configuration or use the class defaults when it is absent. Inside an initialized DRN Hosting request scope, ScopeContext.UserId supplies the ambient user ID. Console and worker code should use injected services; see Scope & Ambient Context.


Setup

If you are using DRN.Framework.Hosting (inheriting from DrnProgramBase), this package is automatically registered and validated.

The package targets .NET 10. Add it to a consumer project with:

dotnet add package DRN.Framework.Utils

For manual registration in a console app or worker with an existing builder:

using DRN.Framework.Utils;
using DRN.Framework.Utils.DependencyInjection;

// Registers Utils services, HybridCache and TimeProvider.
builder.Services.AddDrnUtils();
// Registers attributed services in the calling application assembly.
builder.Services.AddServicesWithAttributes();

AddServicesWithAttributes() also calls AddDrnUtils() when scanning a consumer assembly, so the first call is optional in that case. Manual applications must supply IConfiguration or IAppSettings. Neither registration call creates an ambient ScopeContext. See UtilsModule.cs and registration extensions.

The sections below show consumer snippets. Names such as Order, OrderDbContext, StatusResponse, SomeAsyncOp, model, and stream variables belong to the consuming application. Import the namespaces of the linked APIs; the snippets do not define an entire application.

HybridCache Registration

AddDrnUtils() registers Microsoft's HybridCache with default in-memory caching. To configure distributed caching (e.g., Redis), add your IDistributedCache registration before calling AddDrnUtils():

// Requires the Microsoft.Extensions.Caching.StackExchangeRedis package.
// Optional: Add distributed cache backend
builder.Services.AddStackExchangeRedisCache(options => 
{
    options.Configuration = "localhost:6379";
});

// HybridCache will use the distributed cache if available
builder.Services.AddDrnUtils();

For DRN Hosting rate limiting, use HybridCache to cache tenant plan, feature flag, or quota policy data. Do not treat HybridCache / IDistributedCache as an atomic distributed rate-limit counter by itself; hard multi-instance quotas need a backend designed for atomic operations, such as Redis with server-side Lua scripts, or enforcement at an API gateway/CDN/WAF layer.

Dependency Injection

Attribute-Based Registration

AddServicesWithAttributes() scans the calling assembly by default; pass an Assembly argument to scan another assembly. Lifetime discovery includes visible, concrete classes. Attribute definitions are in LifetimeAttribute.cs.

Attribute Lifetime Usage
[Singleton<T>] Singleton [Singleton<IMyService>] public class MyService : IMyService
[Scoped<T>] Scoped [Scoped<IMyService>] public class MyService : IMyService
[Transient<T>] Transient [Transient<IMyService>] public class MyService : IMyService
[SingletonWithKey<T>] Singleton (Keyed) [SingletonWithKey<IMyService>("key")]
[ScopedWithKey<T>] Scoped (Keyed) [ScopedWithKey<IMyService>("key")]
[TransientWithKey<T>] Transient (Keyed) [TransientWithKey<IMyService>("key")]
[HostedService] Singleton [HostedService] public class MyWorker : BackgroundService
[Config] Singleton [Config("Section")] public class MySettings
[ConfigRoot] Singleton [ConfigRoot] public class RootSettings

[Singleton<T>], [Scoped<T>], [Transient<T>], and their keyed variants accept an optional tryAdd parameter (default: true). When true, TryAdd is used so existing registrations are not overwritten. Set it to false to allow multiple implementations of the same service type.

Assembly scan metadata is cached, while registration modules and startup-validation state remain isolated to each service collection and provider. Repeating registration for the same assembly on one service collection is idempotent.

Hosted Services

Use [HostedService] to register IHostedService/BackgroundService implementations without manual AddHostedService<T>() calls. The class must implement IHostedService; otherwise the attribute is silently ignored.

[HostedService]
public class MyBackgroundWorker : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Do periodic work
            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }
}

Validation & Testing

DrnProgramBase automatically runs this validation at startup.

  • Validation: Ensure all registrations are resolvable via ValidateServicesAddedByAttributesAsync().
// In Program.cs
await app.Services.ValidateServicesAddedByAttributesAsync();

In integration tests with DRN.Framework.Testing:

[Theory, DataInline]
public async Task Validate_Dependencies(DrnTestContext context)
{
    context.ServiceCollection.AddServicesWithAttributes(); // Register local assembly
    await context.ValidateServicesAsync(); // Verifies attribute-registered services can be resolved
}

Scoped Cancellation

ICancellationUtils owns a root and keyed child scopes within the current DI service scope.

Intent API Effect
Cancel all scoped work cancellation.Root.Cancel() or cancellation.Root.Merge(token) Reaches every existing and later-created child.
Cancel a component or workflow GetOrCreateScope(key).Cancel() or .Merge(token) Affects only that group.
Cancel one operation A local linked CancellationTokenSource Affects only caller-owned work.
public sealed class CheckoutWorkflow(ICancellationUtils cancellation)
{
    private static readonly CancellationScopeKey ScopeKey =
        CancellationScopeKey.For<CheckoutWorkflow>();

    public async Task RunAsync(
        CancellationToken workflowLifetimeToken,
        CancellationToken operationToken)
    {
        var scope = cancellation.GetOrCreateScope(ScopeKey);

        using var operationSource = CancellationTokenSource
            .CreateLinkedTokenSource(scope.Token, workflowLifetimeToken, operationToken);

        await SomeAsyncOp(operationSource.Token);
    }

    public void CancelWorkflow() => cancellation.GetOrCreateScope(ScopeKey).Cancel();

    public void CancelEverything() => cancellation.Root.Cancel();
}

The same key returns the same scope and token. Root cancellation reaches every child, while child cancellation does not affect the root or other groups. Canceled scopes cannot be reset.

Keys can be type-owned or ownerless. Prefer CancellationScopeKey.For<T>() for a compile-time type or For(Type) for a runtime type. Add a name with For<T>(name) or For(Type, name) when one type owns multiple intentional groups. Use CancellationScopeKey.For(name) only when different types intentionally share one group.

Names use ordinal, case-sensitive equality and must be non-null developer-defined constants of at most 128 characters (empty string and whitespace are permitted). Keys are opaque and factory-created; the default value is invalid.

Ownerless keys share one ordinal-name namespace within the current ICancellationUtils service scope. Although empty and whitespace names are valid, prefer qualified, centrally defined names such as "MyPackage.CheckoutShutdown", because unrelated callers using the same ownerless name receive the same scope and can cancel each other's work.

Do not derive keys from request data, user input, instance IDs, or operation IDs because these values represent individual work rather than shared component or workflow lifetimes. ICancellationUtils owns returned scopes; callers own and dispose local linked sources.

For root-wide migration, replace cancellation.Cancel(), Merge(token), Token, and IsCancellationRequested with their cancellation.Root equivalents.

Module Registration & Startup Actions

Services can require complex registration logic or post-startup actions. Attributes inheriting from ServiceRegistrationAttribute handle this.

A class can carry multiple registration attributes. Discovery enumerates all of them, deduplicates equal attributes across the assembly, and retains each distinct module's service descriptors for startup validation. Use GetModuleAttributes(Type) for plural discovery; GetModuleAttribute(Type) requires exactly one attribute.

Example: DrnContext<T> (in DRN.Framework.EntityFramework) is decorated with [DrnContextServiceRegistration], which:

  1. Registers the DbContext.
  2. Runs startup migration handling; Development auto-migration occurs when DrnDevelopmentSettings:AutoMigrateDevelopment is enabled (default: true).
using DRN.Framework.EntityFramework.Context;
using Microsoft.EntityFrameworkCore;

public class MyDbContext : DrnContext<MyDbContext>
{
    public MyDbContext() : base(null) { }
    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { }
}

The parameterless constructor supports design-time creation; the options constructor supports DI. See DrnContext.cs.

Configuration

IAppSettings

Access configuration using strongly-typed environment checks and utility methods.

public class MyService(IAppSettings settings)
{
    public void DoWork()
    {
        if (settings.IsDevelopmentEnvironment) { /* dev-only logic */ }
        if (settings.IsStagingEnvironment) { /* staging-only logic */ }
        
        var conn = settings.GetRequiredConnectionString("Default");
        var value = settings.GetValue<int>("MySettings:Timeout", 30);
        var debugSummary = settings.GetDebugView().ToSummary(); // best-effort key-name redaction
    }
}

GetDebugView(includeRawValues: true) only includes raw values in Development. Summaries apply best-effort key-name redaction, not a complete security boundary; review them before logging or exposure. Child keys remain listed even when a provider also defines a scalar value for the parent section, and summary paths use the value provider's key casing. Object-based configuration helpers serialize through the framework JSON defaults and therefore use camelCase keys; explicit key/value configuration preserves the key text supplied by the caller.

Configuration Attributes ([Config])

Bind classes directly to configuration sections. These are registered as Singletons.

[Config("PaymentSettings")] // Binds to "PaymentSettings" section
public class PaymentOptions 
{ 
    public string ApiKey { get; set; } = string.Empty;
}

[Config] // Binds to "FeatureFlags" section (class name)
public class FeatureFlags { public bool Preview { get; set; } }

[ConfigRoot] // Binds to root configuration
public class RootSettings { public string Environment { get; set; } = string.Empty; }

[Config] defaults to annotation validation, binding non-public properties, and rejecting unknown configuration keys. [ConfigRoot] binds the root and disables unknown-key rejection. Startup annotation validation runs through ValidateServicesAddedByAttributesAsync().

Configuration Sources

DRN Hosting's AddDrnSettings loads configuration in this order, with later sources taking precedence. AddDrnUtils() alone does not load these sources:

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. User Secrets when the application assembly is available
  4. Environment variables (ASPNETCORE_, DOTNET_, then unprefixed)
  5. Mounted Settings:
    • /appconfig/key-per-file-settings/*
    • Every file in /appconfig/json-settings/, loaded as JSON
  6. Command-line arguments

Environment is required and must be Development, Staging, or Production. DRN validates the value used to select appsettings.{Environment}.json; define it in appsettings.json, environment variables, mounted settings, or command-line arguments, and do not override it in environment-specific JSON or user secrets.

Override the mount directory by registering IMountedSettingsConventionsOverride. See Hosting configuration extensions.

IAppSettings Troubleshooting

Symptom Cause Solution
ConfigurationException on startup Missing or invalid required configuration Inspect the reported key and correct its source value
Environment setting is missing Required Environment key not configured Set Environment to Development, Staging, or Production in appsettings.json, environment variables, mounted settings, or command-line arguments
GetRequiredConnectionString throws Connection string not found Verify key exists under ConnectionStrings section
IsDevelopmentEnvironment always false Resolved Environment is not Development Set the Environment configuration key to Development in an applicable source
Mounted settings not loading Wrong mount path Verify files exist at /appconfig/json-settings/ or override via IMountedSettingsConventionsOverride
Environment variables not binding Wrong naming format Use __ (double underscore) for nested keys: MySection__MyKey

App Data Settings

DrnAppDataSettings controls required temp/data roots. Overrides use process environment variables because roots resolve before DRN configuration.

Environment variable Purpose
DrnAppDataSettings__TempPath Overrides the temp base; the resolved temp path is <TempPath>/<EntryAssemblyNameNormalized>.
DrnAppDataSettings__DataPath Overrides the resolved data root as <DataPath>; the resolved temp path is <DataPath>/Temp/<EntryAssemblyNameNormalized> when temp is unset.

Set DrnAppDataSettings:RequireTemp or DrnAppDataSettings:RequireData to fail startup when the resolved path is not valid.

DrnAppFeatures

Feature flags and runtime knobs bound from the DrnAppFeatures configuration section via [Config].

This JSON shows Development defaults. Replace SeedKey with private key material for Staging or Production.

{
  "DrnAppFeatures": {
    "SeedData": false,
    "SeedKey": "Peace at home! Peace in the world! - Mustafa Kemal Atatürk (1931)",
    "DisableRequestBuffering": false,
    "MaxRequestBufferingSize": 0,
    "DrnRateLimit": {
      "Disabled": false,
      "TokenLimit": 100,
      "ReplenishmentSeconds": 60,
      "TokensPerPeriod": 100,
      "PreAuthTokenLimit": 1000,
      "PreAuthReplenishmentSeconds": 60,
      "PreAuthTokensPerPeriod": 1000,
      "PostAuthTokenLimit": 0,
      "PostAuthReplenishmentSeconds": 0,
      "PostAuthTokensPerPeriod": 0
    }
  }
}

DrnRateLimit is the configuration key; application code reads the same settings through IAppSettings.Features.RateLimit. Shared values apply to both DRN Hosting rate limiting phases. Phase-specific values set to 0 inherit the shared value; positive phase-specific values override it. Treat these values as global defaults; tenant plan, feature-flag, and account-specific quotas belong in DRN Hosting rate-limit rules. See the Hosting README rate limiting settings for operational guidance, endpoint metadata behavior, and production scaling notes.

Nested option objects must be validated explicitly before relying on child data annotations for startup safety. DrnAppFeatures validates DrnRateLimit as part of root validation because plain Validator.TryValidateObject does not recursively walk nested objects by itself.

Seed keys and derived settings

DrnAppFeatures.SeedKey feeds AppSecuritySettings. BLAKE3 derive-key mode uses a distinct DRN Framework context string for each output:

Output Representation Use
AppHashKey Base64Url-encoded 32 bytes Private hashing key
AppEncryptionKey Base64Url-encoded 32 bytes Private encryption key
AppKey 8 characters Public application discriminator
AppSeed Signed 64-bit value Seed-dependent operations

Changing SeedKey changes app-specific names, rate-limit keyed hashes, Development default Nexus key material, and seed-dependent operations. AppSettings enforces these constraints at startup:

  • The built-in default SeedKey (DrnAppFeatures.DefaultSeedKey) is only permitted in the Development environment and is rejected in Staging, Production, or NotDefined.
  • The sample SeedKey (DrnAppFeatures.SampleSeedKey) is only permitted during test execution (TestEnvironment.DrnTestContextEnabled == true) and is rejected in all non-test application runs.
Settings reference
Property Type Default Description
ApplicationStartedBy string? null Identifies which test started the application (set automatically by DrnTestContext).
SeedData bool false Enables data seeding on startup.
SeedKey string "Peace at home!…" Secret key for seed operations. Enforced [SecureKey(MinLength = 58)]. Built-in default is permitted only in Development; sample key is permitted only during test execution.
InternalRequestHttpVersion string "1.1" HTTP version used by IInternalRequest.
InternalRequestProtocol string "http" Protocol scheme used by IInternalRequest (e.g., http, https).
UseMonotonicDateTimeProvider bool false Reserved experimental flag for monotonic time-provider behavior data; it is not wired as a provider switch.
DisableRequestBuffering bool false Disables request body buffering entirely. Use for high-throughput services (e.g., file upload endpoints).
MaxRequestBufferingSize int 0 (→ 30,000) Maximum request body size to buffer in bytes. Values below 10,000 are ignored; 0 uses the 30,000-byte default.
DrnRateLimit.Disabled bool false Disables both pre-auth and post-auth DRN Hosting rate limiting layers.
DrnRateLimit.PartitionLogMode RateLimitPartitionLogMode KeyedHash Controls rejected IP/partition logging. KeyedHash logs deterministic keyed hashes for correlation; PlainText logs raw values and should be limited to controlled development or dedicated audit sinks.
DrnRateLimit.TokenLimit int 100 Token bucket burst capacity. Must be positive.
DrnRateLimit.ReplenishmentSeconds int 60 Token replenishment period in seconds. Must be positive.
DrnRateLimit.TokensPerPeriod int 100 Tokens added per replenishment period. Must be positive.
DrnRateLimit.PreAuthTokenLimit int 1000 Coarse pre-auth burst capacity for shared B2B NAT/VPN/CDN egress addresses. 0 inherits TokenLimit.
DrnRateLimit.PreAuthReplenishmentSeconds int 60 Pre-auth replenishment period. 0 inherits ReplenishmentSeconds.
DrnRateLimit.PreAuthTokensPerPeriod int 1000 Pre-auth tokens per period. 0 inherits TokensPerPeriod.
DrnRateLimit.PostAuthTokenLimit int 0 Optional post-auth burst capacity. 0 inherits TokenLimit.
DrnRateLimit.PostAuthReplenishmentSeconds int 0 Optional post-auth replenishment period. 0 inherits ReplenishmentSeconds.
DrnRateLimit.PostAuthTokensPerPeriod int 0 Optional post-auth tokens per period. 0 inherits TokensPerPeriod.

Request buffering and rate limiting settings are consumed by DRN.Framework.Hosting. See the Hosting README for middleware details.

NexusAppSettings and Nexus Keys

NexusAppSettings configures Nexus routing, generator instances, secure/plain IDs, and the key ring used by SourceKnownEntityIdUtils. Entity ID generation derives AppId from [EntityType<TApp>] metadata. Configured NexusAppSettings.AppId controls the host's client routing partition and host domain partition alignment in Entity Framework.

The following example illustrates the key format. Supply private key material in deployed applications.

{
  "NexusAppSettings": {
    "MacType": "blake3",
    "NexusAddress": "localhost:5988",
    "AppId": 5,
    "AppInstanceId": 12,
    "UseSecureSourceKnownIds": true,
    "Keys": [
      {
        "KeyMaterial": "0123456789abcdef0123456789abcdef",
        "Format": "Utf8",
        "Default": true
      }
    ]
  }
}

Keys must contain exactly one default key. Generation always uses the default key. Parsing tries the default key first and then the remaining configured keys, so old IDs remain parseable during key rotation while the previous key stays in the key ring.

ByteEncoding Requirement
Utf8 Default when omitted. KeyMaterial must be exactly 32 UTF-8 bytes. ASCII 32-character values satisfy this; non-ASCII values are valid only when the UTF-8 byte count is exactly 32.
Hex KeyMaterial must hex-decode to exactly 32 bytes, normally 64 hex characters. A 32-character hex string is rejected because it decodes to 16 bytes.
Base64 KeyMaterial must Base64-decode to exactly 32 bytes.
Base64UrlEncoded KeyMaterial must Base64Url-decode to exactly 32 bytes. This is the format used by Development default key-material generation.

Invalid user-provided keys are not hashed, stretched, truncated, repaired, or treated as another format. Validation rejects malformed encodings, empty keys, wrong decoded lengths, and raw values that are not exactly 32 UTF-8 bytes. Exception messages avoid including the secret key value. NexusKey.SampleKeyMaterial is forbidden in all environments, including tests. See NexusAppSettings.cs.

When Development has no default Nexus key, AppSettings derives deterministic 32-byte material from AppSecuritySettings. It stores the result in memory as Base64UrlEncoded, then applies the same BLAKE3 MAC/encryption key separation used for configured keys. This fallback is not random and is unavailable outside Development.

Logging (IScopedLog)

IScopedLog aggregates structured operational data, metrics, checkpoints, and exceptions for a logical scope. In DRN Hosting request scopes, Hosting enriches and emits that aggregate as a single log entry.

Core Features

  • Contextual: Every ScopedLog has a stable CorrelationId and captures an active W3C TraceId when available. Hosting adds request and user context.
  • Aggregation: Groups all actions, metrics, and exceptions into a single structured log entry.
  • Performance Tracking: Built-in measurement for code block durations and execution counts.
  • Exception Recording: AddException records exception details without changing control flow; callers remain responsible for recovery, rethrowing, and excluding sensitive data.

Scope events

ScopeEvent has Id, Outcome, and Reason properties. Id uses .NET's EventId type from Microsoft.Extensions.Logging, which holds a numeric ID and an optional name.

using DRN.Framework.Utils.Logging;
using Microsoft.Extensions.Logging;

public static class OrderLogEvents
{
    public static readonly EventId OrderProcessed = new(1, nameof(OrderProcessed));
}

Inside an operation with an injected IScopedLog:

log.WithEvent(new ScopeEvent(OrderLogEvents.OrderProcessed, "success", "completed"));

WithEvent sets the first event as primary. EventId, EventName, EventOutcome, and EventReason expose it directly and appear in GetLogs(). Later calls retain their ScopeEvent values under AdditionalEvents without replacing the primary event. LogScoped passes the primary .NET EventId to ILogger and preserves exception/warning severity.

CopyFrom keeps destination correlation, trace, and primary event ownership. Source events are retained as additional events when a primary already exists.

GetLogs detaches action and additional-event lists under the writer lock. Later additions do not change earlier snapshots. CopyFrom uses the same list snapshot. Objects stored inside lists remain caller-owned; this is not a deep clone.

OpenTelemetry correlation

TraceId captures Activity.Current.TraceId at scope construction when a W3C activity exists. Otherwise it is null and omitted from GetLogs(). CorrelationId is always generated for the scope. HTTP TraceIdentifier stays separate. These values remain stable; creating a log does not start or export a trace.

With the OpenTelemetry logging provider configured, native log TraceId, SpanId, and TraceFlags come from the activity active when the log is emitted. Emit within that activity for native correlation. The scoped snapshot does not populate native trace fields or restore ended activities. See OpenTelemetry log correlation.

Use module-owned catalogs such as OrderLogEvents with stable IDs and names. IDs are unique within a module, not across all libraries. Filter dedicated events by logger category and ID. Scope events share this model through composition, not inheritance.

Operation data

AddProperties reads public instance getters and skips indexers. Ignored properties are marked without invoking their getters. Other getter exceptions still propagate.

public class OrderService(IScopedLog logger)
{
    public void ProcessOrder(int orderId)
    {
        // 1. Measure execution time and count
        using var _ = logger.Measure("ProcessOrder"); 
        
        // 2. Add structured data (Key-Value)
        logger.Add("OrderId", orderId); 
        logger.AddIfNotNullOrEmpty("Referrer", "PartnerA");

        // 3. Track execution checkpoints
        logger.AddToActions("Validating order"); 
        
        try 
        {
            // ... logic ...
            // 4. Flatten and add complex objects that are safe to log
            logger.AddProperties("User", new { Name = "John", Role = "Admin" });
        }
        catch(Exception ex)
        {
            // 5. Record the exception, then preserve failure semantics
            logger.AddException(ex, "Failed to process order");
            throw;
        }
    }
}

HTTP Client Factories (IExternalRequest, IInternalRequest)

These factories wrap Flurl with framework JSON conventions and exact HTTP version requests. They do not configure retries or circuit breakers. See FlurlRequestFactory.cs.

External Requests

Use IExternalRequest for standard external API calls. It pre-configures DefaultJsonSerializer and enforces HTTP version policies.

public class PaymentService(IExternalRequest request)
{
    public async Task Process()
    {
        // Requests exactly HTTP/1.1.
        var response = await request.For("https://api.example.com", HttpVersion.Version11)
            .AppendPathSegment("v1/charges")
            .PostJsonAsync(new { Amount = 1000 })
            .FromJsonAsync<ExternalApiResponse>();
    }
}

Internal Requests (Service Mesh)

IInternalRequest builds service URLs from a host name and the configured InternalRequestProtocol and InternalRequestHttpVersion. Overloads accept explicit secure and HTTP version values. In a Linkerd service mesh, HTTP can be used with mesh-managed mTLS. The factory selects the configured scheme; it does not discover infrastructure or negotiate a protocol switch.

Instead of using IInternalRequest directly in business logic, wrap it in a typed request factory for better maintainability and configuration encapsulation.

// 1. Typed internal request factory
public interface INexusRequest { IFlurlRequest For(string path); }

[Singleton<INexusRequest>]
public class NexusRequest(IInternalRequest request, IAppSettings settings) : INexusRequest
{
    private readonly string _nexusAddress = settings.NexusAppSettings.NexusAddress;
    public IFlurlRequest For(string path) => request.For(_nexusAddress).AppendPathSegment(path);
}

// 2. Client Usage
public class NexusClient(INexusRequest request)
{
    public async Task<HttpResponse<string>> GetStatusAsync() =>
        await request.For("status").GetAsync().ToStringAsync();
}

Primary Handler Injection

InternalRequest and ExternalRequest accept an optional HttpMessageHandler? via constructor dependency injection. When an HttpMessageHandler is registered in DI (such as ApplicationContextRouterHandler during integration testing), FlurlClient instances use the injected handler for in-memory routing and interception without modifying global static Flurl state.

Response conversion and failures

Buffered response converters (ToStringAsync, ToBytesAsync, and FromJsonAsync) capture HttpStatus and Payload, then dispose the response even if reading or deserialization fails. Call converters as extension methods on Task<IFlurlResponse> or IFlurlResponse; HttpResponse models the converted result and no longer exposes static conversion entry points.

HttpResponse.StatusClass classifies the status as Informational, Success, Redirection, ClientError, ServerError, or Unknown; IsSuccessStatusCode is true only for 2xx. Flurl normally follows redirects, so a 3xx snapshot represents a redirect that remained final. Use AllowAnyHttpStatus() when the throwing converters should return non-success responses for direct inspection.

Use the TryToStringAsync, TryToBytesAsync, or TryFromJsonAsync<T> counterparts when transport, timeout, response-read, or deserialization failures must be inspected without catching exceptions:

var result = await request.For("status").GetAsync().TryFromJsonAsync<StatusResponse>();
if (result.Failure is { } failure)
{
    // Apply failure policy here; Payload may be unavailable.
    log.Add("HttpFailureKind", failure.Kind.ToString()); // Injected IScopedLog.
}

if (result.StatusClass == HttpStatusClass.ClientError)
{
    // Handle 4xx. HttpStatus and a successfully converted Payload remain available.
}
else if (result.StatusClass == HttpStatusClass.ServerError)
{
    // Handle 5xx according to the caller's retry policy.
}

HTTP error statuses and processing failures are independent. For example, a 422 with readable JSON has StatusClass.ClientError and no Failure, while malformed JSON returned with 200 has StatusClass.Success, IsSuccess == false, and Failure.Kind == Deserialization. Try converters propagate cancellation. HttpFailure.Message and HttpFailure.Exception are available for local diagnostics, may contain request details, and must be redacted before logging or exposure; both are ignored by System.Text.Json serialization.

Call result.ThrowIfFailure() after inspection to rethrow a captured transport, timeout, response-read, or deserialization exception with its original stack preserved. The method does not throw for a 3xx, 4xx, or 5xx response without a processing failure; inspect StatusClass or IsSuccessStatusCode when status enforcement is required.

Streaming ownership

Use using for streaming responses so the payload and response are released together:

using var response = await request.For("export").GetAsync().ToStreamAsync();
await response.Payload!.CopyToAsync(destination);

HttpResponse<T>.Dispose() is idempotent and disposes any IDisposable payload. TryToStreamAsync() transfers the same ownership to HttpCallResult<T>; dispose that result after consuming its stream payload.

Scope & Ambient Context (ScopeContext)

ScopeContext provides ambient access to request-scoped data after a DRN Hosting request scope has been initialized. Use injected services during startup, in background work, and outside request scopes.

  • Contextual Identity: Access UserId, TraceId, and Authenticated status within an initialized request scope.
  • Static Accessors: Provides request-scope access to IAppSettings, IScopedLog, and IServiceProvider.
  • RBAC Helpers: Built-in support for role and claim checks.
  • Test Initialization: ScopeContext.InitializeForTest(...) resets the async-local scope before seeding test services, user, log, and trace data.

IScopedUser exposes authenticated identity and claim state. Use GetClaimParameter<TValue> for typed claims; ScopeContext.GetClaimParameter<TValue> provides ambient access to the same contract.

Claim configuration and identity boundaries

AuthenticationClaimConfig is the shared claim contract. Hosting registers the result of DrnProgramBase.ConfigureAuthenticationClaims() once; ordinary Identity applications need no override. Standalone helpers use AuthenticationClaimConfig.Default. Subject, Name, Email, and Roles each expose a canonical Type and immutable Aliases; Mfa identifies one exact completed-MFA type/value.

Mapping Default canonical type Explicit aliases
Subject ClaimTypes.NameIdentifier sub
Name ClaimTypes.Name name
Email ClaimTypes.Email email
Roles ClaimTypes.Role roles
Mfa amr=mfa None

Subject = new("uid") replaces the entire mapping, accepting only uid. Add aliases explicitly, for example new("uid", "external_id"). Scalar aliases must agree; roles combine only selected types. Subject agreement checks include case variants of configured types and aliases; unconfigured standard subject claims are ignored. ScopedUser.Id is null for missing or conflicting primary account evidence. Generic claim lookup remains case-insensitive; mapped security decisions use exact types.

Scoped name/email use the primary identity and return null for conflicting selected values or issuers. IsInRole checks all selected roles from authenticated identities across issuers. Generic claim groups retain their issuer filters.

Canonical types govern issuance and native NameClaimType/RoleClaimType; aliases are additional DRN inputs and do not rewrite claims or alter native authorization. Hosting configures Identity's claim options so its factory emits matching claims and metadata directly. Future authentication integrations must produce the same contract, mapping aliases into canonical claims only when needed, rejecting ambiguous evidence and excluding unselected case variants that native lookups could accept. Only validated identities belong in the application principal. Preserve claim provenance and identity boundaries; never infer MFA from otp, arbitrary acr, or a claim's name. See Hosting integration.

MFA completion and assurance

Scoped users, MfaFor, MfaPrincipal, and Hosting authorization share the same config without requiring Identity services. Setup/pending credentials cannot prove MFA; multiple authenticated identities must agree on subject and issuer.

The default subject mapping retains single-identity subjectless completion and same-object proof compatibility, including equivalent copies of the default mapping. Custom subject mappings, stronger assurance, Identity operations, and renewal require account evidence. Evaluate the final authorized User for account-security decisions.

For stronger opt-in checks, MfaPrincipal.IsRecent(principal, config, trustedIssuer, maximumAge, utcNow, authenticationTimeClaimType) and IsPhishingResistant(principal, config, trustedIssuer, assuranceClaim) require the completed marker and additional evidence on the same authenticated identity, from the specified issuer. All authenticated identities must have an unambiguous matching subject and issuer. Setup/pending credentials, missing subjects and untrusted evidence fail closed. IsCompleted and the default Hosting Mfa policy retain their current semantics.

IsRecent defaults to auth_time, accepts integer Unix seconds, rejects future/malformed/conflicting timestamps, and includes the exact maximum-age boundary. Pass the current time from TimeProvider.GetUtcNow(); a negative maximum age is invalid configuration. Authentication recency is not necessarily MFA recency: use a provider-guaranteed verified-MFA timestamp claim when that is the requirement. Renewal preserves existing auth_time; these helpers do not issue claims.

IsPhishingResistant requires an explicit MfaClaimConfig for an assurance marker distinct from the completed marker. Configure it only for an issuer/value that guarantees phishing-resistant authentication; generic amr=mfa and passkey labels do not automatically establish this. Provider validation/mapping and preservation of additional assurance claims remain application responsibilities. Missing assurance after renewal returns false.

The implementation is in MfaPrincipal.cs; claim mappings are defined in AuthenticationClaimConfig.cs.

Ambient application data

ScopeData is separate caller-owned ambient storage and is not automatically copied into IScopedLog. Use SetFlag and typed SetParameter values for validated application data.

var currentUserId = ScopeContext.UserId;
var traceId = ScopeContext.TraceId;
var settings = ScopeContext.Settings; // Static IAppSettings access
var logger = ScopeContext.Log; // Static IScopedLog access

var isAdmin = ScopeContext.IsUserInRole("Admin");

var tenantId = ScopeContext.GetClaimParameter<Guid>("tenant-id");
ScopeContext.Data.SetFlag("show-preview", true);
ScopeContext.Data.SetParameter("page-size", 50);

TOTP Generation and Verification

TotpUtils.GenerateTotpCode(sharedKey) generates an authenticator code from a Base32 shared secret; TotpUtils.VerifyTotpCode(sharedKey, code) checks a submitted code. Defaults are six digits, 30-second steps, and ±1-step verification drift. Overloads accept an explicit timestamp and custom settings.

Verification is stateless: callers must enforce atomic per-account replay protection and attempt limits before accepting authentication. Bounded clock drift does not prevent code reuse. The utility does not issue MFA claims. See TotpUtils.cs for parameter validation details.

Data Utilities

App Data Roots (IAppData)

IAppData exposes validated temp/data roots. Normal startup recreates temp; DRN test contexts preserve sibling test data.

public class ExportService(IAppData appData)
{
    public string GetExportPath(string fileName) =>
        appData.Temp.GetPath("exports", fileName);
}

Use AppDataPathResult.GetPath(...) for traversal-safe child paths.

Temp and Data expose Path, DirectoryExists, and Status. The status describes resolution time; it is not a live filesystem check. GetPath rejects empty or invalid roots and paths escaping the root. It does not create the requested child directory. See AppDataPathResult.cs and App Data Settings.

Encodings (EncodingExtensions)

Unified API for binary-to-text encodings and model serialization-encoding.

  • Encodings: Base64, Base64Url (Safe for URLs), Hex, and Utf8.
  • Integrated: model.Encode(ByteEncoding.Hex) and hexString.Decode<TModel>(ByteEncoding.Hex).

Base32Encoding provides strict RFC 4648 Base32 encoding and decoding separately from ByteEncoding. Encoding produces canonical padded output by default and supports unpadded output for protocols such as authenticator shared keys. Decoding accepts canonical padded or unpadded input case-insensitively and rejects invalid lengths, padding, characters, and non-zero trailing bits.

var encoded = Base32Encoding.Encode(bytes);
var unpadded = Base32Encoding.Encode(bytes, includePadding: false);
var decoded = Base32Encoding.Decode(unpadded);

AES-256 Single-Block Encryption (Aes256)

Aes256 accepts and returns one Vector128<byte> block. It exposes explicit x86/ARM runtime-intrinsic and portable .NET AES paths; the default Encrypt and Decrypt methods select runtime intrinsics when available and otherwise use the portable provider. Construction therefore remains portable, while explicit runtime-intrinsic methods throw PlatformNotSupportedException on unsupported hosts.

Methods Implementation
Encrypt / Decrypt Runtime intrinsics with automatic portable fallback
EncryptRuntimeIntrinsics / DecryptRuntimeIntrinsics Explicit x86 AES-NI or ARM AES intrinsics
EncryptWithFramework / DecryptWithFramework Explicit cross-platform .NET AES provider

A live instance supports concurrent calls. Intrinsic operations read pre-expanded round keys without locks or per-call allocation. The source remarks record portable-provider concurrency verification against .NET 10.0.10: each operation creates its own cipher state without changing the configured key. This is version-specific evidence and must be rechecked when changing the runtime. Dispose the instance after all callers finish to clear intrinsic schedules and dispose portable key state.

Aes256 is a deterministic, single-block ECB primitive with no authentication. Do not compose it into multi-block ECB encryption.

using var aes = new Aes256(key);
Vector128<byte> ciphertext = aes.Encrypt(plaintext);
Vector128<byte> recovered = aes.Decrypt(ciphertext);

Vector128<byte> portableCiphertext = aes.EncryptWithFramework(plaintext);
if (Aes256.IsSupported)
{
    Vector128<byte> intrinsicPlaintext = aes.DecryptRuntimeIntrinsics(portableCiphertext);
}

Authenticated Encryption (AesGcmEncryptorBase)

For payload encryption with authentication, derive from AesGcmEncryptorBase and provide a stable, application-specific context. IAppSecuritySettings.CreateAesGcm(context) derives a dedicated 32-byte key from AppEncryptionKey using BLAKE3 and creates a cipher with 16-byte tags. Intermediate key buffers are cleared.

using DRN.Framework.Utils.Data.Encryption;
using DRN.Framework.Utils.Settings;

public sealed class ExportEncryptor(IAppSecuritySettings settings) : AesGcmEncryptorBase(settings)
{
    protected override string Context => "ExampleApp ExportPayload v1";
}

With injected IAppSecuritySettings securitySettings and payload bytes:

using var encryptor = new ExportEncryptor(securitySettings);
var encrypted = encryptor.Encrypt(payload);
var recovered = encryptor.Decrypt(encrypted.Nonce, encrypted.Ciphertext, encrypted.Tag);

Encrypt generates a fresh random 12-byte nonce and returns AesGcmEncryptedData(Ciphertext, Nonce, Tag). Keep all three values for decryption. Span overloads accept caller-owned buffers; use 12 bytes for the nonce, 16 for the tag, and the payload length for ciphertext/plaintext. Authentication failures throw. Keep the derivation context stable for existing data and dispose the encryptor after use. See AesGcmEncryptorBase.cs.

Hashing (HashExtensions)

Hash extensions support cryptographic and non-cryptographic algorithms.

  • Blake3: Default cryptographic hash.
  • XxHash3: Non-cryptographic hashing for lookup and cache keys; not an integrity check against attackers.
  • Security: Keyed hashing support (HashWithKey) for integrity protection.
  • Streams: Stream overloads hash files and large payloads without first materializing them as BinaryData; prefer these overloads for file and upload hashing.
var hash = data.Hash(HashAlgorithm.Blake3);
var fileHash = fileStream.Hash(HashAlgorithm.Sha256);

JSON & Document Utilities

  • Safe JSON Merge Patch: JsonMergePatch.SafeApplyMergePatch(target, patch) implements RFC 7396 processing semantics without mutating either input. Semantic no-ops reuse the target; changed object targets are cloned once and merged without repeated subtree cloning. MergeResult is a readonly record struct, Json is nullable for a root-level JSON null result, and Changed reports only actual document changes.
  • In-Place JSON Merge Patch: JsonMergePatch.ApplyMergePatchInPlace(ref target, patch) and ApplyMergePatchInPlace(targetObject, patchObject) perform full RFC 7396 merge patch operations directly in-place, preserving existing nested object references and updating the ref target reference if the root type changes.
  • Resource Safety: All merge methods validate the complete patch depth before applying changes. The repository unit suite includes every RFC 7396 Appendix A example.
  • Query String Serialization: QueryParameterSerializer flattens nested objects and arrays into query strings for API clients.

Merge patch object properties set to null remove those properties; arrays and other non-object patches replace the target. The default maximum patch depth is 64 and must be positive. The query serializer has a separate default depth limit of 10. See JsonMergePatch.cs and QueryParameterSerializer.cs.

Serialization & Streams

  • Unified Extensions: model.Serialize(method) supports both JSON and Query String formats.
  • Safe Stream Consumption: ToBinaryDataAsync and ToArrayAsync extensions with MaxSizeGuard to prevent memory exhaustion from untrusted streams.
var json = model.Serialize(SerializationMethod.SystemTextJson);
var query = model.Serialize(SerializationMethod.QueryString);
var bytes = await requestStream.ToBinaryDataAsync(maxSize: 1024 * 1024);

The stream helpers default to a 10 MiB limit and throw ValidationException when it is exceeded. Seekable streams are read from the current position and restored afterward; non-seekable streams are consumed. The helpers leave the input stream open and accept cancellation. See StreamExtensions.cs.

Serialize(SystemTextJson) uses JsonSerializer.Serialize(model) with its default options. Deserialize<T> supports JSON only; query-string deserialization is not provided.

Programmatic Validation

Extensions for programmatic validation using System.ComponentModel.DataAnnotations.

  • Contextual: Integrates with DRN.Framework.SharedKernel.ValidationException for standardized error reporting across layers.

Entity Creation-Date Filters (IEntityDateTimeUtils)

IEntityDateTimeUtils filters SourceKnownEntity.Id by its 250ms Source-Known ID creation tick without requiring database timestamp columns. Each date boundary maps to minimum and maximum scalar long ID bounds for efficient query evaluation.

public class OrderService(IEntityDateTimeUtils dateTimeUtils)
{
    public IQueryable<Order> GetOrdersInDateRange(IQueryable<Order> query, DateTimeOffset start, DateTimeOffset end)
    {
        return dateTimeUtils.CreatedBetween(query, start, end, inclusive: true);
    }
}
Filter Inclusive boundary Exclusive boundary
CreatedAfter Id >= tick.Min Id > tick.Max
CreatedBefore Id <= tick.Max Id < tick.Min
CreatedBetween Id >= begin.Min && Id <= end.Max Id > begin.Max && Id < end.Min
CreatedOutside Id <= begin.Max \|\| Id >= end.Min Id < begin.Min \|\| Id > end.Max

CreatedBetween and CreatedOutside normalize reversed endpoints. When both endpoints fall in the same tick, inclusive Between selects that entire tick and exclusive Between selects nothing. Inclusive Outside selects all rows; exclusive Outside excludes that tick. Apply(query, EntityCreatedFilter) dispatches by filter type and requires an end date for Between and Outside. See EntityDateTimeUtils.cs.

Pagination

IPaginationUtils paginates by the internal SourceKnownEntity.Id (long). External GUID cursors are parsed and checked for validity, then converted to internal IDs for filtering. Secure GUID ciphertext is not used as the sort key.

public class OrderDto(Order order) : Dto(order)
{
    public bool Active { get; } = order.Active;
}

public class OrderService(IPaginationUtils pagination, OrderDbContext dbContext)
{
    public async Task<PaginationResultModel<OrderDto>> GetRecentOrdersAsync(PaginationRequest request)
    {
        var query = dbContext.Orders.Where(x => x.Active);
        var result = await pagination.GetResultAsync(query, request);
        return result.ToModel(order => new OrderDto(order));
    }
}

The query must support EF Core asynchronous execution. GetResultAsync accepts an optional cancellation token, handles next/previous/refresh navigation, and fetches one extra item for page metadata. Explicit page jumps use Skip; ordinary cursor navigation uses ID comparisons. A total-count query runs only when UpdateTotalCount is requested. See PaginationUtils.cs.

Bit Packing

For scenarios requiring custom ID generation or compact binary data structures, use NumberBuilder and NumberParser. NumberBuilder<TNumber> is a ref struct; NumberParser is a value-type parser for low-allocation bit manipulation.

// Use NumberBuilder to pack data into a long
var builder = NumberBuilder.GetLong();
builder.TryAddNibble(0x05);  // Add 4 bits
builder.TryAddUShort(65535); // Add 16 bits
long packedValue = builder.GetValue();

// Use NumberParser to unpack
var parser = NumberParser.Get(packedValue);
byte nibble = parser.ReadNibble();
ushort value = parser.ReadUShort();

Use the same NumberBuildDirection and residue-bit settings for packing and parsing. The defaults are most-significant-first with 32 residue bits for signed long. TryAdd* returns false when there is insufficient remaining capacity; values are masked to the requested bit width. Validate value ranges before packing when truncation is unacceptable. See NumberBuilder.cs and NumberParser.cs.

Validators

Reusable validators live under DRN.Framework.Utils.Validators.

using DRN.Framework.Utils.Validators;

var validation = await JpegValidator.ValidateAsync(requestStream, maxLength: 1024 * 1024);
if (!validation.IsValid)
{
    var message = validation.ErrorReason switch
    {
        JpegValidationErrorReason.MaxLengthExceeded => "Profile picture exceeds the maximum allowed size.",
        JpegValidationErrorReason.InvalidMaxLength => "Profile picture maximum size must be zero or greater.",
        _ => "Profile picture must be a valid JPEG image."
    };
    throw ExceptionFor.Validation(message);
}

var imageBytes = validation.ImageData;

JpegValidator performs structural JPEG checks for markers, segment bounds, frame metadata, scan metadata, scan data presence, and optional maximum byte length. JpegValidationResult.ErrorReason distinguishes MaxLengthExceeded, InvalidMaxLength, and InvalidJpeg failures. Use ValidateAsync when validating an upload stream and keeping the validated bytes for persistence.

The default limit is 10 MiB. Validate and ValidateAsync return bytes only through the validated end-of-image marker, excluding trailing data. Span-based IsValid also requires any trailing bytes to be permitted padding. Structural validation does not decode image pixels. See JpegValidator.cs.

Diagnostics

Development Status

DevelopmentStatus collects database model and migration information during context startup. HasPendingChanges reports pending model changes; inspect each model's Flags.HasPendingMigrations for unapplied migrations. It is not a continuous database monitor. See DevelopmentStatus.cs.

public class StartupService(DevelopmentStatus status, IScopedLog log)
{
    public void CheckStatus()
    {
        if (status.HasPendingChanges)
        {
            log.AddToActions("Warning: Pending database changes detected");
            foreach (var model in status.Models)
            {
                 model.LogChanges(log, "Development");
            }
        }
    }
}

Time & Async

High-Performance Time (TimeStampManager)

TimeStampManager provides cached UTC time for repeated timestamp reads such as ID generation. Values are truncated to 250ms boundaries; the updater waits 10ms between finishing one update and starting the next. Refresh delay and timestamp precision are separate.

long precisionTicks = TimeStampManager.CurrentTimestamp(EpochTimeUtils.DefaultEpoch);
DateTimeOffset now = TimeStampManager.UtcNow; // Cached UTC time truncated to 250ms precision

Backward clock drift below five seconds freezes the cached timestamp until the clock catches up. Drift of at least five seconds requests application shutdown and causes timestamp reads to throw ClockDriftException. See TimeStampManager.cs.

Async-Safe Timer (RecurringAction)

RecurringAction prevents overlapping callbacks. Its period is a delay in milliseconds after a callback finishes, not a fixed interval between start times.

using var worker = new RecurringAction(async () => {
    await DoHeavyWork();
}, period: 1000, start: true);

worker.Stop();
worker.Start(); // Resume after stopping

Stop() prevents an active callback from rescheduling the timer after it completes. The callback itself is allowed to finish.

Subscribe to OnActionFailed to observe callback exceptions. Dispose() stops scheduling but does not wait for an active callback; Start() throws after disposal. See RecurringAction.cs.

Time

AddDrnUtils() uses TryAddSingleton to register TimeProvider.System. A previously registered TimeProvider is retained, allowing callers to supply testable time.

ID Generation & Validation

Source-known IDs separate the internal long ID from the external SourceKnownEntityId GUID. The external form adds entity metadata and a keyed integrity check; it can be plain or encrypted.

ID generation is automatically handled by DrnContext when SourceKnownEntities are saved.

Generation Modes

The Generate method dispatches to secure or plain generation based on the UseSecureSourceKnownIds flag in NexusAppSettings (defaults to true). Explicit GenerateSecure and GeneratePlain methods are also available to bypass the flag.

Method Behavior
Generate Dispatches to secure or plain based on UseSecureSourceKnownIds
GenerateSecure AES-256-ECB encryption of the full 16-byte GUID block
GeneratePlain Plaintext with visible 8D8D version/variant markers
ToSecure Converts a plain ID to its secure form (idempotent)
ToPlain Converts a secure ID to its plain form (idempotent)

The secure variant encrypts the entire 16-byte GUID with Aes256 as a pseudo-random permutation (PRP). For this single block, ECB is equivalent to CBC with a zero IV and uses no nonce. It is deterministic: equal blocks under the same key produce equal ciphertext. Integrity comes from the separate 32-bit BLAKE3 keyed MAC, not from AES. BLAKE3 derives distinct MAC and encryption keys from the decoded NexusKey material.

Generation uses the default NexusKey. Parse uses a default-first key-ring fallback, so IDs generated before key rotation can still be parsed while the previous key remains configured.

SourceKnownEntityIdUtils is a singleton and reuses each key-ring entry's Aes256 instance. Intrinsic and portable paths preserve the same encrypted ID format. See AES-256 single-block encryption for concurrency, runtime-verification and disposal requirements.

// Generate with flag-based dispatch (secure by default)
var entityId = sourceKnownEntityIdUtils.Generate<User>(id);

// Explicitly secure
var secureId = sourceKnownEntityIdUtils.GenerateSecure<User>(id);

// Explicitly plain (visible markers for debugging/development)
var plainId = sourceKnownEntityIdUtils.GeneratePlain<User>(id);

// Convert between secure and plain forms (idempotent)
var convertedSecureId = sourceKnownEntityIdUtils.ToSecure(plainId);
var convertedPlainId = sourceKnownEntityIdUtils.ToPlain(secureId);

Generate the internal ID first, or use the parameterless external-ID overload:

long internalId = sourceKnownIdUtils.Next<User>();
var externalId = sourceKnownEntityIdUtils.Generate<User>(internalId);
var anotherId = sourceKnownEntityIdUtils.Generate<User>();

User must derive from SourceKnownEntity and carry the required entity/app metadata. Next<TEntity>() derives the app partition from that metadata and uses the configured instance ID. Explicit Next/Generate overloads accept app and instance IDs. GeneratePlain<TEntity>() and GenerateSecure<TEntity>() also generate a new internal ID when called without arguments. See SourceKnownIdUtils.cs and SourceKnownEntityIdUtils.cs.

Parse & Validation

Parse accepts secure and plaintext IDs and verifies their integrity.

Parse(Guid) returns a result with Valid == false for an invalid ID. Validate<TEntity> throws when integrity, entity type, or application partition does not match. A valid ID does not grant access to an entity; authorization remains the application's responsibility.

Add rate limiting to endpoints that accept SourceKnownEntityId from untrusted sources to prevent brute-force attacks.

Users can validate incoming IDs (e.g., from APIs) using multiple approaches depending on the context:

1. Injectable Utility (Recommended for Service Layer)

var sourceKnownId = sourceKnownEntityIdUtils.Validate<User>(externalGuidId);

2. SourceKnownRepository (Recommended for Data Access)

// Method on SourceKnownRepository<TEntity>
var sourceKnownId = userRepository.GetEntityId(externalGuidId); 

3. SourceKnownEntity (Recommended for Domain Logic)

// Helper on SourceKnownEntity base class
var sourceKnownId = userInstance.GetEntityId<User>(externalGuidId);

GUID Byte Layout

The plaintext form of a SourceKnownEntityId (SKEID) packs identity, integrity, time-addressing, and UUID V8 compatibility (RFC 9562 §5.8) into a single 128-bit GUID. Secure IDs are opaque ciphertext and are not guaranteed to retain UUID version or variant bits.

Byte(s) Purpose
0 Epoch index (8 bits; current releases support epoch 0)
1–4 SKID upper half (32 bits, sign-toggled)
5 SKID low byte 0 (MSB of SKID lower half / timestamp LSB)
6 Version marker (0x8D, UUID V8, RFC 9562 §5.8)
7 Entity type (8 bits, up to 256 entity types)
8 Variant marker (0x8D, RFC 4122 compatible)
9–11 SKID low bytes (remaining 24 bits)
12–15 BLAKE3 keyed MAC (32 bits, integrity verification)

Epoch & Time Addressing

SourceKnownEntityIds use epoch-based time addressing for monotonic ordering. Current releases support the first epoch, which starts on 2025-01-01 and spans approximately 68 years ($2^{31}$ seconds total coverage, split across two halves).

Property Value
Epoch start 2025-01-01
Supported duration ~68 years ($2^{31}$ seconds)
Supported epoch 0

Current releases support epoch 0 only; generation rejects timestamps outside its supported range.

Concurrency

Lock-Free Atomic Utilities (LockUtils)

LockUtils provides static helpers for lock-free atomic operations built on Interlocked. Use these primitives to coordinate concurrent access without OS-level locks.

Method Purpose
TryClaimLock(ref int) Atomically claims a lock (0 → 1). Returns true if successful.
TryClaimScope(ref int) Returns a disposable LockScope that auto-releases on dispose.
ReleaseLock(ref int) Unconditionally releases a lock (→ 0).
TrySetIfEqual<T>(ref T?, T, T?) Atomic CAS for reference types; sets value if current is the same reference as comparand.
TrySetIfNull<T>(ref T?, T) Sets value only if current is null.
TrySetIfNotEqual<T>(ref T?, T, T?) Sets value only if current is not the same reference as comparand (retry loop).
TrySetIfNotNull<T>(ref T?, T) Sets value only if current is not null.

TrySetIfNotEqual and TrySetIfNotNull use bounded retries (maxRetries, default 100). A false result can mean either that the comparison condition was not met or that retries were exhausted.

using DRN.Framework.Utils.Concurrency;

public sealed class WorkGate
{
    private int _lock;
    private object? _instance;

    public bool TryRun(Action work)
    {
        using var scope = LockUtils.TryClaimScope(ref _lock);
        if (!scope.Acquired) return false;
        work();
        return true;
    }

    public bool TryInitialize(object instance) =>
        LockUtils.TrySetIfNull(ref _instance, instance);
}

Extensions

Extensions cover .NET types, DI descriptors, HTTP diagnostics, and dynamic method discovery.

Reflection & MethodUtils

MethodUtils caches generic and non-generic method discovery and execution. Prefer [UnsafeAccessor] when the target type is known and accessible at compile time; use reflection for dynamically discovered types.

  • Invoke: instance.InvokeMethod("Name", args) and type.InvokeStaticMethod("Name", args).
  • Generics: instance.InvokeMethod("Name", typeArgs, args) and type.InvokeStaticMethod("Name", typeArgs, args).
  • Argument Overloads: Specialized 0, 1, 2, 3 argument and Span<object?> overloads avoid a params argument array. Boxing and other caller allocations can still occur.
  • Caching & Execution: FindMethod caches discovery through MethodCacheKey; invocation uses runtime MethodInvoker.
  • Uncached Discovery: type.FindMethodUncached(...) for explicit cache-bypassing scenarios (e.g. one-off startup discovery).

Pass generic type arguments as an explicit Type[], such as instance.InvokeMethod("Name", [typeof(string)]). A single typeof(string) argument selects an ordinary method accepting Type. The former generic/fast aliases were removed; use FindMethod, FindMethodUncached, InvokeMethod, and InvokeStaticMethod. For an already resolved method, use MethodInvoker.Create(methodInfo) or a compiled delegate. See MethodUtils.cs.

Service Collection

Advanced DI container manipulation for testing and modularity.

  • Querying: sc.GetAllAssignableTo<TService>() retrieves all descriptors matching a type.
  • Replacement: ReplaceScoped, ReplaceSingleton, and ReplaceInstance for mocking/overriding dependencies in integration tests.

String & Binary Extensions

  • Parsing: string.Parse<T>() and string.TryParse<T>(out result) using the modern IParsable<T> interface.
  • Binary: ToStream() and ToByteArray() shortcuts with UTF8 default.
  • FileSystem: GetLines() for IFileInfo with efficient physical path reading.

Casing and safe path helpers live in DRN.Framework.SharedKernel.Extensions.

Type & Assembly Extensions

  • Discovery: assembly.GetSubTypes(typeof(T)) and assembly.GetTypesAssignableTo(to).
  • Instantiation: assembly.CreateSubTypes<T>() automatically discovers and instantiates classes with parameterless constructors.
  • Metadata: type.GetAssemblyName() returns a clean assembly name.

Flurl & HTTP Diagnostics

  • Logging: PrepareScopeLogForFlurlExceptionAsync() adds Flurl failure diagnostics to IScopedLog, and DRN Hosting applies it to unhandled Flurl exceptions. Captured request and response data is not automatically redacted; catch sensitive failures before they reach Hosting, or use the Try* converters and log only sanitized fields.
  • Status Codes: GetGatewayStatusCode() preserves 4xx, 503, and 504 statuses and maps other statuses to 502.
  • Testing: ClearFilteredSetups() utility for complex test scenarios.

Object & Dictionary Extensions

  • Deep Discovery: instance.GetGroupedPropertiesOfSubtype(type) groups properties whose declared type is a subtype of the requested type. It excludes exact-type matches and does not traverse collection elements. The default recursion limit is 5; property getters are evaluated and can throw.
  • Dictionary Utility: GetAndCastValueOrDefault returns a typed value or the supplied fallback; UpdateIf changes an existing entry only when its predicate passes. The dictionary itself must be non-null.
  • Bit Manipulation: GetBitPositions() for long values and bitmask generators for signed/unsigned lengths.
// Discovery and Instantiation
var implementations = typeof(IMyInterface).Assembly.CreateSubTypes<IMyInterface>();

// Modern Parsing
int value = "123".Parse<int>();

// Binary shortcuts
using var body = "payload".ToStream();

Suggested Consumer Global Usings

global using DRN.Framework.SharedKernel;
global using DRN.Framework.SharedKernel.Extensions;
global using DRN.Framework.Utils.DependencyInjection;

For complete examples, see Sample.Hosted.


Documented with the assistance of DiSC OS


Semper Progressivus: Always Progressive

Commit Info

Author: Duran Serkan KILIÇ
Date: 2026-09-09 11:24:34 +0300
Hash: 042fe6657917c7b6edc21dd2cc7879143639d9ca

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on DRN.Framework.Utils:

Package Downloads
DRN.Framework.EntityFramework

DRN.Framework.EntityFramework provides DrnContext with conventions to develop rapid and effective domain models. ## Commit Info Author: Duran Serkan Date: 2026-09-13 19:21:17 +0300 Hash: e98f5868dbf1de8f4f683536ec1bdf7e6723e074

DRN.Framework.Hosting

DRN.Framework.Hosting ## Commit Info Author: Duran Serkan Date: 2026-09-13 19:21:17 +0300 Hash: e98f5868dbf1de8f4f683536ec1bdf7e6723e074

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.10.0-preview003 47 9/13/2026
0.10.0-preview002 72 9/9/2026
0.10.0-preview001 108 9/7/2026
0.9.9-preview004 109 8/27/2026
0.9.9-preview003 111 8/26/2026
0.9.9-preview002 134 8/20/2026
0.9.9-preview001 132 8/16/2026
0.9.8 128 8/12/2026
0.9.8-preview004 109 8/12/2026
0.9.8-preview003 115 8/9/2026
0.9.8-preview002 109 8/8/2026
0.9.8-preview001 127 8/8/2026
0.9.7 146 7/29/2026
0.9.6 152 7/15/2026
0.9.6-preview004 155 7/7/2026
0.9.6-preview003 148 7/1/2026
0.9.6-preview002 172 6/29/2026
0.9.6-preview001 145 6/28/2026
0.9.5 161 6/14/2026
0.9.5-preview011 141 6/14/2026
Loading failed

## Version 0.10.0

### Bug Fixes

*   **NuGet Release Notes**: Package metadata includes only the latest version section, excluding historical releases and the documentation footer. Packing rejects missing version sections and release notes over 35,000 characters; the bundled Markdown retains the full history.

*   **Multiple Registration Attributes**: Module discovery now processes all service registration attributes on a class, preserving distinct modules and their startup validation instead of throwing when multiple attributes are present.
*   **Custom Subject Mappings**: Subject agreement checks now consider only configured types and aliases, including their case variants. Unconfigured standard subject claims no longer invalidate a custom account mapping; configure them as aliases when they must participate in agreement checks.
*   **Scoped Log Snapshots**: `GetLogs` and `CopyFrom` detach framework-managed lists under the writer lock. Later actions or events no longer mutate existing snapshots.
*   **Property Logging**: `AddProperties` skips indexers, static properties, and properties without public getters. Ignored getters remain unevaluated.

### Security

*   **Configured Authentication Claims**: `AuthenticationClaimConfig` supplies Identity canonical types, explicit aliases, and an exact MFA marker. Custom mappings replace defaults rather than merging unrelated claims. Shared validators reject contradictory subject evidence without rewriting claims. Native claim types and metadata are owned by the authentication integration. Default subjectless completion compatibility remains limited to its existing boundaries.
*   **Native Claim Consistency**: Scoped name/email select primary-identity evidence; `IsInRole` considers all configured roles from authenticated identities across role issuers. Native agreement requires matching claims and identity metadata from the authentication integration. Generic issuer-filtered claim-group lookup remains unchanged.

*   **Opt-In MFA Assurance**: Added issuer-bound `MfaPrincipal.IsRecent` and `IsPhishingResistant` checks requiring completed MFA and additional evidence on the same authenticated identity. Recency rejects future, malformed and conflicting timestamps; phishing resistance requires an explicit distinct assurance mapping. Existing completed-MFA behavior remains unchanged; these helpers do not issue evidence or enforce a new default policy.
*   **Provider-Neutral MFA Principal Checks**: Added `MfaPrincipal` for authenticated credential-state and account-bound MFA evaluation. `MfaFor.MfaCompleted` now rejects setup/pending credentials and conflicting authenticated subjects/issuers even when a completed-MFA marker is present. No ASP.NET Identity services are required.

### Breaking Changes

*   **Unified Claim API**: Replace `SubjectClaimType = "uid"` with `Subject = new("uid")`, and migrate name/email/role fields to `Name`, `Email`, and `Roles`. Aliases are explicit constructor arguments. `SubjectClaims` and `MfaPrincipal` now take the complete `AuthenticationClaimConfig` instead of separate marker/string mappings; standalone defaults use `AuthenticationClaimConfig.Default`. `MfaClaimConfig` remains the nested marker and explicit assurance value type. Development consumers must recompile and migrate calls; no duplicate configuration shim is retained.
*   **Native Claim Integration**: Removed `ClaimNormalization`. Use configured Identity claim options or an authentication handler that emits the canonical claims and native name/role metadata. Explicit aliases remain DRN inputs; provider-specific conversion is deferred to integrations. Account/MFA validation and renewal preservation remain active.

*   **Ambiguous Subject Evidence**: Single authenticated identities with conflicting subject aliases, including case variants, now fail account checks. `ScopedUser.Id` is null for missing or ambiguous primary subjects. Completed-MFA markers require an exact claim type and value match.

*   **IScopedLog Events and Correlation**: Custom implementations must provide non-null `CorrelationId`, nullable `TraceId`, `Event`, `EventId`, `EventName`, `EventOutcome`, and `EventReason`, and implement `WithEvent(ScopeEvent)`. Keep the first event as primary and retain later events separately. Correlation stays stable for the scope; `TraceId` must represent a real W3C activity, not a generated fallback.
*   **Strict Entity-Bound ID Generation**: Constrained `ISourceKnownIdUtils.Next<TEntity>()` and `SourceKnownIdUtils.Generate<TEntity>(byte appId, byte appInstanceId)` to `where TEntity : SourceKnownEntity` (previously `where TEntity : class`), and restricted dynamic `Next(SourceKnownEntity)`, `Next(Type, ...)`, and `Generate(Type, ...)` invocations strictly to `SourceKnownEntity` subclasses. This eliminates ambient fallback and ensures partition enforcement across all generator paths.
   *   *Migration*: Derive domain types from `SourceKnownEntity` and annotate concrete classes with `[EntityType<TApp>]` for attribute-derived partition dispatch via `Next<TEntity>()`, or supply explicit partition values via `SourceKnownIdUtils.Generate<TEntity>(appId, appInstanceId)`.
*   **ISourceKnownIdUtils Contract Expansion**: Added `Next(SourceKnownEntity entity)` and `Next(Type entityType, byte appId, byte appInstanceId, DateTimeOffset? epoch)` as required members on `ISourceKnownIdUtils`.
   *   *Migration*: Custom implementations must implement both overloads by delegating to existing generator utilities or `SourceKnownIdUtils.Generate(Type, ...)`.
*   **ISourceKnownEntityIdUtils Contract Expansion**: Added parameterless `Generate<TEntity>()`, `GeneratePlain<TEntity>()`, and `GenerateSecure<TEntity>()` as required members on `ISourceKnownEntityIdUtils`.
   *   *Migration*: Custom implementations must implement all three overloads by forwarding to `ISourceKnownIdUtils.Next<TEntity>()` and corresponding conversion operations.
*   **MethodUtils API Simplification**: Removed legacy discovery and invocation aliases (`FindGenericMethod`, `FindNonGenericMethod`, `InvokeFast`, `InvokeGenericMethodFast`, `InvokeStaticMethodFast`, etc.), and removed the `params` modifier from generic 0-argument `InvokeMethod` and `InvokeStaticMethod` overloads to prevent single-`Type` arguments from hijacking non-generic methods.
   *   *Migration*: Use unified `FindMethod`, `FindMethodUncached`, `InvokeMethod`, and `InvokeStaticMethod` APIs. Pass generic type arguments explicitly as a `Type[]` array (e.g. `instance.InvokeMethod("Method", [typeof(T)])`), while non-generic methods accepting `Type` can be called directly (e.g. `instance.InvokeMethod("Method", typeof(T))`). For pre-resolved method execution, use native `MethodInvoker.Create(methodInfo)` or compiled delegates.
*   **EquatableSequence<T> Struct Conversion**: Converted `EquatableSequence<T>` from `sealed record` class to `readonly record struct` implementing `IEquatable<EquatableSequence<T>>` with SIMD span-based sequence equality.
   *   *Migration*: Recompile against the new struct definition and update code relying on reference identity or class inheritance.
*   **IScopedUser Contract Expansion**: Custom implementations must provide `ExemptionProof? Exemption`. `ExemptionScheme`, `ExemptionPrincipal`, and `HasExemptionScheme` have default implementations derived from it.

### New Features

*   **Scope Events**: Added `ScopeEvent` for structured logging with .NET `EventId`. `ScopedLog` retains primary and additional events, provides a stable scope correlation ID, and captures an existing W3C trace ID. `LogScoped` forwards the primary event ID while preserving severity.
*   **Scoped User Authentication Scheme Exemption**: `IScopedUser` and `ScopedUser` track active `Exemption` (`ExemptionProof`) alongside convenience `ExemptionScheme` and `ExemptionPrincipal` properties, allowing authorization handlers and middleware to verify the authenticated scheme exemption without mutating `HttpContext.User`.
*   **Provider-Neutral MFA Claim Model**: Immutable `AuthenticationClaimConfig.Mfa` defaults to `MfaClaimConfig.AspNetIdentity` (`amr=mfa`). `MfaFor.MfaCompleted` resolves the shared config and matches exact evidence among repeated claims without Identity services.
*   **Base32 and TOTP Utilities**: Added `Base32Encoding` and `TotpUtils` for Base32 encoding and authenticator-code generation and verification. Verification is stateless; callers must enforce replay protection and attempt limits. See [TOTP usage and limitations](README.md#totp-generation-and-verification).
*   **Context-Separated AES-256-GCM Encryption**: Added `AesGcmEncryptorBase` and `AesGcmEncryptedData` under `DRN.Framework.Utils.Data.Encryption` for context-separated AES-256-GCM symmetric encryption with automatic BLAKE3 subkey derivation from `IAppSecuritySettings`. Added `CreateAesGcm(string context)` on `IAppSecuritySettings` to derive dedicated 32-byte keys from `AppEncryptionKey` via BLAKE3 with automatic intermediate buffer zeroing.
*   **Key Material Security Enforcement**: `AppSettings` rejects the default `DrnAppFeatures.DefaultSeedKey` outside `Development`, and rejects `DrnAppFeatures.SampleSeedKey` whenever `TestEnvironment.DrnTestContextEnabled` is not set, failing fast with `ConfigurationException`. `NexusKey` unconditionally forbids sample key material (`sample-nexus-key-material-000000`) across all environments and test executions at construction time.
*   **HttpMessageHandler DI Injection**: `InternalRequest` and `ExternalRequest` accept an optional `HttpMessageHandler?` via dependency injection, enabling in-memory test routing (such as `ApplicationContextRouterHandler`) without global static Flurl state mutation.
*   **Compiled Dynamic ID Generation**: Added `Next(SourceKnownEntity entity)` and `Next(Type entityType, ...)` with static warmup (`SourceKnownIdUtils.Warmup`). Steady-state lookups use direct compiled delegates cached in an immutable `FrozenDictionary` snapshot with `ConcurrentDictionary` dynamic fallback, eliminating reflection on entity persistence hot paths.
*   **MethodUtils Fast Invocation**: Upgraded `MethodUtils` with `System.Reflection.MethodInvoker` runtime native stubs, zero-allocation specialized overloads (0, 1, 2, 3 arguments, `Span<object?>`, and `params object?[]`), unified single-lookup cache entry (`MethodCacheEntry`) backed by an immutable `FrozenDictionary` snapshot with 100% lock-free fast-path reads and synchronized cold-path writes (`FindMethod`, `InvokeMethod`, `InvokeStaticMethod`), and public uncached discovery (`FindMethodUncached`).
*   **Self-Generating EntityId Overloads**: Added parameterless `Generate<TEntity>()`, `GeneratePlain<TEntity>()`, and `GenerateSecure<TEntity>()` on `ISourceKnownEntityIdUtils` and `SourceKnownEntityIdUtils` (`where TEntity : SourceKnownEntity`) to generate and construct `SourceKnownEntityId` in a single type-safe call.
*   **DrnServiceContainer Binary Compatibility**: Retained 2-parameter public constructor `DrnServiceContainer(Assembly, LifetimeAttribute[])` alongside the 3-parameter overload to maintain binary compatibility for precompiled consumers.

---

Documented with the assistance of [DiSC OS](https://github.com/duranserkan/DRN-Project/blob/develop/.agent/rules/DiSCOS.md)

---
**Semper Progressivus: Always Progressive**