IEntity<TKey> (if entity has a serial int ID, use IEntityWithSerial))Idpublic interface IEntity;
public interface IEntity<TKey> : IEntity
{
public TKey Id { get; set; }
}
An owner with an optional foreign key to one of its own child rows — while the child’s foreign key back is required, and therefore cascades — makes the two tables reference each other. Prefer marking the child instead: a flag or a rank column identifies the same row with no foreign key. Startup validation warns about the shape.
Where the reference has to stay, two things need handling:
DeleteBehavior.ClientSetNull — NO ACTION
in the database, EF nulls the reference on the tracked owner. SQLite does not enforce this.UPDATE of its own.SaveChangesBreakingDeleteCycles / SaveChangesBreakingDeleteCyclesAsync (Regira.Entities.EFcore.Extensions)
do that. Call them from the context’s own overrides — both of them, or synchronous callers stay broken:
public override int SaveChanges(bool acceptAllChangesOnSuccess)
=> this.SaveChangesBreakingDeleteCycles(base.SaveChanges, acceptAllChangesOnSuccess);
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken token = default)
=> this.SaveChangesBreakingDeleteCyclesAsync(base.SaveChangesAsync, acceptAllChangesOnSuccess, token);
Pass acceptAllChangesOnSuccess to the extension and base.SaveChanges itself as the delegate. The extension
nulls the optional side with a direct UPDATE, tells the change tracker the database no longer holds the
reference, and then runs the save exactly once with your flag, all inside one transaction. Nothing is accepted
before that save returns: a save the database rejects leaves every change pending, and the retry (EF’s own or
yours) drops the reference again. SaveChanges(false) + AcceptAllChanges() therefore behaves exactly as it
does without the extension, and the count returned is the save’s own — the UPDATE is not counted.
A save without such a pair is a single round trip and opens no transaction. The change tracker is read only
when the model has two entity types referencing each other and the provider is relational; any other context
pays a cached lookup and nothing else, and the in-memory provider, which orders no deletes, saves the pair on
its own. Already inside a transaction of your own — or a TransactionScope — the extension joins it rather
than opening a second one, which is what lets the pattern work under EnableRetryOnFailure() inside EF’s own
CreateExecutionStrategy().Execute(...) recipe. A bare BeginTransaction() under a retrying strategy is
refused by EF’s own SaveChanges, extension or not.
Use SearchObject for filtering entities.
SearchObject<TKey> class (or SearchObject when TKey is of type int)ICollection<TKey> when filtering on key-properties for flexibilityMinCreated, MaxCreated, MinLastModified, MaxLastModified) are interpreted as UTC; local kinds are converted, unspecified kinds are assumed UTCpublic record SearchObject : SearchObject<int>;
public record SearchObject<TKey> : ISearchObject<TKey>
{
public TKey? Id { get; set; }
public ICollection<TKey>? Ids { get; set; }
public ICollection<TKey>? Exclude { get; set; }
public string? Q { get; set; }
public DateTime? MinCreated { get; set; }
public DateTime? MaxCreated { get; set; }
public DateTime? MinLastModified { get; set; }
public DateTime? MaxLastModified { get; set; }
public ArchivedFilter? Archived { get; set; }
}
Archived Property (soft delete):
ArchivedFilter — Excluded (archived rows invisible), Included (live + archived), Only (archived only)?archived=; null falls back to DefaultArchivedFilter configured on UseEntities()IArchivable filter only — other global filters (tenant/owner scoping) keep applyingQ Property (General Text Search):
Q property serves as a general text search fieldIHasTitle or IHasDescriptionQKeywordHelper for wildcard support (*) in search queriesEntitySortBy is used.ISortedQueryBuilder implementationspublic enum EntitySortBy
{
Default,
Id,
IdDesc,
Created,
CreatedDesc,
LastModified,
LastModifiedDesc,
}
EntityIncludes is used.[Flags]
public enum MyEntityIncludes
{
Default = 0,
// Add custom options here
Option1 = 1 << 0,
Option2 = 1 << 1,
All = Option1 | Option2
}