The shared, persistence-agnostic vocabulary every other DKNet.EfCore.* package builds on — entity base classes,
domain-event contracts, and the attributes that steer audit, sequence, and mapping behaviour.
IEntity<TKey> / IAuditedProperties / IEventEntity /
IConcurrencyEntity<T> / ISoftDeletableEntity contracts, so they interoperate on the same model instead of each
inventing its own marker interface.Microsoft.EntityFrameworkCore.Abstractions, System.ComponentModel.Annotations, and FluentResults, not
Microsoft.EntityFrameworkCore. A domain project can define entities, raise events, and declare audit rules
without pulling in the EF Core runtime.AddEvent(...) records a business fact on the entity;
what dispatches it is a separate package’s problem, so the domain method has no messaging dependency.[AuditLog], [IgnoreAuditLog], [SensitiveData],
[RaisesEvent], [Sequence], [SqlSequence], and [IgnoreEntity] put audit, event, and mapping rules on the
type they describe rather than in configuration elsewhere.DKNet.EfCore.Extensions configures
the RowVersion token or the audit columns for you.Reach for this package first when modelling a new domain entity in a DKNet-based solution.
dotnet add package DKNet.EfCore.Abstractions
There is no DI registration for this package — it is pure types (base classes, interfaces, attributes) referenced
at compile time by your domain project and by the DKNet packages that implement runtime behavior against them.
Minimum usage is simply deriving your entity from Entity (or Entity<TKey>):
using DKNet.EfCore.Abstractions.Entities;
public class Product : Entity // Entity<Guid>
{
private Product() { } // EF Core
public Product(string name, decimal price) : base(Guid.NewGuid())
{
Name = name;
Price = price;
}
public string Name { get; private set; } = null!;
public decimal Price { get; private set; }
}
Runtime behavior — actually dispatching events, writing audit logs, redacting sensitive fields, enforcing row ownership, encrypting columns — comes from the sibling packages described in Where it fits below; this package only supplies the shapes they agree on.
IEntity<TKey>, Entity<TKey> / EntityIEntity<out TKey> is the minimal contract: a single TKey Id { get; }. Entity<TKey> is the base class you
actually derive from — it implements IEntity<TKey> and IEventEntity (see below), exposes Id with a
private setter, and overrides ToString() as "{TypeName} '{Id}'". Entity is a convenience specialization with
TKey = Guid, the recommended default for distributed systems (globally unique, no DB round-trip needed to
generate).
public class Category : Entity<int>
{
private Category() { }
public Category(int id, string name) : base(id) => Name = name;
public string Name { get; private set; } = null!;
}
Both constructors that take an id exist mainly for EF Core data-seeding scenarios (per their own XML docs) —
day-to-day creation typically leaves EF Core / a value generator to assign Id.
IEventEntity, Entity<TKey>IEventEntity lets an entity queue up domain events during a business operation, to be drained and published once
the surrounding SaveChanges succeeds:
AddEvent(object eventObj) — queue an already-built event instance.AddEvent<TEvent>() — queue a type; the concrete event is produced later by mapping the entity itself onto
TEvent. This overload requires an IMapper to be registered wherever the queue is drained — DKNet.EfCore.Events
throws EventException at dispatch time if none is registered.GetEvents() — returns (object[] Events, Type[] EventTypes) for both queues (used by the dispatcher, not
usually called from domain code).ClearEvents() — empties both queues.Entity<TKey> already implements IEventEntity for you via two internal Collection<> fields, so every entity
deriving from Entity/Entity<TKey> gets event-queuing for free:
public class Order : Entity
{
public void Place()
{
// ... business logic ...
AddEvent(new OrderPlacedEvent(Id));
}
}
Queuing an event here does nothing by itself — see “Composition” for how DKNet.EfCore.Events drains and publishes
the queue.
[RaisesEvent], EventOperations[RaisesEvent] is an alternative (or complement) to hand-calling AddEvent(...): apply it to the entity class to
declare that a persistence operation should raise a payload automatically, without touching the entity’s method
bodies. It is repeatable (AllowMultiple = true) — apply once per event the entity raises.
Breaking change: the string-form argument used to be the generated record’s name verbatim. It is now only the optional label segment of a name composed by fixed convention — every existing string-form declaration produces a differently-named record after upgrading, and each one must be revisited. Before:
[RaisesEvent("CustomerTouched", EventOperations.Created)]generatedCustomerTouched. After, the same declaration generatesCustomerTouchedCreatedEvent(entity name + label + operation +Event).
Three forms:
using DKNet.EfCore.Abstractions.Entities;
using DKNet.EfCore.Abstractions.Events;
using DKNet.EfCore.DtoGenerator;
// Type-naming form — names an existing [GenerateDto] payload record (DKNet.EfCore.DtoGenerator)
[GenerateDto(typeof(Order), Exclude = new[] { "InternalNote" })]
public partial record OrderPlacedEvent;
[RaisesEvent(typeof(OrderPlacedEvent), EventOperations.Created)]
[RaisesEvent(typeof(OrderStatusChangedEvent), EventOperations.Updated, nameof(Order.Status))]
public class Order : Entity
{
public string Status { get; set; } = string.Empty;
}
// Label-less convention form — no hand-written payload record; the build generates and names it by convention
[RaisesEvent(EventOperations.Created)]
public class Customer : Entity
{
}
// generates CustomerCreatedEvent
// Label convention form — the label is composed into the generated name
[RaisesEvent("Touched", EventOperations.Created)]
public class Product : Entity
{
}
// generates ProductTouchedCreatedEvent
EventOperations is a [Flags] enum with Created = 1, Updated = 2, Deleted = 4 — combine flags
(Created | Updated) to raise the same event type for more than one lifecycle transition. For Updated rules, the
optional trailing properties (nameof(...)-checked) narrow the rule to fire only when at least one listed
direct property changed; an empty list fires on any change. Nested/owned-value changes never satisfy the
narrowing — only direct properties of the carrying entity are observed.
Both convention forms (label and label-less) name their generated record by a fixed, non-configurable formula — never a literal name — composed in this order:
Event.For example, [RaisesEvent(EventOperations.Updated, nameof(Customer.Tier))] on Customer composes
CustomerTierUpdatedEvent; [RaisesEvent(EventOperations.Created | EventOperations.Updated)] composes
CustomerCreatedUpdatedEvent. A declaration with no operation named (Operations == 0) is a build error
(DKRAISEVT007) — it can never raise anything. A composed name that isn’t a single valid C# identifier (e.g. a
label containing whitespace or punctuation) is a build error (DKRAISEVT005) and generates no record. Two
declarations composing to the identical name are always an error, never silently merged: on the same entity that’s
DKRAISEVT008, across different entities in the same namespace it’s DKRAISEVT006.
Either convention form can also narrow the shape of its composed payload record with Exclude/Include
named arguments — they never affect the composed name, only the record’s properties:
using DKNet.EfCore.Abstractions.Entities;
using DKNet.EfCore.Abstractions.Events;
[RaisesEvent(EventOperations.Created, Exclude = new[] { "InternalNote" })]
[RaisesEvent("Touched", EventOperations.Updated, Include = new[] { nameof(Customer.Name), nameof(Customer.Email) })]
public class Customer : Entity
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string InternalNote { get; set; } = string.Empty;
}
Exclude and Include are mutually exclusive on one declaration (DKRAISEVT009); a non-empty Include is the
whole truth for the payload shape and overrides the project-wide DtoGeneratorExclusions MSBuild property
(see DKNet.EfCore.DtoGenerator’s docs) for that declaration.DKRAISEVT010).Include names one by name — Include narrows which of the entity’s own scalar properties ship, it never pulls
a navigation property in.DKRAISEVT011) — that form’s named payload record already
owns its own shape via its own [GenerateDto] Exclude/Include.DtoGeneratorExclusions list now also applies to composed convention-form payloads that set
neither filter (or only Exclude), narrowing them the same way it narrows hand-written [GenerateDto] DTOs.This attribute alone raises nothing at runtime: DKNet.EfCore.DtoGenerator validates the rule at build time (the
named type must be a [GenerateDto] payload generated from the same entity for the type-naming form; the
composed name must be a valid, non-colliding identifier for the convention forms), and DKNet.EfCore.Events’ save
hook reads [RaisesEvent] via reflection to actually raise the payload after a successful save, composing the same
name from the same EventNameComposer source the build uses — the two can never disagree. A project that
references only DKNet.EfCore.Abstractions and DKNet.EfCore.DtoGenerator builds cleanly with rules declared and
simply never raises them until the application also registers DKNet.EfCore.Events.
IAuditedProperties, IAuditedEntity<TKey>, AuditedEntity<TKey> / AuditedEntityIAuditedProperties declares the four audit fields every audited entity needs: CreatedOn, CreatedBy,
UpdatedOn, UpdatedBy (all decorated [IgnoreAuditLog] on the interface itself — see feature 6). CreatedBy
and UpdatedBy also carry [MaxLength(500)]. IAuditedEntity<TKey> combines IEntity<TKey> and
IAuditedProperties into one contract.
This package declares the fields; it never fills them. Two hook packages do, and which one depends on whether a signed-in user is available for the save:
| Source of the value | Filled by | When |
|---|---|---|
Your own domain method (SetCreatedBy/SetUpdatedBy) |
the entity itself | always wins — the hooks below never overwrite it |
The signed-in user, via ICurrentUserProvider.GetCurrentUser() |
DKNet.EfCore.AuditLogs’ EfCoreAuditHook |
a provider is registered (AddCurrentUserProvider<TDbContext, TProvider>()) and returns a non-empty value for that save |
The tenant ownership key, via IDataOwnerProvider.GetOwnershipKey() |
DKNet.EfCore.DataAuthorization’ DataOwnerHook |
no current-user value is available for that save — no provider registered, or one that returned null/empty |
With neither hook registered, the four properties stay exactly as your code left them.
AuditedEntity<TKey> / AuditedEntity (Guid-keyed) is the base class you actually derive from. It implements the
four properties with private setters plus two derived, [NotMapped] conveniences — LastModifiedBy (falls back to
CreatedBy when never updated) and LastModifiedOn (falls back to CreatedOn) — and two protected mutators:
public class Invoice : AuditedEntity
{
private Invoice() { }
public static Invoice Create(string createdBy)
{
var invoice = new Invoice();
invoice.SetCreatedBy(createdBy); // no-ops if CreatedBy is already set
return invoice;
}
public void MarkPaid(string updatedBy) => SetUpdatedBy(updatedBy);
}
SetCreatedBy(userName, createdOn = null) — a no-op once CreatedBy is already non-empty (first write wins);
throws ArgumentException (via ArgumentException.ThrowIfNullOrWhiteSpace) for a null/blank userName.
Defaults createdOn to DateTimeOffset.UtcNow.SetUpdatedBy(userName, updatedOn = null) — silently ignored if the supplied updatedOn is older than the
currently stored UpdatedOn (out-of-order update guard); otherwise validates userName the same way and
defaults updatedOn to UTC now.Merely deriving from AuditedEntity does not populate these fields for you on every save — that stamping is
performed by whichever hook or interceptor your application wires up (DKNet ships this behavior in
DKNet.EfCore.AuditLogs and DKNet.EfCore.DataAuthorization, both running on the DKNet.EfCore.Hooks pipeline;
they detect IAuditedProperties and stamp CreatedBy on Added and UpdatedBy on Modified entries, from the
signed-in user or the ownership key per the table above — see “Composition”). Implementing IAuditedProperties is also the switch that turns an entity
into a candidate for audit logging (feature 6) — an entity that doesn’t implement it is invisible to
DKNet.EfCore.AuditLogs regardless of any attributes you put on it.
IConcurrencyEntity<TType>Declares a nullable RowVersion (typed TType, e.g. byte[]), pre-annotated [Timestamp] and
[Column(Order = 1000)] so the concurrency token consistently sorts last in generated schemas, plus
SetRowVersion(TType rowVersion) to update it. Implement this on an entity to opt into EF Core’s row-version
concurrency check:
public class Account : Entity, IConcurrencyEntity<byte[]>
{
public byte[]? RowVersion { get; private set; }
public void SetRowVersion(byte[] rowVersion) => RowVersion = rowVersion;
}
DKNet.EfCore.Extensions’ DefaultEntityTypeConfiguration<TEntity> detects IConcurrencyEntity<> by reflection and
automatically configures the RowVersion property as a concurrency token / row-version column with
ValueGeneratedOnAddOrUpdate() — you do not need to configure it yourself in OnModelCreating if you use that base
configuration.
ISoftDeletableEntityDeclares IsDeleted, DeletedOn, DeletedBy ([MaxLength(250)]) plus a Delete(byUser, deletedOn = null) method
that returns FluentResults.IResultBase so implementers can fail the delete (e.g. business-rule violation) without
throwing:
public class Document : Entity, ISoftDeletableEntity
{
public bool IsDeleted { get; private set; }
public DateTimeOffset? DeletedOn { get; private set; }
public string? DeletedBy { get; private set; }
public IResultBase Delete(string byUser, DateTimeOffset? deletedOn = null)
{
IsDeleted = true;
DeletedBy = byUser;
DeletedOn = deletedOn ?? DateTimeOffset.UtcNow;
return Result.Ok();
}
}
Gotcha: this is a contract only. No shipped DKNet package currently implements the query-filter or interceptor
side (translating “call Delete” into “actually filter it out of default queries” or “turn a hard delete into a
soft one automatically”). Implementing ISoftDeletableEntity gives you a consistent shape to code your own global
query filter (modelBuilder.Entity<T>().HasQueryFilter(e => !e.IsDeleted)) against — it does not wire one up for
you.
[Sequence], [SqlSequence]The two sequence attributes are used together: [SqlSequence] goes on the enum that names your sequences and
fixes their database schema; [Sequence] goes on each enum field and describes that one sequence.
using DKNet.EfCore.Abstractions.Attributes;
[SqlSequence("billing")] // schema for every sequence in this enum; defaults to "seq"
public enum InvoiceSequences
{
[Sequence(typeof(long), StartAt = 1000, IncrementsBy = 1, FormatString = "INV-{1:00000000}")]
InvoiceNumber,
[Sequence] // int, all database defaults
CreditNoteNumber
}
FormatString has a fixed two-placeholder contract, applied by DKNet.EfCore.Extensions’
NextSeqValueWithFormat: {1} is the sequence value, and the literal token DateTime is rewritten to {0}
and bound to DateTime.UtcNow — so "T{DateTime:yyMMdd}{1:00000}" renders as T26090100042. A format string
that uses {0} directly gets the timestamp, not the number.
[Sequence] targets AttributeTargets.Field — it goes on the enum member, not on an entity property. The
constructor’s optional Type defaults to int and only byte, short, int and long are accepted; anything
else throws NotSupportedException while the attribute is being constructed. The four numeric options
(StartAt, IncrementsBy, Min, Max) all default to -1, which DKNet.EfCore.Extensions reads as “leave it
to the database” — only values greater than zero are applied. Cyclic is always applied, defaulting to true.
Full property list: Configuration reference.
DKNet.EfCore.Extensions registers these against the model during UseAutoConfigModel, and only when the provider
is SQL Server or Npgsql. Each sequence is named Seq_{enumMemberName} in the [SqlSequence] schema, and an enum
member without its own [Sequence] is skipped. Read a value back with
dbContext.NextSeqValue(InvoiceSequences.InvoiceNumber), or NextSeqValueWithFormat(...) to get FormatString
applied.
[AuditLog], [IgnoreAuditLog], [SensitiveDataAttribute]These three attributes are pure markers read by DKNet.EfCore.AuditLogs (there is zero behavior in this package
itself) but they only make sense in terms of that consumer, so they’re covered together:
[AuditLog] on a class: with AuditLogBehaviour.OnlyAttributedAuditedEntities, marks the entity type as
one that should be captured (entities are otherwise skipped under that behaviour unless attributed).[AuditLog] on a property: dual meaning depending on the active AuditPropertyPolicy — under the default
RedactSensitive policy it forces plaintext capture even if the property name matches a built-in
sensitive-name pattern; under the strict OnlyAttributedProperties policy it allow-lists the property for
capture at all (properties not attributed are skipped entirely under that policy).[IgnoreAuditLog] on a class or property: unconditionally excludes it from audit logging, regardless of
behaviour/policy. It’s why IAuditedProperties.CreatedOn/CreatedBy/UpdatedOn/UpdatedBy (and
IConcurrencyEntity<TType>.RowVersion) are pre-decorated with it in this package — the audit trail’s own
bookkeeping fields never audit-log themselves.[SensitiveDataAttribute] on a property: always redacts the value in the audit log, even when [AuditLog] is
also present on the same property (redaction wins over the allow-list). It also gates the property in API
responses once a host opts in — see Declaring a property sensitive
below.[AuditLog] // only needed under OnlyAttributedAuditedEntities behaviour
public class Customer : AuditedEntity
{
public string Email { get; private set; } = null!;
[SensitiveDataAttribute] // always redacted in the audit log
public string NationalId { get; private set; } = null!;
}
Requires IAuditedProperties. Verified against DKNet.EfCore.AuditLogs’ AuditLogExtensions.BuildAuditLog:
an entity that doesn’t implement IAuditedProperties is skipped before any of these attributes are even inspected
— attaching [AuditLog] to a plain Entity does nothing.
[SensitiveData][SensitiveData] is one declaration on the domain property, read by two independent consumers:
DKNet.EfCore.AuditLogs) — the value is replaced with "***REDACTED***" in the
captured audit entry. This is the original behaviour and it is unchanged.DKNet.EfCore.Extensions) — the property is omitted from the
serialized JSON unless the caller is authenticated and holds one of the roles the declaration names. This
consumer is opt-in per host; see
DKNet.EfCore.Extensions.The attribute takes an optional params string[] roles:
using DKNet.EfCore.Abstractions.Attributes;
using DKNet.EfCore.Abstractions.Entities;
public class Product : AuditedEntity
{
public string Name { get; private set; } = null!;
public decimal Price { get; private set; }
[SensitiveData("pricing")] // only callers in the "pricing" role
public decimal SupplierCostPrice { get; private set; }
[SensitiveData("pricing", "audit")] // either role is enough
public decimal SupplierRebate { get; private set; }
[SensitiveData] // any authenticated caller
public string SupplierReferenceCode { get; private set; } = null!;
}
| Declaration | Roles |
Who receives the property (when a host has opted in) |
|---|---|---|
[SensitiveData] |
empty | Any authenticated caller. Naming no role does not mean “everyone” — an unauthenticated or unknown caller is still refused. |
[SensitiveData("pricing")] |
["pricing"] |
An authenticated caller for whom IsInRole("pricing") is true. |
[SensitiveData("pricing", "audit")] |
["pricing", "audit"] |
An authenticated caller in at least one of the named roles. |
Roles is never null — it is an empty IReadOnlyList<string> when no role is named. Role names are matched by
ClaimsPrincipal.IsInRole, whose comparison is the one your identity stack configures (ordinal by default), so
write the role names exactly as they appear in the caller’s claims.
Nothing here reaches the API on its own: this package defines the attribute, DKNet.EfCore.DtoGenerator carries
it onto the generated response model, and DKNet.EfCore.Extensions is where a host turns the filtering on.
[IgnoreEntity]A class-level marker documented as excluding a type “from the automatic entity mapper”, intended for delivered
(non-EF-mapped) types. Verified caveat: as of this writing, no shipped DKNet package (Extensions,
DtoGenerator, etc.) actually inspects this attribute — its only current references in the solution are its own
definition and its own unit tests asserting attribute metadata (AttributeUsage, sealed, etc.). Treat it as a
declared-but-not-yet-wired extension point rather than something that changes runtime EF Core discovery today;
don’t rely on it to keep a type out of your model until you’ve confirmed the specific mapper/generator you’re using
reads it.
[CrudCreate], [CrudUpdate], [CrudAction]Three method/constructor markers that declare which members of an entity become generated HTTP endpoints. They are
declared here but consumed entirely by DKNet.SlimBus.Generators, which emits the request records, handlers,
and minimal-API registration from them — this package contributes only the attribute types.
using DKNet.EfCore.Abstractions.Attributes;
using DKNet.EfCore.Abstractions.Entities;
public class Order : Entity
{
[CrudCreate] // one per entity; ctor or method
public Order(string customer) => Customer = customer;
[CrudUpdate] // any public instance method
public void Rename(string customer) => Customer = customer;
[CrudAction("approval", Verb = CrudActionVerb.Patch)]
public void Approve(string approvedBy) => ApprovedBy = approvedBy;
public string Customer { get; private set; }
public string? ApprovedBy { get; private set; }
}
[CrudCreate] — on a public constructor or method, at most once per entity. Its parameters become the Create
request.[CrudUpdate] — on any public instance method; one generated request per marked method. The first one keeps the
plain PUT {id} route, later ones get PUT {id}/{kebab-case-method-name}.[CrudAction] — a named operation at its own {id}/{segment} route. Verb picks Post (default), Put or
Patch; there is no Delete. The positional route argument overrides the kebab-cased method name.All three expose a Name property that overrides the generated request type’s name. Marking one member with both
[CrudUpdate] and [CrudAction] is a build error. Property tables:
Configuration reference; the generated code, routes, and full diagnostics list live in
DKNet.SlimBus.Generators.
IEventPublisher, DefaultEventPublisher, IEventItem / EventItemIEventPublisher is the sink domain events are handed to: PublishAsync(object, CancellationToken) for one event,
PublishAsync(IEnumerable<object>, CancellationToken) for a batch. DefaultEventPublisher is an abstract base
that implements the batch overload as a sequential foreach over the single-event abstract method — you only
override PublishAsync(object, ...) to get a working batch implementation. Register one or more IEventPublisher
implementations in DI; DKNet.EfCore.Events fans every dispatched event out to all of them.
IEventItem is an optional shape for the event payload itself: AdditionalData ([JsonIgnore], an
IDictionary<string,string> meant for message-header routing/filtering data that should not appear in the
serialized event body) and EventType (a string type-tag). EventItem is the matching abstract record base,
defaulting AdditionalData to an ordinal-case-insensitive dictionary and EventType to GetType().FullName.
DKNet.EfCore.Events’ dispatcher stamps AdditionalData[nameof(sourceType)] (i.e. key "sourceType") with the
originating entity’s full type name for every dispatched event that implements IEventItem, whether or not you
derive from EventItem yourself.
There is no options object and no DI registration in this package — its entire customisation surface is the attributes below plus the interfaces you implement. Every default is taken from the attribute’s own source.
RaisesEventAttribute (DKNet.EfCore.Abstractions.Events)[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] — repeatable, one per event the
entity raises, and never inherited by a derived entity.
Three constructor forms, exactly one of which each declaration uses:
| Form | Signature | Names the payload |
|---|---|---|
| Type-naming | (Type eventType, EventOperations operations, params string[] properties) |
You do — it must be a [GenerateDto] record generated from the same entity. |
| Label convention | (string label, EventOperations operations, params string[] properties) |
Composed: entity + label + properties + operations + Event. |
| Label-less convention | (EventOperations operations, params string[] properties) |
Composed: entity + properties + operations + Event. |
| Member | Type | Default | Effect |
|---|---|---|---|
EventType |
Type? |
null |
Set only by the type-naming form; the payload record to raise. |
Label |
string? |
null |
Set only by the label form; one extra segment in the composed name. Takes no part in the payload’s shape. |
Operations |
EventOperations |
required, no default | Created = 1, Updated = 2, Deleted = 4, combinable. 0 is a build error (DKRAISEVT007). |
Properties |
IReadOnlyList<string> (trailing params) |
empty | Narrows an Updated rule to fire only when a listed property changed. Empty means any change qualifies. Direct properties only. |
Exclude |
string[] |
[] |
Convention forms only. Properties to drop from the composed payload. Mutually exclusive with Include (DKRAISEVT009); a build error on the type-naming form (DKRAISEVT011). |
Include |
string[] |
[] |
Convention forms only. When non-empty, the whole truth for the composed payload’s shape, overriding the project-wide DtoGeneratorExclusions. Same exclusivity and same type-form restriction as Exclude. |
SequenceAttribute (DKNet.EfCore.Abstractions.Attributes)[AttributeUsage(AttributeTargets.Field)] — applies to an enum member, not to an entity property.
| Member | Type | Default | Effect |
|---|---|---|---|
Type (ctor arg, optional) |
Type |
typeof(int) |
The sequence’s data type. Only byte, short, int, long; anything else throws NotSupportedException when the attribute is constructed. |
Cyclic |
bool |
true |
Whether the sequence wraps back to its minimum after reaching its maximum. |
StartAt |
long |
-1 |
Starting value. Applied only when greater than zero; otherwise the database default stands. |
IncrementsBy |
int |
-1 |
Step size. Applied only when greater than zero. |
Min |
long |
-1 |
Minimum value. Applied only when greater than zero. |
Max |
long |
-1 |
Maximum value. Applied only when greater than zero. |
FormatString |
string? |
null |
Format applied by NextSeqValueWithFormat. {1} is the sequence value; the literal token DateTime becomes {0} bound to DateTime.UtcNow. |
SqlSequenceAttribute (DKNet.EfCore.Abstractions.Attributes)[AttributeUsage(AttributeTargets.Enum)].
| Member | Type | Default | Effect |
|---|---|---|---|
Schema (ctor arg, optional) |
string |
"seq" |
Database schema every sequence declared by that enum is created in. Read-only after construction. |
DKNet.EfCore.Abstractions.Attributes)Only SensitiveDataAttribute carries configuration; for the rest, presence is the whole configuration. All are
sealed and Inherited = false.
| Attribute | Targets | Effect |
|---|---|---|
AuditLogAttribute |
Class, Property |
On a class: opts the entity in under AuditLogBehaviour.OnlyAttributedAuditedEntities. On a property: forces plaintext capture past the sensitive-name patterns, and allow-lists it under AuditPropertyPolicy.OnlyAttributedProperties. |
IgnoreAuditLogAttribute |
Class, Property |
Excludes it from audit logging unconditionally, whatever the behaviour and policy. |
SensitiveDataAttribute |
Property |
Always redacts the value in the audit log, even alongside [AuditLog] on the same property. Also gates the property in API responses for hosts that opted into role-aware serialization. |
IgnoreEntityAttribute |
Class |
Declared as an opt-out from automatic entity mapping — see the caveat in Excluding a class from automatic mapping. |
SensitiveDataAttribute’s own surface:
| Member | Type | Default | Effect |
|---|---|---|---|
roles (trailing params string[] ctor arg) |
string[] |
empty | Role names permitted to receive the property in an API response. [SensitiveData] with no argument stays valid. |
Roles |
IReadOnlyList<string> |
empty, never null |
Read-only view of the declared role names. Empty means any authenticated caller, not everyone. |
DKNet.EfCore.Abstractions.Attributes)Consumed by DKNet.SlimBus.Generators. All three are AllowMultiple = false.
| Attribute | Targets | Member | Type | Default | Effect |
|---|---|---|---|---|---|
CrudCreateAttribute |
Constructor, Method |
Name |
string? |
null |
Overrides the generated Create request type’s name. |
CrudUpdateAttribute |
Method |
Name |
string? |
null |
Overrides the generated Update request type’s name. |
CrudActionAttribute |
Method |
route (ctor arg, optional) |
string? |
null → kebab-cased method name |
Route segment appended after {id}/. |
Verb |
CrudActionVerb |
Post |
Post, Put or Patch. There is no Delete. |
||
Name |
string? |
null |
Overrides the generated request type’s name. |
| Contract | Member with a default | Default | Effect |
|---|---|---|---|
IEntity<out TKey> |
— | — | TKey Id { get; } only. |
IAuditedProperties |
— | — | CreatedOn, CreatedBy, UpdatedOn, UpdatedBy; all four carry [IgnoreAuditLog], and the two By properties [MaxLength(500)] — note DKNet.EfCore.Extensions overrides that to 255 in DefaultEntityTypeConfiguration<T>. |
IConcurrencyEntity<TType> |
RowVersion |
— | Pre-annotated [Timestamp] and [Column(Order = 1000)] so the token sorts last in generated schemas. |
ISoftDeletableEntity |
Delete(byUser, deletedOn) |
deletedOn defaults to null |
Returns IResultBase, so an implementation can refuse the delete without throwing. |
IEventItem / EventItem |
AdditionalData |
ordinal case-insensitive dictionary | [JsonIgnore]; meant for message headers. DKNet.EfCore.Events stamps sourceType into it. |
IEventItem / EventItem |
EventType |
GetType().FullName |
String type tag carried on the serialized payload. |
IEventPublisher / DefaultEventPublisher |
batch PublishAsync |
sequential foreach |
Override only the single-event method to get a working batch implementation. |
The one mechanism in this package with real machinery behind it is [RaisesEvent] name composition — the same
EventNameComposer source file is compiled into this assembly and linked into DKNet.EfCore.DtoGenerator, so
the name the build emits and the name the save hook looks up can never disagree:
Otherwise this package is deliberately inert — every other EfCore package supplies the runtime behavior against the types declared here. Concretely (all verified by reading the consuming source, not assumed):
DKNet.EfCore.Events is the direct consumer of IEventEntity/IEventPublisher/IEventItem/
RaisesEventAttribute/EventOperations. Its EventContext scans tracked entries for IEventEntity, calls
GetEvents()/ClearEvents() on each, and (for AddEvent<TEvent>() queue entries) maps the entity onto TEvent
via a registered IMapper. Its EventHook reads [RaisesEvent] off each changed entity’s type via reflection,
evaluates EventOperations/property narrowing against the save’s EntityState, and hands the resulting event
instances to every registered IEventPublisher.DKNet.EfCore.Hooks supplies the IBeforeSaveHookAsync/IAfterSaveHookAsync/IHookAsync pipeline that
DKNet.EfCore.Events’ EventHook (and DKNet.EfCore.DataAuthorization’s DataOwnerHook) plug into — this
package defines no hook types itself, only the entity-side contracts hooks act on.
Entity<TKey> doesn’t reference DKNet.EfCore.Hooks at all; the hook pipeline reaches in from the outside via
the IEventEntity/IAuditedProperties interfaces.DKNet.EfCore.AuditLogs is the sole consumer of [AuditLog], [IgnoreAuditLog], and
[SensitiveDataAttribute], and it gates everything on IAuditedProperties first (see Audit-log opt-in and redaction below). It also reads
IAuditedProperties.CreatedBy/UpdatedBy directly for the “who” side of each audit entry — and writes them
first, from ICurrentUserProvider, when one is registered: CreatedBy/CreatedOn on Added entries,
UpdatedBy/UpdatedOn on Modified ones, before the entry is captured.DKNet.EfCore.DataAuthorization stamps IAuditedProperties.CreatedBy/UpdatedBy from the row’s ownership
key — but only for a save where no ICurrentUserProvider value was available, since otherwise AuditLogs owns
those fields and this hook stamps IOwnedBy.OwnedBy alone. It generally targets entities exposing the
abstractions in this package (DataOwnerHook inspects IAuditedProperties, and works alongside
IConcurrencyEntity/Entity<TKey>-shaped models).DKNet.EfCore.Encryption does not reuse [SensitiveDataAttribute] — it defines its own,
narrower-purpose [Encrypted] attribute (DKNet.EfCore.Encryption.Attributes.EncryptedAttribute) for
column-level encryption via a value converter. Don’t conflate the two: [SensitiveDataAttribute] only affects
what AuditLogs writes to the audit trail; it has no effect on how a value is stored.DKNet.EfCore.DtoGenerator validates [RaisesEvent] rules at build time (RaisesEventValidator) — that the
named type-form payload was generated from the same entity, and that narrowing properties are direct existing
properties — and, for the string form, source-generates the default-shape payload record. [GenerateDto]
payload records referenced by the type-naming form of [RaisesEvent] are a DtoGenerator concept, not part of
this package.DKNet.EfCore.Extensions reads IEntity<TKey>.Id (by convention, nameof) to configure the primary key and
its value generator (including a Guid v7 generator), detects IAuditedProperties to configure the four audit
columns, and detects IConcurrencyEntity<> to configure RowVersion as a row-version concurrency token — all in
DefaultEntityTypeConfiguration<TEntity>. It also reads [Sequence] to register database sequences.DKNet.EfCore.Specifications’s IRepositorySpec is not generic over TEntity at the interface level and
does not require IEntity<TKey> or Entity<TKey>, but the DKNet convention (and the worked examples across the
docs) is to back specifications with Entity/Entity<TKey>-derived aggregates so the rest of the stack (events,
audit, concurrency) applies uniformly.IAuditedProperties, IConcurrencyEntity<>,
ISoftDeletableEntity, and IEventEntity are pure contracts — none of them stamp values, filter queries, or
dispatch anything on their own. Each needs its matching runtime package registered (Extensions for concurrency
column config, Hooks+Events/DataAuthorization for stamping and dispatch, your own query filter for soft
delete).AddEvent<TEvent>() requires an IMapper. Only the object-instance overload (AddEvent(object)) is
mapper-free; the generic overload throws EventException at dispatch time (not at call time) if no IMapper is
registered — a domain project referencing only DKNet.EfCore.Abstractions will compile fine and only fail at
runtime once DKNet.EfCore.Events tries to drain the queue.[RaisesEvent] is inert without DKNet.EfCore.Events. A project referencing Abstractions +
DtoGenerator builds and packs cleanly with rules declared, and nothing ever raises until the consuming
application also registers DKNet.EfCore.Events’ save hook.[RaisesEvent] narrowing is shallow. Only direct properties of the carrying entity qualify for Updated
narrowing; a change confined to a nested owned value never satisfies it. Narrowing a rule whose operations has
no Updated flag is accepted by the compiler but is a no-op reported as a build warning by DtoGenerator.[IgnoreEntity] currently has no consumer in this repo (see Excluding a class from automatic mapping above) — don’t treat it as a working
exclusion mechanism without confirming the specific tool you’re using reads it.SetCreatedBy/SetUpdatedBy are first-write-wins / monotonic, not “always overwrite”. SetCreatedBy is a
no-op once CreatedBy is set; SetUpdatedBy silently ignores a supplied updatedOn older than the current
UpdatedOn. Both still validate userName even when they’re about to no-op on the timestamp check.[Sequence] goes on an enum field, not an entity property. It is AttributeTargets.Field, and its numeric
options all default to -1 meaning “leave it to the database” — a StartAt/Min/Max/IncrementsBy of zero
or less is silently not applied rather than rejected.[CrudCreate]/[CrudUpdate]/[CrudAction] are inert unless the
project also references DKNet.SlimBus.Generators; this package only defines the attribute types.[SensitiveData] affects the audit log and, for hosts that opt in, API response serialization — nothing
else. It does not encrypt or otherwise protect the value at rest (that is
DKNet.EfCore.Encryption’s [Encrypted]), it does not redact it in your own ILogger output, and it does not
touch request deserialization or model binding — a caller who cannot read the property may still be able to
send it.[SensitiveData("pricing")] on its own behaves exactly
like [SensitiveData]: redacted in the audit log, returned in full by every API response. The response
filtering only exists once the host calls UseRoleAwareSensitiveData on the JsonSerializerOptions that
serializes those responses — see
DKNet.EfCore.Extensions.Microsoft.EntityFrameworkCore. This is intentional (keeps the domain layer persistence-
technology-agnostic) but means nothing in this package can validate itself against a real DbContext — mistakes
(e.g. a [Sequence] on an unsupported type) surface as an attribute-construction NotSupportedException, not an
EF Core model-building error.RowVersion concurrency tokens, [Sequence] registration. Also the
host opt-in that turns [SensitiveData] role names into response filtering. Reach for it to make the
declarations here take effect.SaveChanges pipeline the runtime packages plug into. Reach for
it when you need a custom before/after-save hook.AddEvent and raised by
[RaisesEvent]. Reach for it to actually publish them.[AuditLog], [IgnoreAuditLog], and (for
redaction) [SensitiveData]. Reach for it for a field-level change trail.[RaisesEvent] and
generation of its payload records. Reach for it when a declared event will not resolve.[Encrypted]
attribute. Reach for it to protect a value at rest; [SensitiveData] here only affects the audit trail.[CrudCreate],
[CrudUpdate] and [CrudAction]. Reach for it when you want those markers to actually produce endpoints.