ASP.NET Core
This guide targets ASP.NET Core on .NET 8 or later (Minimal API or MVC). The Avsb.SDK NuGet package adds an AddAvsb() extension for your service collection, middleware that reads the signed-in user for each request, and one AvsbServer class you inject anywhere you need a flag. By the end you will read flags in controllers, Razor Pages, and Blazor components, and track events through the same client.
Install
Add the Avsb.SDK NuGet package with dotnet add package.
Obtain your SDK key
Open your A vs B project and click Environments in the sidebar. Copy the SDK key for the environment this build talks to.
Register the SDK in Program.cs
Call AddAvsb() on the service collection, then await InitAsync() once before the app starts serving requests.
Inject AvsbServer into a controller
Constructor-inject AvsbServer and read typed flags inside your actions.
Read a flag
Every typed method returns a Flag<T>. Pass the current request's context so the read is scoped to the right visitor.
Track an event
Record a conversion with _avsb.Track or a full order with TrackPurchaseAsync.
Identify a user
AvsbMiddleware builds a context from the signed-in user on every request. To evaluate for someone else mid-request, build a second context and pass it straight to the getter.
Install the package:
dotnet add package Avsb.SDKVersion 1.0.1 of Avsb.SDK is on its way to NuGet with this release. Until it arrives there, dotnet add package reports that no versions are available.
Open your A vs B project and click Environments in the sidebar. It sits on its own there, next to Settings, not inside it. Each environment card shows a masked SDK key with Reveal and Copy buttons.
- Environments lives in the sidebar on its own, not inside Settings.
- Click Reveal to see the full key, then Copy to copy it.
Store your SDK key in User Secrets for development, or in appsettings.json per environment everywhere else:
dotnet user-secrets set "Avsb:SdkKey" "sdk_production_..."Register the SDK in Program.cs:
using Avsb.SDK;using Avsb.SDK.AspNetCore;var builder = WebApplication.CreateBuilder(args);builder.Services.AddAvsb(options =>{ options.SdkKey = builder.Configuration["Avsb:SdkKey"]!;});var app = builder.Build();// AddAvsb only registers the client. Nothing fetches the flag data for you,// so ask for it once before the app starts taking requests.var avsb = app.Services.GetRequiredService<AvsbServer>();await avsb.InitAsync();// Build the per-request context from the signed-in user, before// AvsbMiddleware runs (it only builds its own default when none is set yet).app.Use(async (context, next) =>{ var userId = context.User.FindFirst("sub")?.Value ?? "anon"; var plan = context.User.FindFirst("plan")?.Value ?? "free"; context.SetAvsbContext(EvalContext.User(userId, attributes: new Dictionary<string, object?> { ["plan"] = plan })); await next(context);});app.UseMiddleware<AvsbMiddleware>();app.MapControllers();await app.RunAsync();AddAvsb only adds AvsbServer to the container. It does not call InitAsync. Skip that call and every flag quietly answers with your default value and Flag.Source set to NotReady, no exception thrown, which looks exactly like every flag being off. Always await InitAsync() once, right after builder.Build().
Inject AvsbServer into a controller:
using Avsb.SDK;using Avsb.SDK.AspNetCore;using Microsoft.AspNetCore.Mvc;[ApiController][Route("[controller]")]public class CheckoutController : ControllerBase{ private readonly AvsbServer _avsb; public CheckoutController(AvsbServer avsb) => _avsb = avsb; [HttpGet] public IActionResult Get() { var ctx = HttpContext.GetAvsbContext(); var showNew = _avsb.GetBoolFlag("checkout_v2", false, ctx); var theme = _avsb.GetStringFlag("ui_theme", "default", ctx); return Ok(new { showNewCheckout = showNew.Value, theme = theme.Value }); }}Reading flags of different types:
var ctx = HttpContext.GetAvsbContext();// Boolean flagvar checkout = _avsb.GetBoolFlag("checkout_v2", false, ctx);if (checkout.IsEnabled()) { /* serve new experience */ }// String flagvar theme = _avsb.GetStringFlag("ui_theme", "default", ctx);// JSON flag (deserialised straight into a type of your choosing)var config = _avsb.GetJsonFlag<PricingConfig>("pricing_config", PricingConfig.Default, ctx);// Every read carries metadata about how it was decidedConsole.WriteLine($"Source: {checkout.Source}, Variation: {checkout.VariationKey}");GetBoolFlag never throws and never guesses. Ask for a boolean and the flag turns out to be a string, and you get your own default back with Source set to NotFound, plus a logged warning naming the mismatch.
Tracking events. Track is for a single conversion; TrackPurchaseAsync is for a full order and is sent right away instead of on the usual batch timer:
// A plain conversion, no extra data._avsb.Track("signup_completed", ctx);// A conversion with a revenue amount attached._avsb.Track("purchase", ctx, new TrackPayload { Revenue = 149.99 });// A full order with line items.await _avsb.TrackPurchaseAsync(ctx, new PurchaseOrder{ OrderId = "ORD-1001", Total = 149.99, Currency = "USD", Items = new[] { new PurchaseOrderItem { Sku = "SKU-1", Price = 149.99, Quantity = 1 }, },});Money is decimal major units, the same way you would show it on a receipt: 149.99, never 14999.
Identity per request
AvsbMiddleware reads HttpContext.User.Identity.Name and stores a context on the request, which you read back with HttpContext.GetAvsbContext(). Override that default earlier in the pipeline with HttpContext.SetAvsbContext(...), as shown in Program.cs above.
To evaluate for someone other than the signed-in user mid-request, for example checking what an account owner would have seen, build a second context and pass it straight to the getter. Every read takes its context as a plain argument, so there is no separate scoped client to fetch first:
var ownerCtx = EvalContext.User(order.AccountOwnerId.ToString());var flag = _avsb.GetBoolFlag("checkout_v2", false, ownerCtx);Using flags in Blazor
Inject AvsbServer into a Blazor Server component. A component has no HttpContext of its own, so build the context yourself from the authentication state, the same fact AvsbMiddleware reads for a normal request. For Blazor WebAssembly, use the browser-side @avsbhq/react or @avsbhq/browser packages instead, since no .NET code runs on the server for that hosting model.
@inject AvsbServer Avsb@inject AuthenticationStateProvider AuthStateProvider@if (_checkout.IsEnabled()){ <NewCheckoutComponent />}else{ <LegacyCheckoutComponent />}@code { private Flag<bool> _checkout; protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var userId = authState.User.Identity?.Name ?? "anon"; _checkout = Avsb.GetBoolFlag("checkout_v2", false, EvalContext.User(userId)); }}Shutdown
AddAvsb registers AvsbServer as a singleton, and it implements IAsyncDisposable. The built-in dependency injection container calls DisposeAsync on every such singleton when the host shuts down, with no extra configuration on your part: the background refresh stops and any queued events are sent before the process exits.
Testing
Give the test host its own AvsbServer, built with Bootstrap set to a small hand-written flag file and Polling/Tracking turned off, so a test run never touches the network. Registering it again after AddAvsb already ran is enough: ASP.NET Core's container hands back the last registration for a type when there is more than one.
using System.Net.Http.Json;using Avsb.SDK;using Microsoft.AspNetCore.Mvc.Testing;using Microsoft.Extensions.DependencyInjection;using Xunit;public sealed class CheckoutControllerTests : IClassFixture<WebApplicationFactory<Program>>{ private const string CheckoutV2OnDatafile = """ { "version": 2, "projectType": "FEATURE_FLAG", "sdkKey": "sdk_test_abcdefgh", "environmentKey": "test", "publishedAt": "2026-01-01T00:00:00.000Z", "collectEndpoint": "https://ingest.avsb.cloud", "flags": [{ "id": "flag_checkout_v2", "key": "checkout_v2", "type": "boolean", "enabled": true, "defaultVariationId": "var_on", "variations": [{ "id": "var_on", "key": "on", "value": true }], "overrides": [], "rules": [] }], "audiences": [] } """; private readonly HttpClient _client; public CheckoutControllerTests(WebApplicationFactory<Program> factory) { var testServer = factory.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddSingleton(new AvsbServer(new AvsbServerOptions { SdkKey = "sdk_test_abcdefgh", Bootstrap = CheckoutV2OnDatafile, Polling = false, Tracking = false, })))); _client = testServer.CreateClient(); } private sealed record CheckoutResponse(bool ShowNewCheckout, string Theme); [Fact] public async Task ReturnsNewCheckout_WhenFlagIsOn() { var response = await _client.GetAsync("/checkout"); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadFromJsonAsync<CheckoutResponse>(); Assert.True(body!.ShowNewCheckout); }}A Bootstrap-only server never touches the network, so the test needs no fake HTTP handler and runs in the time a normal unit test takes.
What's next
- Multi-context targeting: combine user and organization contexts in one evaluation.
- Spring Boot integration: the same pattern for Java.