Skip to content

Middleware API

SmartSql executes all SQL operations through a linked-list middleware pipeline. Each middleware receives an ExecutionContext, performs its work, and delegates to the next middleware. This design enables clean separation of concerns: statement resolution, SQL preparation, caching, transaction management, data source routing, command execution, and result deserialization are each handled by independent middleware components.

At a Glance

ConceptDescription
IMiddlewareCore interface: Invoke<T>, InvokeAsync<T>, and Next pointer
IOrderedDetermines execution order via int Order property
AbstractMiddlewareBase class with filter support and lifecycle hooks
PipelineBuilderConstructs the linked list from registered middleware
ExecutionContextShared context flowing through the entire pipeline

Interface Definitions

IMiddleware

csharp
public interface IMiddleware : IOrdered
{
    IMiddleware Next { get; set; }
    void Invoke<TResult>(ExecutionContext executionContext);
    Task InvokeAsync<TResult>(ExecutionContext executionContext);
}

The Next pointer forms a singly linked list. Each middleware calls Next.Invoke<TResult>() (or the async variant) to continue the pipeline.

IOrdered

csharp
public interface IOrdered
{
    int Order { get; }
}

The Order property controls where a middleware sits in the pipeline. Lower values execute first. Built-in middlewares use these orders:

Built-in Middleware Chain

mermaid
flowchart TD
    subgraph pipeline["SmartSql Middleware Pipeline"]
        style pipeline fill:#161b22,stroke:#30363d,color:#e6edf3
        M0["InitializerMiddleware<br>Order: 0<br>Resolves Statement, DataSource"]
        M1["PrepareStatementMiddleware<br>Order: 100<br>Builds SQL, creates DbParameters"]
        M2["CachingMiddleware<br>Order: 200<br>Cache check/populate (optional)"]
        M3["TransactionMiddleware<br>Order: 300<br>Wraps in transaction if requested"]
        M4["DataSourceFilterMiddleware<br>Order: 400<br>Selects read vs write data source"]
        M5["CommandExecuterMiddleware<br>Order: 500<br>Executes DbCommand"]
        M6["ResultHandlerMiddleware<br>Order: 600<br>Deserializes DataReader results"]
        Custom["Custom Middleware<br>Order: > 600<br>User-defined logic"]

        M0 --> M1 --> M2 --> M3 --> M4 --> M5 --> M6 --> Custom
    end

Middleware Details

OrderClassKey Responsibility
0InitializerMiddlewareResolves the Statement from SmartSqlConfig.SqlMaps, sets data source choice (Read/Write), resolves result maps, parameter maps, cache references, and auto-converters
100PrepareStatementMiddlewareBuilds the final SQL string by evaluating dynamic XML tags and creates DbParameter objects from the request parameters using TypeHandlerFactory
200CachingMiddlewareChecks the cache before executing the query. On cache miss, lets the pipeline continue and then caches the result. Only active when cache is configured for the statement and no transaction is active.
300TransactionMiddlewareIf a transaction isolation level is specified on the statement/request and no transaction is active, wraps the downstream pipeline in a TransactionWrap
400DataSourceFilterMiddlewareUses IDataSourceFilter.Elect() to select the appropriate data source (read or write) if one is not already set on the session
500CommandExecuterMiddlewareExecutes the actual DbCommand via ICommandExecuter. For queries, opens a DataReader and passes it downstream. For non-queries, sets the result directly and terminates the pipeline (Result.End = true)
600ResultHandlerMiddlewareUses the DeserializerFactory to select the appropriate deserializer and maps the DataReader to entities. Closes and disposes the DataReader when done.

AbstractMiddleware Base Class

All built-in middlewares extend AbstractMiddleware, which provides:

mermaid
classDiagram
    class IMiddleware {
        <<interface>>
        +Next IMiddleware
        +Invoke~TResult~(ExecutionContext) void
        +InvokeAsync~TResult~(ExecutionContext) Task
    }
    class IOrdered {
        <<interface>>
        +Order int
    }
    class ISetupSmartSql {
        <<interface>>
        +SetupSmartSql(SmartSqlBuilder) void
    }
    class AbstractMiddleware {
        <<abstract>>
        #FilterType Type
        #Filters IList~IInvokeMiddlewareFilter~
        +Invoke~TResult~(ExecutionContext) void
        +InvokeAsync~TResult~(ExecutionContext) Task
        #SelfInvoke~TResult~(ExecutionContext) void
        #SelfInvokeAsync~TResult~(ExecutionContext) Task
        #OnInvoking(ExecutionContext) void
        #OnInvoked(ExecutionContext) void
        #InvokeNext~TResult~(ExecutionContext) void
        +SetupSmartSql(SmartSqlBuilder) void
        +Order int*
    }
    class CachingMiddleware {
        +Order: 200
    }
    class CommandExecuterMiddleware {
        +Order: 500
    }

    IMiddleware --|> IOrdered
    AbstractMiddleware ..|> IMiddleware
    AbstractMiddleware ..|> ISetupSmartSql
    CachingMiddleware --|> AbstractMiddleware
    CommandExecuterMiddleware --|> AbstractMiddleware

Lifecycle Hooks

AbstractMiddleware provides overridable hooks in this order:

mermaid
sequenceDiagram
autonumber
    participant Caller
    participant MW as AbstractMiddleware
    participant Self as SelfInvoke
    participant Next as Next Middleware
    participant Filters as Filters

    Caller->>MW: Invoke(ctx)
    MW->>Filters: OnInvoking(ctx)
    MW->>Self: SelfInvoke(ctx)
    MW->>Filters: OnInvoked(ctx)
    alt ctx.Result.End == false
        MW->>Next: InvokeNext(ctx)
        MW->>MW: OnNextInvoked(ctx)
    end
HookWhenTypical Use
OnInvoking(ctx)Before SelfInvokePre-processing, validation
SelfInvoke<T>(ctx)Main middleware logicCore middleware work
OnInvoked(ctx)After SelfInvokePost-processing, logging
InvokeNext<T>(ctx)Delegates to NextPipeline continuation
OnNextInvoked<T>(ctx)After Next completesCleanup after downstream

Filter System

Filters attach to specific middleware types and run before/after that middleware's SelfInvoke. This provides cross-cutting concerns without modifying the middleware itself.

Filter Interfaces

InterfaceMethodWhen Called
IInvokeFilterOnInvoking(ctx)Before SelfInvoke
IInvokeFilterOnInvoked(ctx)After SelfInvoke
IAsyncInvokeFilterOnInvokingAsync(ctx)Async before SelfInvoke
IAsyncInvokeFilterOnInvokedAsync(ctx)Async after SelfInvoke
IFilter(marker)Base interface for all filters
IPrepareStatementFilterExtends IInvokeMiddlewareFilterTargets PrepareStatementMiddleware

A middleware declares which filter type it supports via the FilterType property. For example, PrepareStatementMiddleware sets FilterType = typeof(IPrepareStatementFilter), so only filters implementing IPrepareStatementFilter will be attached to it.

Creating Custom Middleware

To create a custom middleware:

Step 1: Implement the Middleware

csharp
public class LoggingMiddleware : AbstractMiddleware
{
    private ILogger _logger;

    // Order determines position in pipeline.
    // Use a value > 600 to run after built-in middlewares.
    public override int Order => 700;

    protected override void SelfInvoke<TResult>(ExecutionContext executionContext)
    {
        _logger.LogInformation(
            "Executing {Type} for {FullSqlId}",
            executionContext.Type,
            executionContext.Request.FullSqlId);
    }

    protected override async Task SelfInvokeAsync<TResult>(ExecutionContext executionContext)
    {
        _logger.LogInformation(
            "Executing {Type} for {FullSqlId}",
            executionContext.Type,
            executionContext.Request.FullSqlId);
    }

    public override void SetupSmartSql(SmartSqlBuilder smartSqlBuilder)
    {
        _logger = smartSqlBuilder.SmartSqlConfig.LoggerFactory
            .CreateLogger<LoggingMiddleware>();
    }
}

Step 2: Register via SmartSqlBuilder

csharp
var builder = new SmartSqlBuilder()
    .UseXmlConfig()
    .AddMiddleware(new LoggingMiddleware())
    .Build();

Custom middlewares are appended after the built-in chain. The PipelineBuilder sorts all middlewares by Order, so even if you add them out of order, they execute correctly.

Creating Custom Filters

csharp
// 1. Define a filter interface (or use IPrepareStatementFilter)
public interface IMyCustomFilter : IInvokeMiddlewareFilter { }

// 2. Implement the filter
public class AuditFilter : IMyCustomFilter
{
    public void OnInvoking(ExecutionContext context)
    {
        // Pre-execution audit
    }

    public void OnInvoked(ExecutionContext context)
    {
        // Post-execution audit
    }

    public Task OnInvokingAsync(ExecutionContext context)
    {
        OnInvoking(context);
        return Task.CompletedTask;
    }

    public Task OnInvokedAsync(ExecutionContext context)
    {
        OnInvoked(context);
        return Task.CompletedTask;
    }
}

// 3. Register the filter
var builder = new SmartSqlBuilder()
    .UseXmlConfig()
    .AddFilter(new AuditFilter())
    .Build();

The filter only activates on middlewares whose FilterType is assignable from the filter interface.

Adding Middleware via SmartSqlBuilder

The AddMiddleware method on SmartSqlBuilder:

mermaid
flowchart TD
    subgraph add["Adding Custom Middleware"]
        style add fill:#161b22,stroke:#30363d,color:#e6edf3
        A["SmartSqlBuilder.AddMiddleware(mw)"] --> B["Middlewares list grows"]
        B --> C["Build() is called"]
        C --> D["PipelineBuilder creates built-in chain"]
        D --> E["Custom middlewares appended by Order"]
        E --> F["PipelineBuilder.Build() sorts & links"]
        F --> G["SmartSqlConfig.Pipeline = head node"]
    end

Middleware Short-Circuiting

Some middlewares terminate the pipeline early by setting executionContext.Result.End = true. When this happens, downstream middlewares are skipped.

MiddlewareShort-Circuits When
CommandExecuterMiddlewareFor Execute, ExecuteScalar, GetDataSet, and GetDataTable operations -- sets result directly without invoking ResultHandlerMiddleware
CachingMiddlewareWhen a cache hit is found (no transaction active) -- sets result from cache, skips command execution entirely
mermaid
flowchart LR
    subgraph short["Short-Circuit Example: Cache Hit"]
        style short fill:#161b22,stroke:#30363d,color:#e6edf3
        A["Initializer"] --> B["PrepareStatement"]
        B --> C["CachingMiddleware"]
        C -->|cache hit| D["Set result from cache<br>Result.End = true"]
        C -->|cache miss| E["Transaction"]
        E --> F["DataSourceFilter"]
        F --> G["CommandExecuter"]
        G --> H["ResultHandler"]
    end

Cross-References

References

SourceDescription
src/SmartSql/IMiddleware.csIMiddleware interface
src/SmartSql/IOrdered.csIOrdered interface
src/SmartSql/Middlewares/AbstractMiddleware.csBase class with lifecycle hooks and filter support
src/SmartSql/Middlewares/InitializerMiddleware.csStatement resolution middleware
src/SmartSql/Middlewares/PrepareStatementMiddleware.csSQL building and parameter creation
src/SmartSql/Middlewares/CachingMiddleware.csCache check and populate
src/SmartSql/Middlewares/TransactionMiddleware.csTransaction wrapping
src/SmartSql/Middlewares/DataSourceFilterMiddleware.csRead/write data source selection
src/SmartSql/Middlewares/CommandExecuterMiddleware.csCommand execution
src/SmartSql/Middlewares/ResultHandlerMiddleware.csResult deserialization
src/SmartSql/Middlewares/Filters/IInvokeMiddlewareFilter.csMiddleware filter interface
src/SmartSql/Filters/IFilter.csBase filter marker interface
src/SmartSql/Filters/IInvokeFilter.csSync filter interface
src/SmartSql/SmartSqlBuilder.csBuilder with AddMiddleware and AddFilter

Released under the MIT License.