2

I am writing a Blazor Server application that needs to persist data for the user.

I have tried the following / the following does not meet the requirements:

  • Session storage - Because it is scoped to the browser tab the data is gone on refresh / not on a new tab.
  • Local storage - Works across multiple tabs and refreshes but stays for future visits to the site (I do not want the data to persist through multiple visits)
  • An AppState approach that is scoped - is once again based on per circuit which is per tab.

Some ideas I had but are unsure how to implement / if they are good ideas:

Other than that I don't have any other good ideas on how to implement this so any ideas / suggestions are welcomed.

Kyle Cintron
  • 316
  • 2
  • 6
  • use scoped dependency injection to save your state per circuit. – Ali Borjian Jun 11 '21 at 14:22
  • 1
    That works per circuit but from my understanding every tab has a new / different circuit, and refreshing also gets you a new circuit as well - both of which I need persistence. – Kyle Cintron Jun 11 '21 at 14:26
  • wht abt transient? – Ali Borjian Jun 11 '21 at 14:27
  • 1
    Transient would make it a new service for every request to the server which would not meet the use case. – Kyle Cintron Jun 11 '21 at 14:33
  • You can [watch Carl Franklin implement a State Bag for Blazor here](https://www.youtube.com/watch?v=BB4lK2kfKf0) – Crowcoder Jun 11 '21 at 14:36
  • @Crowcoder Unfortunately this does not work either - bother methods are refreshed with new services / objects on reloads and new tabs. – Kyle Cintron Jun 11 '21 at 14:49
  • I guess you can implement it server side since you are not using wasm according to the question tags. – Crowcoder Jun 11 '21 at 14:52
  • What do mean implement it server side? If it is declared as scoped on refresh/new tabs it will be anew service and if you used cascading parameters the same applies as a refresh would make that component refresh no? – Kyle Cintron Jun 11 '21 at 14:54
  • You can use a Singleton service to persist data across the entire app, including users. You'll have to key any state data by UserId, since the logic will be shared by all. Even if you are signed in on multiple devices, this should work. – Bennyboy1973 Jun 11 '21 at 14:56
  • 1
    If you have a user identity then you can associate data with the user in a database (or similar) and look it up on any tab or even on different browsers. – Crowcoder Jun 11 '21 at 15:00
  • Use a database to store the state. Assuming the same instance of the server for a state service can be flawed depending on the deployment. – Brian Parker Jun 11 '21 at 15:24
  • I have created my own version of user Session data using the Blazor CircuitHandler. You can take a look at this post: https://stackoverflow.com/a/66581485/10787774. The CircuitHandler is good because it detects disconnect and then can discard the user data. – Jason D Jun 11 '21 at 19:11
  • What's the advantage of discarding user data? If I'm lucky enough to collect any input from a user (in my case, progress through online tests), then I'm happy indeed to pick up where they left off, on a different day and device. – Bennyboy1973 Jun 13 '21 at 01:30
  • You can check out my [answer](https://stackoverflow.com/a/70489728/4546246). Maybe it helps to you too. – Fatih Dec 26 '21 at 20:46

2 Answers2

2

Well, this is not original to me, but here's how I persist Tenant for the logged in user for as long as they are connected.

public class GlobalService
{
    public event Action<PropertyChangedEventArgs> PropertyChanged;

    Subscriber _Tenant;
    public Subscriber Tenant
    {
        get
        {
            return _Tenant;
        }
        set
        {
            if (!object.Equals(_Tenant, value))
            {
                var args = new PropertyChangedEventArgs() { Name = "Tenant", NewValue = value, OldValue = _Tenant, IsGlobal = true };
                _Tenant = value;
                PropertyChanged?.Invoke(args);
            }
        }
    }
}

public class PropertyChangedEventArgs
{
    public string Name { get; set; }
    public object NewValue { get; set; }
    public object OldValue { get; set; }
    public bool IsGlobal { get; set; }
}

And I register it in ConfigureServices like so

services.TryAddScoped<GlobalService>();
John White
  • 705
  • 4
  • 12
0

One possible approach is to imitate the legacy session variables of ASP.net webforms. These rely on a cookie without expiration date. So is the lifetime of the cookie equal to the lifetime of the browser session. The main job is to be done in _Host.cshtml where we’ll create a guid that will serve as session identifier. This guid will be valid for the whole browser session (all tabs).

  • Create this property: public Guid NewGuid { get; } = Guid.NewGuid();
  • Inject HttpContextAccessor : public HostModel(IHttpContextAccessor httpContextAccessor)
  • Get the cookie value:
string g = httpContextAccessor.HttpContext.Request.Cookies["SessionGuid"] ?? "";
if (Guid.TryParse(g, out Guid guid))
    request.SessionGuid = guid;
else
    request.SessionGuid = NewGuid

In this code, request is a cascading parameter that will be passed to all components.

In the markup, you add this in the body <script>createSessionGuid('@Model.NewGuid');</script>

This use this small javascript function which have to be loaded:

function createSessionGuid(newguid) {
    var current = getCookie("SessionGuid");
    if ((current === null) || (current === "")) {
        var guid = newguid;
        document.cookie = "SessionGuid=" + guid + "; path=/";
        console.log("SessionGuid created : " + guid);
    }
    else {
        console.log("SessionGuid found : " + current);
    }
}

By this way, you get a session id, without using JSInterop, thus without using any async code.

The rest of the story is quite simple: just create a sessions class, which is a dictionary of dictionaries (concurrent dictionaries). The key in the top dictionary should be the session id. The keys in the second level dictionary are just the session variable names.

Edit : How to use CascadingValue in _Host.cshtml?

In _Host.cshtml: see the last parameter.

<component type="typeof(App)" render-mode="ServerPrerendered" param-Request="Model.request" />

In the code-behind of _Host.cshtml

public BaseClasses.Request request;
public HostModel(IHttpContextAccessor httpContextAccessor)
{
    request = new BaseClasses.Request();
    httpContextAccessor.HttpContext.Request.Cookies.Where(kvp => kvp.Key != "SessionGuid").ToList().ForEach(kvp => request.Cookies.Add(kvp.Key, kvp.Value));
    string g = httpContextAccessor.HttpContext.Request.Cookies["SessionGuid"] ?? "";
    if (Guid.TryParse(g, out Guid guid))
        request.SessionGuid = guid;
    else
        request.SessionGuid = NewGuid;
}

This info is retrieved in MainLayout.razor.cs and starts cascading there.

[CascadingParameter]
private BaseClasses.Request Request { get; set; }
  • How to use CascadingValue in _Host.cshtml or its code behind? I think the code you shown says this. – mz1378 Jun 24 '21 at 14:14