DRN.Framework.SharedKernel 0.10.0-preview003

Prefix Reserved
This is a prerelease version of DRN.Framework.SharedKernel.
dotnet add package DRN.Framework.SharedKernel --version 0.10.0-preview003
                    
NuGet\Install-Package DRN.Framework.SharedKernel -Version 0.10.0-preview003
                    
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.SharedKernel" Version="0.10.0-preview003" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DRN.Framework.SharedKernel" Version="0.10.0-preview003" />
                    
Directory.Packages.props
<PackageReference Include="DRN.Framework.SharedKernel" />
                    
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.SharedKernel --version 0.10.0-preview003
                    
#r "nuget: DRN.Framework.SharedKernel, 0.10.0-preview003"
                    
#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.SharedKernel@0.10.0-preview003
                    
#: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.SharedKernel&version=0.10.0-preview003&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=DRN.Framework.SharedKernel&version=0.10.0-preview003&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.SharedKernel

Lightweight package containing domain primitives, exceptions, and shared code suitable for Contract and Domain layers.

TL;DR

  • No runtime package dependencies - Targets .NET 10; includes build-time domain analyzers
  • Domain primitives - SourceKnownEntity, AggregateRoot, DomainEvent for DDD patterns
  • Typed exceptions - ExceptionFor creates exceptions with status codes consumed by DRN Hosting
  • JSON conventions - Global System.Text.Json defaults with camelCase, enums-as-strings
  • Shared extensions - Casing and safe path helpers for lower-layer packages
  • Source Known IDs - Internal long keys and external Guid identifiers with type and partition validation

Table of Contents


QuickStart: Beginner

Define an entity and collect a domain event. SharedKernel supplies the domain types; EntityFramework integration assigns missing IDs during save and wires ID operations during save or materialization.

using DRN.Framework.SharedKernel.Domain;

[EntityType<DefaultApp>(1)] // Unique byte identifier for this entity type
public class User : AggregateRoot
{
    public string Name { get; private set; }

    public User(string name)
    {
        Name = name;
        AddDomainEvent(new UserCreated(this)); // Collected on the entity for infrastructure handling
    }
}

public class UserCreated(User user) : EntityCreated(user)
{
    public string UserName => user.Name;
}

The constructor alone does not assign EntityId or CreatedAt. The event reads EntityId from its entity, so it observes the ID assigned later. Event collection does not publish the event.

QuickStart: Advanced

Use the User above with a repository implementation. This service maps entities to DTOs, filters by creation date, and reports missing users:

using System;
using System.Threading.Tasks;
using DRN.Framework.SharedKernel;
using DRN.Framework.SharedKernel.Domain;
using DRN.Framework.SharedKernel.Domain.Pagination;
using DRN.Framework.SharedKernel.Domain.Repository;

public class UserDto(SourceKnownEntity? entity = null) : Dto(entity)
{
    public string Name { get; init; } = string.Empty;
}

public class UserService(ISourceKnownRepository<User> repository)
{
    public async Task<PaginationResultModel<UserDto>> GetUsersAsync(PaginationRequest request)
    {
        var filter = EntityCreatedFilter.After(DateTimeOffset.UtcNow.AddDays(-30));
        var result = await repository.PaginateAsync(request, filter);

        return result.ToModel(user => new UserDto(user) { Name = user.Name });
    }

    public async Task<UserDto> GetUserAsync(Guid id)
    {
        var user = await repository.GetOrDefaultAsync(id);
        if (user is null)
            throw ExceptionFor.NotFound($"User {id} not found");

        return new UserDto(user) { Name = user.Name };
    }
}

Domain Primitives

Domain types live in DRN.Framework.SharedKernel.Domain. Persistence integration belongs to DRN.Framework.EntityFramework and DrnContext.

Entity and AggregateRoot

  • SourceKnownEntity: Holds internal identity, external identity, domain events, and audit timestamps.
  • AggregateRoot: Marker class for DDD aggregate roots.
  • AggregateRoot<TModel>: Adds a writable Model through IEntityWithModel<TModel>, where TModel : class. Model starts as null! and must be populated by the application.

Entity and aggregate base constructors accept an optional internal long id = 0.

Entity member Contract
long Id Internal setter; ignored by JSON; column order IdColumnOrder = 0
Guid EntityId External ID from EntityIdSource; serialized under the name Id
DateTimeOffset CreatedAt Derived from the encoded ID timestamp
DateTimeOffset ModifiedAt Protected internal setter; concurrency check; ModifiedAtColumnOrder = 1
SourceKnownEntityId EntityIdSource Internal setter; ignored by JSON

SourceKnownEntity implements IHasEntityId, IEquatable<SourceKnownEntity>, and IComparable<SourceKnownEntity>. See SourceKnownEntity.cs and AggregateRoot.cs.

Application Partitions

IAppId requires static abstract byte AppId { get; } in the range 0..127. [EntityType<TApp>(byte)] binds an entity type byte to that application partition. The non-generic EntityTypeAttribute is abstract. Attributes are not inherited, and each entity can declare only one.

Partition AppId and Value Attribute example
DefaultApp 0 [EntityType<DefaultApp>(1)] for standalone domains
NexusApp 126 [EntityType<NexusApp>(1)]; Nexus also supplies a domain-derived NexusEntityTypeAttribute accepting NexusEntityTypes
TestApp 127 [TestEntityType(1)], equivalent to [EntityType<TestApp>(1)]

IAppId.DefaultAppId, NexusAppId, and TestAppId expose these constants; MaxAppId is 127. Different partitions may reuse an entity type byte. See IAppId.cs.

Compile-Time Roslyn Analyzers

DRN.Framework.SharedKernel includes built-in Roslyn analyzers (DRN.Framework.SharedKernel.Analyzers) delivering compile-time domain validation transitively to referencing projects and NuGet consumers:

Diagnostic ID Severity Title Description
DRN0001 Error Missing [EntityType] attribute Concrete, effectively non-private SourceKnownEntity classes must declare [EntityType<TApp>(byte)] or a supported derived domain attribute.
DRN0002 Error Duplicate EntityType value Every entity class in the domain compilation and referenced assemblies must have a unique EntityType byte value per AppId.
DRN0003 Error Invalid [EntityType] usage [EntityType] attribute must not be placed on abstract classes, private classes, or non-SourceKnownEntity types.
DRN0004 Warning Duplicate entity class name Warns when multiple entities across the domain model share identical unqualified class names within the same AppId to prevent EF Core and messaging collisions.
DRN0005 Error Multiple AppIds in single compilation Allows at most one non-test partition unless a multi-application or test exemption applies. TestApp (127) is excluded from the count.
DRN0006 Error Unresolvable or non-constant AppId in [EntityType] Enforces that IAppId implementations declare a constant value (public const byte Value = ...; or public const byte AppId = ...;) so partition identities can be read from metadata across assembly boundaries.
DRN0007 Error AppId outside the supported range Enforces that statically resolved IAppId values used by [EntityType] declarations are between 0 and 127, matching Source-Known ID runtime constraints.
DRN0008 Error Unsupported entity attribute constructor Derived attributes must reach EntityTypeAttribute<TApp> through one constructor per class, with one byte or byte-backed enum parameter forwarded unchanged to the base constructor.
DRN0009 Error Invalid constant application partition Constant AppId values in EntityTypeId construction and property initializers/assignments must be within 0..127.
DRN0010 Error Undefined constant entity ID format Constant SourceKnownEntityIdFormat arguments and assignments must be defined enum values.
DRN0011 Warning Hidden entity identity member Descendant members must not hide Id, EntityId, or EntityIdSource, with or without the new keyword.
DRN0012 Warning Concrete generic entity Concrete, effectively non-private entities must not introduce type parameters or be nested inside generic containers. Use abstract generic bases with separately attributed concrete descendants.
DRN0013 Error Concrete entity type required Rejects known abstract entities in entity/utility/repository metadata calls, method groups, direct typeof lookups, and Source-Known repository bindings, including derived types, declarations, and aliases.
DRN0014 Error Entity ID operations are not initialized Rejects GetEntityId, ToSecure, or ToPlain calls on provably fresh entities in straight-line code, including simple local aliases and constructor this calls.
var identity = new EntityTypeId(7, 200); // DRN0009
operations.Parse(externalGuid, (SourceKnownEntityIdFormat)99); // DRN0010
SourceKnownEntity.GetEntityTypeId<AggregateRoot>(); // DRN0013
ISourceKnownRepository<AggregateRoot> repository; // DRN0013

// Resolve the concrete runtime type through the instance overload instead.
SourceKnownEntity.GetEntityTypeId(aggregate);

Usage checks apply to test projects too. DRN0009 and DRN0010 inspect constants without propagating mutable local values; use non-constant inputs for runtime rejection tests. Dynamic values still require runtime validation.

DRN0013 allows runtime-instance metadata overloads, application-only Validate<TApp>, unresolved type parameters, and unbound generic definitions. It does not trace dynamic Type values or consumer wrappers. Use concrete bindings such as ISourceKnownRepository<Order>: forwarding TEntity through SourceKnownRepository<AppDbContext, TEntity> is valid, but later binding it to AggregateRoot is rejected. No public parameterless constructor is required.

Entity ID operations are attached during EF materialization or while saving an Added entity, before the database save succeeds. Constructing an entity with an existing numeric ID, calling Add, or obtaining an ID record through the utility does not itself attach operations. Initialization and IsPendingInsert do not prove persistence.

var order = new Order(); // Constructor must be available and provably passive.
var alias = order;
alias.GetEntityId<Customer>(customerGuid); // DRN0014

// Use injected operations when working before entity initialization.
var customerId = ids.Validate<Customer>(customerGuid);

DRN0014 recognizes source constructor chains with empty bodies or instance-field/auto-property assignments from constants/parameters, constant member initializers, and empty standard List<T> initializers. Compiled constructors and object initializers are unknown. Tracking stops at unknown calls, arbitrary getters/setters, control flow, awaits, captures, or ref aliases. Potentially null inputs and static metadata helpers are excluded. No diagnostic proves initialization; runtime guards remain required.

Derived entity attributes use a pass-through constructor:

using DRN.Framework.SharedKernel.Domain;

public enum DomainEntityTypes : byte { Order = 2 }

public sealed class DomainEntityTypeAttribute(DomainEntityTypes kind)
    : EntityTypeAttribute<DefaultApp>((byte)kind);

Parameter names may differ. Primary and ordinary constructors are supported. Reordered, additional, fixed-value, overloaded, or transformed mappings produce DRN0008. AppId comes from the generic TApp binding. A derived property hiding AppId does not change it.

The analyzer checks source attribute declarations even when no entity uses them yet. For compiled references, Roslyn exposes constructor signatures but not bodies. Signatures are checked; forwarding relies on producer-side validation. Run the analyzer when building domain attribute libraries. It cannot prove forwarding in a precompiled library built with validation disabled.

An entity is effectively private if it or any containing type is private. Such entities are excluded from required-attribute and collision checks in both local and referenced analysis; annotating one locally reports DRN0003.

  • Cross-assembly checks: Hosts and aggregators detect collisions across referenced domain modules at compilation end. Diamond dependencies such as A -> B -> Common and A -> C -> Common are deduplicated by Roslyn symbol equality.
  • DRN0005 exemptions: Set <AllowMultipleAppIds>true</AllowMultipleAppIds>, <IsTestProject>true</IsTestProject>, or <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>. Test assembly names also qualify when they contain .Test., start with Test., or end with .Tests or .Test, case-insensitively.

See SourceKnownEntityTypeAnalyzer.cs for diagnostic checks and the package project for analyzer packaging.

Identity Rule: Always use Guid EntityId (mapped as Id in DTOs) for all public-facing contracts, API route parameters, and external lookups. The internal long Id must never be exposed outside the infrastructure/domain boundaries.

DTO Mapping Rule: DTOs should implement a primary constructor accepting a SourceKnownEntity? to automatically map Id, CreatedAt, and ModifiedAt. Avoid manual mapping of these fields.

Entity Exposure Prohibition: Entities must never be exposed via public APIs. Always map to DTOs or Response Models. Entities are only permitted in Razor Pages (Internal UI).

The advanced quickstart defines UserDto once and uses its base constructor for identity and timestamp mapping. Dto.AdditionalData stores JSON extension data. See Dto.cs.

Domain Events

Entities collect events through AddDomainEvent and expose them through GetDomainEvents. SharedKernel does not supply publication or outbox dispatch. EF save interception invokes lifecycle hooks; override GetCreatedEvent, GetModifiedEvent, or GetDeletedEvent to supply events. Each hook returns null by default.

Event contract excerpt from DomainEvent.cs:

public interface IDomainEvent
{
    Guid Id { get; }
    DateTimeOffset Date { get; }
    Guid EntityId { get; }
}

public abstract class DomainEvent(SourceKnownEntity sourceKnownEntity) : IDomainEvent
{
    public Guid Id { get; protected init; } = Guid.NewGuid();
    public Guid EntityId => sourceKnownEntity.EntityId;
    public DateTimeOffset Date { get; protected init; } = DateTimeOffset.UtcNow;
}

public abstract class EntityCreated(SourceKnownEntity sourceKnownEntity) : DomainEvent(sourceKnownEntity);
public abstract class EntityModified(SourceKnownEntity sourceKnownEntity) : DomainEvent(sourceKnownEntity);
public abstract class EntityDeleted(SourceKnownEntity sourceKnownEntity) : DomainEvent(sourceKnownEntity);

SourceKnownEntityId & SourceKnownId

The identifier system has three forms:

Form Representation Purpose
Source Known ID (SKID) 64-bit long, 8 bytes Sortable database key containing creation time, application, instance and sequence fields
Source Known Entity ID (SKEID) 128-bit Guid Adds entity type, an epoch byte and a 4-byte keyed BLAKE3 MAC for validation without a database lookup
Secure SKEID 128-bit Guid Encrypts one SKEID block with AES-256-ECB to conceal its encoded fields

ISourceKnownEntityIdOperations defines Generate, nullable/nonnullable Parse, all Validate overloads, ToSecure, and ToPlain in SharedKernel. Utils implements it through ISourceKnownEntityIdUtils and SourceKnownEntityIdUtils. EF interceptors wire operations into entities. ID validation checks structure and identity metadata; application authorization still controls access to the entity.

Parse(Guid, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault) accepts ConfiguredDefault, Secure, Plain or Auto from DRN.Framework.SharedKernel.Domain. ConfiguredDefault selects the immutable ISourceKnownEntityIdOperations.DefaultFormat, initialized from Nexus settings (Secure or Plain). Secure only decrypts and verifies, Plain only verifies plaintext, and Auto retains plain-first detection with decryption fallback. Domain GUID helpers forward the format to parsing, then validate stored validity and the requested identity. Generated records also validate validity and identity without another format check. Conversions accept both forms. Format is non-nullable: omit it or pass ConfiguredDefault instead of null. Compiled consumers require rebuilding and custom implementations or method-group bindings require signature updates. See Utils parsing and validation.

Record contract excerpt from SourceKnownEntityId.cs:

public readonly record struct SourceKnownId(
    long Id,
    DateTimeOffset CreatedAt, // 250ms tick precision
    uint InstanceId,          // 18-bit per-tick sequence
    byte AppId,               // 7 bits, 0..127
    byte AppInstanceId        // 6 bits, 0..63
);

public readonly record struct SourceKnownEntityId(
    SourceKnownId Source,
    Guid EntityId,
    byte EntityType,
    bool Valid,
    bool Secure
);

Valid reports parsing validity. Secure identifies the encrypted form of EntityId. EntityTypeId combines EntityType and Source.AppId. The sequence layout is implemented in SourceKnownIdUtils.cs; MAC and encryption belong to SourceKnownEntityIdUtils.cs.

Trusted minimum generation time

SourceKnownGenerationTime sets one process-wide epoch and minimum UTC for new IDs. The default origin is 2025-01-01T00:00:00Z; the default minimum is 2026-09-09T00:00:00Z. Only encoded epoch 0, including both halves of its 33-bit timestamp range, is supported.

Configure before startup or first ID/epoch use:

SourceKnownGenerationTime.Initialize(
    minimumUtc: "2026-09-01T00:00:00Z",
    defaultEpoch: "2025-01-01T00:00:00Z");

An override can move the minimum earlier or later. Supplying an epoch requires an explicit minimum in the same call. Both inputs require ISO 8601 UTC ending in Z or +00:00, with seconds and up to seven fractional digits. The minimum rounds upward to a 250ms boundary relative to the epoch; an unrepresentable minimum fails.

Startup or first generation freezes both values before checking time, even if that check fails. Reading the configured epoch, parsing IDs, and converting historical dates also freeze the pair. Identical settings or omitted overrides retain it; later conflicts fail. Historical parsing and GUID reconstruction from existing numeric IDs do not enforce the generation floor.

Keep the origin unchanged across every service and restart using a dataset. IDs do not store it, so changing it changes the interpretation of existing timestamps. Generation rejects times outside epoch 0 rather than rolling over. Hosted configuration uses SourceKnownIdSettings; see Utils configuration and clock behavior.

Source baseline contract

The default minimum is maintained in SourceKnownGenerationTimePolicy.MinimumGenerationUtc. All builds use the same source value. It detects clocks below the configured floor but does not prove current UTC or prevent rollback across restarts.

ID Validation & Retrieval Strategies

Context Call Requirement
Service sourceKnownEntityIdUtils.Validate<User>(externalGuid) Inject the Utils ID utility
Repository userRepository.GetEntityId(externalGuid) Use ISourceKnownRepository<User>
Domain entity userInstance.GetEntityId<User>(externalGuid) Entity ID assigned and ID operations wired

GetEntityId helpers throw UnprocessableEntityException while the current entity is pending insertion. Missing ID operations cause ConfigurationException. For a parsed SourceKnownEntityId, choose validation by the expected boundary:

Method Checks
ValidateId() Stored Valid is true; no expected identity
Validate<TEntity>() Stored validity, entity type and the entity's declared application partition
Validate(EntityTypeId expected) Stored validity and both supplied identity components

Parsed-record validation has no format parameter and accepts either representation. It neither reads settings nor reauthenticates the GUID. Records are publicly constructible, so treat them as trusted internal values rather than authenticated transport input. Operations Parse/Validate authenticate untrusted GUIDs and select the format: ConfiguredDefault uses DefaultFormat, while Auto enables mixed-format detection. Undefined enum values throw ArgumentOutOfRangeException at that parsing boundary.

Identity validation requires an expected application partition: use Validate<TEntity>() to obtain it from the entity declaration, or Validate(new EntityTypeId(entityType, expectedAppId)) to supply both components. EntityTypeId requires both constructor arguments, including explicit 0 for the default partition, and has no implicit conversion from byte. The former Validate(byte) overload is removed; validation never derives the expected partition from the incoming ID.

Domain helpers follow the same contract for GUID and numeric IDs: use GetEntityId<TEntity>(id) or GetEntityId(id, new EntityTypeId(entityType, expectedAppId)), including nullable forms. The former byte-only helpers are removed. The low-level GetEntityId(Guid, bool validate = true) helper checks parsing validity only, like ValidateId(). SharedKernel does not read application configuration; Utils supplies configured expectations and EF repositories supply the target entity's declared identity.

All domain GUID helpers and repository single/batch/enumerable GUID helpers accept an optional final format argument. It reaches the injected parser; omission uses its configured default. For example, entity.GetEntityId<User>(guid, SourceKnownEntityIdFormat.Plain) and repository.GetEntityIds(guids, format: SourceKnownEntityIdFormat.Auto). Undefined formats are rejected for null inputs and empty batches too. Numeric generation and conversions retain their existing behavior. Ordinary calls still compile, but changed signatures require binary consumers to rebuild and custom implementations/method-group bindings to update.

Secure ↔ Plain Conversion

Entities, repositories and the Utils utility expose ToSecure and ToPlain. Conversion validates the ID and is idempotent: an ID already in the requested form is returned unchanged. Entity conversion requires wired ID operations.

The following alternatives assume the named entity, repository or utility is available:

var secureFromEntity = userInstance.ToSecure(entityId);
var plainFromEntity = userInstance.ToPlain(secureFromEntity);

var secureFromRepository = userRepository.ToSecure(entityId);
var plainFromRepository = userRepository.ToPlain(secureFromRepository);

var secureFromUtility = sourceKnownEntityIdUtils.ToSecure(entityId);
var plainFromUtility = sourceKnownEntityIdUtils.ToPlain(secureFromUtility);

SourceKnownRepository

ISourceKnownRepository<TEntity> defines queries, mutations, identity conversion and pagination for AggregateRoot entities. EntityFramework supplies the default implementation. Subclasses own entity updates, additional filters and query includes.

Contract from SourceKnownRepository.cs, grouped by operation:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using DRN.Framework.SharedKernel.Domain;
using DRN.Framework.SharedKernel.Domain.Pagination;
using DRN.Framework.SharedKernel.Domain.Repository;

public interface ISourceKnownRepository<TEntity> where TEntity : AggregateRoot
{
    RepositorySettings<TEntity> Settings { get; set; }
    CancellationToken CancellationToken { get; }
    void CancelWhen(CancellationToken token);
    void CancelChanges();
    Task<int> SaveChangesAsync();

    // Predicates & Counts
    Task<bool> AllAsync(Expression<Func<TEntity, bool>> predicate);
    Task<bool> AnyAsync(Expression<Func<TEntity, bool>>? predicate = null);
    Task<long> CountAsync(Expression<Func<TEntity, bool>>? predicate = null);
    
    // Identity Conversion & Validation
    SourceKnownEntityId GetEntityId(Guid id, bool validate = true, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault);
    SourceKnownEntityId? GetEntityId(Guid? id, bool validate = true, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault);
    SourceKnownEntityId GetEntityId<TOtherEntity>(Guid id, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault) where TOtherEntity : SourceKnownEntity;
    SourceKnownEntityId? GetEntityId<TOtherEntity>(Guid? id, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault) where TOtherEntity : SourceKnownEntity;
    SourceKnownEntityId[] GetEntityIds(IReadOnlyCollection<Guid> ids, bool validate = true, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault);
    SourceKnownEntityId?[] GetEntityIds(IReadOnlyCollection<Guid?> ids, bool validate = true, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault);
    SourceKnownEntityId[] GetEntityIds<TOtherEntity>(IReadOnlyCollection<Guid> ids, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault) where TOtherEntity : SourceKnownEntity;
    SourceKnownEntityId?[] GetEntityIds<TOtherEntity>(IReadOnlyCollection<Guid?> ids, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault) where TOtherEntity : SourceKnownEntity;
    IEnumerable<SourceKnownEntityId> GetEntityIdsAsEnumerable(IEnumerable<Guid> ids, bool validate = true, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault);
    IEnumerable<SourceKnownEntityId?> GetEntityIdsAsEnumerable(IEnumerable<Guid?> ids, bool validate = true, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault);
    IEnumerable<SourceKnownEntityId> GetEntityIdsAsEnumerable<TOtherEntity>(IEnumerable<Guid> ids, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault) where TOtherEntity : SourceKnownEntity;
    IEnumerable<SourceKnownEntityId?> GetEntityIdsAsEnumerable<TOtherEntity>(IEnumerable<Guid?> ids, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault) where TOtherEntity : SourceKnownEntity;
    SourceKnownEntityId ToSecure(SourceKnownEntityId id);
    SourceKnownEntityId ToPlain(SourceKnownEntityId id);
    
    // Data Access
    Task<TEntity[]> GetAllAsync();
    Task<TEntity> GetAsync(Guid id, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault);
    Task<TEntity> GetAsync(SourceKnownEntityId id);
    Task<TEntity?> GetOrDefaultAsync(Guid id, bool validate = true, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault);
    Task<TEntity?> GetOrDefaultAsync(SourceKnownEntityId id, bool validate = true);
    
    // Batch Retrieval
    Task<TEntity[]> GetAsync(IReadOnlyCollection<Guid> ids, SourceKnownEntityIdFormat format = SourceKnownEntityIdFormat.ConfiguredDefault);
    Task<TEntity[]> GetAsync(IReadOnlyCollection<SourceKnownEntityId> ids);
    // Modification
    void Add(params IReadOnlyCollection<TEntity> entities);
    void Remove(params IReadOnlyCollection<TEntity> entities);
    Task<int> CreateAsync(params IReadOnlyCollection<TEntity> entities);
    Task<int> DeleteAsync(params IReadOnlyCollection<TEntity> entities);
    Task<int> DeleteAsync(params IReadOnlyCollection<Guid> ids);
    Task<int> DeleteAsync(IReadOnlyCollection<Guid> ids, SourceKnownEntityIdFormat format);
    Task<int> DeleteAsync(params IReadOnlyCollection<SourceKnownEntityId> ids);

    // Pagination
    Task<PaginationResultModel<TEntity>> PaginateAsync(PaginationRequest request, EntityCreatedFilter? filter = null);
    Task<PaginationResultModel<TEntity>> PaginateAsync(
        PaginationResultInfo? resultInfo = null, long jumpTo = 1, int pageSize = -1, int maxSize = -1,
        PageSortDirection direction = PageSortDirection.None, long totalCount = -1, bool updateTotalCount = false);
    IAsyncEnumerable<PaginationResultModel<TEntity>> PaginateAllAsync(PaginationRequest request, EntityCreatedFilter? filter = null);
}

GetAllAsync() returns all matching entities in a single query. This should be used only when the result set is guaranteed to be small or for specific maintenance tasks. Avoid in public-facing APIs.

Settings and Cancellation

RepositorySettings<TEntity> provides IgnoreAutoIncludes, AsNoTracking, ScopeKey, and named predicate filters through AddFilter, RemoveFilter, and ClearFilters. Both boolean settings default to false. See RepositorySettings.cs.

  • A null ScopeKey uses the root cancellation scope. A configured key selects a child scope shared by repositories using that key.
  • CancellationToken exposes the effective scope token. CancelChanges() cancels that scope, and CancelWhen(token) links a lifetime token to it.
  • Cancellation lasts for the scope's lifetime. With a null key, it affects root-linked operations too.
  • For a single operation, create a local linked token source and pass its token to an operation that accepts it. Do not merge an operation-only token through CancelWhen.

CancellationScopeKey lives in DRN.Framework.SharedKernel.Cancellation. Create keys through its For(...) factories. See the key contract and the EntityFramework repository.

Filtering

EntityCreatedFilter supplies After, Before, Between, and Outside factories. Each defaults to inclusive: true. In the default implementation, inclusive boundaries include the full 250ms ID tick; exclusive boundaries exclude it. Between and Outside normalize reversed endpoints.

With a repository and pagination request in scope:

// Example: Get records created in the last 7 days
var filter = EntityCreatedFilter.After(DateTimeOffset.UtcNow.AddDays(-7));
var result = await repository.PaginateAsync(request, filter);

See EntityCreatedFilter.cs and EntityDateTimeUtils.cs.


Pagination

Pagination uses first/last entity IDs as cursors for forward, backward and refresh requests. It does not provide a database snapshot across requests.

Setting or operation Behavior
PaginationRequest.From() Starts at page 1, size 10, maximum 100, ascending order
Changed size, effective maximum or direction Resets to page 1 with a fresh cursor
Omitted settings during reset Retains the previous size, maximum and direction
Maximum size Capped at 1,000 before comparison; repeating an above-threshold limit preserves navigation
From(resultInfo, jumpTo: ...) Limits movement to ten pages in the requested direction; 100 to 1 targets 90, and 1 to 100 targets 11
RequestNextPage(), RequestPreviousPage(), RequestRefresh() Creates navigation requests from result metadata
RequestPage(n) Creates a direct page request; does not apply From's ten-page clamp
TotalCount, UpdateTotalCount Carries a known count or requests recalculation; -1 means unspecified
PaginateAllAsync Streams pages as IAsyncEnumerable<PaginationResultModel<TEntity>>

The three-argument PageSize constructor can override the maximum threshold for in-process requests. That override is not serializable. See PaginationRequest.cs, PageSize.cs, and PaginationResultBase.cs.

API Integration

PaginationRequest supports query-string model binding. This controller uses the quickstart's User and UserDto:

using System.Threading.Tasks;
using DRN.Framework.SharedKernel.Domain.Pagination;
using DRN.Framework.SharedKernel.Domain.Repository;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("users")]
public class UsersController(ISourceKnownRepository<User> repository) : ControllerBase
{
    [HttpGet]
    public async Task<PaginationResultModel<UserDto>> GetAsync([FromQuery] PaginationRequest request)
    {
        var result = await repository.PaginateAsync(request);
        return result.ToModel(user => new UserDto(user) { Name = user.Name });
    }
}

Usage

// Inside a service method with repository in scope.
var request = PaginationRequest.DefaultWith(size: 20, direction: PageSortDirection.Descending);
var result = await repository.PaginateAsync(request);

if (result.Info.HasNext)
{
    var nextRequest = result.Info.RequestNextPage();
    var nextPage = await repository.PaginateAsync(nextRequest);
}

Exceptions

ExceptionFor creates DrnException subclasses with Status and Category. DRN Hosting's HttpScopeMiddleware maps the status to an HTTP response and aborts invalid status values. SharedKernel alone does not handle HTTP requests.

Factory Method Exception Type HTTP Status
ExceptionFor.Validation(msg) ValidationException 400
ExceptionFor.Unauthorized(msg) UnauthorizedException 401
ExceptionFor.Forbidden(msg) ForbiddenException 403
ExceptionFor.NotFound(msg) NotFoundException 404
ExceptionFor.Conflict(msg) ConflictException 409
ExceptionFor.Expired(msg) ExpiredException 410
ExceptionFor.UnprocessableEntity(msg) UnprocessableEntityException 422
ExceptionFor.Configuration(msg) ConfigurationException 500
ExceptionFor.Jackpot(msg) JackpotException 500
ExceptionFor.MaliciousRequest(msg) MaliciousRequestException Abort

Every factory accepts string message, optional Exception? exception = null, and optional string? category = "default". The base exception defaults to status 500 and category DrnException.DefaultCategory ("default"). It also exposes a string-keyed Data dictionary. MaliciousRequestException.Status is short.MaxValue, the abort sentinel used by Hosting.

using DRN.Framework.SharedKernel;

throw ExceptionFor.NotFound("User not found", category: "Users");

See Exceptions.cs and HttpScopeMiddleware.cs.


JsonConventions

JsonConventions.DefaultOptions centralizes System.Text.Json settings. Its static initializer replaces the serializer's static default option instances. Utils convention setup initializes it, including setup used by DrnTestContext. Hosting also configures HTTP JSON and MVC options.

Setting Value
New options JsonSerializerDefaults.Web
Property names camelCase, case-insensitive reading
Enums JsonStringEnumConverter
Trailing commas Allowed
Number handling AllowReadingFromString
Maximum depth 32
long and long? JSON numbers within -9,007,199,254,740,991..9,007,199,254,740,991; strings outside that range

Int64ToStringConverter and Int64NullableToStringConverter accept numeric tokens and integer strings. The nullable converter preserves null. SetJsonDefaults(options) updates supplied mutable options or creates new options when omitted. SetHtmlSafeWebJsonDefaults(options) also sets JavaScriptEncoder.Default; Hosting uses it for MVC.

using System.Text.Json;
using DRN.Framework.SharedKernel.Json;

var json = JsonSerializer.Serialize(new { Count = long.MaxValue }, JsonConventions.DefaultOptions);
// {"count":"9223372036854775807"}

See JsonConventions.cs and IntegerSafeIntervalForJs.cs.


Attributes

[IgnoreLog]

Marks classes, structs, properties or fields for exclusion by logging code that honors the attribute. IgnoredLog(object?) checks the runtime type and returns false for null. IgnoredLog(PropertyInfo) also ignores properties typed as object or carrying an ignored property type. See IgnoreLogAttribute.cs.

[SecureKey]

Validates string properties, fields or parameters. Defaults require 16 to 256 characters, uppercase, lowercase, a digit and a special character. The allowed set contains ASCII and Turkish letters, digits, space, and !*()-_; space counts as special. Sequential and repeated-character limits are configurable through MaxSequentialChars (4) and MaxRepeatedChars (3). Null values fail validation. See SecureKeyAttribute.cs for the exact character checks.


Shared Extensions

SharedKernel owns low-level extensions needed without higher-layer dependencies.

using DRN.Framework.SharedKernel.Extensions;

var schema = "OrderHistory".ToSnakeCase();     // order_history
var typeName = "sample hosted".ToPascalCase(); // SampleHosted

var root = "/data/app";
var file = root.GetPathWithinDirectory("exports", "orders.json");

GetPathWithinDirectory() resolves a full path, rejects paths outside the root, and rejects symbolic links or reparse points in child components below that root. Use it for file-serving, manifest, upload and app-data child paths. It checks the path at resolution time; it does not lock the filesystem against later changes. IsPathWithinDirectory() checks lexical containment only and does not resolve symbolic links.

See StringExtensions.cs and PathExtensions.cs.


AppConstants

AppConstants is in DRN.Framework.SharedKernel. Values are initialized once from the process, entry assembly, environment and local network:

Property Value
int ProcessId Environment.ProcessId
Guid AppInstanceId A new GUID for this process initialization
string EntryAssemblyName Entry assembly name, or "Entry Assembly Not Found"
string EntryAssemblyNameNormalized Entry assembly name converted with ToPascalCase()
string EntryAssemblyFullName Full entry assembly name, or "Entry Assembly Not Found"
string LocalAppDataPath Configured data path or application-specific local data directory
string TempPath Application-specific temp directory from the fallback order below
string LocalIpAddress Local IPv4 address, falling back to loopback

LocalAppDataPathEnvVariable is DrnAppDataSettings__DataPath. TempPathEnvVariable is DrnAppDataSettings__TempPath. These are process environment overrides, resolved before DRN configuration.

TempPath appends EntryAssemblyNameNormalized in this order:

  1. DrnAppDataSettings__TempPath/<EntryAssemblyNameNormalized>
  2. DrnAppDataSettings__DataPath/Temp/<EntryAssemblyNameNormalized>
  3. <LocalApplicationData>/Temp/<EntryAssemblyNameNormalized>

LocalAppDataPath uses DrnAppDataSettings__DataPath as configured, otherwise <LocalApplicationData>/<EntryAssemblyNameNormalized>. Path resolution can return an empty string when no usable root is available. IAppData in Utils owns directory creation, temp cleanup, test temp preservation and safe child paths.

See AppConstants.cs and AppData.cs.


Global Usings

Suggested consumer usings for projects that work heavily with SharedKernel types:

global using DRN.Framework.SharedKernel.Domain;
global using DRN.Framework.SharedKernel.Domain.Pagination;
global using DRN.Framework.SharedKernel.Domain.Repository;
global using DRN.Framework.SharedKernel;
global using DRN.Framework.SharedKernel.Extensions;
global using DRN.Framework.SharedKernel.Json;

For complete examples, see Sample.Hosted.


Documented with the assistance of DiSC OS


Semper Progressivus: Always Progressive

Commit Info

Author: Duran Serkan
Date: 2026-09-13 19:21:17 +0300
Hash: e98f5868dbf1de8f4f683536ec1bdf7e6723e074

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.
  • net10.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on DRN.Framework.SharedKernel:

Package Downloads
DRN.Framework.Utils

DRN.Framework.Utils package contains common codes for other DRN.Framework packages, projects developed with DRN.Framework. ## 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 0 9/13/2026
0.10.0-preview002 59 9/9/2026
0.10.0-preview001 121 9/6/2026
0.9.9-preview004 114 8/27/2026
0.9.9-preview003 124 8/26/2026
0.9.9-preview002 141 8/20/2026
0.9.9-preview001 137 8/16/2026
0.9.8 137 8/12/2026
0.9.8-preview004 115 8/12/2026
0.9.8-preview003 124 8/9/2026
0.9.8-preview002 122 8/8/2026
0.9.8-preview001 130 8/8/2026
0.9.7 151 7/29/2026
0.9.6 148 7/15/2026
0.9.6-preview004 176 7/7/2026
0.9.6-preview003 152 7/1/2026
0.9.6-preview002 180 6/29/2026
0.9.6-preview001 150 6/28/2026
0.9.5 166 6/14/2026
0.9.5-preview011 153 6/14/2026
Loading failed

## Version 0.10.0

### Breaking Changes

*   **Entity ID Format Contracts**: GUID operations add `SourceKnownEntityIdFormat` (`ConfiguredDefault = 0`, `Secure = 1`, `Plain = 2`, `Auto = 3`), defaulting to `ConfiguredDefault`. Custom `ISourceKnownEntityIdOperations` implementations must expose an immutable Secure/Plain `DefaultFormat`. Rebuild consumers and update GUID method signatures and method-group bindings. Format is enforced when parsing GUIDs; parsed-record validation takes no format argument and does not reauthenticate GUIDs. Null GUIDs remain supported; undefined formats throw even for null GUIDs and empty GUID batches.
*   **Explicit Expected Identity**: Replace byte-only identity arguments with `new EntityTypeId(entityType, expectedAppId)`: `Generate(long, byte)` becomes `Generate(long, EntityTypeId)`, `HasSameEntityType(byte)` becomes `HasSameEntityTypeId(EntityTypeId)`, and `Validate(byte)` becomes `Validate(EntityTypeId)`. Domain `GetEntityId` accepts this composite identity or a generic entity type. Validation checks both components; `ValidateId()` and the domain GUID helper's boolean option remain validity-only.
*   **Generation Time Policy**: New IDs use a minimum of `2026-09-09T00:00:00Z` and an epoch of `2025-01-01T00:00:00Z` by default. Configure overrides through `SourceKnownGenerationTime.Initialize(minimumUtc, defaultEpoch)` before startup or first ID/epoch use; an explicit epoch requires a minimum. Both values then freeze. Historical reads remain exempt from the floor, and every service and restart using a dataset must retain its origin. See [configuration and limits](README.md#trusted-minimum-generation-time).
*   **Entity Partitions and Analyzers**: Annotate concrete, effectively non-private entities with `[EntityType<TApp>(byte)]` or a supported derived attribute; entity type values must be unique per AppId. Derived attributes must forward one byte or byte-backed enum argument unchanged. Transitive analyzers validate declarations/partitions and require `<AllowMultipleAppIds>true</AllowMultipleAppIds>` for production projects combining partitions. Invalid constant partitions/formats and abstract metadata/repository bindings are errors; duplicate names, hidden identity members, and concrete generic entities produce warnings. Generic forwarding, runtime-instance overloads, and dynamic values remain supported. See [diagnostics and migration requirements](README.md#compile-time-roslyn-analyzers).
*   **Entity Operation Initialization**: `DRN0014` rejects `GetEntityId`, `ToSecure`, and `ToPlain` on provably fresh entities, including aliases and constructor `this`. Use injected ID utilities before initialization. Unknown effects and potentially null inputs remain runtime concerns; initialization does not prove persistence.

### New Features

*   **Application Partitioning**: Added `IAppId`, `EntityTypeAttribute<TApp>`, and `EntityTypeId(entityType, appId)`. Built-in partitions are `DefaultApp` (0), `NexusApp` (126), and `TestApp` (127); `[TestEntityType(byte)]` selects the test partition. `SourceKnownEntity.GetAppId` and `GetEntityTypeId` expose entity metadata.

### Bug Fixes

*   **Pagination**: `PaginationRequest.From()` defaults to page size 10, preserves omitted options, and resets pagination when size, effective maximum size, or sort direction changes. Page jumps preserve direction and are bounded to ten pages without integer underflow.
*   **IgnoredLog Null Handling**: `IgnoredLog(this object? obj)` returns `false` when given `null` input instead of throwing a `NullReferenceException`.

---

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

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