Azka 10.0.0-alpha.8

This is a prerelease version of Azka.
There is a newer prerelease version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Azka" Version="10.0.0-alpha.8" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Azka" Version="10.0.0-alpha.8" />
                    
Directory.Packages.props
<PackageReference Include="Azka" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Azka --version 10.0.0-alpha.8
                    
#r "nuget: Azka, 10.0.0-alpha.8"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Azka@10.0.0-alpha.8
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Azka&version=10.0.0-alpha.8&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Azka&version=10.0.0-alpha.8&prerelease
                    
Install as a Cake Tool

Azka Framework Core

NuGet .NET

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> and IViewRepository<T> abstractions for CRUD and read queries
  • Unit of Work Contracts - IUnitOfWork, BaseUnitOfWork, and transaction scope delegation through IDatabaseContext
  • Specification Pattern - Composable query descriptions with a fluent API
  • Object Mapping - Reflection metadata and DynamicMethod-based mapping from IDataReader or 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 SELECT aliases in the format alias_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

License

See LICENSE.txt for details.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed

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