EfScimRepositoryBase Reference¶
EfScimRepositoryBase<TUser, TGroup, TContext> is an abstract EF Core base repository
that implements IUserGroupDataRepository<TUser, TGroup> with zero boilerplate.
Namespace: EzSCIM.EfCore
Package: EzSCIM.EfCore
Signature¶
public abstract class EfScimRepositoryBase<TUser, TGroup, TContext>
: IUserGroupDataRepository<TUser, TGroup>
where TUser : class, IScimEntity
where TGroup : class, IScimEntity
where TContext : DbContext
Generic constraints¶
| Parameter | Constraint | Description |
|---|---|---|
TUser |
class, IScimEntity |
Your EF user entity |
TGroup |
class, IScimEntity |
Your EF group entity |
TContext |
DbContext |
Your EF DbContext subclass |
Minimum implementation¶
using EzSCIM.EfCore;
using Microsoft.EntityFrameworkCore;
public class AppUserGroupRepository
: EfScimRepositoryBase<AppUser, AppGroup, AppDbContext>
{
public AppUserGroupRepository(AppDbContext context) : base(context) { }
protected override DbSet<AppUser> Users => Context.Users;
protected override DbSet<AppGroup> Groups => Context.Groups;
}
That's all you need
Two property overrides. All CRUD operations, Id generation, timestamp management, and unique constraint handling are inherited from the base class.
Provided methods¶
User operations¶
| Method | Behavior |
|---|---|
GetUserAsync(string id) |
Users.FindAsync(id) |
QueryUsers() |
Users.AsQueryable() — used by filter translator |
CreateUserAsync(TUser) |
Generates GUID if Id is empty, sets CreatedAt/ModifiedAt, calls SaveChangesAsync |
UpdateUserAsync(string id, TUser) |
Loads existing, calls OnBeforeUpdateUserAsync, saves |
DeleteUserAsync(string id) |
Loads and removes, returns false if not found |
Group operations¶
| Method | Behavior |
|---|---|
GetGroupAsync(string id) |
Groups.FindAsync(id) |
QueryGroups() |
Groups.AsQueryable() |
CreateGroupAsync(TGroup) |
Same as CreateUser — Guid, timestamps, save |
UpdateGroupAsync(string id, TGroup) |
Loads existing, calls OnBeforeUpdateGroupAsync, saves |
DeleteGroupAsync(string id) |
Loads and removes, returns false if not found |
Extension hooks¶
Override these methods when your entity has JSON columns or navigation properties that require manual handling during updates.
OnBeforeUpdateUserAsync¶
Called by UpdateUserAsync before SaveChangesAsync. Default behavior:
copies all scalar columns via CurrentValues.SetValues(updated).
protected override Task OnBeforeUpdateUserAsync(AppUser existing, AppUser updated)
{
// Default: copies all scalar columns
Context.Entry(existing).CurrentValues.SetValues(updated);
// Example: handle JSON column manually // (1)
existing.EmailsJson = updated.EmailsJson;
existing.PhoneNumbersJson = updated.PhoneNumbersJson;
return Task.CompletedTask;
}
- JSON columns are not copied by
SetValues— they must be assigned explicitly.
OnBeforeUpdateGroupAsync¶
Same pattern for groups.
protected override Task OnBeforeUpdateGroupAsync(AppGroup existing, AppGroup updated)
{
Context.Entry(existing).CurrentValues.SetValues(updated);
existing.MembersJson = updated.MembersJson;
return Task.CompletedTask;
}
Unique constraint handling¶
CreateUserAsync catches DbUpdateException and wraps unique-key violations
as InvalidOperationException. This is translated to 409 Conflict by the SCIM
controller layer.
Supported databases:
| Database | Detection method |
|---|---|
| SQL Server | Error codes 2601 and 2627 |
| PostgreSQL | SqlState 23505 |
| SQLite | UNIQUE constraint failed message |
Automatic — no action needed
The 409 Conflict response is handled automatically. You don't need to catch
DbUpdateException in your code.
Protected field¶
/// <summary>The underlying DbContext instance. Available to all overrides.</summary>
protected readonly TContext Context;
Full example with JSON columns¶
DemoUserGroupRepository with JSON column overrides
public class DemoUserGroupRepository
: EfScimRepositoryBase<DemoUserEntity, DemoGroupEntity, AppDbContext>
{
public DemoUserGroupRepository(AppDbContext ctx) : base(ctx) { }
protected override DbSet<DemoUserEntity> Users => Context.Users;
protected override DbSet<DemoGroupEntity> Groups => Context.Groups;
protected override Task OnBeforeUpdateUserAsync(
DemoUserEntity existing, DemoUserEntity updated)
{
// Use CurrentValues for scalars, but handle JSON columns explicitly
Context.Entry(existing).CurrentValues.SetValues(updated);
existing.EmailsJson = updated.EmailsJson;
existing.PhoneNumbersJson = updated.PhoneNumbersJson;
existing.AddressesJson = updated.AddressesJson;
return Task.CompletedTask;
}
protected override Task OnBeforeUpdateGroupAsync(
DemoGroupEntity existing, DemoGroupEntity updated)
{
Context.Entry(existing).CurrentValues.SetValues(updated);
existing.MembersJson = updated.MembersJson;
return Task.CompletedTask;
}
}
Next: Multi-provider: SQL Server / PostgreSQL →
Back: IScimEntity →