File: BlazorWasmHotReloadMiddleware.cs
Web Access
Project: ..\..\..\src\BuiltInTools\BrowserRefresh\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj (Microsoft.AspNetCore.Watch.BrowserRefresh)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Watch.BrowserRefresh
{
    /// <summary>
    /// A middleware that manages receiving and sending deltas from a BlazorWebAssembly app.
    /// This assembly is shared between Visual Studio and dotnet-watch. By putting some of the complexity
    /// in here, we can avoid duplicating work in watch and VS.
    ///
    /// Mapped to <see cref="ApplicationPaths.BlazorHotReloadMiddleware"/>.
    /// </summary>
    internal sealed class BlazorWasmHotReloadMiddleware
    {
        internal sealed class Update
        {
            public int Id { get; set; }
            public Delta[] Deltas { get; set; } = default!;
        }
 
        internal sealed class Delta
        {
            public string ModuleId { get; set; } = default!;
            public string MetadataDelta { get; set; } = default!;
            public string ILDelta { get; set; } = default!;
            public string PdbDelta { get; set; } = default!;
            public int[] UpdatedTypes { get; set; } = default!;
        }
 
        private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        };
 
        public BlazorWasmHotReloadMiddleware(RequestDelegate next, ILogger<BlazorWasmHotReloadMiddleware> logger)
        {
            logger.LogDebug("Middleware loaded");
        }
 
        internal List<Update> Updates { get; } = [];
 
        public Task InvokeAsync(HttpContext context)
        {
            // Multiple instances of the BlazorWebAssembly app could be running (multiple tabs or multiple browsers).
            // We want to avoid serialize reads and writes between then
            lock (Updates)
            {
                if (HttpMethods.IsGet(context.Request.Method))
                {
                    return OnGet(context);
                }
                else if (HttpMethods.IsPost(context.Request.Method))
                {
                    return OnPost(context);
                }
                else
                {
                    context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
                    return Task.CompletedTask;
                }
            }
 
            // Don't call next(). This middleware is terminal.
        }
 
        private async Task OnGet(HttpContext context)
        {
            if (Updates.Count == 0)
            {
                context.Response.StatusCode = StatusCodes.Status204NoContent;
                return;
            }
 
            await JsonSerializer.SerializeAsync(context.Response.Body, Updates, s_jsonSerializerOptions);
        }
 
        private async Task OnPost(HttpContext context)
        {
            var update = await JsonSerializer.DeserializeAsync<Update>(context.Request.Body, s_jsonSerializerOptions);
            if (update == null)
            {
                context.Response.StatusCode = StatusCodes.Status400BadRequest;
                return;
            }
 
            // It's possible that multiple instances of the BlazorWasm are simultaneously executing and could be posting the same deltas
            // We'll use the sequence id to ensure that we're not recording duplicate entries. Replaying duplicated values would cause
            // ApplyDelta to fail.
            if (Updates is [] || Updates[^1].Id < update.Id)
            {
                Updates.Add(update);
            }
        }
    }
}