Skip to content

AspNetCore Mise

SteffenBlake edited this page Sep 28, 2026 · 1 revision

Mise in ASP.NET Core

Mise's Partie integration provides readers for query handlers and transactional writers for command handlers. A database bundle resolves a named connection string and supplies the matching provider factory.

Install the integration

Choose the package for your database in the project declaring routes:

Database Package Bundle attribute
SQL Server Brigade.Net.Partie.Extensions.Mise.SqlServer [MiseSqlServerBundle(name)]
PostgreSQL Brigade.Net.Partie.Extensions.Mise.PostgreSQL [MisePostgreSqlBundle(name)]
SQLite Brigade.Net.Partie.Extensions.Mise.SQLite [MiseSqliteBundle(name)]
MySQL Brigade.Net.Partie.Extensions.Mise.MySQL [MiseMySqlBundle(name)]
MariaDB Brigade.Net.Partie.Extensions.Mise.MariaDb [MiseMariaDbBundle(name)]

Each package includes the shared Partie/Mise integration and its database runtime. Install the matching mapping engine in the project declaring tables and rows; see Database setup.

Configure and register

{
  "ConnectionStrings": {
    "CourseDatabase": "Host=localhost;Database=courses;Username=app;Password=change-me"
  }
}
[BrigadeGroup("/api/v1")]
[AspNetSystemBundle]
[PartieSystemBundle]
[ExpoSystemBundle]
public static partial class ApiRoutes
{
    [BrigadeGroup("/courses")]
    [MisePostgreSqlBundle("CourseDatabase")]
    private static partial class Courses
    {
        [CourseListHandlerRoute.Get]
        public static partial void List();

        [CourseRenameHandlerRoute.Put("/{courseId}")]
        public static partial void Rename();
    }
}

The HTTP bundle maps outcomes. The core bundle controls command transactions. Expo validation follows them when the application uses Expo. The database bundle belongs on the group or route selecting that connection.

Call app.UsePartieRoutes() after building the application. See Routes and groups and ASP.NET Core bundles.

Query handler

// CourseListHandler.cs
public sealed record CourseListContext([Provide] DbReader Reader);

public sealed class CourseListHandler :
    IQueryHandler<Unit, IReadOnlyList<CourseRow>, CourseListContext>
{
    public static Task<Result<IReadOnlyList<CourseRow>>> RunAsync(
        CourseListContext context,
        Unit query,
        CancellationToken cancellationToken
    )
    {
        var sql = new PostgreSqlQueryBuilder()
            .Select($"{CourseTbl.CourseIdCol:raw}")
            .Select($"{CourseTbl.TitleCol:raw}")
            .Select($"{CourseTbl.CapacityCol:raw}")
            .From($"{CourseTbl.Table:raw}")
            .OrderBy($"{CourseTbl.CourseIdCol:raw}");

        return context.Reader.ListAsync<CourseRow>(sql, cancellationToken);
    }
}

Use the mappings in Table mappings and Result rows. The Provider runs when the context requests DbReader, and owns it until downstream work finishes.

Command request

// CourseRenameCmd.cs
public sealed record CourseRenamePayload(string Title);

public sealed class CourseRenameCmd
{
    [FromPath]
    public required int CourseId { get; init; }

    [FromPayload]
    public required CourseRenamePayload Body { get; init; }
}

Add Expo attributes to the outer request and payload when they need runtime validation. C# required alone is not an Expo rule.

Command handler

// CourseRenameHandler.cs
public sealed record CourseRenameContext([Provide] DbWriter Writer);

public sealed class CourseRenameHandler :
    ICommandHandler<CourseRenameCmd, Unit, CourseRenameContext>
{
    public static async Task<Result<Unit>> RunAsync(
        UnitOfWork unitOfWork,
        CourseRenameContext context,
        CourseRenameCmd command,
        CancellationToken cancellationToken
    )
    {
        var sql = new PostgreSqlCommandBuilder()
            .Update($"{CourseTbl.Table:raw}")
            .Set($"title = {command.Body.Title}")
            .Where($"{CourseTbl.CourseIdCol:raw} = {command.CourseId}");

        var updated = await context.Writer.ExecuteAsync(sql, cancellationToken);

        return updated.FlatMap<int, Unit>(count =>
        {
            if (count == 0)
            {
                return new NotFound("Course not found.");
            }

            return Unit.Default;
        });
    }
}

The supplied writer uses the command transaction. This handler returns NotFound for zero affected rows and Unit for completion. The surrounding unit of work chooses rollback or commit from that result; HTTP mapping sends 404 or 204.

Provider ordering and ownership

Each database bundle contains its named configuration Provider, query reader Provider, command transaction Provider, and command writer Provider. Configuration supplies an IDbConfig; the reader and transaction Providers select the last supplied configuration.

The writer Provider requires the paired Mise transaction. Requesting the writer creates the writer scope but opens the connection and transaction only when database work begins. The unit of work disposes the transaction before the writer.

Register a more specific database bundle at the scope selecting that database. Multiple bundles can also register multiple transaction sources, so review the resulting context selection and unit-of-work collection rather than assuming only one bundle participates.

Do not dispose provided resources or complete their transaction in the handler. Enumerate streams before returning from the Provider scope. See Resource lifetimes, Partie contexts, and Transactions.

Clone this wiki locally