DRN.Framework.SharedKernel
0.10.1-preview007
Prefix Reserved
dotnet add package DRN.Framework.SharedKernel --version 0.10.1-preview007
NuGet\Install-Package DRN.Framework.SharedKernel -Version 0.10.1-preview007
<PackageReference Include="DRN.Framework.SharedKernel" Version="0.10.1-preview007" />
<PackageVersion Include="DRN.Framework.SharedKernel" Version="0.10.1-preview007" />
<PackageReference Include="DRN.Framework.SharedKernel" />
paket add DRN.Framework.SharedKernel --version 0.10.1-preview007
#r "nuget: DRN.Framework.SharedKernel, 0.10.1-preview007"
#:package DRN.Framework.SharedKernel@0.10.1-preview007
#addin nuget:?package=DRN.Framework.SharedKernel&version=0.10.1-preview007&prerelease
#tool nuget:?package=DRN.Framework.SharedKernel&version=0.10.1-preview007&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.Attributes;
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)
{
[Personal]
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
Register one project catalog as EmbeddedResource or AdditionalFiles. Build targets forward it to compiler analysis and publish metadata retained in reference assemblies. Analyzers inherit catalogs from referenced SharedKernel consumers without loading dependency code. Legacy diagnostic-rules.json is accepted. Identical files are deduplicated; distinct own catalogs are rejected. DRN0021 reports invalid catalogs. Rules are isolated per compilation. Backend runtime uses explicit classifications rather than catalog name matching. See configuration and schema.
The shared catalog includes cvv, cvc, pincode, username, firstname, lastname, dateofbirth, birthdate, and iban. Matching public readable properties require classification or a reviewed false-positive annotation, including case and separator variants.
DRN0019 exempts EF migration column definitions and constraint selectors recognized through EF API symbols and column-operation metadata. Data objects and logging inside migrations remain checked. See schema boundaries.
DRN0020 checks statically known dictionary keys, including nested initializers and direct local writes. Use AddDictionary/SanitizeDictionary classifications or an exact adjacent DiagnosticNameExemption comment. Configured analyzer paths and other children remain checked. Exploration is bounded. Dynamic and unexamined keys have no runtime name fallback. See dictionary classifications.
Configured paths remain enforced at AddDictionary/SanitizeDictionary calls. Inline positive classification constructors with constant exact keys protect their whole entries. NonSensitive and unknown classification expressions cannot waive configured paths. Dictionary name checks, including ILogger scopes, accept known null, empty and exact [redacted] values.
DRN0015 through DRN0020 and DRN0022 analyze generated sources without reporting diagnostics in generated code. Runtime classification remains active for DRN capture APIs.
DRN0017 accepts the exact compile-time [redacted] value and checks AddIfNotNullOrEmpty except constant null/empty values. Exact adjacent comments acknowledge reviewed names. Configured analyzer paths, classified values, declarations, and children remain enforced. Backend runtime uses explicit classifications without scanning names again.
Browser diagnostics and compiler analysis consume the same baseline catalog, including ipadd and client. Reviewed operational IP addresses and client identifiers need property exemptions or exact capture comments. Backend runtime protection uses explicit classifications.
Compiler checks cover statically known values and paths. See application responsibilities for dynamic data.
DRN0022 checks sensitive ILogger placeholders, scope dictionary keys, and direct classified-property captures. Remove or redact sensitive values; standard ILogger does not apply DRN runtime redaction. Reviewed name-only false positives can use adjacent comments. DRN0023 reports malformed comments. DRN0024 warns only on unused DiagnosticNameExemption comments, never redundant attributes or reason/policy exemptions. See logging boundaries.
Diagnostic classification also applies to all public readable instance non-indexer model properties in directly and transitively dependent projects, even without logging calls. DRN0015 requires classification or explicit Sensitive(false) for matching names. DRN0016 rejects invalid opt-outs and offers Use Sensitive(true) as an individual code fix. DRN0017 reports matched scalar/untyped capture paths and external property names, and DRN0018 identifies unsupported custom Microsoft classification families. DRN0019 requires matching anonymous properties, including nested and inferred members, to move to an annotated named class or record. Named model properties own classification without built-in logging-prefix matches. [IgnoreLog], reason strings and capture policies do not suppress declaration requirements. Personal/Sensitive-derived company attributes are supported. See classification, code fixes, migration and runtime boundaries.
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 |
Supports integrity, entity-type, and application-partition validation without a database lookup |
| Secure SKEID | 128-bit Guid |
Conceals the encoded identity fields |
ISourceKnownEntityIdOperations defines Generate, nullable/nonnullable Parse, all Validate overloads, ToSecure, and ToPlain in SharedKernel. Utils provides these operations through ISourceKnownEntityIdUtils. EntityFramework makes ID operations available on saved and queried 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 accepts only secure IDs, Plain accepts only plain IDs, and Auto accepts either valid form. Domain GUID helpers apply the selected format and requested identity checks. 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. Use the ID operations API to generate, parse, and convert identifiers.
SourceKnownEntityId equality and hashing use the external GUID. Ordering uses application partition, entity type, and numeric source ID. Different GUID representations of the same source identity can compare as zero while remaining unequal. Consequently, hash-based and sorted collections can deduplicate these values differently. Normalize representations or supply an explicit comparer when collection identity matters.
Numeric Source-Known IDs can be negative. Their sign does not indicate validity.
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 the same in every build configuration. 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");
For custom exceptions, prefer an existing semantic base when it fits:
using DRN.Framework.SharedKernel;
public class OrderAlreadyShippedException(string message)
: ConflictException(message); // Inherits Status = 409.
public class ServiceUnavailableException(string message)
: DrnException(message, null, null, 503); // Explicit status when no existing base fits.
public class UnexpectedOrderException(string message)
: System.Exception(message); // Unmapped exception: Hosting defaults to 500.
Direct DrnException subclasses also default to 500 when status is omitted. For eligible API/HTMX responses, Hosting infers the standard problem type from status, not the class name, message, or Category. Custom types are optional and do not change status. See Hosting mappings, overrides, and special cases.
See Exceptions.cs and HttpScopeMiddleware.cs.
JsonConventions
JsonConventions.DefaultOptions centralizes System.Text.Json settings. Initializing these conventions changes serializer defaults process-wide, including calls that do not pass explicit options. Utils convention setup and DrnTestContext apply these defaults. 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
[Sensitive] and [Personal]
Opt a class, struct or property into diagnostic redaction. [Sensitive] uses DataClassifications.Sensitive; [Personal] uses DataClassifications.Personal. Both derive from Microsoft's DataClassificationAttribute under the DRN.Classification taxonomy and may themselves be inherited by company attributes. Fields may carry classification for other consumers but are omitted by object capture. Derived Sensitive attributes must remain positive. Scoped logs read classified getters once at capture and redact snapshot output; direct sanitizer calls skip classified getters. Positive classifications and [IgnoreLog] on mapped interface properties protect concrete and nested captures too. [IgnoreLog] takes precedence during capture, but never satisfies classification analysis. Direct [Sensitive(false)] exempts one property from built-in name rules, with optional Justification; positive classification and configured protection still win. Interface exemptions do not waive concrete property name rules. Binding, business serialization, and storage are unchanged. See classification examples and diagnostic policy.
[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 KILIÇ
Date: 2026-11-04 22:48:16 +0300
Hash: a831c2948b69b40c32534acfcd7d053e49a2d76f
| 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
- Microsoft.Extensions.Compliance.Abstractions (>= 10.11.0)
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 KILIÇ Date: 2026-11-04 22:48:16 +0300 Hash: a831c2948b69b40c32534acfcd7d053e49a2d76f |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.10.1-preview007 | 57 | 11/4/2026 |
| 0.10.1-preview006 | 134 | 10/27/2026 |
| 0.10.1-preview005 | 135 | 10/25/2026 |
| 0.10.1-preview004 | 178 | 10/14/2026 |
| 0.10.1-preview003 | 137 | 10/4/2026 |
| 0.10.1-preview002 | 126 | 10/4/2026 |
| 0.10.1-preview001 | 126 | 10/4/2026 |
| 0.10.0 | 158 | 9/20/2026 |
| 0.10.0-preview004 | 132 | 9/19/2026 |
| 0.10.0-preview003 | 135 | 9/13/2026 |
| 0.10.0-preview002 | 183 | 9/9/2026 |
| 0.10.0-preview001 | 174 | 9/6/2026 |
| 0.9.9-preview004 | 136 | 8/27/2026 |
| 0.9.9-preview003 | 150 | 8/26/2026 |
| 0.9.9-preview002 | 165 | 8/20/2026 |
| 0.9.9-preview001 | 162 | 8/16/2026 |
| 0.9.8 | 161 | 8/12/2026 |
| 0.9.8-preview004 | 137 | 8/12/2026 |
| 0.9.8-preview003 | 142 | 8/9/2026 |
| 0.9.8-preview002 | 141 | 8/8/2026 |
## Version 0.10.1
### Breaking Changes
* **Model Capture Analysis**: DRN0025 rejects captures that exceed the compiler's model-analysis work limit. Capture a smaller model or explicitly classify sensitive subtrees so configured paths cannot silently escape analysis.
* **Diagnostic Field Coverage**: Classification requirements include CVV/CVC/PIN-code fields, usernames, first/last names, dates of birth, IBAN, client and IP-address patterns. Annotate matching model properties or explicitly mark reviewed false positives. Browser and compiler checks use the same baseline catalog.
* **Dictionary Capture Classification**: DRN0020 checks statically known keys, including nested initializers and direct writes. Use explicit dictionary classifications or reviewed exact-name comments. Configured analyzer paths cannot be waived. Backend runtime has no dynamic name fallback.
* **Model Classification Enforcement**: Transitive compiler errors require matching public readable instance properties to declare classification or `[Sensitive(false)]`. This applies even when properties are not logged or use `[IgnoreLog]`. Move matching anonymous properties to annotated classes or records. Custom Microsoft classifications outside the DRN Personal/Sensitive families require a supported classification for compiler verification. Review models and anonymous configuration/schema projections when upgrading. See the [diagnostic guide](../DRN.Framework.Utils/Logging/Diagnostics/README.md) for capture rules and exemptions.
* **Logging Boundary Enforcement**: DRN0022 reports sensitive ILogger placeholders, scope dictionary keys, and direct classified-property captures, including inherited properties implementing classified interfaces on the receiver type. Remove or redact affected values before logging. Standard ILogger does not apply DRN runtime redaction.
### New Features
* **Logging Name Exemptions**: Adjacent `// DiagnosticNameExemption: ExactName | Nonempty justification` comments acknowledge DRN0017/DRN0020/DRN0022 name matches. They do not waive classified values, configured analyzer paths, declaration requirements, or child checks. DRN0023 reports malformed comments and DRN0024 warns about unused comments. Comments do not provide runtime redaction.
* **Project Classification Rules**: Register `data-classification-rules.json` as an EmbeddedResource or AdditionalFiles item to extend compiler classification checks. Rules also inherit from referenced SharedKernel consumers. The legacy filename `diagnostic-rules.json` remains accepted. DRN0021 reports invalid, unreadable, oversized, or ambiguous catalogs.
* **Sensitive Data Classification**: Added inheritable `[Sensitive]` and `[Personal]` attributes and support for application-derived classifications. Use `[Sensitive(false)]` for reviewed property-name false positives. Positive classification overrides opt-outs. Editor fixes offer Personal, Sensitive, false-positive classification, and correction of invalid opt-outs. These attributes do not change business serialization or binding.
### Bug Fixes
* **Entity Attribute Analyzer**: Incomplete primary-constructor base lists no longer cause analyzer exceptions while editing derived entity attributes.
---
Documented with the assistance of [DiSC OS](https://github.com/duranserkan/DRN-Project/blob/develop/.agent/rules/DiSCOS.md)
---
**Semper Progressivus: Always Progressive**