2

The application is Blazor Server and the question is very similar to Scope in Middleware and Blazor Component but it has not been active for long and I've clarified a few parts.

I've written a middleware that reads a cookie from the current request. A scoped service has been injected (singleton is not suitable since it's per user) via InvokeAsync and it gets updated with a value from the cookie. The same service is injected in a page component but unfortunately it's not the same instance of the service.

I've tried both render-mode="Server" and render-mode="ServerPrerendered". They both behave differently as you would expect but nevertheless it is not the same instance as the one created in the middleware. In render-mode Server the service is injected once as you expected and in render-mode ServerPrerendered the service is injected twice, once for the prerendered page and once for the interactive page. My goal is to have the same scoped service injected for the request in the middleware to also be injected in the page component. Is this possible?

Code for adding middleware (a bit simplified but still same problem). I've added some filtering since I'm only interested in the page request:

app.UseWhen(
    context =>
        {
            return (context.Request.Path.StartsWithSegments("/_content") ||
                    context.Request.Path.StartsWithSegments("/_framework") ||
                    context.Request.Path.StartsWithSegments("/_blazor") ||
                    context.Request.Path.StartsWithSegments("/images") ||
                    context.Request.Path.StartsWithSegments("/favicon.ico") ||
                    context.Request.Path.StartsWithSegments("/css")) == false;                     
        }
    , builder => builder.UseSettingsMiddleware());

Adding the scoped service:

public void ConfigureServices(IServiceCollection services)
{
   /* all other services added before this */

   services.AddScoped<IThemeService, ThemeService>();
}

The middleware:


public class ThemeMiddleware
{
    private readonly RequestDelegate _next;
    private string _id;

    public ThemeMiddleware(RequestDelegate next)
    {
        _next = next;
        _id = Guid.NewGuid().ToString()[^4..];
    }

    public async Task InvokeAsync(HttpContext httpContext, IThemeService themeService)
    {            
        var request = httpContext.Request;
        string path = request.Path;

        string theme = request.Cookies["App.Theme"];
            
        Debug.WriteLine($"Middleware [{_id}]: Service [{themeService.GetId()}] | Request Path={path} | Theme={theme}");

        if(string.IsNullOrEmpty(theme) == false)
        {
            themeService.SetTheme(theme);
        }                                              

        await _next(httpContext);
    }
}

The service:

public class ThemeService : IThemeService, IDisposable
{
    string _theme = "default";
    string _id;
    string dateTimeFormat = "ss.fffffff";

    public ThemeService()
    {
        _id = Guid.NewGuid().ToString()[^4..];
    }

    public void Dispose() { }

    public string GetId() { return _id; }
            
    public string GetTheme()
    {            
        Debug.WriteLine($"ThemeService [{_id}]: GetTheme={DateTime.Now.ToString(dateTimeFormat)}");
        return _theme;
    }

    public void SetTheme(string theme)
    {
        Debug.WriteLine($"ThemeService [{_id}]: SetTheme={DateTime.Now.ToString(dateTimeFormat)}");
        _theme = theme;
    }
}

The component (basically same code also exists in MainLayout.razor):

@page "/"
@inject IThemeService ThemeService

@code {
    
    protected override async Task OnInitializedAsync()
    {        
        System.Diagnostics.Debug.WriteLine($"Index.razor: Service [{ThemeService.GetId()}]");
    }
}

Output

render-mode=Server

Middleware [399d]: Service [1f37] | Request Path=/ | Theme=dark
ThemeService [1f37]: SetTheme=00.5996142
MainLayout.razor: Service [4e96]
ThemeService [4e96]: GetTheme=01.0375910
Index.razor: Service [4e96]

render-mode=ServerPrerendered

Middleware [982d]: Service [5fa8] | Request Path=/ | Theme=dark
ThemeService [5fa8]: SetTheme=03.2477461
MainLayout.razor: Service [5fa8]
ThemeService [5fa8]: GetTheme=03.3576799
Index.razor: Service [5fa8]
MainLayout.razor: Service [d27c]
ThemeService [d27c]: GetTheme=03.9510551
Index.razor: Service [d27c]

The service id is actually the same in the prerendered request but not in the interactive one which is the one that counts. Any ideas on how to move forward?

Andreas
  • 31
  • 5
  • Unfortunately, you might need a different approach. The obvious "easy" option for simple data required at startup is to pass it as a parameter to the `App` component that your render as the base of your Blazor server. That could - in turn - be used to prime the ThemeService that is in scope for Blazor. – Mister Magoo Dec 23 '21 at 16:19
  • You may also be able to just read the cookie directly in the Blazor code using JSInterop. – Mister Magoo Dec 23 '21 at 16:19
  • Please also paste the dependency injection details of your services - are they registered as scoped, etc. – aleksander_si Dec 26 '21 at 15:30
  • I updated the post with service registration @aleksander_si. As you can see it is registered as scoped which is important since the information is per user. – Andreas Dec 27 '21 at 10:44
  • @Andreas check if my answer is helpful. – aleksander_si Dec 27 '21 at 12:52

2 Answers2

0

I have seen a similar problem and was able to solve it as follows:

  1. Prepare a wrapper class similar to the one below:
public static class Wrapper<T>
{
    private static AsyncLocal<T> _value = new AsyncLocal<T>();

    public static T CurrentValue
    {
        get
        {
            return _value.Value;
        }
        set
        {
            _value.Value = value;
        }
    }
}
  1. prepare a middleware:
public class TestMiddleware<T>: IMiddleware
{
    public virtual async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        Wrapper<T>.CurrentValue = /* set references */;

        await next(context);
    }
}
  1. You may now access the Wrapper<T>.CurrentValue from a Blazor page or a scoped / transient service which was instantiated in the current Blazor circuit.

The root cause is, if I remember correctly by looking in the source code, that the Blazor DI scope is a new instance and not the middleware DI scope.

aleksander_si
  • 1,021
  • 10
  • 28
  • I tried your suggestion @aleksander_si but it doesn't work and I suspect it's due to not being the same async contexts or not spawned from the same context. It basically behaved exactly the same as the initial code. I did however implement the feature in a more simple way and I'll update the post shortly. – Andreas Dec 28 '21 at 09:57
  • Just one additional note - the order of middleware is important - you should add it before mapping endpoints. See the WebApp sample of my suggestion: https://github.com/akovac35/Logging.Samples – aleksander_si Dec 28 '21 at 19:46
  • Yeah, endpoint-mappings are the last things being done. – Andreas Dec 29 '21 at 07:54
0

I opted for another solution to the initial problem, i.e. read a cookie-value and have it available in an injected service on the component-level. I would have preferred to handle this as a middleware, but unfortunately never found a way to do this except when using a singleton service and that is not an option.

Startup.cs:

services.AddScoped<IThemeService, ThemeService>();

ThemeService.cs:

public class ThemeService : IThemeService, IDisposable
{
    string _id;
    string _theme = "default";
    string _themeCookieName;

    public ThemeService(IHttpContextAccessor contextAccessor, IOptions<MyConfiguration> myConfig)
    {
        _id = Guid.NewGuid().ToString()[^4..];
        _themeCookieName = myConfig.Value.ThemeCookieName;
        RetrieveTheme(contextAccessor);
    }

    void RetrieveTheme(IHttpContextAccessor contextAccessor)
    {
        var request = contextAccessor.HttpContext.Request;
        string theme = request.Cookies[_themeCookieName];
        if (string.IsNullOrEmpty(theme) == false)
        {
            _theme = theme;
        }
    }

    public void Dispose() { }

    public string GetId() { return _id; }

    public string GetTheme() { return _theme; }

    public void SetTheme(string theme) { _theme = theme; }
}

MainLayout.razor:

@inject IThemeService ThemeService

/* markup */

@code {
    bool _darkTheme = false;
    MudTheme _theme = new DefaultTheme();

    protected override async Task OnInitializedAsync()
    {
        var currentTheme = ThemeService.GetTheme();
        _darkTheme = currentTheme == "dark";
        _theme = _darkTheme ? new DarkTheme() : new DefaultTheme();
    }
}

Andreas
  • 31
  • 5