在服务器端Blazor应用程序中,我想存储页面导航之间保留的某些状态。我该怎么办?
常规ASP.NET Core会话状态似乎不可用,因为很可能适用Session and app sate in ASP.NET Core中的以下注释:
SignalR不支持会话 应用,因为SignalR Hub可能 独立于HTTP上下文执行。例如,这可能发生 当集线器在其生存期内超出了长时间的轮询请求时 请求的HTTP上下文。
GitHub问题Add support to SignalR for Session提到您可以使用Context.Items。但是我不知道如何使用它,即我不知道如何访问HubConnectionContext
实例。
我对会话状态有哪些选择?
答案 0 :(得分:4)
Steve Sanderson goes in depth how to save the state.
对于服务器端的浏览器,您需要使用JavaScript中的任何存储实现,例如cookie,查询参数,或者可以使用local/session storage。
当前有NuGet软件包通过BlazorStorage或IJSRuntime
之类的Microsoft.AspNetCore.ProtectedBrowserStorage
实现该功能
现在最棘手的部分是服务器端的blazor正在预渲染页面,因此您的Razor视图代码将在服务器上运行和执行,甚至不会显示在客户端的浏览器上。这会导致出现一个问题,其中IJSRuntime
和localStorage
目前不可用。 您将需要禁用预渲染或等待服务器生成的页面被发送到客户端的浏览器并建立与服务器的连接
在预渲染期间,没有与用户浏览器的交互式连接,并且该浏览器还没有任何可运行JavaScript的页面。因此,当时无法与localStorage或sessionStorage进行交互。如果尝试这样做,将会收到类似于当前无法发出JavaScript互操作调用的错误。这是因为组件正在预渲染。
要禁用预渲染:
(...)打开您的
_Host.razor
文件,然后删除对Html.RenderComponentAsync
的呼叫。然后,打开您的Startup.cs
文件,并将对endpoints.MapBlazorHub()
的调用替换为endpoints.MapBlazorHub<App>("app")
,其中App
是您的根组件的类型,而“ app”是一个CSS选择器,指定根组件应放置在文档中的什么位置。
要继续渲染时:
@inject YourJSStorageProvider storageProvider
bool isWaitingForConnection;
protected override async Task OnInitAsync()
{
if (ComponentContext.IsConnected)
{
// Looks like we're not prerendering, so we can immediately load
// the data from browser storage
string mySessionValue = storageProvider.GetKey("x-my-session-key");
}
else
{
// We are prerendering, so have to defer the load operation until later
isWaitingForConnection = true;
}
}
protected override async Task OnAfterRenderAsync()
{
// By this stage we know the client has connected back to the server, and
// browser services are available. So if we didn't load the data earlier,
// we should do so now, then trigger a new render.
if (isWaitingForConnection)
{
isWaitingForConnection = false;
//load session data now
string mySessionValue = storageProvider.GetKey("x-my-session-key");
StateHasChanged();
}
}
在实际答案中,要保留页面之间的状态,应使用CascadingParameter
。
克里斯·桑蒂(Chris Sainty)解释为
级联值和参数是一种将值从组件传递到其所有后代的方法,而不必使用传统的组件参数。
这将是一个参数,该参数将是一个类,其中包含您的所有状态数据并公开可以通过您选择的存储提供程序加载/保存的方法。在Chris Sainty's blog,Steve Sanderson's note或Microsoft docs
上对此进行了解释更新:Microsoft has published new docs explaining Blazor's state management
Update2:请注意,当前的BlazorStorage对于具有最新.NET SDK预览的服务器端Blazor不能正常工作。您可以按照this issue的方法发布我的临时解决方法
答案 1 :(得分:4)
这里是 ASP.NET Core 5.0+ (ProtectedSessionStorage
, ProtectedLocalStorage
) 的相关解决方案:https://docs.microsoft.com/en-gb/aspnet/core/blazor/state-management?view=aspnetcore-5.0&pivots=server
一个例子:
@page "/"
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
User name: @UserName
<p/><input value="@UserName" @onchange="args => UserName = args.Value?.ToString()" />
<button class="btn btn-primary" @onclick="SaveUserName">Save</button>
@code {
private string UserName;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
UserName = (await ProtectedSessionStore.GetAsync<string>("UserName")).Value ?? "";
StateHasChanged();
}
await base.OnAfterRenderAsync(firstRender);
}
private async Task SaveUserName() {
await ProtectedSessionStore.SetAsync("UserName", UserName);
}
}
请注意,此方法存储加密的数据。
答案 2 :(得分:3)
您可以使用Blazored.SessionStorage包将数据存储在会话中。
`@inject Blazored.SessionStorage.ISessionStorageService sessionStorage`
@code {
protected override async Task OnInitializedAsync()
{
await sessionStorage.SetItemAsync("name", "John Smith");
var name = await sessionStorage.GetItemAsync<string>("name");
}
}
答案 3 :(得分:2)
@JohnB暗示了穷人的国家态度:使用作用域服务。在服务器端Blazor中,作用域服务绑定到SignalR连接。这是最接近会话的内容。它对于单个用户当然是私有的。但是它也很容易丢失。重新加载页面或修改浏览器地址列表中的URL会加载以启动新的SignalR连接,创建新的服务实例,从而丢失状态。
因此,首先创建状态服务:
public class SessionState
{
public string SomeProperty { get; set; }
public int AnotherProperty { get; set; }
}
然后在 App 项目(不是服务器项目)的 Startup 类中配置服务:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<SessionState>();
}
public void Configure(IBlazorApplicationBuilder app)
{
app.AddComponent<Main>("app");
}
}
现在您可以将状态注入任何Blazor页面:
@inject SessionState state
<p>@state.SomeProperty</p>
<p>@state.AnotherProperty</p>
更好的解决方案仍然是超级欢迎。
答案 4 :(得分:1)
这是一个完整的代码示例,说明如何使用Blazored/LocalStorage保存会话数据。例如用于存储已登录的用户等。确认已从3.0.100-preview9-014004
版本开始工作
@page "/login"
@inject Blazored.LocalStorage.ILocalStorageService localStorage
<hr class="mb-5" />
<div class="row mb-5">
<div class="col-md-4">
@if (UserName == null)
{
<div class="input-group">
<input class="form-control" type="text" placeholder="Username" @bind="LoginName" />
<div class="input-group-append">
<button class="btn btn-primary" @onclick="LoginUser">Login</button>
</div>
</div>
}
else
{
<div>
<p>Logged in as: <strong>@UserName</strong></p>
<button class="btn btn-primary" @onclick="Logout">Logout</button>
</div>
}
</div>
</div>
@code {
string UserName { get; set; }
string UserSession { get; set; }
string LoginName { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await GetLocalSession();
localStorage.Changed += (sender, e) =>
{
Console.WriteLine($"Value for key {e.Key} changed from {e.OldValue} to {e.NewValue}");
};
StateHasChanged();
}
}
async Task LoginUser()
{
await localStorage.SetItemAsync("UserName", LoginName);
await localStorage.SetItemAsync("UserSession", "PIOQJWDPOIQJWD");
await GetLocalSession();
}
async Task GetLocalSession()
{
UserName = await localStorage.GetItemAsync<string>("UserName");
UserSession = await localStorage.GetItemAsync<string>("UserSession");
}
async Task Logout()
{
await localStorage.RemoveItemAsync("UserName");
await localStorage.RemoveItemAsync("UserSession");
await GetLocalSession();
}
}
答案 5 :(得分:1)
我找到了一种在服务器端会话中存储用户数据的方法。我通过使用 CircuitHandler Id 作为用户访问系统的“令牌”来做到这一点。只有用户名和 CircuitId 存储在客户端 LocalStorage 中(使用 Blazored.LocalStorage);其他用户数据存储在服务器中。我知道代码很多,但这是我能找到的在服务器端确保用户数据安全的最佳方式。
UserModel.cs(用于客户端 LocalStorage)
public class UserModel
{
public string Username { get; set; }
public string CircuitId { get; set; }
}
SessionModel.cs(我的服务器端会话的模型)
public class SessionModel
{
public string Username { get; set; }
public string CircuitId { get; set; }
public DateTime DateTimeAdded { get; set; } //this could be used to timeout the session
//My user data to be stored server side...
public int UserRole { get; set; }
etc...
}
SessionData.cs(保存服务器上所有活动会话的列表)
public class SessionData
{
private List<SessionModel> sessions = new List<SessionModel>();
private readonly ILogger _logger;
public List<SessionModel> Sessions { get { return sessions; } }
public SessionData(ILogger<SessionData> logger)
{
_logger = logger;
}
public void Add(SessionModel model)
{
model.DateTimeAdded = DateTime.Now;
sessions.Add(model);
_logger.LogInformation("Session created. User:{0}, CircuitId:{1}", model.Username, model.CircuitId);
}
//Delete the session by username
public void Delete(string token)
{
//Determine if the token matches a current session in progress
var matchingSession = sessions.FirstOrDefault(s => s.Token == token);
if (matchingSession != null)
{
_logger.LogInformation("Session deleted. User:{0}, Token:{1}", matchingSession.Username, matchingSession.CircuitId);
//remove the session
sessions.RemoveAll(s => s.Token == token);
}
}
public SessionModel Get(string circuitId)
{
return sessions.FirstOrDefault(s => s.CircuitId == circuitId);
}
}
CircuitHandlerService.cs
public class CircuitHandlerService : CircuitHandler
{
public string CircuitId { get; set; }
public SessionData sessionData { get; set; }
public CircuitHandlerService(SessionData sessionData)
{
this.sessionData = sessionData;
}
public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
{
CircuitId = circuit.Id;
return base.OnCircuitOpenedAsync(circuit, cancellationToken);
}
public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)
{
//when the circuit is closing, attempt to delete the session
// this will happen if the current circuit represents the main window
sessionData.Delete(circuit.Id);
return base.OnCircuitClosedAsync(circuit, cancellationToken);
}
public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
{
return base.OnConnectionDownAsync(circuit, cancellationToken);
}
public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
{
return base.OnConnectionUpAsync(circuit, cancellationToken);
}
}
Login.razor
@inject ILocalStorageService localStorage
@inject SessionData sessionData
....
public SessionModel session { get; set; } = new SessionModel();
...
if (isUserAuthenticated == true)
{
//assign the sesssion token based on the current CircuitId
session.CircuitId = (circuitHandler as CircuitHandlerService).CircuitId;
sessionData.Add(session);
//Then, store the username in the browser storage
// this username will be used to access the session as needed
UserModel user = new UserModel
{
Username = session.Username,
CircuitId = session.CircuitId
};
await localStorage.SetItemAsync("userSession", user);
NavigationManager.NavigateTo("Home");
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
services.AddServerSideBlazor();
services.AddScoped<CircuitHandler>((sp) => new CircuitHandlerService(sp.GetRequiredService<SessionData>()));
services.AddSingleton<SessionData>();
services.AddBlazoredLocalStorage();
...
}
答案 6 :(得分:0)
完全不使用会话状态(我没有尝试过,但是我怀疑AddSession
甚至在Blazor下也不起作用,因为会话ID基于cookie,并且HTTP大多不在图中) 。即使对于非Blazor Web应用程序,也没有可靠的机制来检测会话结束,因此会话清除充其量是混乱的。
相反,注入支持持久性的IDistributedCache
实现。最受欢迎的示例之一是Redis cache。在我的一个工作项目中,我正在尝试使用Microsoft Orleans进行分布式缓存。我没有随意分享我们内部的实现,但是您可以在我的仓库here中看到有关此实现的早期示例。
在后台,会话状态只是一个字典(在会话ID上键入),其中包含另一个键值对字典。使用长期可靠的密钥(例如,经过身份验证的用户ID)重现该方法很简单。不过,我什至没有走那么远,因为在我通常只需要一个或两个键的情况下,不断地对整个字典进行序列化和反序列化会带来很多不必要的开销。取而代之的是,我使用我唯一的用户ID为各个值键添加前缀,并直接存储每个值。
答案 7 :(得分:0)
请参考以下存储库以了解服务器端会话的实现: https://github.com/alihasan94/BlazorSessionApp
在 Login.razor 页面上,编写以下代码:
@page "/"
@using Microsoft.AspNetCore.Http
@using Helpers;
@using Microsoft.JSInterop;
@inject SessionState session
@inject IJSRuntime JSRuntime
@code{
public string Username { get; set; }
public string Password { get; set; }
}
@functions {
private async Task SignIn()
{
if (!session.Items.ContainsKey("Username") && !session.Items.ContainsKey("Password"))
{
//Add to the Singleton scoped Item
session.Items.Add("Username", Username);
session.Items.Add("Password", Password);
//Redirect to homepage
await JSRuntime.InvokeAsync<string>(
"clientJsMethods.RedirectTo", "/home");
}
}
}
<div class="col-md-12">
<h1 class="h3 mb-3 font-weight-normal">Please Sign In</h1>
</div>
<div class="col-md-12 form-group">
<input type="text" @bind="Username" class="form-control" id="username"
placeholder="Enter UserName" title="Enter UserName" />
</div>
<div class="col-md-12 form-group">
<input type="password" @bind="Password" class="form-control" id="password"
placeholder="Enter Password" title="Enter Password" />
</div>
<button @onclick="SignIn">Login</button>
SessionState.cs
using System.Collections.Generic;
namespace BlazorSessionApp.Helpers
{
public class SessionState
{
public SessionState()
{
Items = new Dictionary<string, object>();
}
public Dictionary<string, object> Items { get; set; }
}
}
SessionBootstrapper.cs (包含用于设置会话的逻辑)
using Microsoft.AspNetCore.Http;
namespace BlazorSessionApp.Helpers
{
public class SessionBootstrapper
{
private readonly IHttpContextAccessor accessor;
private readonly SessionState session;
public SessionBootstrapper(IHttpContextAccessor _accessor, SessionState _session)
{
accessor = _accessor;
session = _session;
}
public void Bootstrap()
{
//Singleton Item: services.AddSingleton<SessionState>(); in Startup.cs
//Code to save data in server side session
//If session already has data
string Username = accessor.HttpContext.Session.GetString("Username");
string Password = accessor.HttpContext.Session.GetString("Password");
//If server session is null
if (session.Items.ContainsKey("Username") && Username == null)
{
//get from singleton item
Username = session.Items["Username"]?.ToString();
// save to server side session
accessor.HttpContext.Session.SetString("Username", Username);
//remove from singleton Item
session.Items.Remove("Username");
}
if (session.Items.ContainsKey("Password") && Password == null)
{
Password = session.Items["Password"].ToString();
accessor.HttpContext.Session.SetString("Password", Password);
session.Items.Remove("Password");
}
//If Session is not expired yet then navigate to home
if (!string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password) && accessor.HttpContext.Request.Path == "/")
{
accessor.HttpContext.Response.Redirect("/home");
}
//If Session is expired then navigate to login
else if (string.IsNullOrEmpty(Username) && string.IsNullOrEmpty(Password) && accessor.HttpContext.Request.Path != "/")
{
accessor.HttpContext.Response.Redirect("/");
}
}
}
}
_Host.cshtml (在此处初始化SessionBootstrapper类)
@page "/"
@namespace BlazorSessionApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
}
@using BlazorSessionApp.Helpers
@inject SessionBootstrapper bootstrapper
<!DOCTYPE html>
<html lang="en">
<body>
@{
bootstrapper.Bootstrap();
}
<app>
<component type="typeof(App)" render-mode="ServerPrerendered" />
</app>
<script src="_framework/blazor.server.js"></script>
<script>
// use this to redirect from "Login Page" only in order to save the state on server side session
// because blazor's NavigateTo() won't refresh the page. The function below refresh
// the page and runs bootstrapper.Bootstrap(); to save data in server side session.
window.clientJsMethods = {
RedirectTo: function (path) {
window.location = path;
}
};
</script>
</body>
</html>
答案 8 :(得分:0)
使用 .net 5.0,您现在拥有 ProtectedSessionStorage,它为您提供加密的浏览器会话数据。
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
@inject ProtectedSessionStorage storage
// Set
await storage.SetAsync("myFlag", "Green");
// Get
var myFlag= await storage.GetAsync<string>("myFlag");
使用 JavaScript 互操作,所以不要在 OnInitialize
中使用,而是在 OnAfterRender
中使用。