| name | drn-utils |
| description | DRN.Framework.Utils - Attribute-based dependency injection, IAppSettings configuration, app data roots, logging, scoped cancellation, validators, extension methods, and core utilities. Keywords: dependency-injection, di, service-registration, configuration, appsettings, appdata, logging, scoped-log, cancellation, cancellation-scope, validators, attributes, scoped, singleton, transient, config, extensions, http-client |
| last-updated | "2026-07-15T00:00:00.000Z" |
| difficulty | intermediate |
| tokens | ~2.5K |
DRN.Framework.Utils
Core utilities: attribute-based DI, configuration, logging, and extensions.
When to Apply
- Setting up dependency injection with attributes
- Accessing configuration via IAppSettings
- Using or extending logging (IScopedLog)
- Coordinating explicit root or named scoped cancellation
- Working with DRN extension methods
- Understanding service registration patterns
Module Registration
services.AddDrnUtils();
AddDrnUtils() registers Microsoft's HybridCache with default in-memory caching. Register IDistributedCache (e.g., Redis) before calling AddDrnUtils() to enable distributed mode.
Attribute-Based Dependency Injection
Lifetime Attributes
| Attribute | Lifetime | Notes |
|---|
[Singleton<TService>] | Singleton | TryAdd by default |
[Scoped<TService>] | Scoped | Most common |
[Transient<TService>] | Transient | New per resolution |
[SingletonWithKey<TService>(key)] | Keyed singleton | Keyed services |
[ScopedWithKey<TService>(key)] | Keyed scoped | Keyed services |
[TransientWithKey<TService>(key)] | Keyed transient | Keyed services |
[HostedService] | Singleton | Auto-registers IHostedService implementations |
[Config("Section")] | Singleton | Binds configuration section to class |
[ConfigRoot] | Singleton | Binds to configuration root |
[!NOTE]
All lifetime attributes accept optional tryAdd parameter (default: true). When true, TryAdd is used so existing registrations are not overwritten. Set to false to allow multiple implementations of the same service type.
public interface IMyService { }
[Scoped<IMyService>]
public class MyService : IMyService { }
Registration & Validation
sc.AddServicesWithAttributes();
serviceProvider.ValidateServicesAddedByAttributesAsync();
Module Registration & Startup Actions
Attributes inheriting from ServiceRegistrationAttribute handle complex registration and post-startup actions. Example: DrnContext<T> uses [DrnContextServiceRegistration] to auto-register the DbContext and trigger migrations in Development.
Configuration (IAppSettings)
public interface IAppSettings
{
IConfiguration Configuration { get; }
DrnAppFeatures Features { get; }
DrnLocalizationSettings Localization { get; }
DrnDevelopmentSettings DevelopmentSettings { get; }
NexusAppSettings NexusAppSettings { get; }
AppEnvironment Environment { get; }
bool IsDevelopmentEnvironment { get; }
bool IsStagingEnvironment { get; }
string AppKey { get; }
string ApplicationName { get; }
string ApplicationNameNormalized { get; }
string GetAppSpecificName(string name, string prefix = "_");
bool TryGetConnectionString(string name, out string connectionString);
string GetRequiredConnectionString(string name);
bool TryGetSection(string key, out IConfigurationSection section);
IConfigurationSection GetRequiredSection(string key);
T? GetValue<T>(string key);
T? GetValue<T>(string key, T defaultValue);
T? Get<T>(string key, bool errorOnUnknownConfiguration = false, bool bindNonPublicProperties = true);
ConfigurationDebugView GetDebugView();
ConfigurationDebugView GetDebugView(bool includeRawValues);
}
ConfigurationDebugView redacts secret-looking values by default, lists child keys even when a provider also defines a scalar value for the parent section, and renders summary paths using the value provider's key casing. Object-to-JSON configuration uses the framework JSON defaults, including camelCase keys; explicit key/value configuration preserves the caller-provided key text.
Config Attribute
[Config("MySection")]
public class MySettings { }
[Config]
public class FeatureFlags { }
[ConfigRoot]
public class RootSettings { }
Configuration Sources (in order, later overrides earlier)
Canonical copy: Maintenance Reference: Configuration Sources.
appsettings.json → 2. appsettings.{Environment}.json → 3. User secrets when the application assembly is available → 4. Environment variables → 5. Mounted settings (/appconfig) → 6. Command line arguments
Environment is required and must be Development, Staging, or Production. DRN validates this value before loading appsettings.{Environment}.json; missing, NotDefined, or unknown values fail startup with ConfigurationException.
Override mount directory via IMountedSettingsConventionsOverride.
| Symptom | Solution |
|---|
ConfigurationException | Add missing key to appsettings.json |
Environment setting is missing | Set Environment to Development, Staging, or Production |
| Env vars not binding | Use __ for nested: Section__Key |
| Mounted settings not loading | Check /appconfig/ or override via IMountedSettingsConventionsOverride |
When grouping options into nested objects, explicitly validate child objects before relying on child data annotations for startup safety; plain Validator.TryValidateObject does not recursively walk nested option objects.
DrnAppFeatures.SeedKey feeds AppSecuritySettings, which derives AppHashKey, AppEncryptionKey, AppKey, and AppSeed through BLAKE3 derive-key mode with distinct DRN Framework context strings. The hash and encryption keys stay Base64Url-encoded 32-byte values, AppKey stays an 8-character public discriminator, and AppSeed stays a signed 64-bit seed value.
App Data Roots
IAppData exposes Temp and Data. Override roots before DRN config with DrnAppDataSettings__TempPath and DrnAppDataSettings__DataPath; data path backs temp as <DataPath>/Temp when temp path is unset. Use GetPath(...) for safe child paths.
Nexus Keys
NexusAppSettings.Keys must contain exactly one default NexusKey. Generation uses the default key; parsing tries the default key first and then the remaining configured keys for rotation fallback.
NexusKey.Format defaults to ByteEncoding.Utf8 and supports:
| Format | Requirement |
|---|
Utf8 | Key is exactly 32 UTF-8 bytes. |
Hex | Hex decodes to exactly 32 bytes, normally 64 hex chars. |
Base64 | Base64 decodes to exactly 32 bytes. |
Base64UrlEncoded | Base64Url decodes to exactly 32 bytes. |
Scoped Logging (IScopedLog)
Request-scoped structured logging. Aggregates data, metrics, and checkpoints, flushing as a single entry.
| Method | Purpose |
|---|
Add(key, value) | Structured log entry |
AddToActions(msg) | Execution trail |
AddProperties(key, object) | Flatten complex objects |
Measure(key) | Capture duration & count |
AddException(ex, msg) | Log exception with details |
Increase(key, by) | Atomically increment counter |
Auto-captured: TraceId, Request (path/method/host/IP), Response (status/length), Exceptions, Duration.
HTTP Clients (IExternalRequest, IInternalRequest)
Wrappers around Flurl with standardized JSON conventions.
var response = await request.For("https://api.example.com", HttpVersion.Version11)
.AppendPathSegment("v1/charges")
.PostJsonAsync(new { Amount = 1000 })
.ToJsonAsync<ExternalApiResponse>();
public interface INexusRequest { IFlurlRequest For(string path); }
[Singleton<INexusRequest>]
public class NexusRequest(IInternalRequest request, IAppSettings settings) : INexusRequest
{
public IFlurlRequest For(string path) => request.For(settings.NexusAppSettings.NexusAddress)
.AppendPathSegment(path);
}
ScopeContext (Ambient Context)
Access scoped data anywhere without parameter passing:
ScopeContext.UserId;
ScopeContext.TraceId;
ScopeContext.Authenticated;
ScopeContext.Settings;
ScopeContext.Log;
ScopeContext.IsUserInRole("Admin");
ScopeContext.IsClaimFlagEnabled("FeatureX");
ID Generation
long internalId = sourceKnownIdUtils.Next<User>();
SourceKnownEntityId externalId = sourceKnownEntityIdUtils.Generate<User>(internalId);
var secureId = sourceKnownEntityIdUtils.ToSecure(entityId);
var plainId = sourceKnownEntityIdUtils.ToPlain(entityId);
ISourceKnownEntityIdUtils inherits ISourceKnownEntityIdOperations (SharedKernel), which defines Generate, Parse, ToSecure, ToPlain. This interface is injected into entities by EF interceptors.
[!NOTE]
ID generation is automatically handled by DrnContext when SourceKnownEntities are saved.
Scoped Cancellation
ICancellationUtils owns a root and typed child scopes for the current DI service scope.
| Intent | Use | Propagation |
|---|
| Cancel all work | cancellation.Root.Cancel() / .Merge(token) | Every existing and later-created child. |
| Cancel a component/workflow | Child .Cancel() / .Merge(token) | That group only. |
| Cancel one operation | Local linked token source | Caller-owned work only. |
private static readonly CancellationScopeKey ScopeKey =
CancellationScopeKey.For<PaymentWorkflow>("capture");
var scope = cancellation.GetOrCreateScope(ScopeKey);
scope.Merge(workflowLifetimeToken);
- The same key returns the same scope and token; canceled scopes cannot be reset.
- Optional names use ordinal, case-sensitive equality and must be developer-defined constants of at most 128 characters. Never use request data, user input, instance IDs, or operation IDs.
ICancellationUtils owns child scopes. Callers own and dispose local linked sources used for operation-only cancellation.
- Replace removed root members with their
cancellation.Root equivalents.
See the package Scoped Cancellation guide for the complete API and migration example.
Concurrency (LockUtils)
Lock-free atomic operations via Interlocked:
| Method | Purpose |
|---|
TryClaimLock(ref int) | Atomically claim (0→1) |
TryClaimScope(ref int) | Disposable auto-release scope |
ReleaseLock(ref int) | Unconditionally release (→0) |
TrySetIfNull<T>(ref T?, T) | CAS set-if-null |
TrySetIfEqual<T>(ref T?, T, T?) | CAS compare-and-swap |
TrySetIfNotEqual<T>(ref T?, T, T?) | Set if current ≠ comparand (retry loop) |
TrySetIfNotNull<T>(ref T?, T) | Set if current is not null |
private int _lock;
using var scope = LockUtils.TryClaimScope(ref _lock);
if (scope.Acquired) { }
Utilities Reference
| Area | Key Types | Purpose |
|---|
| Data Encoding | EncodingExtensions | Base64, Base64Url, Hex, Utf8 |
| Hashing | HashExtensions | Blake3 (crypto), XxHash3 (fast), keyed and stream/file hashing; prefer stream overloads for files and large payloads |
| JSON | JsonMergePatch | RFC 7386 merge patch with depth protection |
| Query Strings | QueryParameterSerializer | Complex objects → query strings |
| Streams | ToBinaryDataAsync | Safe consumption with MaxSizeGuard |
| Validation | ValidationExtensions | DataAnnotations-based programmatic validation |
| Validators | JpegValidator, JpegValidationResult, JpegValidationErrorReason | Structural, stream-based, size-bounded JPEG validation with typed error reasons |
| App data | IAppData, AppDataPathResult, DrnAppDataSettings | Validated temp/data roots with traversal-safe child path resolution |
| Pagination | IPaginationUtils | Cursor-based via SourceKnownEntityId |
| Cancellation | ICancellationUtils, ICancellationScope, CancellationScopeKey | Explicit root, stable typed named groups, and caller-owned local links |
| Diagnostics | DevelopmentStatus | Track pending DB model changes at startup |
Bit Packing (NumberBuilder / NumberParser)
Zero-allocation bit manipulation for custom ID generation:
var builder = NumberBuilder.GetLong();
builder.TryAddNibble(0x05);
builder.TryAddUShort(65535);
long packed = builder.GetValue();
var parser = NumberParser.Get(packed);
byte nibble = parser.ReadNibble();
ushort value = parser.ReadUShort();
Time & Async
long seconds = TimeStampManager.CurrentTimestamp(EpochTimeUtils.DefaultEpoch);
DateTimeOffset now = TimeStampManager.UtcNow;
var worker = new RecurringAction(async () => await DoWork(), period: 1000, start: true);
worker.Stop();
TimeProvider singleton registered to TimeProvider.System by default for testable time.
Extension Methods
var replacement = new MyService();
services.ReplaceInstance<MyService>(typeof(IMyService), new[] { replacement }, ServiceLifetime.Singleton);
services.ReplaceTransient<IMyService, MyService>(replacement);
services.ReplaceScoped<IMyService, MyService>(replacement);
services.ReplaceSingleton<IMyService, MyService>(replacement);
services.GetAllAssignableTo<TService>();
"hello world".ToStream();
int v = "123".Parse<int>(); bool ok = "abc".TryParse<int>(out _);
assembly.GetTypesAssignableTo<TInterface>();
assembly.GetSubTypes(typeof(T));
assembly.CreateSubTypes<T>();
type.GetAssemblyName();
instance.InvokeMethod("Name", args);
type.InvokeStaticMethod("Name", args);
instance.InvokeGenericMethod("Name", typeArgs, args);
await PrepareScopeLogForFlurlExceptionAsync();
exception.GetGatewayStatusCode();
object.GetGroupedPropertiesOfSubtype<T>();
number.GetBitPositions();
Related Skills
Global Usings
global using DRN.Framework.SharedKernel;
global using DRN.Framework.Utils.DependencyInjection;