Skip to content
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

Using DryIoc causes Blazor to crash #567

Closed
MaxwellDAssistek opened this issue Apr 11, 2023 · 19 comments
Closed

Using DryIoc causes Blazor to crash #567

MaxwellDAssistek opened this issue Apr 11, 2023 · 19 comments
Assignees
Labels
bug Something isn't working
Milestone

Comments

@MaxwellDAssistek
Copy link

MaxwellDAssistek commented Apr 11, 2023

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
Copy link
Owner

dadhi commented Apr 12, 2023

@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
Copy link
Author

MaxwellDAssistek commented Apr 12, 2023

@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
Copy link
Owner

dadhi commented Apr 12, 2023

@MaxwellDAssistek

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

@dadhi dadhi self-assigned this Apr 12, 2023
@dadhi dadhi added the bug Something isn't working label Apr 12, 2023
@MaxwellDAssistek
Copy link
Author

MaxwellDAssistek commented Apr 12, 2023

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
Copy link
Author

MaxwellDAssistek commented Apr 12, 2023

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
Copy link
Owner

dadhi commented Apr 12, 2023

@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
Copy link
Author

@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
Copy link
Owner

dadhi commented Apr 12, 2023

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
Copy link
Author

MaxwellDAssistek commented Apr 12, 2023

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

@dadhi
Copy link
Owner

dadhi commented Apr 12, 2023

@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 added a commit that referenced this issue Apr 12, 2023
@dadhi
Copy link
Owner

dadhi commented Apr 12, 2023

@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
Copy link
Owner

dadhi commented Apr 12, 2023

@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.

dadhi added a commit that referenced this issue Apr 12, 2023
@MaxwellDAssistek
Copy link
Author

@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
Copy link
Owner

dadhi commented Apr 12, 2023

@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 7ee0ea1.

@MaxwellDAssistek
Copy link
Author

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

dadhi added a commit that referenced this issue Apr 13, 2023
…egistration for Error.WaitForScopedServiceIsCreatedTimeoutExpired for #567
dadhi added a commit that referenced this issue Apr 13, 2023
…egistration for Error.WaitForScopedServiceIsCreatedTimeoutExpired for #567
@dadhi
Copy link
Owner

dadhi commented Apr 14, 2023

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

@MaxwellDAssistek
Copy link
Author

@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
Copy link
Owner

dadhi commented Apr 14, 2023

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

@dadhi
Copy link
Owner

dadhi commented Apr 14, 2023

@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?

dadhi added a commit that referenced this issue Apr 17, 2023
@dadhi dadhi modified the milestones: v6.0.0, v5.4.0 Apr 21, 2023
@dadhi dadhi closed this as completed Apr 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants