aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

Blazor Server App - Problem with accessing request scope in DelegatingHandler

Open mdzieg opened this issue 1 year ago • 5 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Describe the bug

I have a problem with getting request scope in blazor server app.

I use HttpClientFactory to communitace with one of my services. I wanted to inject dynamic header into each request. To do so I created custom DelegatingHandler. Because HttpClient handlers are created in a separate scope (https://andrewlock.net/understanding-scopes-with-ihttpclientfactory-message-handlers/) I took the approach with injecting IHttpContextAccessor.

Here is draft of the handler:

public class MyHeaderHandler : DelegatingHandler
{
	private readonly IHttpContextAccessor _httpContextAccessor;

	public MyHeaderHandler(IHttpContextAccessor httpContextAccessor)
	{
		_httpContextAccessor = httpContextAccessor;
	}

	protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
	{
		var prov = _httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(IMyProvider)) as IMyProvider;

		...
	}
}

IMyProvider (registered as scoped service in DI) instance has some data set during request processing. I need to access that data.

The problem is that the instance I get from IHttpContextAccessor from MyHeaderHandler is different from the instance I get in blazor components or other services used there directly (not via DelegatingHandler).

I tried to set SuppressHandlerScope option to true, but no luck:

serviceCollection.Configure<HttpClientFactoryOptions>(builder.Name, options =>
		{
			options.SuppressHandlerScope = true;
		});

AFAIK this approach works in WebApi projects and the problem is with Blazor apps. To me it looks like a BUG. If this method of getting request scope is not correct, I would be more than happy to get some advice here.

Expected Behavior

IHttpContextAccessor.HttpContext.RequestServices gives access to request scope.

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

8.0.304

Anything else?

No response

mdzieg avatar Aug 23 '24 08:08 mdzieg

Using CircuitServicesAccessor link does not work for me. I get null on services collection.

Details: It works when I first open the main app page (or any other page) and then navigate to the page which results in using HttpClient. But when I navigate to the desired page directly from the address bar, then I do not get services in circuit accessor even though ServicesAccessorCircuitHandler is called on each request. ServicesAccessorCircuitHandler gets called after DelegatingHandler.

public class CircuitServicesAccessor
{
	static readonly AsyncLocal<IServiceProvider> blazorServices = new();

	public IServiceProvider? Services
	{
		get => blazorServices.Value;
		set => blazorServices.Value = value;
	}
}

public class ServicesAccessorCircuitHandler : CircuitHandler
{
	readonly IServiceProvider services;
	readonly CircuitServicesAccessor circuitServicesAccessor;

	public ServicesAccessorCircuitHandler(IServiceProvider services,
		CircuitServicesAccessor servicesAccessor)
	{
		this.services = services;
		this.circuitServicesAccessor = servicesAccessor;
	}

	public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
		Func<CircuitInboundActivityContext, Task> next)
	{
		return async context =>
		{
			circuitServicesAccessor.Services = services;
			await next(context);
			circuitServicesAccessor.Services = null;
		};
	}
}

mdzieg avatar Aug 23 '24 08:08 mdzieg

I added repro project here: https://github.com/mdzieg/blazor_circuit_accessor It reflects the way we initialize blazor app in our project. README contains repro steps;

mdzieg avatar Aug 23 '24 14:08 mdzieg

@mdzieg seems that you aren't using Blazor web.

I suspect you need to setup the circuit accessor in these two callbacks too

image

javiercn avatar Aug 23 '24 20:08 javiercn

@javiercn I added two more overrides and tested the app. Although breakpoints in those extra methods are hit before my component code is executed, the context value is not present. The Order of execution when opening /counter page is the following:

  1. ServicesAccessorCircuitHandler->OnCircuitOpenedAsync
  2. ServicesAccessorCircuitHandler->OnConnectionUpAsync
  3. MainLayout->OnInitialized (context value set here)
  4. MyDelegatingHandler->SendAsync (conext value consumed here but null Services collection)
  5. CreateInboundActivityHandler

mdzieg avatar Aug 24 '24 09:08 mdzieg

Thank you, @MackinnonBuck! Will it be included only in net9 or also in net8?

mdzieg avatar Aug 28 '24 08:08 mdzieg

@mdzieg we are unlikely to port this to 8.0 since only affects the old hosting model based on MVC, unless we receive significant feedback otherwise.

We recommend moving to Blazor Web with InteractiveServer components which essentially serves the same purpose.

javiercn avatar Aug 30 '24 15:08 javiercn