DRN.Framework.SharedKernel
0.10.0-preview003
Prefix Reserved
dotnet add package DRN.Framework.SharedKernel --version 0.10.0-preview003
NuGet\Install-Package DRN.Framework.SharedKernel -Version 0.10.0-preview003
<PackageReference Include="DRN.Framework.SharedKernel" Version="0.10.0-preview003" />
<PackageVersion Include="DRN.Framework.SharedKernel" Version="0.10.0-preview003" />
<PackageReference Include="DRN.Framework.SharedKernel" />
paket add DRN.Framework.SharedKernel --version 0.10.0-preview003
#r "nuget: DRN.Framework.SharedKernel, 0.10.0-preview003"
#:package DRN.Framework.SharedKernel@0.10.0-preview003
#addin nuget:?package=DRN.Framework.SharedKernel&version=0.10.0-preview003&prerelease
#tool nuget:?package=DRN.Framework.SharedKernel&version=0.10.0-preview003&prerelease
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,DomainEventfor DDD patterns - Typed exceptions -
ExceptionForcreates exceptions with status codes consumed by DRN Hosting - JSON conventions - Global
System.Text.Jsondefaults with camelCase, enums-as-strings - Shared extensions - Casing and safe path helpers for lower-layer packages
- Source Known IDs - Internal
longkeys and externalGuididentifiers with type and partition validation
Table of Contents
- QuickStart: Beginner
- QuickStart: Advanced
- Domain Primitives
- SourceKnownRepository
- Pagination
- Exceptions
- JsonConventions
- Attributes
- Shared Extensions
- AppConstants
- Global Usings
- Related Packages
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 writableModelthroughIEntityWithModel<TModel>, whereTModel : class.Modelstarts asnull!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 -> CommonandA -> C -> Commonare 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 withTest., or end with.Testsor.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
ScopeKeyuses the root cancellation scope. A configured key selects a child scope shared by repositories using that key. CancellationTokenexposes the effective scope token.CancelChanges()cancels that scope, andCancelWhen(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:
DrnAppDataSettings__TempPath/<EntryAssemblyNameNormalized>DrnAppDataSettings__DataPath/Temp/<EntryAssemblyNameNormalized><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;
Related Packages
- DRN.Framework.Utils - Configuration, logging, and DI utilities
- DRN.Framework.EntityFramework - EF Core integration with DrnContext
- DRN.Framework.Hosting - Web application hosting
- DRN.Framework.Testing - Testing utilities
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 | Versions 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. |
-
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 |
## 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**