Basilisque.DataAccess.EntityFramework.SqlServer 1.0.0-Preview00039

This is a prerelease version of Basilisque.DataAccess.EntityFramework.SqlServer.
dotnet add package Basilisque.DataAccess.EntityFramework.SqlServer --version 1.0.0-Preview00039
                    
NuGet\Install-Package Basilisque.DataAccess.EntityFramework.SqlServer -Version 1.0.0-Preview00039
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Basilisque.DataAccess.EntityFramework.SqlServer" Version="1.0.0-Preview00039" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Basilisque.DataAccess.EntityFramework.SqlServer" Version="1.0.0-Preview00039" />
                    
Directory.Packages.props
<PackageReference Include="Basilisque.DataAccess.EntityFramework.SqlServer" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Basilisque.DataAccess.EntityFramework.SqlServer --version 1.0.0-Preview00039
                    
#r "nuget: Basilisque.DataAccess.EntityFramework.SqlServer, 1.0.0-Preview00039"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Basilisque.DataAccess.EntityFramework.SqlServer@1.0.0-Preview00039
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Basilisque.DataAccess.EntityFramework.SqlServer&version=1.0.0-Preview00039&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Basilisque.DataAccess.EntityFramework.SqlServer&version=1.0.0-Preview00039&prerelease
                    
Install as a Cake Tool

Basilisque - Data Access Entity Framework

Overview

This project provides functionality for data access with Entity Framework Core.

NuGet Basilisque.DataAccess.EntityFramework.SqlServer
NuGet Basilisque.DataAccess.EntityFramework.SQLite
NuGet Basilisque.DataAccess.EntityFramework.PostgreSQL
License SonarCloud

Entity stamping and dependency injection

BaseDbContext<TDbContext> automatically configures and runs entity stamping. Entities implementing the interfaces in Basilisque.DataAccess.EntityFramework.Base.Stamping can receive creation and modification timestamps and user IDs.

The generated dependency-registration chain registers the scoped stamping interceptor, the timestamp handler, and a user-stamp dispatcher with closed handlers for Guid, string, int, and long. It also registers the Core user-context services through the library's dependencies; no separate Core registration is required.

For example, register the base library and its dependencies with:

Basilisque.DataAccess.EntityFramework.Base.IServiceCollectionExtensions.RegisterServices(services);

Applications normally use their own generated registration entry point, which includes the base library through the dependency chain. Calling IDependencyRegistrator.RegisterServices(services) directly only registers that assembly's services, not its dependencies.

Resolve DbContexts from a dependency-injection scope. Set the current user through IWritableUserContext<Guid> from that same scope before saving changes. When no user is available, timestamps are still applied, but user stamp values are not changed. The IDbProviderServiceProvider wrapper is transient so each context resolves the interceptor and user context from its own scope. Singleton connection-string builders only use the wrapper's configuration-section metadata.

Core provides user contexts through its open generic registrations. For the four standard key types, the EF registration additionally supplies closed, typed factories for the default Core context and its read/write proxies. They share the same scoped WritableUserContext<TKey> instance; existing closed registrations are preserved. If the open generic Core implementation has already been customized, its registration is retained instead of adding a closed default. Later customizations for standard key types should replace the corresponding closed registrations.

The dispatcher discovers the user-key types from configured CLR and shadow stamp properties in the EF model and selects handlers from IEnumerable<IUserStampHandler> in the current scope. It does not construct generic types dynamically. For example, set the current string user in the context's scope:

using Basilisque.Core.Auth;
using Microsoft.Extensions.DependencyInjection;

scope.ServiceProvider.GetRequiredService<IWritableUserContext<string>>().UserId = "user-123";

For another key type, explicitly register a closed handler under the non-generic IUserStampHandler interface after the generated registration chain:

using Basilisque.DataAccess.EntityFramework.Base.Stamping;

services.AddScoped<IUserStampHandler>(sp =>
    new UserStampHandler<MyUserId>(sp.GetRequiredService<IUserContext<MyUserId>>()));

The corresponding IUserContext<MyUserId> must also be available. Core's open generic services support it in normal .NET deployments; Native AOT deployments must provide statically reachable closed context/proxy registrations for custom key types. Registering only IUserStampHandler<MyUserId> does not add it to the dispatcher's non-generic handler collection.

Applications can replace a standard handler by adding an IUserStampHandler with the same UserKeyType after the default registrations. The last registration for each key type wins and is invoked once per save. If any configured user-key type has no handler, saving throws an explicit exception identifying the missing key type and the registration contract. Registration of custom user-context implementations, proxies, or convenience interfaces is the application's responsibility.

Design-time factories register the interceptor independently and do not require application user-context services. No stamp handlers are registered by default at design time, so explicitly supplied seed values are not overwritten by stamping. The standard handler and generic Core context/proxy factories use statically closed types. This removes dynamic generic construction from user-stamp dispatch, but does not establish Native AOT or trimming compatibility for EF model creation, queries, providers, or the complete library. Those deployments require separate publish and execution validation.

Soft delete

Soft deletion is opt-in. ISoftDelete is the default and provides DateTimeOffset? DeletedAt and Guid? DeletedBy. Use ISoftDelete<TKey> for other value-type user keys, such as int or long, and ISoftDeleteOfRef<TKey> for reference-type keys, such as string. Both variants have nullable user keys. Use ISoftDeleteTimestamp when only the deletion time is needed.

DeletedAt is the sole source of deletion state: null means active, non-null means deleted. The interfaces provide a read-only, calculated IsDeleted convenience property; no separate flag is persisted. Default interface members are accessed through the interface unless the entity also declares its own calculated property. In LINQ queries, use DeletedAt, not the calculated IsDeleted property.

using Basilisque.DataAccess.EntityFramework.Base.SoftDelete;
using Basilisque.DataAccess.EntityFramework.Base.Stamping;

public class Document : ISoftDelete, IStampChanges
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public DateTimeOffset? DeletedAt { get; set; }
    public Guid? DeletedBy { get; set; }
    public DateTimeOffset CreatedAt { get; set; }
    public Guid CreatedBy { get; set; }
    public DateTimeOffset ModifiedAt { get; set; }
    public Guid ModifiedBy { get; set; }
}

BaseDbContext<TDbContext> configures soft deletion during model finalization, after application model configuration. The existing save interceptor converts tracked Remove and RemoveRange operations into updates of the deletion time. Creation stamps and unrelated stored columns are preserved, including when deleting an attached stub containing only its key.

The deletion time is functional state and is set independently of audit stamping. The deleting user is populated by the existing stamp handlers and scoped Core user contexts. Guid, string, int, and long work without extra handler registration; other user-key types use the same explicit IUserStampHandler registration as ordinary stamping. Nullable deletion keys use the underlying key type for dispatch and user contexts: for example, Guid? DeletedBy uses UserStampHandler<Guid> and IUserContext<Guid>, not a separate nullable-key context. With modification stamps configured, soft deletion sets ModifiedAt and ModifiedBy too, using the same timestamp/user as a Remove deletion. Without a current user, the deletion still works and DeletedBy can remain null. Active rows have null deletion time and user values. Repeating a soft delete on an already deleted, loaded entity does not change its deletion audit. Setting DeletedAt directly also soft-deletes an entity and preserves the supplied time rather than replacing it with the save time.

Shadow properties are supported:

modelBuilder.Entity<DocumentWithoutInterfaces>()
    .UseShadowSoftDelete<DocumentWithoutInterfaces, Guid>();

Use UseShadowSoftDelete() for the default nullable Guid user key, UseShadowSoftDeleteOfRef<TEntity, string>() for a nullable string user key, or UseShadowSoftDeleteTimestamp() for only the deletion time. Ordinary DbContexts must call modelBuilder.ApplySoftDeleteConfigurations() after their application model configuration and install the existing UseEFCoreStamping(serviceProvider) interceptor.

Querying deleted data

The named Basilisque:SoftDelete filter excludes deleted rows from LINQ queries. Disable only this filter to keep application filters, such as tenant isolation:

var includingDeleted = context.Set<Document>()
    .IgnoreQueryFilters([SoftDeleteModelBuilderExtensions.QueryFilterName]);

Existing named filters are preserved. Anonymous application filters are retained under the reserved name Basilisque:ApplicationQueryFilter, because EF Core cannot combine anonymous and named filters. IgnoreQueryFilters() without names disables all filters, including tenant filters; use it with care.

Query filters are not an authorization boundary. Already tracked entities, Local, and cached results from Find are not hidden by a filter. EF's normal required- navigation/filter behavior also applies.

Physical deletion and restore

To physically delete an opted-in entity, explicitly allow hard deletion around the save operation:

using (SoftDeleteSuppressor.AllowHardDelete())
{
    context.Remove(document);
    await context.SaveChangesAsync();
}

The bypass supports nested scopes and flows across awaits. It does not disable query filters. StampSuppressor.Suppress() suppresses user and creation/modification stamping, not soft deletion: the deletion time is still set and the row is retained and hidden. Non-opted-in entities keep normal EF deletion behavior.

To restore an entity, load it with the soft-delete filter disabled, set DeletedAt to null, and save. DeletedBy is cleared automatically, including with stamping suppressed. Modification stamps update unless suppressed. No deletion history is retained; historical auditing is a separate concern.

Relationships and limitations

Soft deletion does not cascade to related entities. Owned data is retained with its owner. Tracked cascade deletions or foreign-key changes on other dependents are rejected before saving rather than physically deleting, soft deleting, or severing relationships implicitly. Configure tracked relationships with DeleteBehavior.ClientNoAction and handle dependents explicitly. Explicitly disabling CascadeDeleteTiming is respected; deferred cascades are validated without forcing unrelated orphan processing. Unloaded dependents are not affected by the update.

Configure inheritance on the root entity. Owned types cannot independently opt in. All writable CLR interface properties must be mapped. A calculated IsDeleted property must not be mapped; explicitly ignore it if it was previously configured. Custom user-key types may require an EF value converter. Add a migration for the new nullable persisted properties.

Soft deletion intercepts tracked SaveChanges operations only. ExecuteDelete, ExecuteUpdate, raw SQL, and external database writers bypass the interceptor and its audit logic. Do not use those APIs for audited soft deletion without implementing the equivalent updates explicitly.

Design-time contexts keep the model filter and removal conversion, including setting the deletion time for Remove. Their default interceptor has no stamp handlers, so user and creation/modification values are not automatically stamped. Explicitly supplied deletion times are preserved; active entities have their deletion user cleared. Native AOT/trimming compatibility of model configuration and the EF provider still requires separate validation.

Design-time service lifetimes

Each BaseDesignTimeDbContextFactory<TDbContext>.CreateDbContext call creates its own service provider and scope. The returned context owns both until it is disposed; subsequent calls and different factory instances do not share configuration or services. Use using or await using for contexts created manually. Use await using if their services require asynchronous disposal.

BaseDbContext<TDbContext> implements this ownership automatically. Derived contexts that override Dispose or DisposeAsync must call the corresponding base method. Contexts outside this hierarchy must implement IDbContextDesignTimeLifetimeOwner. Its SetDesignTimeServiceLifetime method accepts an IDesignTimeServiceLifetime which must be stored exactly once and detached before disposal. After disposing the context itself, dispose that lifetime in a finally block, using DisposeAsync in the asynchronous path. Detaching first prevents recursive disposal because the service scope also tracks the context. Contexts without this contract are rejected with an explicit exception; failed creation releases the newly created services.

License

The Basilisque framework (including this repository) is licensed under the Apache License, Version 2.0.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-Preview00039 31 10/5/2026
1.0.0-Preview00032 60 9/11/2026
1.0.0-Preview00027 79 7/20/2026
0.0.1-Preview00025 67 7/16/2026