
Return the intent, run the effect: a Wolverine deep read
A deep read of Jeremy Miller's August 13 case study on using Wolverine's returned side-effect values to keep C# business endpoints focused, testable, and explicit about execution boundaries.
Why this post
On August 13, 2026, Jeremy Miller published "Using the Wolverine 'Side Effect' Model to Simplify Code", a short C# case study with a useful functional-programming question underneath it: when should a business method return a description of work instead of performing that work itself? The post is worth reading because it identifies the exact dependency that turns a small endpoint into an infrastructure-shaped function, then moves that dependency behind a returned value without pretending that every
await needs a new abstraction.The example comes from CritterWatch, Miller's forthcoming monitoring console for the Critter Stack. An endpoint adds a tenant to a monitored service and also records an audit event. The first version works, but its signature says much more about the application's infrastructure than about the business operation. Miller's refactoring makes the audit intent explicit, leaves the endpoint close to a pure function, and lets Wolverine perform the effect after the endpoint returns.
The argument, end to end
Miller begins with an HTTP endpoint that takes six parameters: a service name, an
AddTenantRequest, an IDocumentSession, an IMessageBus, an AuditLogService, and an HttpContext. The omitted body appends an event, publishes a command, and writes an audit record. The endpoint therefore mixes the operation's business inputs with the services needed to carry it out. The original code and explanation are in Miller's post.The audit-related part is especially revealing. The endpoint needs
AuditLogService to record what happened, and it needs HttpContext only to reach httpContext.User, which supplies a ClaimsPrincipal for the audit actor. The endpoint does not otherwise inspect the request context. Miller's point is not that dependency injection is wrong. His point is that the signature now carries a large object and an infrastructure service because one small piece of post-operation bookkeeping happens to live there.That shape changes the test. A test must provide a document store, a message bus, an audit service, and a request context, then query the audit table to see whether the correct action, service, and operator were recorded. The business question is simple, but the test observes it through a collection of infrastructure objects. The code can still be tested, yet the test boundary is wider than the behavior requires.
Miller's alternative is to return the audit intent. Wolverine's
ISideEffect marker interface has no method of its own. A handler or HTTP endpoint returns an object that implements the marker, and Wolverine looks for a public Execute() or ExecuteAsync() method on that object after the handler returns. Wolverine resolves the parameters of that execution method as dependencies in the generated chain. The source post shows the interface and the execution convention.The audit record then contains both the data that describes the event and the code that knows how to persist it:
public record AuditLog(
string Action,
string ServiceName,
string? TargetUri = null,
string? Details = null,
Dictionary<string, string>? Parameters = null,
string? InitiatedBy = null) : ISideEffect
{
public Task ExecuteAsync(AuditLogService auditLog, ClaimsPrincipal? user)
{
var actor = string.IsNullOrWhiteSpace(InitiatedBy)
? AuditActor.From(user)
: InitiatedBy;
return auditLog.LogAsync(
Action, ServiceName, TargetUri, Details, Parameters, actor);
}
}The endpoint can now return a record with the information it already knows:
[WolverinePost("/test/audited/{serviceName}")]
public static AuditLog Post(string serviceName)
=> new("TestAction", serviceName, Details: "smoke");The endpoint no longer declares
AuditLogService, ClaimsPrincipal, or HttpContext. Wolverine resolves AuditLogService and ClaimsPrincipal when it executes the returned value, while the endpoint only constructs the description of the audit event. The result is not a magical disappearance of side effects. The effect still runs. The design changes which function owns the description and which layer owns execution.Miller then adds an important qualification. Wolverine's official documentation describes side effects as work that is processed inline with the originating message and within the same logical transaction, unlike cascading messages. The returned value is therefore a way to isolate the decision from the doing, not a promise that the work has become asynchronous, durable, or eventually consistent. Wolverine's side-effects documentation makes that boundary explicit.
The post connects the refactoring to Wolverine's broader advice: keep business or workflow routing logic as pure functions when possible, so the logic stays easy to test without infrastructure and mock objects. Miller also argues that removing unnecessary asynchronous calls reduces code noise. He closes with a wider observation about reviewing AI-generated code and preserving human judgment, but the concrete lesson remains the side-effect boundary: a small returned description can keep an endpoint focused when the same dependency pattern appears across several endpoints. The official best-practices page gives the framework's corresponding guidance on pure business functions.
Key details
The signature exposes the wrong dependency
The first endpoint's six parameters are not all equal.
serviceName and request describe the operation. IDocumentSession and IMessageBus support the omitted event and command work. AuditLogService and HttpContext exist for the audit record. Miller says that two parameters are present for the audit log alone, and that the request context exists for one expression, httpContext.User.That distinction matters in a functional design because a function's inputs communicate what the function needs to decide. A request context is a container for many concerns, while the audit decision needs only an actor. The original signature makes the broad container look like a business input. The refactored signature makes the endpoint's actual input surface visible.
The testing consequence follows directly. A test of the audit decision should be able to inspect an
AuditLog value. It should not need to start a store, bus, audit service, and request context merely to discover what the endpoint intended to record. The returned record creates that inspection point without claiming that persistence itself is pure.ISideEffect is a marker, not an effect interpreter
The interface is deliberately small:
public interface ISideEffect
: IWolverineReturnType, INotToBeRouted;The interface does not define an
Execute method, a result type, or a generic effect algebra. Wolverine treats it as a return-type signal and then follows a convention on the concrete value. That choice keeps the application code compact, but it also means the behavior depends on Wolverine's runtime and code-generation rules. The pattern is therefore framework-directed functional design, not a general-purpose effect system that can be moved unchanged to another host language.The execution method's parameters are the second half of the convention.
AuditLog.ExecuteAsync asks for AuditLogService and ClaimsPrincipal?, so Wolverine resolves those dependencies at execution time. The endpoint does not thread them through a signature that never uses them for its own decision.
AuditLog value, Wolverine resolves the execution dependencies, and ExecuteAsync performs the audit write after the endpoint returns. The diagram visualizes the mechanism in Miller's post; it is not an image from the source post.The useful unit is the description
The
AuditLog record carries the action, service name, optional target URI, details, parameters, and an optional initiating actor. Its ExecuteAsync method supplies a fallback actor from the resolved ClaimsPrincipal when InitiatedBy is empty. The value therefore contains the stable facts of the event, while execution supplies the service and request-derived context at the boundary where the effect actually runs. Miller's code defines these fields and this fallback.This split gives tests two different targets. A unit test can check that an endpoint returns the right
AuditLog data. A separate integration test can check that Wolverine invokes ExecuteAsync and that the audit service persists the result. The source article does not claim that one test category replaces the other. It shows how to keep the business decision from requiring the second category just to inspect the first.The framework boundary has real semantics
The official documentation says Wolverine processes these side effects inline with the originating message and inside the same logical transaction. That fact limits the analogy to purely functional programming. The endpoint returns a value, but the surrounding workflow still has operational semantics: execution order, transaction scope, dependency resolution, and failure behavior belong to Wolverine.
That boundary also explains why a returned effect is not automatically a queue. A cascading message, an outbox record, and an inline side effect can all represent work that happens after a decision, but they do not have the same delivery or transaction guarantees. The source's example is useful precisely because it improves local code shape without changing the feature into a different delivery model.
The post supplies a practical cutoff
Miller does not recommend wrapping every effect in
ISideEffect. He gives two limits:- The pattern is a poor fit when the effect's result feeds the rest of the current method. In that case, the method needs the call and its result before it can continue.
- The pattern is also unnecessary when a one-line
awaithas no distracting dependencies and nobody needs to test the operation in isolation.
His strongest signal is repetition and dependency ratio. In the example, an audit operation was carried through five endpoints, and each endpoint carried two audit-related parameters plus a request context to express one fact about an operation that had already happened. Miller treats that ratio as the reason to introduce the abstraction, not the mere presence of an
await.He names audit, notification, telemetry, and outbound email as examples where a description can be useful. Those are source-author recommendations, not a universal rule. The reader still has to inspect whether the effect's timing, result, failure handling, and transaction scope fit the application.
What transfers to other FP codebases
The most transferable idea is not the
ISideEffect name. It is the separation between deciding that an effect should happen and performing an effect whose result is irrelevant to the decision. Many functional systems express that separation with an effect value, an instruction, a command, an event, or a free-monad-like program. Wolverine expresses it with a C# record plus a framework convention.The choice has three practical consequences for an FP-curious developer working in a mainstream language:
- Return data when the data is the thing you want to test. A value gives a test an observable description without requiring the real effect service.
- Keep execution semantics visible. A returned object does not tell you whether the effect is inline, transactional, retried, queued, or durable. Read the host framework's documentation before treating the pattern as a reliability guarantee.
- Use repetition to justify indirection. A single isolated
awaitmay be clearer than a marker interface and a convention. Repeated dependency plumbing across several entry points is stronger evidence that the effect deserves its own value.
Wolverine's pure-function guidance supports the first point, while its side-effects documentation supplies the second. Miller's post supplies the concrete trade-off for the third. Together, they describe a narrow but useful FP move: make the business decision a value-producing function, then let the application boundary interpret that value.
Verbatim quotes
"TheHttpContextparameter didn't move somewhere else — it stopped existing."Jeremy Miller, "Using the Wolverine 'Side Effect' Model to Simplify Code", August 13, 2026.
"Pure functions are great for testability"Jeremy Miller, the same post, summarizing the practical motivation for the refactoring.
"That ratio is the tell."Jeremy Miller, the same post, referring to repeated dependency plumbing around a small post-operation fact.
Source
- Jeremy Miller, "Using the Wolverine 'Side Effect' Model to Simplify Code", published August 13, 2026.
- Wolverine documentation, Side Effects.
- Wolverine documentation, Prefer Pure Functions for Business Logic.
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.
Related content
- Sign in to comment.
