IHostedService calling DataPortal methods #1831
-
I have configured Csla via Startup.cs in a ASP.NET Core 3.1 web application using Razor. I set the context manager to be Csla.AspNetCore.ApplicationContextManager and everything works as expected throughout the website. However when I register a IHostedService (it cannot access scoped services) because I need something to run every minute I get a null reference exception when calling DataPorta.Create Is there a way to call DP methods independently ? (e.g. in this case there is no IHttpContextAccessor available or HttpContext; I just want a BO command to execute). |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 27 replies
-
What exactly is null? Can you provide the ToString of the exception? |
Beta Was this translation helpful? Give feedback.
-
OK, so from what I can find (such as https://forums.asp.net/t/2144171.aspx?Access+HttpContext+in+HostedService) the It looks like such a service should be registered as a transient service, and any services it requires should be injected into the service: dotnet/extensions#553 I think the issue with the data portal is that it is using an API to invoke dependency injection based on the service provider captured in Startup.cs - and that should be fine - except that the service provider instance is stored in This is something I'll need to keep in mind when implementing #1068 - and also I hope that #1068 ultimately helps address this issue. I'm somewhat surprised that your workaround actually works, in that the service provider is captured in |
Beta Was this translation helpful? Give feedback.
-
When I used CSLA a long time ago, before .net core support was supported, I manged to get this all working like so: First, create an application context manager that doesn't rely on HttpContext: /// <summary>
/// Default context manager for the user property
/// and local/client/global context dictionaries.
/// </summary>
public class ApplicationContextManager : IContextManager
{
private AsyncLocal<IPrincipal> _user = new AsyncLocal<IPrincipal>() { Value = new global::Csla.Security.UnauthenticatedPrincipal() };
private static ContextDictionary _globalContext;
private AsyncLocal<ContextDictionary> _localContext = new AsyncLocal<ContextDictionary>();
private AsyncLocal<ContextDictionary> _clientContext = new AsyncLocal<ContextDictionary>();
private const string _globalContextName = "Csla.GlobalContext";
/// <summary>
/// Returns a value indicating whether the context is valid.
/// </summary>
public bool IsValid
{
get { return true; }
}
/// <summary>
/// Gets the current user principal.
/// </summary>
/// <returns>The current user principal</returns>
public virtual IPrincipal GetUser()
{
return _user.Value;
// return Thread.CurrentPrincipal;
}
/// <summary>
/// Sets teh current user principal.
/// </summary>
/// <param name="principal">User principal value</param>
public virtual void SetUser(IPrincipal principal)
{
_user.Value = principal;
// IPrincipal current = _user.Value;
// Thread.CurrentPrincipal = principal;
}
/// <summary>
/// Gets the local context dictionary.
/// </summary>
public ContextDictionary GetLocalContext()
{
// LocalDataStoreSlot slot = Thread.GetNamedDataSlot(_localContextName);
// return (ContextDictionary)Thread.GetData(slot);
return _localContext.Value;
}
/// <summary>
/// Sets the local context dictionary.
/// </summary>
/// <param name="localContext">Context dictionary</param>
public void SetLocalContext(ContextDictionary localContext)
{
// LocalDataStoreSlot slot = Thread.GetNamedDataSlot(_localContextName);
// Thread.SetData(slot, localContext);
_localContext.Value = localContext;
}
/// <summary>
/// Gets the client context dictionary.
/// </summary>
public ContextDictionary GetClientContext()
{
//if (ApplicationContext.ExecutionLocation == ExecutionLocations.Client)
//{
// return (ContextDictionary)AppDomain.CurrentDomain.GetData(_clientContextName);
//}
//else
//{
// LocalDataStoreSlot slot = Thread.GetNamedDataSlot(_clientContextName);
// return (ContextDictionary)Thread.GetData(slot);
//}
return _clientContext.Value;
}
/// <summary>
/// Sets the client context dictionary.
/// </summary>
/// <param name="clientContext">Context dictionary</param>
public void SetClientContext(ContextDictionary clientContext)
{
//if (ApplicationContext.ExecutionLocation == ExecutionLocations.Client)
//{
// AppDomain.CurrentDomain.SetData(_clientContextName, clientContext);
//}
//else
//{
// LocalDataStoreSlot slot = Thread.GetNamedDataSlot(_clientContextName);
// Thread.SetData(slot, clientContext);
//}
_clientContext.Value = clientContext;
}
/// <summary>
/// Gets the global context dictionary.
/// </summary>
public ContextDictionary GetGlobalContext()
{
return _globalContext;
//LocalDataStoreSlot slot = Thread.GetNamedDataSlot(_globalContextName);
//return (ContextDictionary)Thread.GetData(slot);
}
/// <summary>
/// Sets the global context dictionary.
/// </summary>
/// <param name="globalContext">Context dictionary</param>
public void SetGlobalContext(ContextDictionary globalContext)
{
_globalContext = globalContext;
//LocalDataStoreSlot slot = Thread.GetNamedDataSlot(_globalContextName);
//Thread.SetData(slot, globalContext);
}
} Next, also implement one that does use /// <summary>
/// Application context manager that uses HttpContextAccessor whenr esolving HttpContext
/// to store context values.
/// </summary>
public class HttpContextAccessorContextMananger : IContextManager
{
private const string _localContextName = "Csla.LocalContext";
private const string _clientContextName = "Csla.ClientContext";
private const string _globalContextName = "Csla.GlobalContext";
private readonly IServiceProvider _serviceProvider;
public HttpContextAccessorContextMananger(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
protected virtual HttpContext GetHttpContext()
{
var httpContextAccessor = (IHttpContextAccessor)_serviceProvider.GetService(typeof(IHttpContextAccessor));
if (httpContextAccessor != null)
{
if (httpContextAccessor.HttpContext == null)
{
// Debugger.Break();
}
return httpContextAccessor.HttpContext;
}
// Debugger.Break();
return null;
}
/// <summary>
/// Gets a value indicating whether this
/// context manager is valid for use in
/// the current environment.
/// </summary>
public bool IsValid
{
get
{
var httpContext = GetHttpContext();
return httpContext != null;
}
}
/// <summary>
/// Gets the current principal.
/// </summary>
public System.Security.Principal.IPrincipal GetUser()
{
return GetHttpContext()?.User;
}
/// <summary>
/// Sets the current principal.
/// </summary>
/// <param name="principal">Principal object.</param>
public void SetUser(System.Security.Principal.IPrincipal principal)
{
var context = GetHttpContext();
if (context != null)
{
context.User = (ClaimsPrincipal)principal;
}
else
{
// Debugger.Break();
}
}
/// <summary>
/// Gets the local context.
/// </summary>
public ContextDictionary GetLocalContext()
{
return (ContextDictionary)GetHttpContext()?.Items[_localContextName];
}
/// <summary>
/// Sets the local context.
/// </summary>
/// <param name="localContext">Local context.</param>
public void SetLocalContext(ContextDictionary localContext)
{
var context = GetHttpContext();
if (context != null)
{
context.Items[_localContextName] = localContext;
}
else
{
// Debugger.Break();
}
}
/// <summary>
/// Gets the client context.
/// </summary>
public ContextDictionary GetClientContext()
{
return (ContextDictionary)GetHttpContext()?.Items[_clientContextName];
}
/// <summary>
/// Sets the client context.
/// </summary>
/// <param name="clientContext">Client context.</param>
public void SetClientContext(ContextDictionary clientContext)
{
var context = GetHttpContext();
if (context != null)
{
context.Items[_clientContextName] = clientContext;
}
else
{
// Debugger.Break();
}
}
/// <summary>
/// Gets the global context.
/// </summary>
public ContextDictionary GetGlobalContext()
{
return (ContextDictionary)GetHttpContext()?.Items[_globalContextName];
}
/// <summary>
/// Sets the global context.
/// </summary>
/// <param name="globalContext">Global context.</param>
public void SetGlobalContext(ContextDictionary globalContext)
{
var context = GetHttpContext();
if (context != null)
{
context.Items[_globalContextName] = globalContext;
}
else
{
// Debugger.Break();
}
}
} Then configure CSLA to use these two context managers:
Now, when CSLA is used without What about I had CSLA obtain the current
The pattern is basically, on application startup, once the application level However specific workloads (like for an incoming request, or perhaps a background job) can create a scoped IServiceProvider and stick it in /// <summary>
/// Anti pattern necessary to achieve DI in some places in CSLA framework - this was written before CSLA was using IServiceProvider or had .net core support at all..
/// </summary>
public static class ServiceProviderCslaExtensions
{
public static void SetAsCslaGlobalContextServiceProvider(this IServiceProvider serviceProvider)
{
global::Csla.ApplicationContext.GlobalContext.SetServiceProvider(serviceProvider);
}
public static void SetAsCslaLocalContextServiceProvider(this IServiceProvider serviceProvider)
{
global::Csla.ApplicationContext.LocalContext.SetServiceProvider(serviceProvider);
}
/// <summary>
/// Returns the local csla service provider if available, but falls back to the global csla service provider if not. Also checks for an active HttpRequest
/// and uses HttpContext.RequestServices if found.
/// </summary>
/// <returns></returns>
public static IServiceProvider GetServiceProviderFromCslaContext()
{
// check local first, then fallback to global.
IServiceProvider sp = null;
sp = global::Csla.ApplicationContext.LocalContext.GetServiceProvider();
if (sp == null)
{
sp = global::Csla.ApplicationContext.GlobalContext.GetServiceProvider();
}
// double check if an http request scoped service provider is available.
// note: this check shouldn't be necessary if the middleware is in play..
var httpContextAccessor = (IHttpContextAccessor)sp.GetService(typeof(IHttpContextAccessor));
if (httpContextAccessor != null)
{
if (httpContextAccessor.HttpContext != null && httpContextAccessor.HttpContext.RequestServices != null)
{
sp = httpContextAccessor.HttpContext.RequestServices;
}
}
return sp;
}
public static TService Locate<TService>()
{
var sp = GetServiceProviderFromCslaContext();
var implementation = sp.GetService<TService>();
return implementation;
}
} and here was the middleware I used: appBuilder.Use(async (context, next) =>
{
// On every incoming request, set request services into CSLA local-context.
context.RequestServices.SetAsCslaLocalContextServiceProvider();
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
}); |
Beta Was this translation helpful? Give feedback.
-
Yes, but I should say that aspnet core creates this scope specifically for a request - but other user / application code is perfectly entitled to create separate DI scopes and csla won't know how to access them at all because there will be no singular way for csla to know about which scope it should be using unless something tells it. For a request, middleware can access HttpContext.RequestServices (and outside a middleware you can check IHttpContextAccessor to get current HttpContext and if there is one get RequestServices) and gives you the IServiceProvider associated with the IServiceScope that asp.net core has created for the reuqest and will dispose at the end of the request. For a background job, the user might create their own DI scope and have their own IServiceProvider which csla can't automatically know about. Csla can know about the application level (root / default scope if you like) because it gets access to this on startup, but csla can't assume that just because there is an httpcontext in play that this is the scope it should be using (this is a fairly safe assumption most of the time but not technically correct) because user code might create a new DI scope even during a reuqest, and they might want csla to run within their more granular scope than the request. Likewise in a background job a user might create several distinct scopes and run csla within each of those, or just one. If a scope is in play, a new / different IServiceProvider is in play, and csla needs to be told what that is, and this has to be local to the caller (I.e propagated similar to local context) |
Beta Was this translation helpful? Give feedback.
-
Thanks I just read the concurrent reply! This is pretty much almost there then! The slight issue I think is remaining is that:
Could you clarify why Csla needs to create a new DI scope? Does it dispose of this scope? If it creates a scope that is never disposed then this basically suffers from some issues. For example IDisposable transients will continue to build up until out of memory exception occurs (di scopes track IDisposable transients and they don't get released until the scope is disposed) |
Beta Was this translation helpful? Give feedback.
OK, so from what I can find (such as https://forums.asp.net/t/2144171.aspx?Access+HttpContext+in+HostedService) the
HttpContext
is not supposed to be available to anyIHostedService
. Apparently that's a design choice in ASP.NET.It looks like such a service should be registered as a transient service, and any services it requires should be injected into the service: dotnet/extensions#553
I think the issue with the data portal is that it is using an API to invoke dependency injection based on the service provider captured in Startup.cs - and that should be fine - except that the service provider instance is stored in
ApplicationContext.LocalContext
, and that (in turn) is stored inHttpContext