Skip to content
Shiny.Net.HttpServer v1 - A lightweight feature rich HTTP Server - Tunnels, Websockets, AOT, ASPNET Featureset, & Works EVERYWHERE!Let me see!

Sessions

builder.Services.AddSessions(o =>
{
o.Protector = new TicketProtector(keyBytes);
o.IdleTimeout = TimeSpan.FromMinutes(30);
});
var app = builder.Build();
app.UseSessions();

ISession is registered as scoped, so a handler or endpoint class takes one in its constructor and never reaches for HttpContext:

[Route("/cart")]
public class CartEndpoints(ISession session)
{
[Post("/{sku}")]
public async Task<IActionResult> Add(string sku, CancellationToken ct)
{
var count = session.GetInt32("items") ?? 0;
session.SetInt32("items", count + 1);
await session.CommitAsync(ct);
return new NoContentResult();
}
}

ctx.Session is there too, for middleware — which has a context and no container scope of its own.

ISession stores byte[], and the extensions cover the usual types without hand-rolling the conversion: GetString/SetString, GetInt32, GetInt64, GetBoolean, GetGuid, GetDateTimeOffset, GetDouble (invariant-culture, so a session written on one machine reads the same on another).

Member Notes
Id Stable across requests; never sent to the client in the clear
IsAvailable True once loaded — reading or writing anything loads it
Keys Loads the session
LoadAsync / CommitAsync Explicit control
TryGetValue / Set / Remove / Clear Clear empties without ending it — the id and cookie stay

Committing also happens automatically when the response completes. Call CommitAsync explicitly when the handler is about to do something that depends on the write having landed — redirecting to a page that reads it back, for instance.

Property Default
Protector required
IdleTimeout 20 minutes — every request that loads the session restarts the clock
CookieName .shiny.session
CookiePath / CookieDomain / / none
HttpOnly true
SecurePolicy SameAsRequest
SameSite Lax

The cookie deliberately has no Expires: a session cookie dies with the browser session, and the store’s idle timeout is what actually bounds its life. SameSite=Lax means the cookie rides top-level navigations but not cross-site posts, which is most of CSRF gone without an anti-forgery token.

The session id travels in a cookie protected by the same TicketProtector cookie authentication uses — encrypted, not merely signed, because an id read from a log or a proxy can be replayed. Session fixation is the oldest trick there is.

var key = TicketProtector.CreateKey(); // 32 random bytes — store these
o.Protector = new TicketProtector(key);
// during a key rotation, old cookies keep working:
o.Protector = new TicketProtector(newKey, oldKey);

TicketProtector.FromSecret("…") derives one from a passphrase. It is a convenience, not a substitute: the strength of everything here is the strength of that string.

The id itself is 256 bits from a cryptographic source. A guessable session id is the same failure as a guessable password, with none of the warning signs.

The store is not touched until something is read or written, and no cookie is issued for a visitor whose session stayed empty — otherwise every request for a static file would mint a session nobody asked for.

ISessionStore is pluggable, with an in-memory default:

builder.Services.AddSessions(
sp => new RedisSessionStore(sp.GetRequiredService<IConnectionMultiplexer>()),
o => o.Protector = new TicketProtector(key)
);

Sessions are lost on a restart, which is the honest trade for having no dependency — anything that must survive one belongs in a database. InMemorySessionStore has a Capacity (10,000 by default) and a Prune() for expired entries.

The commit runs in a finally. State written before a handler threw is still saved — it is state the user already caused.

Put UseSessions() before anything that reads a session, and after authentication if a handler keys session state by user.