BlazorSS app w/ ref to Csla.AspNetCore : Null Reference error when using a Hosted service with timer #2687
-
Issue: When my BSS app has a reference to Csla.AspNetCore then I get (at startup) a Null Reference error in Csla.AspNet ApplicationContextManager.cs Line 121 because HttpContext is NULL because the code that calls it is initiated inside a Hosted service (e.g. no request and thus not HttpContext). I'm not sure this is a bug, so posting here as a question. This hosted service has a timer causing it to run at app startup and then every n minutes afterwards (refreshes translations from db). Here is the Hosted service: using Microsoft.Extensions.Options;
using TestBlazor.Configuration;
namespace TestBlazor.Services.Translation;
public class TranslationsRefreshService : IHostedService
{
private System.Threading.Timer? _timer;
private readonly IServiceScopeFactory _serviceProvider;
private readonly TranslationsLoadedStatusService _tranlationLoadedStatus;
private readonly TestBlazorConfiguration _config;
public TranslationsRefreshService(IServiceScopeFactory serviceProvider, TranslationsLoadedStatusService tranlationLoadedStatus, TestBlazorConfiguration config)
{
_serviceProvider = serviceProvider;
_tranlationLoadedStatus = tranlationLoadedStatus;
_config = config;
}
public Task StartAsync(CancellationToken cancellationToken)
{
// timer repeats call to DoWork on scheduled basis (minimum 5 minutes)
int time = (_config.LocalizationRefreshMinutes < 5) ? 30 : _config.LocalizationRefreshMinutes;
_timer = new Timer(
DoWork,
null,
TimeSpan.Zero,
TimeSpan.FromMinutes(time)
);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
private void DoWork(object? state)
{
//business objects are scoped services while this IHostedService is a singleton...so we have to create a scope here so we can fetch scoped services
using (var scope = _serviceProvider.CreateScope())
{
try
{
var sp = scope.ServiceProvider;
var translationRecordFactory = sp.GetRequiredService<TranslationRecordInfoListFactory>();
LoadFreshTranslations(translationRecordFactory).Wait();
}
catch (Exception ex)
{
Common.JTTracing.WriteErrorToTraceWithMessageAndCallerInfoAndTimestamp("Error loading/refreshing translations", ex);
}
finally
{
_tranlationLoadedStatus.FirstTranslationsLoadAttemptCompleted = true;
}
}
}
public async Task LoadFreshTranslations(TranslationRecordInfoListFactory translationsFactory)
{
//... fetch from db and refresh engine
}
} Here is its registration in Program.cs builder.Services.AddHostedService<TestBlazor.Services.Translation.TranslationsRefreshService>(); //Refresh translations service I noticed if I remove the reference to Csla.AspNetCore then that error goes away. So to get around this I first removed the reference to Csla.AspNetCore. The registration in Program.cs builder.Services.AddScoped<Csla.Core.IContextManager, BlazorApplicationContextManager>(); And here is the class public class BlazorApplicationContextManager : IContextManager
{
private System.Security.Principal.IPrincipal Principal { get; set; }
private ContextDictionary LocalContext { get; set; } = new ContextDictionary();
private ContextDictionary ClientContext { get; set; } = new ContextDictionary();
private ApplicationContext _applicationContext { get; set; }
private Guid _guid = Guid.NewGuid(); //testing to see unique instances in DI
/// <summary>
/// Creates an instance of the object, initializing it
/// with the required IServiceProvider.
/// </summary>
/// <param name="httpContextAccessor">HttpContext accessor</param>
public BlazorApplicationContextManager()
{
}
/// <summary>
/// Gets the current principal.
/// </summary>
public System.Security.Principal.IPrincipal GetUser()
{
var result = Principal;
if (result == null)
{
result = new Csla.Security.CslaClaimsPrincipal();
SetUser(result);
}
return result;
}
/// <summary>
/// Sets the current principal.
/// </summary>
/// <param name="principal">Principal object.</param>
public void SetUser(System.Security.Principal.IPrincipal principal)
{
Principal = principal;
}
/// <summary>
/// Gets the local context.
/// </summary>
public ContextDictionary GetLocalContext()
{
return LocalContext;
}
/// <summary>
/// Sets the local context.
/// </summary>
/// <param name="localContext">Local context.</param>
public void SetLocalContext(ContextDictionary localContext)
{
LocalContext = localContext;
}
/// <summary>
/// Gets the client context.
/// </summary>
/// <param name="executionLocation"></param>
public ContextDictionary GetClientContext(ApplicationContext.ExecutionLocations executionLocation)
{
return ClientContext;
}
/// <summary>
/// Sets the client context.
/// </summary>
/// <param name="clientContext">Client context.</param>
/// <param name="executionLocation"></param>
public void SetClientContext(ContextDictionary clientContext, ApplicationContext.ExecutionLocations executionLocation)
{
ClientContext = clientContext;
}
/// <summary>
/// Gets or sets a reference to the current ApplicationContext.
/// </summary>
public virtual ApplicationContext ApplicationContext
{
get
{
return _applicationContext;
}
set
{
_applicationContext = value;
}
}
public bool IsValid => true;
} That works great - so far so good. So I guess my questions are:
Thanks y'all. |
Beta Was this translation helpful? Give feedback.
Replies: 10 comments 61 replies
-
I'm not sure how to maintain context in this case - where some code needs to rely on HttpContext and some code needs to rely on something else (I don't know what actually). How would you implement this scenario? Or to put it another way, where would you maintain state (like the current user) in a hosted service? Where does the current user come from in that case? |
Beta Was this translation helpful? Give feedback.
-
Based on my work on the Csla tests, I might be able to help with an idea. It's a bit of a cheat - and not absolutely ideal - but you could create your own ServiceCollection for the hosted service to use, rather than using the root one. It turns out - and of course I learned this from Rocky! - that creating your own DI container isn't all that hard. If you create an instance of ServiceCollection, configure it the way you want, and then call Build() on it, you get a ServiceProvider that will do DI in a different way to what is going on elsewhere. This is what I do in the unit tests now, so that different unit tests can run with different configuration. Using this trick, you could create your own ServiceProvider to use within your background/hosted service. That ServiceProvider could be configured with a different implementation of IContextManager that doesn't go anywhere near HttpContext. The best way to do this would be to refactor your config code in Startup.cs (or Program.cs, is it now?) so that all of your DI config is in an extension method (extending IServiceCollection) that you can call from anywhere. Then, you can use the extension method from both your original startup code AND from your hosted service. This way, all of the config is guaranteed to be the same. After the extension method in the hosted service has run in your hosted service, add a registration for a different implementation of IContextManager, build it and use that ServiceProvider instance to do your DI in the hosted service. Rough pseudo-code: // In a new file, or whatever
public static class AllMyLovelyConfigurationExtensions
{
public static IServiceCollection AddAllMyLovelyConfiguration(IServiceCollection services)
{
// TODO: Add ALL of your DI config here, including ...
services.AddCsla();
}
}
// In Startup.cs:
services.AddAllMyLovelyConfiguration();
// In your hosted service
IServiceCollection services = new ServiceCollection();
services.AddAllMyLovelyConfiguration();
services.AddSingleton<IContextManager, ApplicationContextManagerStatic>(); // Overrides the default, because it's registered later
IServiceProvider serviceProvider = services.Build();
// TODO: Use serviceProvider to instantiate anything you need from DI, including IDataPortal<T>
var translationRecordFactory = sp.GetRequiredService<TranslationRecordInfoListFactory>();
... |
Beta Was this translation helpful? Give feedback.
-
Answers to other questions, or maybe other answers to the same questions ... I'm guessing this is Csla5? The reason you end up with a context manager that is looking in HttpContext is because Csla tries to load an appropriate one automatically by asking for types that might or might not exist in series, until one is found. Because you have a reference to Csla.AspNetCore, the type that implements IContextManager from within that assembly answers the call to arms during startup. The one that makes sense in AspNetCore is the one that uses HttpContext, so that that should explain the behaviour you are seeing.
|
Beta Was this translation helpful? Give feedback.
-
@swegele I think the code you posted for BlazorApplicationContextManager is looking more and more attractive. The one change I would recommend is to also accept an instance of AuthenticationStateProvider in the constructor and use it to respect changes to the auth state made outside of Csla, by wiring up a handler for the AuthenticationStateChanged event it exposes. This covers scenarios such as login session timeouts, if the auth mechanism in use implements it. The handler should await the task and update the stored Principal object once it completes. Object assignments are guaranteed to be atomic, so no locking is required. If it is possible to do so without causing a recursive loop, you could also try calling the NotifyAuthenticationStateChanged method from your SetUser method. You can only do so if the IPrincipal that is passed is a ClaimsPrincipal of course. If you can avoid a recursive loop then this would be an ideal solution, because users of the framework could either follow the Blazor code samples to log in, or the Csla samples, and both would work perfectly. I think. I have a nagging doubt about a recursive loop here. I wonder if an equals test might overcome that? It's late and I'm not sure, but see how you get on. The context manager would need registering as scoped for the event handlers to work correctly. |
Beta Was this translation helpful? Give feedback.
-
OK, so here is what I think the changes would look like, roughly. Note: this is untested! // Firstly, we need to implement IDisposable to avoid a memory leak through our use of event handlers
public class BlazorApplicationContextManager : IContextManager, IDisposable
{
...
// Add a few private variables
private readonly ICslaAuthenticationStateProvider _authenticationStateProvider;
private bool _handlingStateChange = false;
// Accept the AuthenticationStateProvider from DI and check it meets our minimum needs
public BlazorApplicationContextManager(AuthenticationStateProvider authenticationStateProvider)
{
if (_authenticationStateProvider is not IHostEnvironmentAuthenticationStateProvider)
{
throw new InvalidOperationException("This context manager must be used in conjunction with an implementer of the IHostEnvironmentAuthenticationStateProvider interface");
}
_authenticationStateProvider = authenticationStateProvider;
// Wire up the event handler that is used by Blazor to inform consumers of an authentication state change
_authenticationStateProvider.AuthenticationStateChanged += AuthenticationStateProvider_StateChanged;
}
// Some event handlers needed to convert the Task<AuthenticationState> to an AuthenticationState
private void AuthenticationStateProvider_StateChanged(Task<AuthenticationState> stateTask)
{
// Set up awaiting the completion of the task
stateTask.ContinueWith(t => AuthenticationStateProvider_StateAvailable(t)).ConfigureAwait(false);
}
private void AuthenticationStateProvider_StateAvailable(Task<AuthenticationState> completedStateTask)
{
// Use the results of the completed task to get the ClaimsPrincipal and store the result
_handlingStateChange = true;
SetUser(completedStateTask.Result.User);
_handlingStateChange = false;
}
public void Dispose()
{
// Unregister the event handler to avoid memory leaks
_authenticationStateProvider.AuthenticationStateChanged -= AuthenticationStateProvider_StateChanged;
} So the changes around this part are reasonably straightforward, but for the slightly peculiarity of two event handlers brought about by needing to convert a Task into its resulting T without sync over async nastiness. This is pretty good as is, but for completeness there is one more bit needed. We should be a responsible citizen of the Blazor authentication infrastructure/community. If someone follows a common Csla practice/code example and changes the principal via SetUser() then we need to tell the whole of Blazor that we have changed the authentication state under them. This involves a change to the SetUser method: public void SetUser(System.Security.Principal.IPrincipal principal)
{
// I think this might be good practice - but it needs verifying.
// Csla has a custom of ClaimsPrincipal to help with serialization
CslaClaimsPrincipal? cslaPrincipal = principal as CslaClaimsPrincipal;
if (cslaPrincipal is null)
{
// TODO: Does this throw an exception if principal is null??
cslaPrincipal = new CslaClaimsPrincipal(principal);
}
Principal = cslaPrincipal;
// If the request came via the Csla route and not the Blazor route, then inform
// the rest of Blazor that we have been asked to change the principal
if (!_handlingStateChange)
{
IHostEnvironmentAuthenticationStateProvider? stateProvider =
_authenticationStateProvider as IHostEnvironmentAuthenticationStateProvider;
Task<AuthenticationState> authenticationState = Task.FromResult(new AuthenticationState(cslaPrincipal));
stateProvider?.SetAuthenticationState(authenticationState);
}
} This bit needed two goes. I originally thought we needed to create a custom AuthenticationStateProvider as well, but it turns out that the built in IHostEnvironmentAuthenticationStateProvider interface can help us out. This is used to expose a method that we can pass the new state into (although via a Task.) This is the reason I've edited my answer above. Now it's only a single class, the approach of changing the RegisterContextManager method to automatically select the Blazor implementation if Csla.Blazor is referenced seems the best way to go. There is actually one more problem. A Blazor Server site can be used for more than just Blazor; it can host WebAPI controllers and the like. We can only have one IContextManager in play, so we probably need to think about enhancing this to do both this AND what the one in Csla.AspNetCore does. That, I leave as an exercise for the reader ;-) |
Beta Was this translation helpful? Give feedback.
-
@swegele @rockfordlhotka Here's a working demo of what I think will do the trick. https://github.com/TheCakeMonster/Examples/tree/master/CSLA6/BlazorAuthentication The context manager is in the WebUI project, although I've set the namespace to Csla.Blazor to indicate where I think it should end up. You can see the code at https://github.com/TheCakeMonster/Examples/blob/master/CSLA6/BlazorAuthentication/WebUI/ApplicationContextManager.cs The demo application - a simple SSB app equivalent of BlazorExample - works. However, it's dependent upon a SQL Server database that backs Identity via EF; hopefully that's not a pain for you if you wanted to run it. I say it works; it mostly works. You can't save a PersonEdit, but that's because of a bug in LocalProxy that is recorded in issue #2702 rather than anything wrong with the ApplicationContextManager. As you may notice in the code, there is one small kludge. Because the context manager is created automatically via DI, we don't have control over when it tries to wire itself up to the AuthenticationStateProvider. Depending on the exact application behaviour it is possible for the context manager to be created very early in the life of the SSB circuit; if this happens, it can be before the AuthenticationStateProvider's SetAuthenticationState method is called - in other words, before there is a task from which to retrieve the authentication state. ServerAuthenticationStateProvider in the Microsoft.AspNetCore.Components.Server namespace throws an exception in that scenario - but unfortunately there is no test you can do to avoid that exception. It's necessary to catch the exception and ignore the problem, safe in the knowledge that the event to which your handler is wired will get fired when SetAuthenticationState does get called. Exception handling is slow so I don't really like this kludge, but it does at least work. The exception would be raised once per user at most - and maybe not at all in the majority of applications. @swegele There were a couple of little gotchas in my code, so I'm not surprised you struggled with it! Sorry about that; I was guessing at what was required, and didn't get it right in a couple of small but important ways. |
Beta Was this translation helpful? Give feedback.
-
fwiw, there are two reasons Csla.Blazor references the Csla.AspNetCore package.
|
Beta Was this translation helpful? Give feedback.
-
No your right, I was just answering in general not from a feature request point of view. edit: it just cracks me up that they say you should never use it - then they tell you how to use it. Not to mention all the misinformation (as you mentioned) out there related to this topic especially. |
Beta Was this translation helpful? Give feedback.
-
OK guys, here is an update and I hope it helps. @rockfordlhotka , I copied your Soooo I decided to listen to what the error was saying and call Here is the modified method. private void InitializeUser()
{
if (AuthenticationStateProvider is IHostEnvironmentAuthenticationStateProvider hostEnvironmentAuthProvider)
{
//i've no idea what i'm doing except to call set before get like error says
var task = new Task<AuthenticationState>(() => new AuthenticationState(UnauthenticatedPrincipal));
//the following set triggers the authentication state changed so why do it again below?
hostEnvironmentAuthProvider.SetAuthenticationState(task);
}
else
{
//??
AuthenticationStateProvider_AuthenticationStateChanged(AuthenticationStateProvider.GetAuthenticationStateAsync());
}
} The AuthenticationStateProvider IS INDEED an public virtual void SetUser(IPrincipal principal)
{
throw new NotSupportedException(nameof(SetUser));
} Now what is interesting is that SetUser method is called by Csla itself!! See manager.SetUser(parentContext.User); I'm stuck there as I'm not sure what the intent was of throwing that NotSupportedException and then the framework being the first offender? |
Beta Was this translation helpful? Give feedback.
-
Are there 3 distinct problems to deal with here:
SWAG suggestion for InitializeUser and helper method: private void InitializeUser()
{
Task<AuthenticationState> getUserTask;
try
{
getUserTask = AuthenticationStateProvider.GetAuthenticationStateAsync();
}
catch (InvalidOperationException ioex)
{
//?? It might be good enough to just test for that ioex but might we inadvertently catch other unrelated ones with same ioex type??
//?? Is it safe to go further and test for the method names being present in the exception without tripping over culture specific error messages
string message = ioex.Message;
if (message.Contains(nameof(AuthenticationStateProvider.GetAuthenticationStateAsync))
&& message.Contains(nameof(IHostEnvironmentAuthenticationStateProvider.SetAuthenticationState)))
{
SafeSetCurrentUser(UnauthenticatedPrincipal);
}
return;
}
AuthenticationStateProvider_AuthenticationStateChanged(getUserTask);
}
private void SafeSetCurrentUser(ClaimsPrincipal principal)
{
if (AuthenticationStateProvider is IHostEnvironmentAuthenticationStateProvider hostEnvironmentAuthProvider)
{
var task = new Task<AuthenticationState>(() => new AuthenticationState(principal));
hostEnvironmentAuthProvider.SetAuthenticationState(task);
}
} SWAG suggestion for SetUser public virtual void SetUser(IPrincipal principal)
{
CurrentPrincipal = principal;
if (CurrentPrincipal is ClaimsPrincipal)
{
SafeSetCurrentUser((ClaimsPrincipal)CurrentPrincipal);
}
else
{
//??
}
} EDIT: And I guess we could keep an eye on this issue being fixed which might make the check in InitizlizeUser method no longer needed in future. EDIT2: Upon looking at the microsoft repo of aspnet code I believe it is safe to look for the method names in that invalidoperationexception that is thrown by them: public override Task<AuthenticationState> GetAuthenticationStateAsync()
=> _authenticationStateTask
?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(SetAuthenticationState)}."); |
Beta Was this translation helpful? Give feedback.
@swegele I think the code you posted for BlazorApplicationContextManager is looking more and more attractive. The one change I would recommend is to also accept an instance of AuthenticationStateProvider in the constructor and use it to respect changes to the auth state made outside of Csla, by wiring up a handler for the AuthenticationStateChanged event it exposes. This covers scenarios such as login session timeouts, if the auth mechanism in use implements it.
The handler should await the task and update the stored Principal object once it completes. Object assignments are guaranteed to be atomic, so no locking is required.
If it is possible to do so without causing a recursive loop, yo…