English | 简体中文
Ling.Audit adds strongly typed audit properties to domain entities at compile time, maintains them through Entity Framework Core, and can resolve the current user and request information from ASP.NET Core. The core contracts remain independent of EF Core and ASP.NET Core, so applications can adopt only the layers they need.
| Package | Purpose | Target frameworks |
|---|---|---|
Ling.Audit |
Audit contracts, source generator, analyzers, and code fixes. | .NET Standard 2.0 |
Ling.Audit.EntityFrameworkCore |
Audit-property maintenance, soft deletion, and entity/field change logs. | .NET 6–10 |
Ling.Audit.AspNetCore |
User, IP address, and client information from the current HTTP request. | .NET 6–10 |
The EF Core and ASP.NET Core packages reference the matching EF Core major version for each target framework.
- Generates audit properties at compile time without reflection or runtime proxies.
- Provides focused interfaces for creation, modification, deletion, and full auditing.
- Supports classes, records, generic types, inheritance, and nested partial types.
- Reports invalid audit declarations through compiler diagnostics and code fixes.
- Automatically maintains audit fields during
SaveChanges. - Converts audited deletes into soft deletes and composes the soft-delete filter with existing query filters.
- Records entity-level and field-level changes, including composite and shadow primary keys.
- Resolves claims from ASP.NET Core Identity, JWT Bearer, OpenID Connect, and custom authentication schemes.
- Supports custom user providers, principal resolvers, serializers, anonymous handlers, and time providers.
- Offers an explicit-transaction mode when business changes and audit logs must commit atomically.
Install the core package in the project that owns the entity types:
dotnet add package Ling.Audit --version 2.1.0Implement the audit interface that matches the entity lifecycle. The type and every containing type must be partial:
using Ling.Audit;
namespace MyApplication.Domain;
public sealed partial class Post : IFullAudited<string>
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
}The source generator supplies the interface properties in the same compilation:
public sealed partial class Post
{
public string? CreatedBy { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public string? LastModifiedBy { get; set; }
public DateTimeOffset? LastModifiedAt { get; set; }
public bool IsDeleted { get; set; }
public string? DeletedBy { get; set; }
public DateTimeOffset? DeletedAt { get; set; }
}Use a basic interface for one value, or a composite interface for a complete lifecycle:
| Interface | Generated properties |
|---|---|
IHasCreator<TKey> |
CreatedBy |
IHasCreationTime |
CreatedAt |
IHasModifier<TKey> |
LastModifiedBy |
IHasModificationTime |
LastModifiedAt |
ISoftDelete |
IsDeleted |
IHasDeleter<TKey> |
DeletedBy |
IHasDeletionTime |
DeletedAt |
ICreationAudited<TKey> |
CreatedBy, CreatedAt |
IModificationAudited<TKey> |
LastModifiedBy, LastModifiedAt |
IDeletionAudited<TKey> |
IsDeleted, DeletedBy, DeletedAt |
IFullAudited<TKey> |
All creation, modification, and deletion properties |
Install the integration that matches the application's target framework:
dotnet add package Ling.Audit.EntityFrameworkCore --version 2.1.0Create a provider for the current user and client context:
using Ling.Audit.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
public sealed class ApplicationAuditUserProvider(
ICurrentDbContext currentDbContext)
: AuditContextProviderBase<Guid>(currentDbContext)
{
protected override AuditContext? GetAuditContext()
{
var currentUser = GetService<ICurrentUser>();
return currentUser?.Id is { } id
? new AuditContext(id, currentUser.Name, null, null)
: null;
}
}Register auditing while configuring the DbContext:
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(connectionString);
options.UseAudit<ApplicationAuditUserProvider, Guid>();
});Mark only entities whose changes should be written to the audit-log tables. Properties are included by default and can be excluded individually:
using Ling.Audit.EntityFrameworkCore;
[Auditable]
public sealed partial class Account : IFullAudited<Guid>
{
public Guid Id { get; set; }
public string DisplayName { get; set; } = string.Empty;
[NotAudited]
public string PasswordHash { get; set; } = string.Empty;
}The same configuration is available through the model builder:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Account>().IsAuditable();
modelBuilder.Entity<Account>()
.Property(account => account.PasswordHash)
.IsNotAudited();
modelBuilder.SetupSoftDeleteQueryFilter();
modelBuilder.ConfigureAuditLogTableNames(
"AuditEntityChanges",
"AuditFieldChanges");
}SetupSoftDeleteQueryFilter combines its predicate with an existing filter instead of replacing tenant or authorization filters. Create a normal EF Core migration after enabling auditing so the audit properties and log tables are added to the database.
The default BestEffort mode preserves the two-save behavior used by earlier releases. Business changes can succeed even if the subsequent audit-log save fails.
For atomic behavior, require an application-owned transaction:
options.UseAudit<ApplicationAuditUserProvider, Guid>(audit =>
{
audit.TransactionMode = AuditTransactionMode.RequireExplicitTransaction;
});await using var transaction =
await dbContext.Database.BeginTransactionAsync(cancellationToken);
dbContext.Add(order);
await dbContext.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);In this mode, an audited save without an active relational transaction is rejected. The business changes and generated audit rows participate in the same transaction.
An audited operation with no resolved user is rejected by default. Allow selected operations on an entity when anonymous access is intentional:
[Auditable(AllowedAnonymous = DataOperation.Create)]
public sealed partial class Registration : ICreationAudited<string>
{
}Global behavior can be changed with AuditOptions.AllowAnonymous, or customized with UseAnonymousHandler<THandler>().
For applications whose audit user ID is a string claim, install the ASP.NET Core package instead of configuring an EF user provider manually:
dotnet add package Ling.Audit.AspNetCore --version 2.1.0using Ling.Audit.AspNetCore;
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(connectionString);
options.UseAspNetCoreAudit();
});The default resolver selects the first authenticated ClaimsIdentity and resolves values in this order:
- Explicit
UserIdClaimTypeorUserNameClaimTypeoptions. - ASP.NET Core
IdentityOptions.ClaimsIdentityclaim types, when Identity is registered. ClaimsIdentity.Namefor the user name.ClaimTypes.NameIdentifier, thensub, for the user ID.ClaimTypes.Name, thenname, for the user name.
Override claim types without changing the application's authentication configuration:
options.UseAspNetCoreAudit(http =>
{
http.UserIdClaimType = "employee_id";
http.UserNameClaimType = "preferred_username";
http.MaxClientNameLength = 256;
});No HttpContext is treated as a background or non-HTTP operation and flows into the configured anonymous policy. The integration reads HttpContext.User; it does not invoke authentication again.
For multiple identities or application-specific selection rules, implement IAuditPrincipalResolver:
public sealed class ApplicationPrincipalResolver : IAuditPrincipalResolver
{
public AuditPrincipal Resolve(HttpContext httpContext)
{
var user = httpContext.User;
return new AuditPrincipal(
user.FindFirst("actor_id")?.Value,
user.Identity?.Name);
}
}options.UseAspNetCoreAudit<ApplicationPrincipalResolver>();Ling.Audit.AspNetCore deliberately uses string user IDs. Applications using Guid, int, or another key type should use Ling.Audit.EntityFrameworkCore and register a matching AuditContextProviderBase<TUserId> implementation.
UseSerializer<TSerializer>()replaces property serialization.UseAnonymousHandler<THandler>()controls rejected anonymous operations.UseAuditTimeProvider<TTimeProvider>()supplies deterministic UTC timestamps.UseAspNetCoreAudit<TPrincipalResolver>()changes HTTP principal selection.
Ling.Auditowns domain-facing contracts and compile-time tooling. It has no EF Core or ASP.NET Core dependency.Ling.Audit.EntityFrameworkCoreowns persistence behavior, soft deletion, and change-log models. It does not depend on HTTP or claims APIs.Ling.Audit.AspNetCoreadapts the current request to the EF Core user-provider contract. Principal resolution remains replaceable and does not perform authentication.
This separation keeps entity contracts reusable across web applications, workers, tests, and other hosts while making request-specific behavior opt-in.
The former Ling.EntityFrameworkCore.Audit package was renamed to Ling.Audit.EntityFrameworkCore. ASP.NET Core claim settings now belong to the dedicated ASP.NET Core integration, and anonymous audited operations are rejected by default.
See the 2.0 migration guide for package, API, transaction, and data-format changes.
The repository includes a runnable ASP.NET Core Minimal API sample in samples/. It combines Ling.Audit, Ling.Audit.EntityFrameworkCore, and Ling.Audit.AspNetCore with SQLite, and demonstrates authenticated user resolution, explicit transactions, soft deletion, and audit-log queries.
dotnet run --project samples/Ling.Audit.Sample/Ling.Audit.Sample.csprojSee samples/README.md for the request examples and production notes.
dotnet build Ling.Audit.sln -c Release
dotnet test Ling.Audit.sln -c ReleaseThe test matrix runs the EF Core and ASP.NET Core integration suites against .NET 6, 7, 8, 9, and 10. Pull requests that change behavior should include corresponding tests.