Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

InvalidOperationException in Blazor 8 state manager #3737

Closed
rockfordlhotka opened this issue Mar 15, 2024 · 9 comments
Closed

InvalidOperationException in Blazor 8 state manager #3737

rockfordlhotka opened this issue Mar 15, 2024 · 9 comments

Comments

@rockfordlhotka
Copy link
Member

@rockfordlhotka Upon further research, I think the errors are related to the new StateManager using TaskCompletionSource. A google search turned up this info:

The exception "System.InvalidOperationException" in Microsoft.AspNetCore.Components.Server.dll is thrown when the component is being statically rendered. This can happen when you're using a TaskCompletionSource and setting it to complete when it is already set to complete, or when you're doing some JsInterop calls on completed tasks.
Here are some things you can try to fix this issue:
Make sure that you're not using a TaskCompletionSource and setting it to complete when it is already set to complete.
Make sure that you're not doing any JsInterop calls on completed tasks.

I'm not sure how to troubleshoot this further, since I'm not that familiar with this new StateManager. Any guidance is appreciated.

Originally posted by @mtavares628 in #3647 (reply in thread)

@rockfordlhotka
Copy link
Member Author

@mtavares628 I did the following:

  1. Create a .NET 7 Blazor Server project
  2. Reference CSLA 8
  3. Configure CSLA 8 - using the new context/state manager
  4. Add stateManager.InitializeAsync calls to each page's OnInitializedAsync method
  5. Run app

I am not seeing any exceptions in the console or host window where the web server is running.

How are you seeing these exceptions?

@mtavares628
Copy link

I'm seeing them in the Output Window when Debugging:

image

@rockfordlhotka
Copy link
Member Author

I wonder what is different in your app from my test.

My config:

builder.Services.AddCsla(o=>o
    .AddAspNetCore()
    .AddServerSideBlazor());

My pages:

@inject Csla.Blazor.State.StateManager stateManager

...

    protected override async Task OnInitializedAsync()
    {
        await stateManager.InitializeAsync();
    }

Because I (and you?) have no wasm pages, at no point should session be "checked out" and so at no point should the task completion source code even be invoked.

Are you seeing the exceptions as a result of a data portal call, or something else?

@mtavares628
Copy link

It appears to be happening in LocalProxy.cs on line 40 of InitializeContext where it tries to get the ApplicationContext:

	private void InitializeContext(out IDataPortalServer _portal, out IServiceScope _logicalServerScope, out ApplicationContext _logicalServerApplicationContext)
	{
		IServiceProvider provider = CallerApplicationContext.CurrentServiceProvider;
		if (Options.UseLocalScope && CallerApplicationContext.LogicalExecutionLocation == ApplicationContext.LogicalExecutionLocations.Client && CallerApplicationContext.IsAStatefulContextManager)
		{
			_logicalServerScope = CallerApplicationContext.CurrentServiceProvider.CreateScope();
			provider = _logicalServerScope.ServiceProvider;
			provider.GetRequiredService<IRuntimeInfo>().LocalProxyNewScopeExists = true;
		}
		else
		{
			_logicalServerScope = null;
		}
		_logicalServerApplicationContext = provider.GetRequiredService<ApplicationContext>();
		_portal = provider.GetRequiredService<IDataPortalServer>();
	}

If you are around, I can show you, just let me know.

@rockfordlhotka
Copy link
Member Author

@mtavares628 I am available for about an hour - waiting for a flight to board. Meet me on Discord?

@mtavares628
Copy link

@rockfordlhotka So I've been troubleshooting further and it seems that it definitely has something to do with the scoped service provider in that InitializeContext method of LocalProxy. When I look at the state of _logicalServerApplicationContext in that method, the Identity of the ClaimsPrincipal is null. I'm not sure why. But if I use the existing CallerApplicationContext service provider to get the _logicalServerApplicationContext, then the Identity is there.

In tracking why my application worked in a prior pre-release, it looks like the pull request which changed things appears to be #3632 in particular this commit from it: d3aa7cc

This was where IntializeContext was changed.

@StefanOssendorf
Copy link
Contributor

If it has something todo with the TaskCompletionSource a way to avoid multiple Sets would be to use the Try* methods which do nothing when it's already in an end state.

@mtavares628
Copy link

@StefanOssendorf - In performing more testing, I don't think the issue has anything to do with the StateManager code with TaskCompletionSource in it.

@rockfordlhotka - I've dug a little deeper and the inner message of the InvalidOperationException is:

GetAuthenticationStateAsync was called before SetAuthenticationState.

I've traced this to me using a custom AuthenticationStateProvider that inherits from ServerAuthenticationStateProvider. All I'm doing with it is adding in roles as claims:

    public class DadAuthenticationStateProvider : ServerAuthenticationStateProvider
    {
        private IUserRoleStore<DadUser> _roleStore;

        public DadAuthenticationStateProvider(IUserRoleStore<DadUser> roleStore)
        {
            _roleStore = roleStore;
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var authState = await base.GetAuthenticationStateAsync();
            if (authState.User.Identity != null && authState.User.Identity.IsAuthenticated)
            {
                var identity = (ClaimsIdentity)authState.User.Identity;
                int UserId;
                if (int.TryParse(identity.FindFirst("sub")?.Value, out UserId))
                {
                    var roleList = (List<string>)(await _roleStore.GetRolesAsync(new DadUser() { Id = UserId }, new CancellationToken(false)));
                    foreach (string item in roleList)
                    {
                        if (!identity.HasClaim("role", item))
                        {
                            identity.AddClaim(new Claim("role", item));
                        }
                    }
                }
            }
            return authState;
        }       
    }

The exception is actually being thrown In the InitializeUser method of ApplicationContextManagerInMemory.cs:

    private void InitializeUser()
    {
      Task<AuthenticationState> task = default;
      try
      {
        task = AuthenticationStateProvider.GetAuthenticationStateAsync();
      }
      catch (InvalidOperationException ex)
      {
        task = Task.FromResult(new AuthenticationState((ClaimsPrincipal)UnauthenticatedPrincipal));
        string message = ex.Message;
        if (message.Contains(nameof(AuthenticationStateProvider.GetAuthenticationStateAsync))
            && message.Contains(nameof(IHostEnvironmentAuthenticationStateProvider.SetAuthenticationState)))
        {
          SetHostPrincipal(task);
        }
        else
        {
          throw;
        }
      }
      AuthenticationStateProvider_AuthenticationStateChanged(task);
    }

While it does have a try-catch for an InvalidOperationException to check for this specific error, the catch isn't being hit because it is being captured within the task object and not immediately thrown at that point. The solution to that would be to use async/await in the method, but unfortunately, this method is being called in the constructor, so running an async method isn't recommended. I also want to note that it does catch the exception if we are not adding a custom scoped AuthenticationStateProvider to the service model and stick with the default one.

Essentially, I see 2 issues to solve here:

  1. How can we adjust this so that the exception is caught and handled correctly?
  2. How can we prevent the exception from being thrown altogether so that SetAuthenticationState is run before the call to GetAuthenticationState?

@rockfordlhotka
Copy link
Member Author

I don't think this is a CSLA issue, so I'm moving this to a discussion instead of an issue.

@rockfordlhotka rockfordlhotka removed their assignment Mar 26, 2024
@MarimerLLC MarimerLLC locked and limited conversation to collaborators Mar 26, 2024
@rockfordlhotka rockfordlhotka converted this issue into discussion #3749 Mar 26, 2024
@github-project-automation github-project-automation bot moved this from In Progress to Done in CSLA Version 8.0.0 Mar 26, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
No open projects
Development

No branches or pull requests

3 participants