SQL Server provider for Zonit.Extensions.Databases - Entity Framework Core repository with fluent query API, ThenInclude, DTO mapping, and full-text search support.
$ dotnet add package Zonit.Extensions.Databases.SqlServerZonit.Extensions.Databases is a flexible library for building repositories and handling CRUD operations on databases.
It provides abstractions and interfaces, making it easy to manage database access and to extend your repositories with custom logic, REST API data, or other external sources.
You can use your own repositories and expand them anytime with additional functions, while still keeping your codebase clean and modular.
Install-Package Zonit.Extensions.Databases.Abstractions
Install-Package Zonit.Extensions.Databases.SqlServer
RepositoryContext<TContext>Where, Include, ThenInclude, OrderBy, Skip, Take and moreAsQuery() needed, just repo.Where(...).GetListAsync()SetPrefix("Schema", "Prefix") for automatic table configurationDatabaseContextBase (no manual call needed)CreateContextAsync() and ContextFactory for custom queriesDeleteAsync() method with automatic soft delete detectionTitle, Price, Culture, etc.// Single RepositoryContext<TContext> contains everything you need
internal class BlogRepository(RepositoryContext<DatabaseContext> context)
: SqlServerRepository<Blog, DatabaseContext>(context), IBlogRepository
{
// Custom method with direct database access
public async Task<Blog?> GetRecentAsync()
{
// Protected method for direct DbContext access
await using var context = await CreateContextAsync();
return await context.Blogs
.Where(b => b.Created > DateTime.UtcNow.AddDays(-7))
.FirstOrDefaultAsync();
}
}
public class Blog
{
public Guid Id { get; set; }
// No [NotMapped] attribute needed - use IgnoreExtensionProperties() in OnModelCreating
public UserModel? User { get; set; }
public Guid? UserId { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime Created { get; private set; } = DateTime.UtcNow;
}
internal class DatabaseContext(DbContextOptions<DatabaseContext> options)
: DatabaseContextBase<DatabaseContext>(options)
{
public DbSet<Blog> Blogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Simplified table naming - replaces manual foreach loop
// Creates tables: [Zonit].[Examples.Blog], [Zonit].[Examples.User]
modelBuilder.SetPrefix("Zonit", "Examples");
// Call base to enable:
// - Value Objects converters
// - Auto-ignore extension properties (configurable via AutoIgnoreExtensionProperties)
base.OnModelCreating(modelBuilder);
}
// Optional: Disable auto-ignoring extension properties
// protected override bool AutoIgnoreExtensionProperties => false;
}
All query methods are available directly on the repository - no AsQuery() call needed!
var blogs = await repo.Where(x => x.Title.Contains("C#")).GetListAsync();
var firstBlog = await repo.Where(x => x.Title == "Hello").GetFirstAsync();
// Single navigation property
var blog = await repo
.Include(x => x.User)
.Where(x => x.Id == blogId)
.GetAsync();
// ThenInclude for nested navigation
var blog = await repo
.Include(x => x.User)
.ThenInclude(u => u.Organization)
.Where(x => x.Id == blogId)
.GetAsync();
// Collection includes (same method, different signature)
var blog = await repo
.Include(x => x.Comments)
.ThenInclude(c => c.Author)
.GetAsync();
var blogs = await repo
.OrderByDescending(x => x.Created)
.ThenBy(x => x.Title)
.GetListAsync();
var page = await repo
.OrderBy(x => x.Created)
.Skip(20)
.Take(10)
.GetListAsync();
var count = await repo.CountAsync();
var exists = await repo.Where(x => x.Title == "Test").AnyAsync();
var blog = await repo.AddAsync(new Blog
{
Title = "Hello World",
Content = "Example content"
});
// By ID
var blog = await repo.GetByIdAsync(blogId);
// With DTO mapping
var blogDto = await repo.GetByIdAsync<BlogDto>(blogId);
// First with filter
var blog = await repo.Where(x => x.Title == "Hello").GetFirstAsync();
// All
var blogs = await repo.GetListAsync();
// By ID with action
await repo.UpdateAsync(blogId, entity => entity.Title = "New Title");
// Or pass entity
blog.Title = "New Title";
await repo.UpdateAsync(blog);
// Default: soft delete if entity implements ISoftDeletable, otherwise hard delete
await repo.DeleteAsync(blogId);
// or
await repo.DeleteAsync(blog);
// Force hard delete (permanently remove even if ISoftDeletable)
await repo.DeleteAsync(blogId, forceDelete: true);
Implement ISoftDeletable interface for automatic soft delete behavior:
// Entity implements interface with optional custom logic
public class Blog : ISoftDeletable
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Status { get; set; } = "active";
// Required by ISoftDeletable
public DateTimeOffset? DeletedAt { get; set; }
// Optional: Override to add custom logic when soft deleting
public void OnSoftDelete()
{
Status = "deleted";
// Add any additional cleanup logic here
}
}
// Usage - automatically sets DeletedAt and calls OnSoftDelete()
await repo.DeleteAsync(blogId);
// Restore (clears DeletedAt)
await repo.RestoreAsync(blogId);
Delete behavior:
| Entity Type | forceDelete: false (default) | forceDelete: true |
|---|---|---|
Implements ISoftDeletable | Soft delete (sets DeletedAt, calls OnSoftDelete()) | Hard delete (removes from DB) |
Does NOT implement ISoftDeletable | Hard delete | Hard delete |
Load data from external sources (API, other services) into your entities:
public class UserExtension : IDatabaseExtension<UserModel>
{
public async Task<UserModel> InitializeAsync(Guid userId, CancellationToken ct = default)
{
// Fetch from external API, cache, or compute
return new UserModel { Id = userId, Name = "Loaded from API" };
}
}
var blog = await repo
.Extension(x => x.User) // Load User via extension
.Where(x => x.Id == blogId)
.GetAsync();
Console.WriteLine(blog.User?.Name); // "Loaded from API"
Inherit from DatabaseContextBase<T> to automatically enable EF Core converters:
| Value Object | Database Type | Max Length | Use Case |
|---|---|---|---|
Culture | NVARCHAR(10) | 10 | Language codes (en-US, pl-PL) |
UrlSlug | NVARCHAR(200) | 200 | SEO-friendly URLs |
Title | NVARCHAR(60) | 60 | Page/content titles |
Description | NVARCHAR(160) | 160 | Meta descriptions |
Content | NVARCHAR(MAX) | - | Large text content |
Url | NVARCHAR(2048) | 2048 | URLs with validation |
Price | DECIMAL(19,8) | - | Product prices (non-negative) |
Money | DECIMAL(19,8) | - | Balances, transactions (can be negative) |
Color | NVARCHAR(100) | 100 | Colors in OKLCH format |
Asset | VARBINARY(MAX) | - | Files with metadata |
public class Article
{
public Guid Id { get; set; }
public Title Title { get; set; } // Auto-converted, max 60 chars
public Description Description { get; set; } // Auto-converted, max 160 chars
public Price Price { get; set; } // Auto-converted, decimal(19,8)
}
// Register DbContext factory
builder.Services.AddDbSqlServer<DatabaseContext>(options =>
options.UseSqlServer(connectionString));
// Register repository - IServiceProvider is optional now!
builder.Services.AddTransient<IBlogRepository, BlogRepository>();
See the Example project included in the repository.