Framework And Web Helper Migration
This page collects the migration guidance that tends to live in shared helpers, framework-specific test plumbing, and FastMoq.Web setup rather than in individual test methods.
Open this page when the highest-churn migration work is in base classes, controller helpers, principals, HttpContext, keyed DI, or service-provider shims.
If the migration churn is specifically in Blazor component tests, MockerBlazorTestBase<T>, RenderParameters, nested component targeting, or bUnit package fallout, use bUnit and Blazor test migration first and come back here for the broader web-helper and framework-helper cleanup.
Shared test helpers first
Before rewriting leaf tests, inspect shared base classes, helper wrappers, and test utilities first.
In mature suites, the highest-leverage migration changes often live there rather than in individual test methods.
Start with helpers that still centralize any of these patterns:
.Objectaccess on rawMock<T>values.Reset()calls on provider-specific mocksFunc<Times>or rawTimeshelper signatures- test-framework output helpers passed through shared constructor-check or diagnostic helper wrappers
- local wrappers around principals, controller contexts, or request setup
- framework service-provider shims such as
InstanceServices,IServiceProvider, or similar test bootstrap plumbing
One deliberate helper migration can remove the same churn from dozens of files.
Keyed services: keep separate doubles when selection matters
If the constructor under test takes the same interface more than once and those parameters are distinguished by DI service keys, one unkeyed GetMock<T>(), GetOrCreateMock<T>(), or AddType<T>() can make the migration look correct while hiding a real behavior difference.
The suite passes, but it can no longer catch a public or private or primary or secondary swap because both production dependencies were collapsed into one double.
Migration rule:
- keep ordinary unit tests ordinary
- but if dependency selection is part of the behavior, use two distinct doubles
- in FastMoq, the preferred keyed options are
GetOrCreateMock<T>(new MockRequestOptions { ServiceKey = ... }),AddKeyedType(...), andGetKeyedObject<T>() - explicit constructor injection with two separate doubles is equally valid when that is clearer
- add one small wiring-focused test only if you also want coverage that the keyed DI contract itself is honored
For the fuller keyed example, see Testing Guide: Keyed Services And Same-Type Dependencies.
Azure Functions worker: typed InstanceServices
Azure Functions worker tests deserve one explicit warning: FunctionContext.InstanceServices should behave like a typed IServiceProvider, not like a shim that returns the same object for every requested service type.
If the suite uses Azure Functions worker helpers, prefer the built-in typed helper path:
using FastMoq.AzureFunctions.Extensions;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
Mocks.AddFunctionContextInstanceServices(services =>
{
services.AddSingleton(new WidgetClock());
});
var context = Mocks.GetObject<FunctionContext>();
var clock = context.InstanceServices.GetRequiredService<WidgetClock>();
Package note:
- if the test project references the aggregate
FastMoqpackage, these Azure Functions helpers are already included - if the test project references
FastMoq.Coredirectly, addFastMoq.AzureFunctionsand importFastMoq.AzureFunctions.Extensionsbefore usingCreateFunctionContextInstanceServices(...)orAddFunctionContextInstanceServices(...) - the generic typed
IServiceProviderhelpers stay inFastMoq.Core
Analyzer note:
FMOQ0013warns on directFunctionContext.InstanceServicesmocking more broadly acrossSetup(...),SetupGet(...), andSetupProperty(...)so those shims move toward the typed helper path- the built-in code fix is narrower: it only appears when
FastMoq.AzureFunctionsis already referenced, and it rewrites the safe tracked-provider casesSetup(x => x.InstanceServices).Returns(provider),SetupGet(x => x.InstanceServices).Returns(provider), andSetupProperty(x => x.InstanceServices, provider)tocontext.AddFunctionContextInstanceServices(provider)and addsusing FastMoq.AzureFunctions.Extensions;when needed SetupSet(...)is not part of this analyzer. When the real need is provider-neutral setter observation rather than Moq setter interception, prefer a fake or stub with PropertyValueCapture<TValue>- broader
IServiceProvidershim warnings still stay warning-only when the right replacement depends on the suite's real service graph, but the built-inFMOQ0013fix now also handles the direct tracked-shim casesGetOrCreateMock<IServiceProvider>(),GetOrCreateMock<IServiceScopeFactory>(),GetOrCreateMock<IServiceScope>(), manual scope-factory extraction such asAddType<IServiceScopeFactory>(provider.GetRequiredService<IServiceScopeFactory>()), directIServiceScope.ServiceProvidersetup, and the pairedCreateScope()pattern when the returned tracked scope also exposes a concrete provider in the same helper block
If a suite already has a local Azure helper wrapper, re-point that wrapper to CreateFunctionContextInstanceServices(...) or AddFunctionContextInstanceServices(...) first and keep the existing call sites stable until the suite is green.
Avoid helpers that return one object such as ILoggerFactory for every GetService(Type) request. Those shims can let the suite keep running while hiding cast failures for typed services such as IOptions<WorkerOptions>.
Typed IServiceProvider helpers
The same rule applies outside Azure Functions.
If a base class or helper still does this:
var provider = Mocks.GetOrCreateMock<IServiceProvider>();
provider.Setup(x => x.GetService(It.IsAny<Type>())).Returns(someSharedObject);
replace it with a typed provider shape:
Mocks.AddServiceProvider(services =>
{
services.AddLogging();
services.AddOptions();
services.AddSingleton(new WidgetClock());
});
That migration is usually the highest-leverage cleanup in suites that centralize framework bootstrap inside a shared helper. One helper rewrite removes a noisy anti-pattern from many tests at once.
Analyzer note:
FMOQ0013warns on direct FastMoqIServiceProvider,IServiceScopeFactory, andIServiceScopeshim setup, plus manual scope-factory extraction, so those helpers move towardCreateTypedServiceProvider(...),CreateTypedServiceScope(...),AddServiceProvider(...), orAddServiceScope(...).- the built-in fix covers the direct tracked-provider and tracked-scope cases, manual scope-factory extraction, direct
IServiceScope.ServiceProvidersetup, and the pairedCreateScope()case whenFMOQ0013can recover the provider expression from the same helper block. It addsusing FastMoq.Extensions;when needed. More entangled shim setups still need manual migration.
If the same helper layer also hand-rolls IOptions<T> setup, move that boilerplate at the same time:
Mocks.SetupOptions(new WorkerClientOptions
{
RetryCount = 3,
Endpoint = "https://fastmoq.dev"
});
Keep AddType(instance) for non-options real instances such as MemoryCache, clocks, or fixed framework objects. Use SetupOptions(...) only for the repeated IOptions<T> wrapper shape.
Temporary compatibility cleanup in shared helpers
When you are already touching shared framework helpers, treat these as high-priority cleanup targets:
GetMock<IServiceProvider>()orGetOrCreateMock<IServiceProvider>()used to emulate typed DI- context-aware compatibility
AddType(...)overloads that are really framework-type customizations - local
FunctionContext.InstanceServiceswrappers that do not build a real provider
Those patterns are still supported to keep v4 migrations moving, but they are the exact spots where a small helper rewrite gives the biggest long-term payback.
Test-framework output helpers: keep the adapter local
If a shared helper still forwards a test-framework output object directly into FastMoq constructor-check helpers, keep the framework adapter local and pass a neutral line-writer callback into FastMoq instead.
Before:
action.EnsureNullCheckThrown(parameterName, constructorName, output);
After:
action.EnsureNullCheckThrown(parameterName, constructorName, output.WriteLine);
Why this matters:
- this FastMoq path is now framework-neutral
- the helper migration is usually one local signature fix rather than a rewrite of every leaf test
- the same shape works with xUnit, NUnit, MSTest, or an in-memory list collector because FastMoq only needs
Action<string>
Recommended helper shape:
- keep the test-framework output type in the test project if that project wants it
- adapt it to
Action<string>at the helper boundary before calling FastMoq - if the helper only used output for occasional diagnostics, prefer dropping the callback entirely instead of preserving framework-specific plumbing forever
Shared logger helper wrappers: use first-party registrations
If a shared helper only exists to register ILoggerFactory, ILogger, or ILogger<T>, replace that wrapper with the first-party logging helpers instead of preserving a private adapter layer.
Direct Mocker registration:
Mocks.AddLoggerFactory();
var logger = Mocks.GetObject<ILogger<WidgetProcessor>>();
logger.LogInformation("processing complete");
Mocks.VerifyLogged(LogLevel.Information, "processing complete");
Typed provider composition:
var loggerFactory = Mocks.CreateLoggerFactory();
Mocks.AddServiceProvider(services =>
{
services.AddSingleton(loggerFactory);
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
});
If the suite wants logger output mirrored to a line writer or other sink after Mocker already exists, use the sink-aware overloads instead of preserving a private logger wrapper:
Mocks.AddLoggerFactory(output.WriteLine, replace: true);
var loggerFactory = Mocks.CreateLoggerFactory((logLevel, eventId, message, exception) =>
{
output.WriteLine($"[{logLevel}] {message}");
});
Keep any test-framework-specific output adapter local to the test project. The adapter should stop at Action<string> or the raw logging callback overload rather than pushing a framework dependency through shared FastMoq helpers. If the suite truly needs extra providers, continue to plug them into CreateLoggerFactory(...) through the logging-builder callback.
Analyzer note:
FMOQ0003prefersVerifyLogged(...)over legacyVerifyLogger(...)when the assertion can stay provider-safe.FMOQ0019prefersSetupOptions(...)over repeated manualIOptions<T>setup.FMOQ0030prefersAddLoggerFactory(...)over directAddType<ILoggerFactory>(new ...output-helper...),AddType<ILogger>(new ...output-helper...), andAddType<ILogger<T>>(new ...output-helper...)registrations when the logger registration is just mirroring logs into an xUnit-style output helper. The diagnostic is advisory only because repo-local logger wrappers can carry extra formatting, filtering, or scope behavior that FastMoq cannot prove is safe to rewrite automatically.FMOQ0036prefersSetupLoggerCallback(...)over trackedGetOrCreateMock<ILogger...>().Setup(x => x.Log(...)).Callback(...)chains when the callback only reads normalized level, event id, message text, or exception output. It intentionally does not rewrite callbacks that inspect raw structured logger state.
Web test helpers
For controller tests, request-driven tests, and IHttpContextAccessor-driven tests, prefer the FastMoq.Web helpers instead of continuing to hand-roll local request and principal setup.
For Blazor component tests, prefer the dedicated bUnit and Blazor test migration guide. This page stays focused on HTTP, controller, principal, and general framework-helper migration rather than component-rendering API changes.
Package note:
- if the test project references the aggregate
FastMoqpackage, these helpers are already available - if the test project references
FastMoq.Coredirectly, addFastMoq.Webbefore migrating local controller, principal, orHttpContexthelpers to the built-in web helper surface - for the full package-choice overview, see Getting Started package choices
The main helpers to look for during migration are:
CreateHttpContext(...)CreateControllerContext(...)SetupClaimsPrincipal(...)AddHttpContext(...)AddHttpContextAccessor(...)
These helpers usually replace a large amount of repetitive local test setup. In many migrated suites, existing repo-local helpers become thin wrappers over FastMoq.Web first, and only later get simplified or removed.
Principal decision table
| Test shape | Preferred helper | Notes |
|---|---|---|
| Role-only test user | SetupClaimsPrincipal(params roles) or CreateControllerContext(params roles) |
Best default when the test only cares about authenticated roles and the compatibility defaults are acceptable. |
| Explicit custom claims | SetupClaimsPrincipal(claims, options) |
Use IncludeDefaultIdentityClaims = false when exact claim preservation matters more than compatibility backfilling. |
Controller test reading ControllerContext.HttpContext.User |
CreateControllerContext(...) |
Prefer assigning ControllerContext directly instead of creating a principal separately and wiring HttpContext.User by hand. |
Important custom-claims note
Custom-claim scenarios can behave differently from role-only helpers.
FastMoq.Web adds compatibility defaults for identity-style claims unless told not to. That means role-only or convenience overloads are useful for common tests, but exact-claim tests should be explicit about the options they want.
If a test must preserve the exact incoming claims for ClaimTypes.Name, ClaimTypes.Email, or related identity values, disable compatibility backfilling:
var principal = Mocks.SetupClaimsPrincipal(
claims,
new TestClaimsPrincipalOptions
{
IncludeDefaultIdentityClaims = false,
});
Preferred v4 MVC controller pattern
When the controller reads from ControllerContext.HttpContext.User, prefer assigning the controller context during component creation.
using FastMoq.Web.Extensions;
public class OrdersControllerTests : MockerTestBase<OrdersController>
{
private HttpContext requestContext = default!;
protected override Action<Mocker>? SetupMocksAction => mocker =>
{
requestContext = mocker
.CreateHttpContext("Admin")
.SetRequestHeader("X-Correlation-Id", "corr-123")
.SetQueryParameter("includeInactive", "true");
mocker.AddHttpContextAccessor(requestContext);
mocker.AddHttpContext(requestContext);
};
protected override Action<OrdersController> CreatedComponentAction => controller =>
{
controller.ControllerContext = Mocks.CreateControllerContext(requestContext);
};
}
That is the preferred v4 migration target for older controller helpers that manually created a DefaultHttpContext, assigned User, and then copied that state into both DI and ControllerContext.
Preferred custom-claims pattern
When the test depends on specific ClaimTypes.Name, email, or other identity semantics, build the principal explicitly and keep compatibility defaults off when exact preservation matters.
using System.Security.Claims;
using FastMoq.Web;
using FastMoq.Web.Extensions;
public class OrdersControllerClaimTests : MockerTestBase<OrdersController>
{
protected override Action<OrdersController> CreatedComponentAction => controller =>
{
var claims = new[]
{
new Claim(ClaimTypes.Name, "Adele Vance"),
new Claim(ClaimTypes.Email, "adele.vance@microsoft.com"),
new Claim(ClaimTypes.Role, "Admin"),
};
controller.ControllerContext = Mocks.CreateControllerContext(
claims,
new TestClaimsPrincipalOptions
{
IncludeDefaultIdentityClaims = false,
});
};
}
This avoids the common migration mistake of assuming the role-only overloads and the custom-claims overloads behave identically.
Migrating local web helpers
If a suite already has local SetupClaimsPrincipal(...), CreateControllerContext(...), or similar helpers, the recommended migration path is:
- Re-point the local wrapper to
FastMoq.Webfirst. - Keep existing call sites stable until the suite is green.
- Simplify or remove the wrapper later only if the direct FastMoq call sites are actually clearer.
That approach keeps the migration low-risk. After v4 migration, those local helpers often become thin wrappers rather than remaining hand-rolled infrastructure.
If the suite already exposes a local CreateHttpRequest(...) helper, use the same rule. Prefer building the underlying request with CreateHttpContext(...), SetRequestHeader(...), SetRequestBody(...), and SetRequestJsonBody(...), then return httpContext.Request from the local wrapper if that is still the clearest call-site shape.
Keep only the repo-specific parts local. For example, if the app stamps custom encoded principal headers, tenant headers, or other product-specific request metadata, keep that serialization in the local helper but stop hand-rolling the DefaultHttpContext, header collection, and request body wiring.
using FastMoq.Web.Extensions;
private static HttpRequest CreateHttpRequest(
Mocker mocks,
IServiceProvider? services = null,
string? requestBody = null,
ClaimsPrincipal? principal = null)
{
var httpContext = principal is null
? mocks.CreateHttpContext(static _ => { })
: mocks.CreateHttpContext(principal, static _ => { });
if (services is not null)
{
httpContext.RequestServices = services;
}
if (requestBody is not null)
{
httpContext.SetRequestBody(requestBody, contentType: "application/json");
}
// Keep app-specific request headers local to the suite.
return httpContext.Request;
}
That keeps the common ASP.NET Core request mechanics on the FastMoq side while still leaving room for suite-specific headers or claim serialization that FastMoq should not hard-code.