MigLib.Web adds a request-oriented computation expression, webResult, for handlers that need a SQLite transaction plus deferred HTTP response mutation.
It ships as a separate package on top of MigLib, so basic runtime consumers do not need the ASP.NET Core surface.
It is designed for ASP.NET Core services that already use MigLib's TxnStep, generated query helpers, txn, and DbRuntime.
- Execute request database work inside one SQLite transaction.
- Reuse
TxnStep<'a>values produced by generated query helpers and customtxnhelpers. - Keep application errors distinct from database failures.
- Make
HttpContextaccess explicit and easy to test. - Defer response mutation until after a successful commit.
- Support pluggable post-commit response effects.
- Keep time access abstract through
IClock.
- Routing, model binding, authentication, or middleware composition.
- A host abstraction beyond ASP.NET Core
HttpContext. - Distributed guarantees for side effects that run after commit.
- Automatic wrapping of arbitrary .NET exceptions into
WebError.
| Type | Meaning |
|---|---|
WebError<'appError> |
`DbError of SqliteException |
WebCtx<'env, 'custom> |
Current environment, active SqliteTransaction, optional HttpContext, and queued response effects |
WebOp<'env, 'appError, 'custom, 'a> |
Request operation: WebCtx<'env, 'custom> -> Task<Result<'a, WebError<'appError>>> |
ResponseEffect<'custom> |
Deferred response step: status, headers, body, JSON, redirect, cookies, or a custom effect |
WebRuntime<'env, 'appError, 'custom> |
Runtime environment, default JSON options, and custom-effect interpreter |
IClock |
Injectable time source for request logic |
IHasDbRuntime |
Environment contract from MigLib.Db required by run and runSimple |
MigLib.Db exposes DbRuntime.RunInTransaction so webResult can reuse the same transaction machinery as dbTxn while mapping database failures into WebError.DbError.
- The caller builds a runtime with
WebRuntime.createor usesrunSimple. runallocates an empty in-memory response plan.runopens a transaction throughruntime.Env.DbRuntime.RunInTransaction DbError.- The
webResultoperation runs with aWebCtxcontainingenv,tx, optionalhttpContext, and the shared response plan. Respond.*helpers appendResponseEffectvalues to the plan. They do not mutateHttpContext.Responseinline.- If the operation returns
Error _, the transaction rolls back and the queued response plan is discarded. - If the operation returns
Ok value, the transaction commits first. - After commit, queued response effects are applied in insertion order to
HttpContext.Response. runreturns the originalOk valueor the firstWebErrorproduced during operation execution or custom-effect application.
Important details:
Respond.*andWeb.httpContextrequireSome HttpContext. Without one they returnMissingHttpContextduring operation execution, which causes rollback.- An operation that never touches
HttpContextcan run withNone. - Response effects are sequenced after commit. This prevents partially-written HTTP responses from being observed for rolled-back transactions.
| Source | Result from run |
Database state | Response effects |
|---|---|---|---|
TxnStep error or SqliteException during DB work |
Error (DbError ex) |
Rolled back | Discarded |
TxnStep<Result<'a, 'appError>> inner error |
Error (AppError appError) |
Rolled back | Discarded |
Result<'a, 'appError> / Task<Result<'a, 'appError>> error |
Error (AppError appError) |
Rolled back | Discarded |
Explicit WebError (Web.failWeb, Result<_, WebError<_>>, etc.) |
That WebError |
Rolled back | Discarded |
Web.httpContext or any Respond.* helper with no context |
Error MissingHttpContext |
Rolled back | Discarded |
Respond.custom interpreter returns Error _ after commit |
That WebError |
Already committed | Earlier effects stay applied |
Only SqliteException, typed application errors, and explicit WebError values participate in the Result contract.
Other exceptions from Task, response writing, or custom code are not normalized and fault the returned task.
Inside webResult, let! / do! support:
WebOp<'env, 'appError, 'custom, 'a>TxnStep<'a>Result<'a, 'appError>Result<'a, WebError<'appError>>Task<Result<'a, 'appError>>Task<Result<'a, WebError<'appError>>>Task<'a>Task
The builder also supports standard control-flow members:
returnreturn!forWebOp,TxnStep, and the supportedResult/Task<Result>formsZeroCombineDelayTryWithTryFinallyUsingWhileFor
Plain Task<'a> and Task are bind-only. They are not valid return! targets.
envreturns the current environment.txreturns the currentSqliteTransaction.tryHttpContextreturnsHttpContext option.httpContextreturnsHttpContextorMissingHttpContext.failconverts an application error intoAppError.failWebreturns any explicitWebError.ignorediscards a successfulWebOpvalue while preserving errors and effects.requireSomeconvertsoptionvalues into eitherOk valueorAppError.ofAppResultliftsResult<'a, 'appError>intoWebOp.ofAppTaskResultliftsTask<Result<'a, 'appError>>intoWebOp.ofTxnAppResultliftsTxnStep<Result<'a, 'appError>>intoWebOp.ofWebResultliftsResult<'a, WebError<'appError>>intoWebOp.
Clock helpers require 'env :> IClock:
utcNowutcNowRfc3339utcNowPlusDaysRfc3339
These keep handler logic deterministic in tests and avoid direct dependency on DateTimeOffset.UtcNow.
Respond queues response effects:
statusCodeheaderappendHeadertexthtmlbytesjsonjsonWithredirectpermanentRedirectsetCookiedeleteCookiecustom
Effects are recorded in the order they appear in the CE and applied in the same order after commit.
CookieSpec is MigLib's serializable cookie description:
CookieSpec.emptyis the zero-value configuration.CookieSpec.toAspNetCoremaps it toCookieOptions.- Only explicitly provided optional values are copied into
CookieOptions.
JSON behavior:
WebRuntime.createusesJsonSerializerDefaults.Webas the runtime default.WebRuntime.withJsonOptionsoverrides the runtime-wide default.Respond.jsonWithoverrides JSON options for one queued write.Respond.jsonserializesnullas JSONnull.
Construction:
WebRuntime.create env applyCustomEffectWebRuntime.withJsonOptions jsonOptions runtimeWebRuntime.createSimple env
Execution:
run runtime httpContext operationrunSimple env httpContext operation
run and runSimple require 'env :> IHasDbRuntime.
MigLib.Web therefore depends on the same DbRuntime that powers dbTxn.
- Use
dbTxnwhen the caller only needs a transaction boundary around database work. - Use
txnto build reusable transaction-scoped helpers. - Use
webResultwhen the caller also needs typed application errors, environment access, optionalHttpContext, and deferred response composition. - Any
TxnStep<'a>can be bound directly insidewebResult. - Use
Web.ofTxnAppResultwhen a transaction-scoped helper returnsTxnStep<Result<'a, 'appError>>. - Generated query helpers,
txnhelpers, and transaction-scoped validation flows therefore compose without ad-hoc adapters.
open System
open System.Threading.Tasks
open Microsoft.Data.Sqlite
open MigLib.Db
open MigLib.Web
type Env =
{ dbRuntime: DbRuntime
fixedNow: DateTimeOffset }
interface IHasDbRuntime with
member this.DbRuntime = this.dbRuntime
interface IClock with
member this.UtcNow() = this.fixedNow
member this.UtcNowRfc3339() = this.fixedNow.ToUniversalTime().ToString("O")
member this.UtcNowPlusDaysRfc3339(days: float) =
this.fixedNow.AddDays days |> fun value -> value.ToUniversalTime().ToString("O")
let createStudent (name: string) =
webResult {
let! createdAt = Clock.utcNowRfc3339
let! id =
fun tx ->
task {
use cmd =
new SqliteCommand(
"INSERT INTO student(name, created_at) VALUES (@name, @created_at)",
tx.Connection,
tx
)
cmd.Parameters.AddWithValue("@name", name) |> ignore
cmd.Parameters.AddWithValue("@created_at", createdAt) |> ignore
let! _ = cmd.ExecuteNonQueryAsync()
use idCmd = new SqliteCommand("SELECT last_insert_rowid()", tx.Connection, tx)
let! idObj = idCmd.ExecuteScalarAsync()
return Ok(idObj |> unbox<int64>)
}
do! Respond.statusCode 201
do! Respond.json {| id = id; createdAt = createdAt |}
return id
}In this example:
- database writes and ID lookup happen inside one SQLite transaction
- the
201status and JSON body are only written after the commit succeeds - any
DbError,AppError, orMissingHttpContextresult skips the response write and rolls back the transaction
- The hosting model is ASP.NET Core because
HttpContext,CookieOptions, andSameSiteModeare part of the public API. - Post-commit response work is intentionally separate from transactional DB work. A custom effect can therefore fail after the database has committed.
webResultdoes not try to own the entire web stack. It is a small orchestration layer for request handlers that need MigLib transaction semantics plus response composition.