Reputation: 229
I have an AspNetCore (core 2.1) web appl that works fine in any single server environment, but times out after a few seconds in the environment with 2 load-balanced web servers.
Here are my startup.cs
and other classes, and a screenshot of my AppSessionState
table. I hope someone can point me to the right path. I've spent 2 days on this and can't find anything else that needs settings or what's wrong with what I'm doing.
Some explanation of below code:
As seen, I've followed the steps to configure the app to use Distributed SQL Server caching and have a helper static class HttpSessionService
which handles adding/getting values from the Session State. Also, I have a Session-Timeout attribute that I annotate each of my controllers to control the session timeouts. And after a few seconds or clicks in the app, as each controller action makes this call
HttpSessionService.Redirect()
this Redirect()
method gets a NULL user session from this line, which causes the app to timeout.
var userSession = GetValues<UserIdentityView>(SessionKeys.User);
I've attached two VS debuggers to both servers and I've noticed that even when all sessions coming to one of the debugger instance (one server) the AspNet Session still returned NULL for the above userSession
value.
Again, this ONLY happens on a distributed environment, i.e. if I stop one of the sites on one of the web servers everything works fine.
I have looked and implemented the session state distributed caching with SQLServer as explained (the same) in different pages, here are few.
https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-3.0
https://www.c-sharpcorner.com/article/configure-sql-server-session-state-in-asp-net-core/
And I do see sessions being written to my created AppSessionState
table, yet the app continues to timeout in the environment with 2 load-balanced servers.
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// Session State distributed cache configuration against SQLServer.
var aspStateConnStr = ConfigurationManager.ConnectionStrings["ASPState"].ConnectionString;
var aspSessionStateSchemaName = _config.GetValue<string>("AppSettings:AspSessionStateSchemaName");
var aspSessionStateTbl = _config.GetValue<string>("AppSettings:AspSessionStateTable");
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = aspStateConnStr;
options.SchemaName = aspSessionStateSchemaName;
options.TableName = aspSessionStateTbl;
});
....
services.AddSession(options =>
{
options.IdleTimeout = 1200;
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
...
services.AddMvc().AddJsonOptions(opt => opt.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime, IDistributedCache distCache)
{
var distCacheOptions = new DistributedCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5));
// Session State distributed cache configuration.
lifetime.ApplicationStarted.Register(() =>
{
var currentTimeUTC = DateTime.UtcNow.ToString();
byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
distCache.Set("cachedTimeUTC", encodedCurrentTimeUTC, distCacheOptions);
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSession(); // This must be called before the app.UseMvc()
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
HttpSessionService.Configure(app.ApplicationServices.GetRequiredService<IHttpContextAccessor>(), distCache, distCacheOptions);
}
HttpSessionService (helper class):
public class HttpSessionService
{
private static IHttpContextAccessor _httpContextAccessor;
private static IDistributedCache _distributedCache;
private static ISession _session => _httpContextAccessor.HttpContext.Session;
public static void Configure(IHttpContextAccessor httpContextAccessor, IDistributedCache distCache)
{
_httpContextAccessor = httpContextAccessor;
_distributedCache = distCache;
}
public static void SetValues<T>(string key, T value)
{
_session.Set<T>(key, value);
}
public static T GetValues<T>(string key)
{
var sessionValue = _session.Get<T>(key);
return sessionValue == null ? default(T) : sessionValue;
}
public static bool Redirect()
{
var result = false;
var userSession = GetValues<UserIdentityView>(SessionKeys.User);
if (userSession == null || userSession?.IsAuthenticated == false)
{
result = true;
}
return result;
}
}
SessionTimeoutAttribute:
public class SessionTimeoutAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var redirect = HttpSessionService.Redirect();
if (redirect)
{
context.Result = new RedirectResult("~/Account/SessionTimeOut");
return;
}
base.OnActionExecuting(context);
}
}
MyController
[SessionTimeout]
public class MyController : Controller
{
// Every action in this and any other controller time out and I get redirected by SessionTimeoutAttribute to "~/Account/SessionTimeOut"
}
Upvotes: 2
Views: 2029
Reputation: 229
Sorry for the late reply on this. I've changed my original implementation, by injecting IDistributedCache interface to all of my controllers and using this setting in the Statusup.cs class in ConfigureServices() function.
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = aspStateConnStr;
options.SchemaName = aspSessionStateSchemaName;
options.TableName = aspSessionStateTbl;
options.ExpiredItemsDeletionInterval = null;
});
That made it work in a web farm. As you can see I'm setting the ExpiredItemsDeletionInterval to null to prevent some basic cache entries from clearing out of cache, but with doing so I ran into another problem that when I attempt to get them I still get null back even if the entry is in the database table. So, that's another thing I'm trying to figure out.
Upvotes: 2
Reputation: 6294
It looks like you're capturing the Session value from HttpContext in your static HttpSessionService instance. That value is per-request so it's definitely going to randomly fail if you capture it like that. You need to go through the IHttpContextAccessor
every time you want to access an HttpContext
value, if you want to get the latest value.
Also, I'd suggest you pass an HttpContext in to your helper methods rather than using IHttpContextAccessor
. It has performance implications and should generally only be used if you absolutely can't pass an HttpContext
through. The places you show here seem to have an HttpContext
available, so I'd recommend using that instead of the accessor.
Upvotes: 1