Home > OS >  Is it possible in Blazor Server to inject scoped service in middleware and inject that same instance
Is it possible in Blazor Server to inject scoped service in middleware and inject that same instance

Time:12-28

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?

CodePudding user response:

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.

CodePudding user response:

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();
    }
}

  • Related