Cookies

Cookies store data across requests.

Key points:

Session State

Storage of user data while the user browses a web app.

  • Backed by a cache (ephemeral)
  • Critical user data should be stored in database and cached only as a performance optimization

Works by:

  • Providing a cookie to the client that contains a session ID
  • Session cookie is sent to the app w/each request and used by the app to fetch session data

Key points:

  • Session cookie is specific to the browser
  • Session cookie is deleted when the browser session expires or when ISession.Clear is called
  • Expiration:
    • App retains session for 20 minutes (default) after last request
    • If client sent a cookie for an expired session, a new session is created w/same session cookie
  • Empty sessions are not retained
  • The session cooke is encrypted via IDataProtector
  • Do not store sensitve data in session state

Configuring Session State

Enable session middleware in Program.cs:

Example
Program.cs

// …
builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromSeconds(10); // This is arbitrarily short
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});
// …
app.UseSession(); // AFTER UseRouting and BEFORE MapRazorPages and MapDefaultControllerRoute
// …

A new session (with a new session cookie) cannot be created after the app has started writing to the response stream.

Loading Session State

Call ISession.LoadAsync to load session records from the underlying IDistributedCache.

  • If this call is not made before calling TryGetValue, Set, or Remove methods, it will be loaded synchronously.

Session Options

Use SessionOptions to override session defaults:

builder.Services.AddSession(options =>
{
    options.Cookie.Name = ".AdventureWorks.Session";
        // How long a session can be idle before its contents are abandoned in server's cache
    options.IdleTimeout = TimeSpan.FromSeconds(10); 
    options.Cookie.IsEssential = true;
});

Set and Get Session Values

Session state is accessed via HttpContext.Session (an ISession implementation).
ISession has extension methods in Microsoft.AspNetCore.Http.

Getting a Session Value — Razor Pages

@page
@using Microsoft.AspNetCore.Http
@model IndexModel
<!-- ... -->
Name: @HttpContext.Session.GetString(IndexModel.SessionKeyName)

Getting a Session Value — MVC

public class IndexModel : PageModel
{
    public const string SessionKeyName = "_Name";
    public const string SessionKeyAge = "_Age";

    private readonly ILogger<IndexModel> _logger;

    public IndexModel(ILogger<IndexModel> logger) { _logger = logger; }

    public void OnGet()
    {
        if (string.IsNullOrEmpty(HttpContext.Session.GetString(SessionKeyName)))
        {
            HttpContext.Session.SetString(SessionKeyName, "The Doctor");
            HttpContext.Session.SetInt32(SessionKeyAge, 73);
        }
        var name = HttpContext.Session.GetString(SessionKeyName);
        var age = HttpContext.Session.GetInt32(SessionKeyAge).ToString();

        _logger.LogInformation("Session Name: {Name} and Age: {Age}", name, age);
    }
}

Serialization

Session data must be serialized to enable a distributed cache scenario.
ISession extension methods (in Web.Extensions) can serialize strings and integers.
Complex types require another mechanism, such as JSON.

Example — Serializing:

public static class SessionExtensions
{
    public static void Set<T>(this ISession session, string key, T value)
    {
        session.SetString(key, JsonSerializer.Serialize(value));
    }

    public static T? Get<T>(this ISession session, string key)
    {
        var value = session.GetString(key);
        return value == null ? default : JsonSerializer.Deserialize<T>(value);
    }
}

Example — Get & Set a serializable object via SessionExtensions

using Microsoft.AspNetCore.Mvc.RazorPages;
using Web.Extensions;    // SessionExtensions

namespace SessionSample.Pages
{
    public class Index6Model : PageModel
    {
        const string SessionKeyTime = "_Time";
        public string? SessionInfo_SessionTime { get; private set; }
        private readonly ILogger<Index6Model> _logger;

        public Index6Model(ILogger<Index6Model> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
            var currentTime = DateTime.Now;

            // Requires SessionExtensions from sample.
            if (HttpContext.Session.Get<DateTime>(SessionKeyTime) == default)
                HttpContext.Session.Set<DateTime>(SessionKeyTime, currentTime);
            
            _logger.LogInformation("Current Time: {Time}", currentTime);
            _logger.LogInformation("Session Time: {Time}", HttpContext.Session.Get<DateTime>(SessionKeyTime));

        }
    }
}

TempData

A property that stores data until the next request.
Available in both Razor Pages and MVC.

Example

A page that creates a customer:

public class CreateModel : PageModel
{
    private readonly RazorPagesContactsContext _context;

    public CreateModel(RazorPagesContactsContext context) { _context = context;     }

    public IActionResult OnGet() { return Page(); }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
            return Page();

        _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";

        return RedirectToPage("./IndexPeek");
    }
}

A page that displays the TempData message:

@page
@model IndexModel

<h1>Peek Contacts</h1>

@{
    if (TempData.Peek("Message") != null)
        <h3>Message: @TempData.Peek("Message")</h3>
}
<!-- ... -->

At the end of this request, TempData["Message"] is NOT deleted because Peek is used.
Similarly, Keep can be used to also persist the data without peeking it.
If TempData was simply viewed via TempData["Message"], it would have been deleted.

TempData Providers

There is a cookie TempData provider and session TempData provider. The cookie TempData` provider (the default) uses its own cookie provider. It is encrypted, encoded, and chunked. It is not compressed—compressing encrypted data can lead to security vulnerabilities.

Choosing a provider

Use the cookie-based TempData provider is if:

  • the app uses TempData sparingly or for data amounts <= 500 bytes
  • the app runs on a server farm

Use the session-based TempData provider if:

  • the app already uses session state, or
  • the app uses TempData frequently or for data amounts > 500 bytes

To use the session TempData provider:

builder.Services.AddRazorPages()
                .AddSessionStateTempDataProvider();

or

builder.Services.AddControllersWithViews()
                .AddSessionStateTempDataProvider();

HttpContext.Items

HttpContext.Items is a collection used to store data while processing a single request. The collection is emptied after the request is processed.

Example

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

ILogger logger = app.Logger;

app.Use(async (context, next) =>
{
    // context.Items["isVerified"] is null
    logger.LogInformation($"Before setting: Verified: {context.Items["isVerified"]}");
    context.Items["isVerified"] = true;
    await next.Invoke();
});

app.Use(async (context, next) =>
{
    // context.Items["isVerified"] is true
    logger.LogInformation($"Next: Verified: {context.Items["isVerified"]}");
    await next.Invoke();
});

app.MapGet("/", async context => { await context.Response.WriteAsync($"Verified: {context.Items["isVerified"]}"); });

app.Run();

Use an object as the item key to avoid a key collision. This is mostly used for middleware that’s shared between apps:

public class HttpContextItemsMiddleware
{
    private readonly RequestDelegate _next;
    public static readonly object HttpContextItemsMiddlewareKey = new();

    public HttpContextItemsMiddleware(RequestDelegate next) => _next = next;

    public async Task Invoke(HttpContext httpContext)
    {
        httpContext.Items[HttpContextItemsMiddlewareKey] = "K-9";
        await _next(httpContext);
    }
}

public static class HttpContextItemsMiddlewareExtensions
{
    public static IApplicationBuilder 
        UseHttpContextItemsMiddleware(this IApplicationBuilder app) =>
            return app.UseMiddleware<HttpContextItemsMiddleware>();
}

Other Mechanisms

Query Strings — Data can be passed from one request to another by adding it to the next request’s query string. This has risks.
Hidden Fields — Data can be saved in hidden form fields and posted back on the next request. This has risks.
Caching — Data can be cached for responses or application-wide.