Azka 10.0.0-alpha.8
See the version list below for details.
dotnet add package Azka --version 10.0.0-alpha.8
NuGet\Install-Package Azka -Version 10.0.0-alpha.8
<PackageReference Include="Azka" Version="10.0.0-alpha.8" />
<PackageVersion Include="Azka" Version="10.0.0-alpha.8" />
<PackageReference Include="Azka" />
paket add Azka --version 10.0.0-alpha.8
#r "nuget: Azka, 10.0.0-alpha.8"
#:package Azka@10.0.0-alpha.8
#addin nuget:?package=Azka&version=10.0.0-alpha.8&prerelease
#tool nuget:?package=Azka&version=10.0.0-alpha.8&prerelease
Azka Framework Core
A lightweight core package for building data access layers in .NET using Repository, Unit of Work, Specification, metadata mapping, and SQL query helper patterns.
Azka is the core building block. It defines contracts and reusable infrastructure, but it does not ship a concrete database provider or ready-to-use repository implementation.
What This Package Provides
- Repository Contracts -
IRepository<T>andIViewRepository<T>abstractions for CRUD and read queries - Unit of Work Contracts -
IUnitOfWork,BaseUnitOfWork, and transaction scope delegation throughIDatabaseContext - Specification Pattern - Composable query descriptions with a fluent API
- Object Mapping - Reflection metadata and DynamicMethod-based mapping from
IDataReaderor dynamic rows - Relationship Metadata - One-to-many and many-to-one metadata through attributes
- SQL Query Helpers - Helpers for SELECT, JOIN, WHERE, ordering, paging, and specification-driven query generation
- Dependency Injection Helper - Assembly scanning for application-defined repository implementations
Applications are expected to provide concrete IDatabaseContext and repository implementations, or use a separate package that supplies them.
What This Package Does Not Provide
- A concrete database provider implementation
- A built-in connection string or database configuration model
- A ready-to-use concrete repository class
- Automatic application startup registration beyond repository assembly scanning
Installation
dotnet add package Azka
This installs the core Azka package. You still need to register your concrete database context and repository implementations in your application.
Core Concepts
Entity Mapping
using Azka.Common.Annotations;
[Table("Users")]
public class User
{
[Key]
[Column("id")]
public int Id { get; set; }
[Column("username")]
public required string Username { get; set; }
[Column("email")]
public required string Email { get; set; }
// One-to-many relationship
[WithMany("user_id")]
public ICollection<Post> Posts { get; set; } = new List<Post>();
}
[Table("Posts")]
public class Post
{
[Key]
[Column("id")]
public int Id { get; set; }
[Column("title")]
public required string Title { get; set; }
[Column("user_id")]
public int UserId { get; set; }
// Many-to-one relationship
[WithOne("user_id")]
public User? User { get; set; }
}
Specification
using Azka.Common.Abstractions.Specifications;
public class ActiveUserPostsSpecification : Specification<User>
{
public ActiveUserPostsSpecification(int userId)
{
Query.Where(u => u.Id == userId);
Query.Include(u => u.Posts);
Query.OrderByDesc(u => u.Id);
}
}
Application Unit of Work Contract
public interface IAppUnitOfWork : IUnitOfWork
{
IRepository<User> Users { get; }
IRepository<Post> Posts { get; }
}
Application Unit of Work Implementation
Your application defines the concrete Unit of Work and exposes the repositories it needs. BaseUnitOfWork resolves those repositories from the application's dependency injection container.
public sealed class AppUnitOfWork : BaseUnitOfWork, IAppUnitOfWork
{
public AppUnitOfWork(IServiceProvider serviceProvider, IDatabaseContext databaseContext)
: base(serviceProvider, databaseContext)
{
}
public IRepository<User> Users => GetRepository<IRepository<User>>();
public IRepository<Post> Posts => GetRepository<IRepository<Post>>();
}
Repository Usage
Services should depend on the application Unit of Work rather than directly constructing repositories.
public class UserService
{
private readonly IAppUnitOfWork _unitOfWork;
public UserService(IAppUnitOfWork unitOfWork) => _unitOfWork = unitOfWork;
public async Task<User?> GetUserWithPostsAsync(int userId)
{
var spec = new ActiveUserPostsSpecification(userId);
return await _unitOfWork.Users.SingleOrDefaultAsync(spec);
}
}
Repository API
Azka core defines repository contracts only. Concrete packages, such as Azka.PostgreSQL, or your own infrastructure project decide how the operations are executed against a database.
Repository Contract Types
| Type | Purpose |
|---|---|
IBaseRepository |
Marker base contract for repository types that can be resolved by BaseUnitOfWork and scanned by dependency injection helpers. |
IViewRepository<T> |
Read-only repository contract for specification-based queries. |
IRepository<T> |
Full repository contract that extends IViewRepository<T> with create, update, delete, and primary-key lookup operations. |
Use IViewRepository<T> when a module should only expose reads. Use IRepository<T> when the module owns writes for the entity.
IViewRepository<T>
IViewRepository<T> uses ISpecification<T> for every read operation, so filtering, includes, ordering, and paging can be described outside the repository implementation.
| Method | Returns | Description |
|---|---|---|
ListAsync(specification, cancellationToken) |
Task<IEnumerable<T>> |
Returns all rows that match the specification. |
FirstOrDefaultAsync(specification, cancellationToken) |
Task<T?> |
Returns the first matching row, or null when no row matches. |
FirstAsync(specification, cancellationToken) |
Task<T> |
Returns the first matching row. The provider decides the exception behavior when no row exists. |
SingleOrDefaultAsync(specification, cancellationToken) |
Task<T?> |
Returns one matching row, or null when no row matches. The provider decides the exception behavior when more than one row exists. |
SingleAsync(specification, cancellationToken) |
Task<T> |
Returns exactly one matching row. The provider decides the exception behavior when zero or multiple rows exist. |
Example read-only repository contract:
public interface IUserReadRepository : IViewRepository<User>
{
}
IRepository<T>
IRepository<T> extends IViewRepository<T> for write-capable repositories.
| Method | Returns | Description |
|---|---|---|
AddAsync(model, cancellationToken) |
Task<T> |
Persists a new entity and returns the entity returned by the provider. |
UpdateAsync(model, cancellationToken) |
Task<int> |
Persists changes to an existing entity and returns the affected row count. |
DeleteAsync(key, cancellationToken) |
Task<int> |
Deletes an entity by primary key and returns the affected row count. Physical vs soft delete depends on the concrete repository or provider. |
GetByIdAsync(key, cancellationToken) |
Task<T> |
Loads one entity by primary key. The provider decides the exception behavior when no row exists. |
Example write-capable repository contract:
public interface IUserRepository : IRepository<User>
{
}
Application-specific repositories can add domain methods, but common queries should usually be represented as specifications so they remain composable and reusable.
Transaction Usage
public class UserService
{
private readonly IAppUnitOfWork _unitOfWork;
public UserService(IAppUnitOfWork unitOfWork) => _unitOfWork = unitOfWork;
public async Task CreateUserWithPostAsync(User user, Post post)
{
await using var transaction = await _unitOfWork.BeginTransactionScopeAsync();
user = await _unitOfWork.Users.AddAsync(user);
post.UserId = user.Id;
await _unitOfWork.Posts.AddAsync(post);
await transaction.CommitAsync();
}
}
The concrete behavior of BeginTransactionScopeAsync depends on your IDatabaseContext implementation.
Unit of Work API
The Unit of Work pattern coordinates repositories and delegates transaction management to the configured IDatabaseContext.
public interface IUnitOfWork : IDisposable
{
Task<IDatabaseTransaction> BeginTransactionScopeAsync(CancellationToken cancellationToken = default);
}
BaseUnitOfWork is the reusable base implementation for application Unit of Work classes.
| Member | Description |
|---|---|
BaseUnitOfWork(IServiceProvider serviceProvider, IDatabaseContext databaseContext) |
Stores the application service provider and database context. Passing null throws ArgumentNullException. |
BeginTransactionScopeAsync(cancellationToken) |
Delegates to IDatabaseContext.BeginTransactionScopeAsync(...). |
GetRepository<T>() |
Resolves a repository from dependency injection and caches it for the lifetime of the Unit of Work instance. T must implement IBaseRepository. |
Dispose() |
Suppresses finalization. The current core implementation does not dispose the injected database context directly; context lifetime is owned by dependency injection. |
Expose repositories through properties on your application Unit of Work:
public interface IAppUnitOfWork : IUnitOfWork
{
IUserRepository Users { get; }
IPostRepository Posts { get; }
}
public sealed class AppUnitOfWork : BaseUnitOfWork, IAppUnitOfWork
{
public AppUnitOfWork(IServiceProvider serviceProvider, IDatabaseContext databaseContext)
: base(serviceProvider, databaseContext)
{
}
public IUserRepository Users => GetRepository<IUserRepository>();
public IPostRepository Posts => GetRepository<IPostRepository>();
}
Because GetRepository<T>() caches resolved repositories, repeated access to _unitOfWork.Users returns the same repository instance for that Unit of Work object.
Transaction Scope Behavior
BeginTransactionScopeAsync(...) returns an IDatabaseTransaction.
public interface IDatabaseTransaction : IAsyncDisposable
{
TransactionToken Token { get; }
bool IsCommitted { get; }
Task CommitAsync(CancellationToken cancellationToken = default);
}
Use await using so the transaction can roll back automatically when the scope exits without a commit.
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
await using var transaction =
await _unitOfWork.BeginTransactionScopeAsync(cancellationToken);
await _unitOfWork.Users.AddAsync(user, cancellationToken);
await _unitOfWork.Posts.AddAsync(post, cancellationToken);
await transaction.CommitAsync(cancellationToken);
}
DatabaseTransaction calls the provider's CommitTransactionAsync(...) on CommitAsync(...). If the transaction is disposed before it is committed, it calls RollbackIfNotCommittedAsync(...).
| Member | Description |
|---|---|
Token |
Transaction identity generated by the database context. It contains an ID, creation time, and transaction depth. |
IsCommitted |
true after CommitAsync(...) completes successfully. |
CommitAsync(cancellationToken) |
Commits the transaction once. Calling it again is a no-op. Calling it after dispose throws ObjectDisposedException. |
DisposeAsync() |
Rolls back through the provider when the transaction has not been committed. |
Support for nested transactions, savepoints, shared connections, and rollback depth is provider-specific. Azka core only defines the contract and the transaction wrapper.
Database Context API
IDatabaseContext is the low-level database boundary used by repositories and Unit of Work. Azka core defines the contract; a provider package supplies the implementation.
public interface IDatabaseContext : IDisposable, IAsyncDisposable
{
Task<T> AddAsync<T>(T entity, CancellationToken cancellationToken = default) where T : class;
Task<int> UpdateAsync<T>(T entity, CancellationToken cancellationToken = default) where T : class;
Task<int> DeleteAsync<T>(T entity, CancellationToken cancellationToken = default) where T : class;
Task<int> DeleteAsync<T>(object id, CancellationToken cancellationToken = default) where T : class;
Task<T> GetAsync<T>(object id, CancellationToken cancellationToken = default) where T : class;
Task<IEnumerable<T>> ListAsync<T>(ISpecification<T> specification, CancellationToken cancellationToken = default) where T : class;
Task<long> CountAsync<T>(ISpecification<T> specification, CancellationToken cancellationToken = default) where T : class;
Task<IDatabaseTransaction> BeginTransactionScopeAsync(CancellationToken cancellationToken = default);
}
The actual interface also exposes FirstAsync, SingleAsync, raw SQL helpers, and connection helpers.
Entity Operations
| Method | Returns | Description |
|---|---|---|
AddAsync<T>(entity, cancellationToken) |
Task<T> |
Inserts a new entity and returns the provider result. |
UpdateAsync<T>(entity, cancellationToken) |
Task<int> |
Updates an entity and returns the affected row count. |
DeleteAsync<T>(entity, cancellationToken) |
Task<int> |
Deletes using the entity instance. |
DeleteAsync<T>(id, cancellationToken) |
Task<int> |
Deletes by primary key. |
GetAsync<T>(id, cancellationToken) |
Task<T> |
Loads one entity by primary key. |
Specification Query Operations
| Method | Returns | Description |
|---|---|---|
ListAsync<T>(specification, cancellationToken) |
Task<IEnumerable<T>> |
Returns all rows that match the specification. |
FirstOrDefaultAsync<T>(specification, cancellationToken) |
Task<T?> |
Returns the first matching row or null. |
FirstAsync<T>(specification, cancellationToken) |
Task<T> |
Returns the first matching row. |
SingleOrDefaultAsync<T>(specification, cancellationToken) |
Task<T?> |
Returns one matching row or null. |
SingleAsync<T>(specification, cancellationToken) |
Task<T> |
Returns exactly one matching row. |
CountAsync<T>(specification, cancellationToken) |
Task<long> |
Counts rows that match the specification. |
Raw SQL Operations
Raw SQL helpers are useful for reports, maintenance commands, or provider-specific queries that do not fit the repository/specification shape.
| Method | Returns | Description |
|---|---|---|
ExecuteNonQueryAsync(sql, parameters, cancellationToken) |
Task<int> |
Executes an insert, update, delete, DDL, or command SQL with parameters. |
ExecuteNonQueryAsync(sql, cancellationToken) |
Task<int> |
Executes a command SQL without parameters. |
ExecuteScalarAsync(sql, parameters, cancellationToken) |
Task<object?> |
Executes SQL and returns the first column of the first row. |
ExecuteScalarAsync(sql, cancellationToken) |
Task<object?> |
Executes scalar SQL without parameters. |
ExecuteQueryAsync(sql, parameters, cancellationToken) |
Task<DataTable> |
Executes SQL and returns the result as a DataTable. |
ExecuteQueryAsync(sql, cancellationToken) |
Task<DataTable> |
Executes query SQL without parameters. |
ExecuteReaderAsync(sql, parameters, cancellationToken) |
Task<IDataReader> |
Executes SQL and returns an IDataReader. The caller is responsible for disposing the reader according to provider rules. |
ExecuteReaderAsync(sql, cancellationToken) |
Task<IDataReader> |
Executes reader SQL without parameters. |
Prefer the overloads with Dictionary<string, object?> parameters when values come from users or external input.
Connection and Transaction Operations
| Method | Returns | Description |
|---|---|---|
GetConnection() |
IDbConnection |
Returns the underlying database connection. |
GetCurrentTransaction() |
IDbTransaction? |
Returns the active database transaction, or null when there is no active transaction. |
OpenConnection() |
void |
Opens the connection synchronously. |
OpenConnectionAsync(cancellationToken) |
Task |
Opens the connection asynchronously. |
CloseConnection() |
void |
Closes the connection synchronously. |
CloseConnectionAsync(cancellationToken) |
Task |
Closes the connection asynchronously. |
BeginTransactionScopeAsync(cancellationToken) |
Task<IDatabaseTransaction> |
Starts a provider-managed transaction scope. |
Repository implementations should generally use the higher-level entity and specification methods. Direct connection access is intended for provider implementations and advanced integration scenarios.
Specification Builder API
The Specification<T> base class provides a fluent API for building queries:
| Method | Description |
|---|---|
Where(Expression) |
Add a filter condition |
Include(Expression) |
Include related entities |
OrderBy(Expression) |
Sort ascending |
OrderByDesc(Expression) |
Sort descending |
Skip(int) |
Skip N records |
Take(int) |
Take N records |
public class ComplexQuerySpecification : Specification<User>
{
public ComplexQuerySpecification(string email, int page, int pageSize)
{
Query.Where(u => u.Email.Contains(email));
Query.Include(u => u.Posts);
Query.OrderBy(u => u.Username);
Query.Skip((page - 1) * pageSize);
Query.Take(pageSize);
}
}
Base Filter Include
BaseFilter provides an Include list for request-driven eager loading. Use navigation property paths as strings, including dot notation for nested navigation properties.
using Azka.Common.Abstractions.Specifications;
using Azka.Common.Abstractions.Specifications.Filters;
public sealed class UserFilter : BaseFilter
{
public StringFilter? Email { get; set; }
}
public sealed class UserSpecification : Specification<User>
{
public UserSpecification(UserFilter filter)
{
BuildBaseFilterSpecification(filter);
if (filter.Email is not null)
BuildSpecification(u => u.Email, filter.Email);
}
}
var filter = new UserFilter
{
Include = ["Posts", "Posts.Comments"],
OrderBy = "Username",
PerPage = 20,
Page = 1
};
Empty, duplicate, or invalid include paths are ignored. Valid paths are converted into specification include expressions before the query is executed.
Attributes
| Attribute | Target | Description |
|---|---|---|
[Table] |
Class | Maps entity to table name and schema |
[Column] |
Property | Maps property to column name |
[Key] |
Property | Identifies primary key |
[WithOne] |
Property | Configures many-to-one relationship (takes foreign key column name) |
[WithMany] |
Property | Configures one-to-many relationship (takes foreign key column name) |
Note: Properties marked with [Key] must also include [Column("...")] so the primary key column name can be resolved.
If schema is not provided, Azka metadata uses public as the default schema.
Attribute Examples
[Table("dbo", "Users")] // With custom schema
public class User { }
[Column("first_name")] // Map to different column name
public string FirstName { get; set; }
[WithOne("department_id")] // Foreign key column name
public Department? Department { get; set; }
[WithMany("department_id")] // Foreign key column name
public ICollection<Employee> Employees { get; set; }
Mapping Rules (Entity Mapper)
- Entities must have a public parameterless constructor.
[Key]must be paired with[Column("...")]to resolve the primary key column.- Columns are read from
SELECTaliases in the formatalias_column(case-insensitive); this is handled automatically when using the built-in query helpers. - If a property has a backing field named
_{camelCaseProperty}, the mapper will set the backing field directly.
Backing Field Mapping Example
If a property has a private backing field named _{camelCaseProperty}, the mapper sets the backing field directly.
[Table("users")]
public class User
{
[Key]
[Column("id")]
public int Id { get; set; }
private string _username = string.Empty;
[Column("username")]
public string Username
{
get => _username;
set => _username = value;
}
[Column("email")]
public string Email { get; set; } = string.Empty;
}
Backing Field for One-to-Many (Read-Only List Exposure)
Use a private List<T> backing field for [WithMany], and expose it as a read-only collection to avoid sharing a mutable list reference.
[Table("users")]
public class User
{
[Key]
[Column("id")]
public int Id { get; set; }
[Column("username")]
public string Username { get; set; } = string.Empty;
[Column("email")]
public string Email { get; set; } = string.Empty;
private List<Post> _posts = new();
[WithMany("user_id")]
public IReadOnlyCollection<Post> Posts => _posts.AsReadOnly();
public void AddPost(Post post)
{
if (post == null) throw new ArgumentNullException(nameof(post));
if (_posts.Contains(post)) return;
post.UserId = Id;
post.User = this;
_posts.Add(post);
}
public void RemovePost(Post post)
{
if (post == null) throw new ArgumentNullException(nameof(post));
if (_posts.Remove(post))
{
post.UserId = default;
post.User = null;
}
}
}
[Table("posts")]
public class Post
{
[Key]
[Column("id")]
public int Id { get; set; }
[Column("title")]
public string Title { get; set; } = string.Empty;
[Column("user_id")]
public int UserId { get; set; }
[WithOne("user_id")]
public User? User { get; set; }
}
Dependency Injection
Azka includes an assembly scanning helper for registering application-defined repositories:
services.AddRepositories(typeof(UserRepository).Assembly);
AddRepositories registers concrete, non-abstract classes that implement IRepository<T> or IViewRepository<T> as scoped services. It does not register IDatabaseContext, IUnitOfWork, database connections, or provider-specific services.
Requirements
- .NET 10.0 or higher
- Microsoft.Extensions.DependencyInjection.Abstractions 10.0.1 or higher
- Microsoft.Extensions.Logging.Abstractions 10.0.1 or higher
License
See LICENSE.txt for details.
Links
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.1)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Azka:
| Package | Downloads |
|---|---|
|
Azka.PostgreSQL
PostgreSQL database provider for Azka Framework, providing high-performance data access using Npgsql. |
|
|
Azka.CQRS
A lightweight CQRS library for .NET that provides command/query pipelines, validation, hooks, and DI integration. |
|
|
Azka.BaseProject
Reusable base project primitives for Azka applications, including auditable entities, multi-tenant fields, core repositories, soft-delete behavior, public ID lookup, and SSO user contracts. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.0.0-rc.1 | 44 | 6/30/2026 |
| 10.0.0-alpha6 | 111 | 6/20/2026 |
| 10.0.0-alpha5 | 171 | 3/16/2026 |
| 10.0.0-alpha4 | 120 | 2/17/2026 |
| 10.0.0-alpha3 | 134 | 2/1/2026 |
| 10.0.0-alpha2 | 126 | 1/31/2026 |
| 10.0.0-alpha1 | 130 | 1/19/2026 |
| 10.0.0-alpha.9 | 63 | 6/25/2026 |
| 10.0.0-alpha.8 | 60 | 6/25/2026 |
| 10.0.0-alpha.7 | 61 | 6/25/2026 |
| 4.0.0-alpha.48 | 198 | 4/30/2025 |
| 4.0.0-alpha.47 | 156 | 4/25/2025 |
| 4.0.0-alpha.46 | 149 | 4/25/2025 |
| 4.0.0-alpha.45 | 162 | 4/25/2025 |
| 4.0.0-alpha.44 | 206 | 4/24/2025 |
| 4.0.0-alpha.43 | 235 | 4/15/2025 |
| 4.0.0-alpha.42 | 218 | 4/14/2025 |
| 4.0.0-alpha.41 | 220 | 4/14/2025 |
| 4.0.0-alpha.40 | 218 | 4/10/2025 |
| 2.0.0 | 787 | 5/21/2022 |
Initial release of Azka Framework
- Repository pattern with CRUD operations
- Unit of Work pattern with nested transaction support
- Specification pattern for composable queries
- High-performance object mapper
- Support for one-to-many and many-to-one relationships