Skip to content

Commit 5c3926e

Browse files
committed
Initial EfCoreKit.Core infrastructure: soft delete, audit, DI
Introduce modular core with soft delete, audit, multi-tenancy, and bulk ops support. Add EfCoreKitDbContext<TContext>, fluent options, DI registration, and extension methods for DbSet/IQueryable. Stub out interceptors, filters, and provider-specific bulk executors. Add internal helpers and global usings. Most logic is scaffolded with TODOs for future implementation.
1 parent 09d8efd commit 5c3926e

30 files changed

Lines changed: 1285 additions & 3 deletions

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
1212
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.0" />
1313
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
14-
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
14+
<PackageVersion Include="MySql.EntityFrameworkCore" Version="10.0.1" />
1515
</ItemGroup>
1616

1717
<!-- DI -->
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,89 @@
1+
using EfCoreKit.Abstractions.Interfaces;
2+
using Microsoft.EntityFrameworkCore;
13

4+
namespace EfCoreKit.Core.Context;
5+
6+
/// <summary>
7+
/// Base <see cref="DbContext"/> that provides automatic soft delete, audit trail,
8+
/// multi-tenancy, and bulk operation support.
9+
/// </summary>
10+
/// <typeparam name="TContext">The derived context type.</typeparam>
11+
public abstract class EfCoreKitDbContext<TContext> : DbContext
12+
where TContext : DbContext
13+
{
14+
private readonly EfCoreKitOptions _options;
15+
private readonly IUserProvider? _userProvider;
16+
private readonly ITenantProvider? _tenantProvider;
17+
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="EfCoreKitDbContext{TContext}"/> class.
20+
/// </summary>
21+
/// <param name="options">The EF Core context options.</param>
22+
/// <param name="kitOptions">The EfCoreKit configuration options.</param>
23+
protected EfCoreKitDbContext(
24+
DbContextOptions<TContext> options,
25+
EfCoreKitOptions kitOptions)
26+
: base(options)
27+
{
28+
_options = kitOptions ?? throw new ArgumentNullException(nameof(kitOptions));
29+
}
30+
31+
/// <summary>
32+
/// Initializes a new instance of the <see cref="EfCoreKitDbContext{TContext}"/> class
33+
/// with user and tenant provider support.
34+
/// </summary>
35+
/// <param name="options">The EF Core context options.</param>
36+
/// <param name="kitOptions">The EfCoreKit configuration options.</param>
37+
/// <param name="userProvider">The user provider for audit trail.</param>
38+
/// <param name="tenantProvider">The tenant provider for multi-tenancy.</param>
39+
protected EfCoreKitDbContext(
40+
DbContextOptions<TContext> options,
41+
EfCoreKitOptions kitOptions,
42+
IUserProvider? userProvider,
43+
ITenantProvider? tenantProvider)
44+
: base(options)
45+
{
46+
_options = kitOptions ?? throw new ArgumentNullException(nameof(kitOptions));
47+
_userProvider = userProvider;
48+
_tenantProvider = tenantProvider;
49+
}
50+
51+
/// <summary>
52+
/// Gets the current EfCoreKit configuration options.
53+
/// </summary>
54+
protected EfCoreKitOptions Options => _options;
55+
56+
/// <inheritdoc />
57+
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
58+
{
59+
OnBeforeSaveChanges();
60+
return await base.SaveChangesAsync(cancellationToken);
61+
}
62+
63+
/// <inheritdoc />
64+
protected override void OnModelCreating(ModelBuilder modelBuilder)
65+
{
66+
base.OnModelCreating(modelBuilder);
67+
ConfigureGlobalFilters(modelBuilder);
68+
}
69+
70+
/// <summary>
71+
/// Hook called before changes are saved. Handles audit trail, soft delete, and tenant assignment.
72+
/// </summary>
73+
private void OnBeforeSaveChanges()
74+
{
75+
// TODO: Implement audit trail (HandleAuditableEntities)
76+
// TODO: Implement soft delete interception (HandleSoftDeletableEntities)
77+
// TODO: Implement tenant assignment (HandleTenantEntities)
78+
}
79+
80+
/// <summary>
81+
/// Applies global query filters for soft delete and multi-tenancy.
82+
/// </summary>
83+
/// <param name="modelBuilder">The model builder.</param>
84+
private void ConfigureGlobalFilters(ModelBuilder modelBuilder)
85+
{
86+
// TODO: Apply ISoftDeletable global filter
87+
// TODO: Apply ITenantEntity global filter
88+
}
89+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,117 @@
1+
using EfCoreKit.Abstractions.Interfaces;
12

3+
namespace EfCoreKit.Core.Context;
4+
5+
/// <summary>
6+
/// Configuration options for EfCoreKit features.
7+
/// Use the fluent methods to enable features before passing to <see cref="ServiceCollectionExtensions"/>.
8+
/// </summary>
9+
public sealed class EfCoreKitOptions
10+
{
11+
/// <summary>
12+
/// Gets a value indicating whether soft delete is enabled for entities implementing <see cref="ISoftDeletable"/>.
13+
/// </summary>
14+
public bool SoftDeleteEnabled { get; private set; }
15+
16+
/// <summary>
17+
/// Gets a value indicating whether cascade soft delete is enabled.
18+
/// </summary>
19+
public bool CascadeSoftDeleteEnabled { get; private set; }
20+
21+
/// <summary>
22+
/// Gets a value indicating whether audit trail is enabled for entities implementing <see cref="IAuditable"/>.
23+
/// </summary>
24+
public bool AuditTrailEnabled { get; private set; }
25+
26+
/// <summary>
27+
/// Gets a value indicating whether full audit logging to a dedicated table is enabled.
28+
/// </summary>
29+
public bool FullAuditLogEnabled { get; private set; }
30+
31+
/// <summary>
32+
/// Gets a value indicating whether multi-tenancy is enabled for entities implementing <see cref="ITenantEntity"/>.
33+
/// </summary>
34+
public bool MultiTenancyEnabled { get; private set; }
35+
36+
/// <summary>
37+
/// Gets the type implementing <see cref="IUserProvider"/>.
38+
/// </summary>
39+
public Type? UserProviderType { get; private set; }
40+
41+
/// <summary>
42+
/// Gets the type implementing <see cref="ITenantProvider"/>.
43+
/// </summary>
44+
public Type? TenantProviderType { get; private set; }
45+
46+
/// <summary>
47+
/// Gets the threshold for logging slow queries. <c>null</c> disables slow query logging.
48+
/// </summary>
49+
public TimeSpan? SlowQueryThreshold { get; private set; }
50+
51+
/// <summary>
52+
/// Enables soft delete for entities implementing <see cref="ISoftDeletable"/>.
53+
/// </summary>
54+
/// <param name="cascade">Whether to enable cascade soft delete for related entities.</param>
55+
/// <returns>This instance for chaining.</returns>
56+
public EfCoreKitOptions EnableSoftDelete(bool cascade = false)
57+
{
58+
SoftDeleteEnabled = true;
59+
CascadeSoftDeleteEnabled = cascade;
60+
return this;
61+
}
62+
63+
/// <summary>
64+
/// Enables audit trail for entities implementing <see cref="IAuditable"/>.
65+
/// </summary>
66+
/// <param name="fullLog">Whether to enable full change logging to a dedicated audit table.</param>
67+
/// <returns>This instance for chaining.</returns>
68+
public EfCoreKitOptions EnableAuditTrail(bool fullLog = false)
69+
{
70+
AuditTrailEnabled = true;
71+
FullAuditLogEnabled = fullLog;
72+
return this;
73+
}
74+
75+
/// <summary>
76+
/// Enables multi-tenancy for entities implementing <see cref="ITenantEntity"/>.
77+
/// </summary>
78+
/// <returns>This instance for chaining.</returns>
79+
public EfCoreKitOptions EnableMultiTenancy()
80+
{
81+
MultiTenancyEnabled = true;
82+
return this;
83+
}
84+
85+
/// <summary>
86+
/// Registers the <see cref="IUserProvider"/> implementation to use for audit trail.
87+
/// </summary>
88+
/// <typeparam name="T">The concrete type implementing <see cref="IUserProvider"/>.</typeparam>
89+
/// <returns>This instance for chaining.</returns>
90+
public EfCoreKitOptions UseUserProvider<T>() where T : class, IUserProvider
91+
{
92+
UserProviderType = typeof(T);
93+
return this;
94+
}
95+
96+
/// <summary>
97+
/// Registers the <see cref="ITenantProvider"/> implementation to use for multi-tenancy.
98+
/// </summary>
99+
/// <typeparam name="T">The concrete type implementing <see cref="ITenantProvider"/>.</typeparam>
100+
/// <returns>This instance for chaining.</returns>
101+
public EfCoreKitOptions UseTenantProvider<T>() where T : class, ITenantProvider
102+
{
103+
TenantProviderType = typeof(T);
104+
return this;
105+
}
106+
107+
/// <summary>
108+
/// Enables slow query logging for queries exceeding the given threshold.
109+
/// </summary>
110+
/// <param name="threshold">The duration threshold.</param>
111+
/// <returns>This instance for chaining.</returns>
112+
public EfCoreKitOptions LogSlowQueries(TimeSpan threshold)
113+
{
114+
SlowQueryThreshold = threshold;
115+
return this;
116+
}
117+
}

src/EfCoreKit.Core/EfCoreKit.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="Microsoft.EntityFrameworkCore" />
14+
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
1415
</ItemGroup>
1516

1617
</Project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,77 @@
1+
using EfCoreKit.Abstractions.Exceptions;
2+
using Microsoft.EntityFrameworkCore;
13

4+
namespace EfCoreKit.Core.Extensions;
5+
6+
/// <summary>
7+
/// Extension methods for <see cref="DbSet{T}"/> providing common query shortcuts.
8+
/// </summary>
9+
public static class DbSetExtensions
10+
{
11+
/// <summary>
12+
/// Gets an entity by its primary key.
13+
/// </summary>
14+
/// <typeparam name="T">The entity type.</typeparam>
15+
/// <param name="dbSet">The DbSet.</param>
16+
/// <param name="id">The primary key value.</param>
17+
/// <param name="cancellationToken">A token to cancel the operation.</param>
18+
/// <returns>The entity if found; otherwise, <c>null</c>.</returns>
19+
public static async Task<T?> GetByIdAsync<T>(
20+
this DbSet<T> dbSet,
21+
object id,
22+
CancellationToken cancellationToken = default) where T : class
23+
{
24+
return await dbSet.FindAsync([id], cancellationToken);
25+
}
26+
27+
/// <summary>
28+
/// Gets an entity by its primary key or throws <see cref="EntityNotFoundException"/>.
29+
/// </summary>
30+
/// <typeparam name="T">The entity type.</typeparam>
31+
/// <param name="dbSet">The DbSet.</param>
32+
/// <param name="id">The primary key value.</param>
33+
/// <param name="cancellationToken">A token to cancel the operation.</param>
34+
/// <returns>The entity.</returns>
35+
/// <exception cref="EntityNotFoundException">Thrown when the entity is not found.</exception>
36+
public static async Task<T> GetByIdOrThrowAsync<T>(
37+
this DbSet<T> dbSet,
38+
object id,
39+
CancellationToken cancellationToken = default) where T : class
40+
{
41+
var entity = await dbSet.FindAsync([id], cancellationToken);
42+
return entity ?? throw new EntityNotFoundException(typeof(T).Name, id);
43+
}
44+
45+
/// <summary>
46+
/// Checks whether an entity with the given primary key exists.
47+
/// </summary>
48+
/// <typeparam name="T">The entity type.</typeparam>
49+
/// <param name="dbSet">The DbSet.</param>
50+
/// <param name="id">The primary key value.</param>
51+
/// <param name="cancellationToken">A token to cancel the operation.</param>
52+
/// <returns><c>true</c> if the entity exists; otherwise, <c>false</c>.</returns>
53+
public static async Task<bool> ExistsAsync<T>(
54+
this DbSet<T> dbSet,
55+
object id,
56+
CancellationToken cancellationToken = default) where T : class
57+
{
58+
var entity = await dbSet.FindAsync([id], cancellationToken);
59+
return entity is not null;
60+
}
61+
62+
/// <summary>
63+
/// Checks whether any entity matches the given predicate.
64+
/// </summary>
65+
/// <typeparam name="T">The entity type.</typeparam>
66+
/// <param name="dbSet">The DbSet.</param>
67+
/// <param name="predicate">The filter predicate.</param>
68+
/// <param name="cancellationToken">A token to cancel the operation.</param>
69+
/// <returns><c>true</c> if any entity matches; otherwise, <c>false</c>.</returns>
70+
public static async Task<bool> ExistsAsync<T>(
71+
this DbSet<T> dbSet,
72+
System.Linq.Expressions.Expression<Func<T, bool>> predicate,
73+
CancellationToken cancellationToken = default) where T : class
74+
{
75+
return await dbSet.AnyAsync(predicate, cancellationToken);
76+
}
77+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,45 @@
1+
using EfCoreKit.Abstractions.Interfaces;
2+
using Microsoft.EntityFrameworkCore;
13

4+
namespace EfCoreKit.Core.Extensions;
5+
6+
/// <summary>
7+
/// Extension methods for <see cref="ModelBuilder"/> to configure EfCoreKit conventions.
8+
/// </summary>
9+
public static class ModelBuilderExtensions
10+
{
11+
/// <summary>
12+
/// Configures the concurrency token for all entities implementing <see cref="IConcurrencyAware"/>.
13+
/// </summary>
14+
/// <param name="modelBuilder">The model builder.</param>
15+
/// <returns>The model builder for chaining.</returns>
16+
public static ModelBuilder ApplyConcurrencyTokens(this ModelBuilder modelBuilder)
17+
{
18+
// TODO: Iterate entity types implementing IConcurrencyAware
19+
// - Configure RowVersion as a concurrency token / row version
20+
return modelBuilder;
21+
}
22+
23+
/// <summary>
24+
/// Configures soft delete global query filters for all entities implementing <see cref="ISoftDeletable"/>.
25+
/// </summary>
26+
/// <param name="modelBuilder">The model builder.</param>
27+
/// <returns>The model builder for chaining.</returns>
28+
public static ModelBuilder ApplySoftDeleteFilters(this ModelBuilder modelBuilder)
29+
{
30+
// TODO: Delegate to SoftDeleteQueryFilter.Apply
31+
return modelBuilder;
32+
}
33+
34+
/// <summary>
35+
/// Configures tenant global query filters for all entities implementing <see cref="ITenantEntity"/>.
36+
/// </summary>
37+
/// <param name="modelBuilder">The model builder.</param>
38+
/// <param name="tenantProvider">The tenant provider.</param>
39+
/// <returns>The model builder for chaining.</returns>
40+
public static ModelBuilder ApplyTenantFilters(this ModelBuilder modelBuilder, ITenantProvider? tenantProvider)
41+
{
42+
// TODO: Delegate to TenantQueryFilter.Apply
43+
return modelBuilder;
44+
}
45+
}

0 commit comments

Comments
 (0)