dadhi / DryIoc

DryIoc is fast, small, full-featured IoC Container for .NET
MIT License
988 stars 122 forks source link

Using DryIoc causes Blazor to crash #567

Closed MaxwellDAssistek closed 1 year ago

MaxwellDAssistek commented 1 year ago

Hello,

When I try to use DryIoc in my Server-size Blazor project, it causes an internal Blazor error.

To reproduce:

  1. Create new Server-side Blazor project.
  2. Install DryIoc.Microsoft.DependencyInjection and Serilog.AspNetCore (needed to see the error)
  3. Replace Program.cs:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using BlazorApp2.Data;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using Serilog;

var builder = WebApplication.CreateBuilder(args);

var container = new Container(
    Rules.Default.With(
        FactoryMethod.ConstructorWithResolvableArguments,
        propertiesAndFields: PropertiesAndFields.Auto)
);

// Here it goes the integration with the existing DryIoc container
var diFactory = new DryIocServiceProviderFactory(container);

builder.Host.UseServiceProviderFactory(diFactory);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(options =>
{
    options.DetailedErrors = true;
});

var logger = new LoggerConfiguration()
    .MinimumLevel.Verbose()
    .ReadFrom.Configuration(builder.Configuration)
    .Enrich.FromLogContext()
    .WriteTo.File("Errors/Log_.txt", rollingInterval: RollingInterval.Day)
    .CreateLogger();

builder.Host.UseSerilog(logger);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();
  1. Run the app and observe that after a few seconds "An unhandled exception has occurred. See browser dev tools for details." pops up.
  2. Open the generated log file to see the actual error:
    2023-04-11 17:34:13.636 -04:00 [DBG] Created circuit zhg1kxyMHSMJkpwgLDEyfzYDVTLQGwD-2VCPcN7QgIY for connection fShBxz1E2UjLoN7J1mkatg
    2023-04-11 17:34:16.732 -04:00 [DBG] Circuit initialization failed
    DryIoc.ContainerException: code: Error.WaitForScopedServiceIsCreatedTimeoutExpired;
    message: DryIoc has waited for the creation of the scoped or singleton service by the "other party" for the 3000 ticks without the completion. 
    You may call `exception.TryGetDetails(container)` to get the details of the problematic service registration.
    The error means that either the "other party" is the parallel thread which has started but is unable to finish the creation of the service in the provided amount of time. 
    Or more likely the "other party"  is the same thread and there is an undetected recursive dependency or 
    the scoped service creation is failed with the exception and the exception was catched but you are trying to resolve the failed service again. 
    For all those reasons DryIoc has a timeout to prevent the infinite waiting. 
    You may change the default timeout via `Scope.WaitForScopedServiceIsCreatedTimeoutTicks=NewNumberOfTicks`
    at DryIoc.ContainerException.WithDetails(Object details, Int32 error, Object arg0, Object arg1, Object arg2, Object arg3) in /_/src/DryIoc/Container.cs:line 14161
    at DryIoc.Throw.WithDetails(Object details, Int32 error, Object arg0, Object arg1, Object arg2, Object arg3) in /_/src/DryIoc/Container.cs:line 14554
    at DryIoc.Scope.WaitForItemIsSet(ImMapEntry`1 itemRef) in /_/src/DryIoc/Container.cs:line 13031
    at DryIoc.Interpreter.InterpretGetScopedOrSingletonViaFactoryDelegate(IResolverContext r, GetScopedOrSingletonViaFactoryDelegateExpression e, IParameterProvider paramExprs, Object paramValues, ParentLambdaArgs parentArgs) in /_/src/DryIoc/Container.cs:line 3793
    at DryIoc.Interpreter.TryInterpretMethodCall(IResolverContext r, MethodCallExpression callExpr, IParameterProvider paramExprs, Object paramValues, ParentLambdaArgs parentArgs, Object& result) in /_/src/DryIoc/Container.cs:line 3481
    at DryIoc.Interpreter.TryInterpret(IResolverContext r, Expression expr, IParameterProvider paramExprs, Object paramValues, ParentLambdaArgs parentArgs, Object& result) in /_/src/DryIoc/Container.cs:line 3149
    at DryIoc.Interpreter.TryInterpretAndUnwrapContainerException(IResolverContext r, Expression expr, Object& result) in /_/src/DryIoc/Container.cs:line 3015
    at DryIoc.Container.ResolveAndCache(Int32 serviceTypeHash, Type serviceType, IfUnresolved ifUnresolved) in /_/src/DryIoc/Container.cs:line 435
    at DryIoc.Container.System.IServiceProvider.GetService(Type serviceType) in /_/src/DryIoc/Container.cs:line 338
    at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
    at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
    at Microsoft.Extensions.DependencyInjection.ComponentServiceCollectionExtensions.<>c.<AddServerSideBlazor>b__0_1(IServiceProvider s)
    at DryIoc.Registrator.ToFactoryDelegate[TService](Func`2 f, IResolverContext r) in /_/src/DryIoc/Container.cs:line 7967
    at DryIoc.Interpreter.InterpretGetScopedOrSingletonViaFactoryDelegate(IResolverContext r, GetScopedOrSingletonViaFactoryDelegateExpression e, IParameterProvider paramExprs, Object paramValues, ParentLambdaArgs parentArgs) in /_/src/DryIoc/Container.cs:line 3799
    at DryIoc.Interpreter.TryInterpretMethodCall(IResolverContext r, MethodCallExpression callExpr, IParameterProvider paramExprs, Object paramValues, ParentLambdaArgs parentArgs, Object& result) in /_/src/DryIoc/Container.cs:line 3481
    at DryIoc.Interpreter.TryInterpret(IResolverContext r, Expression expr, IParameterProvider paramExprs, Object paramValues, ParentLambdaArgs parentArgs, Object& result) in /_/src/DryIoc/Container.cs:line 3149
    at DryIoc.Interpreter.TryInterpret(IResolverContext r, Expression expr, IParameterProvider paramExprs, Object paramValues, ParentLambdaArgs parentArgs, Object& result) in /_/src/DryIoc/Container.cs:line 3196
    at DryIoc.Interpreter.InterpretGetScopedOrSingletonViaFactoryDelegate(IResolverContext r, GetScopedOrSingletonViaFactoryDelegateExpression e, IParameterProvider paramExprs, Object paramValues, ParentLambdaArgs parentArgs) in /_/src/DryIoc/Container.cs:line 3800
    at DryIoc.Interpreter.TryInterpretMethodCall(IResolverContext r, MethodCallExpression callExpr, IParameterProvider paramExprs, Object paramValues, ParentLambdaArgs parentArgs, Object& result) in /_/src/DryIoc/Container.cs:line 3481
    at DryIoc.Interpreter.TryInterpret(IResolverContext r, Expression expr, IParameterProvider paramExprs, Object paramValues, ParentLambdaArgs parentArgs, Object& result) in /_/src/DryIoc/Container.cs:line 3149
    at DryIoc.Interpreter.TryInterpretAndUnwrapContainerException(IResolverContext r, Expression expr, Object& result) in /_/src/DryIoc/Container.cs:line 3015
    at DryIoc.Container.ResolveAndCache(Int32 serviceTypeHash, Type serviceType, IfUnresolved ifUnresolved) in /_/src/DryIoc/Container.cs:line 435
    at DryIoc.Container.System.IServiceProvider.GetService(Type serviceType) in /_/src/DryIoc/Container.cs:line 338
    at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
    at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
    at Microsoft.AspNetCore.Components.Server.Circuits.CircuitFactory.CreateCircuitHostAsync(IReadOnlyList`1 components, CircuitClientProxy client, String baseUri, String uri, ClaimsPrincipal user, IPersistentComponentStateStore store)
    at Microsoft.AspNetCore.Components.Server.ComponentHub.StartCircuit(String baseUri, String uri, String serializedComponentRecords, String applicationState)
dadhi commented 1 year ago

@MaxwellDAssistek Hi, thanks for notifying.

Could you please do as stated in the error message and wrap the code in try/catch and get the error details via exception.TryGetDetails(container) in the catch?

The only caveat, you need to ask for the actual container via app.Services.GetRequiredService<IContainer>();

MaxwellDAssistek commented 1 year ago

@dadhi So there is no code to try/catch. Everything happens in a separate Blazor thread which I have no code running in at all.

What I was able to do is put a breakpoint in DryIoc.ContainerException.WithDetails and I was able to run:

  new ContainerException(details, error, string.Format(GetMessage(ErrorCheck.Unspecified, error), Print(arg0), Print(arg1), Print(arg2), Print(arg3))).TryGetDetails(BlazorApp2.Globals.MainContainer)

(Globals is a static class I made just to store the container instance)

Unfortunately all I get is: "Unable to get the service registration for the problematic factory with FactoryID=1000"

I was able to go up the stack and look at the locals in DryIoc.Interpreter.InterpretGetScopedOrSingletonViaFactoryDelegate

s = {CurrentScopeReuse.GetScopedViaFactoryDelegateExpression} null
scope = {Scope} {Name=null}
map = {ImMapEntry<object>} {H:1000,V:System.Object}
$exception = {ContainerException} DryIoc.ContainerException: code: Error.WaitForScopedServiceIsCreatedTimeoutExpired;\r\nmessage: DryIoc has waited for the creation of the scoped or singleton service by the "other party" for the 3000 ticks without the completion. \r\nYou may call `exception.T…
result = {object} null
id = {int} 1000
r = {Container} container with scope {Name=null}\r\n with Rules with {TrackingDisposableTransients, SelectLastRegisteredFactory} and without {ThrowOnRegisteringDisposableTransient, VariantGenericTypesInResolvedCollection}\r\n with FactorySelector=SelectLastRegisteredFactory\r…
noItem = object
oldMap = {ImMapEntry<object>} {H:1000,V:System.Object}
parentArgs = {Interpreter.ParentLambdaArgs} null
e = {CurrentScopeReuse.GetScopedOrSingletonViaFactoryDelegateExpression} r.CurrentOrSingletonScope.GetOrAddViaFactoryDelegate(1000, r => new DefaultCircuitAccessor() {Circuit = Convert(r.CurrentOrSingletonScope.GetOrAddViaFactoryDelegate(999, value(DryIoc.FactoryDelegate), r), Circuit)}, r)
newMap = {ImMapEntry<object>} {H:1000,V:System.Object}
lambda = {Expression} null
paramValues = {Container} container with scope {Name=null}\r\n with Rules with {TrackingDisposableTransients, SelectLastRegisteredFactory} and without {ThrowOnRegisteringDisposableTransient, VariantGenericTypesInResolvedCollection}\r\n with FactorySelector=SelectLastRegisteredFactory\r…
paramExprs = OneParameterLambdaExpression
otherItemRef = {ImMapEntry<object>} {H:1000,V:System.Object}
itemRef = {ImMapEntry<object>} {H:1000,V:System.Object}
disp = {IDisposable} null
lambdaConstExpr = {ConstantExpression} null
dadhi commented 1 year ago

@MaxwellDAssistek

Thank you for the info. Hope, I can extract useful bits from it.

MaxwellDAssistek commented 1 year ago

This is where that service is added: https://github.com/dotnet/aspnetcore/blob/a7bf7ccdcff6475f144c300a0d21421b6efbb1b3/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs#L69-L70

Maybe the services that are added to builder.Services are not properly register with DryIoc.

I noticed that if I do builder.Services.AddSingleton<WeatherForecastService>();, container.GetServiceRegistrations().ToList() gives no results. But if I do container.Register<WeatherForecastService>(); I do get that service.

MaxwellDAssistek commented 1 year ago

Here is a seemingly related Microsoft article: https://learn.microsoft.com/en-us/aspnet/core/migration/50-to-60-samples?view=aspnetcore-7.0#aspnet-core-6-7

Looks like DryIoc needs to expose an IServiceCollection in some way and that might be the only way to register MS things, since they all use extension methods of IServiceCollection.

var diFactory = new DryIocServiceProviderFactory(container);
builder.Host.UseServiceProviderFactory(diFactory);
builder.Host.ConfigureContainer<IServiceCollection>((_, collection) =>
{
    collection.AddRazorPages();
    collection.AddServerSideBlazor(options =>
    {
        options.DetailedErrors = true;
    });
});

This gives: System.InvalidCastException: Unable to cast object of type 'DryIoc.Container' to type 'Microsoft.Extensions.DependencyInjection.IServiceCollection'.

Edit: This might all be a red-herring. I added container.Populate(builder.Services); after builder.Build() and now the services show up in container.GetServiceRegistrations().ToList() so I doubt any of the above is helpful. 😞

dadhi commented 1 year ago

@MaxwellDAssistek

I noticed that if I do builder.Services.AddSingleton();, container.GetServiceRegistrations().ToList() gives no results. But if I do container.Register(); I do get that service.

The problem here is that when you're integrating the existing container with MS.DI via new DryIocServiceProviderFactory(container), by default it will clone the existing registry. This means whatever is added later to builder.Services or to container won't be visible to each other. To share the services, you need to pass the second parameter as following new DryIocServiceProviderFactory(container, RegistrySharing.Share); I've already got feedback that it is unexpected behavior for many people, so in the next DryIoc.MS.DI version it will be sharing by default.

Looks like DryIoc needs to expose an IServiceCollection in some way and that might be the only way to register MS things, since they all use extension methods of IServiceCollection

I am not sure about this, never heard of this requirement. But will check, thanks for the link.

MaxwellDAssistek commented 1 year ago

@dadhi Still no luck unfortunately. I still get the same exception with the following Program.cs:

using BlazorApp2;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using BlazorApp2.Data;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using Serilog;

var builder = WebApplication.CreateBuilder(args);

var container = new Container(
    Rules.MicrosoftDependencyInjectionRules.With(
        FactoryMethod.ConstructorWithResolvableArguments,
        propertiesAndFields: PropertiesAndFields.Auto)
);
Globals.MainContainer = container;

// Here it goes the integration with the existing DryIoc container
var diFactory = new DryIocServiceProviderFactory(container, RegistrySharing.Share);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(options =>
{
    options.DetailedErrors = true;
});
builder.Services.AddSingleton<WeatherForecastService>();

var logger = new LoggerConfiguration()
    .MinimumLevel.Verbose()
    .ReadFrom.Configuration(builder.Configuration)
    .Enrich.FromLogContext()
    .WriteTo.File("Errors/Log_.txt", rollingInterval: RollingInterval.Day)
    .CreateLogger();

builder.Host.UseSerilog(logger);

builder.Host.UseServiceProviderFactory(diFactory);

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();
dadhi commented 1 year ago

This is where that service is added: https://github.com/dotnet/aspnetcore/blob/a7bf7ccdcff6475f144c300a0d21421b6efbb1b3/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs#L69-L70

I am still not sure about the root cause, but regarding this service Circuit you may try to check something for me... Try to override its registration with the following:

container.RegisterDelegate<ICircuitAccessor, ICircuit>(
     circuitAccessor => circuitAccessor.Circuit, 
     Reuse.ScopedOrSingleton, 
     ifAlreadyRegistered: IfAlreadyRegistered.Replace);
MaxwellDAssistek commented 1 year ago

@dadhi Unfortunately, the issue is that ICircuitAccessor is marked internal so its impossible for me to do anything with it without rebuilding aspnetcore.

dadhi commented 1 year ago

@MaxwellDAssistek Don't do this :) Yeah, the internal is good for authors to avoid breaking changes, but hard for user hacking. Ok, I will try to re-create the app and update you later.

dadhi commented 1 year ago

@MaxwellDAssistek I have added the sample. The error is caused by propertiesAndFields: PropertiesAndFields.Auto in the rules. I may imagine that DryIoc tries to resolve the property and the constructor parameter, which form a recursive dependency. So if you plan to inject some properties, and you are not in the full control of every service (like here with a lot of framework stuff), I would suggest avoiding .Auto and specify only the particular properties in the individual registrations.

dadhi commented 1 year ago

@MaxwellDAssistek ...or you may use container rules with

PropertiesAndFields.All(serviceInfo: (MemberInfo member, Request request) => MySpecificPropertiesOnly(memberInfo));

and filter the properties in MySpecificPropertiesOnly based on its DeclaringType, Namespace, Name, etc.

MaxwellDAssistek commented 1 year ago

@dadhi I can confirm that removing PropertiesAndFields fixed it! I should've guessed that it could be the cause, but it just never caused any issues in the other places we use DryIoc like MAUI.

Thank you for your help!

dadhi commented 1 year ago

@MaxwellDAssistek Thanks for investing into this issue. Actually, it helped me to figure out how to quickly prototype required properties support (of C# 11), see https://github.com/dadhi/DryIoc/commit/7ee0ea1b09077da70ef6f5b75dac1136c0fe8565.

MaxwellDAssistek commented 1 year ago

That's a fantastic. I think we will end up switching to that right away!

dadhi commented 1 year ago

@MaxwellDAssistek Sorry for bothering, I was looking into the related issues (#544). Just wondering, did you see something like this in your project?

MaxwellDAssistek commented 1 year ago

@dadhi We don't use file uploads in our projects currently so we did not have that issue, but I was able to reproduce it using the code provided and added what the actual exception is that is hiding. What we've found with Blazor is that they like to hide their internal exceptions under the "Debug" log level so we need to lower the log level to that in order to see the exceptions (as I did in the sample code here using Serilog).

dadhi commented 1 year ago

@MaxwellDAssistek Thank you very much, I saw the details on #547.

dadhi commented 1 year ago

@MaxwellDAssistek I have "quickly" added the net7.0 target to both DryIoc.dll and DryIoc.Microsoft.DependencyInjection and released the DryIoc.Microsoft.DependencyInjection.6.2.0-preview-01 on NuGet (maybe need some time to indexing). If you have time, could you check it out?