'C# Autofac ParameterFilterAttribute hangs when using Task.Run()

I have 3 classes: Apple1, Apple2, and Apple3. Apple2 depends on Apple1 and Apple3 depends on Apple2 like follows.

public class Apple1
{
}
public class Apple2
{
    public Apple2(Apple1 apple1) {}
}
public class Apple3
{
    public Apple3([Optional] Apple2 apple2) {}
}

I register the three into Autofac

var builder = new ContainerBuilder();
builder.RegisterType<Apple1>().AsSelf().InstancePerLifetimeScope().WithAttributeFiltering();
builder.RegisterType<Apple2>().AsSelf().InstancePerLifetimeScope().WithAttributeFiltering();
builder.RegisterType<Apple3>().AsSelf().InstancePerLifetimeScope().WithAttributeFiltering();

The [Optional] attribute is my customized attribute inherited from ParameterFilterAttribute like the following

[AttributeUsage(AttributeTargets.Parameter)]
public class OptionalAttribute : ParameterFilterAttribute
{
    public override object? ResolveParameter(ParameterInfo parameter, IComponentContext context)
    {
        try
        {
            if (context.TryResolve(parameter.ParameterType, out object? instance))
            {
                return instance;
            }
            return null;
        }
        catch (DependencyResolutionException)
        {
            return null;
        }
    }
    public override bool CanResolveParameter(ParameterInfo parameter, IComponentContext context)
    {
        return true;
    }
}

When I resolve Apple3, as the constructor parameter Apple2 apple2 is decorated by [Optional], this OptionalAttribute snippet works and resolves parameters. It can work as my expectation: return the instance if successful to resolve or return null if failing/faulted. But once the code in ResolveParameter it wrappered with Task.Run(), it will always hang.

[AttributeUsage(AttributeTargets.Parameter)]
public class OptionalAttribute : ParameterFilterAttribute
{
    public override object? ResolveParameter(ParameterInfo parameter, IComponentContext context)
    {
        var task = Task.Run(() => 
        {
            try
            {
                Console.WriteLine("Start task");
                // The below statement never finishes.
                if (context.TryResolve(parameter.ParameterType, out object? instance))
                {
                    Console.WriteLine("TryResolve: true");
                    return instance;
                }
                Console.WriteLine("TryResolve: false");
                return null;
            }
            catch (DependencyResolutionException)
            {
                Console.WriteLine("Exception");
                return null;
            }
        });
        return task.Result;
    }
    public override bool CanResolveParameter(ParameterInfo parameter, IComponentContext context)
    {
        return true;
    }
}

The output will be only

Start Task

The entire source code is

using Autofac;
using Autofac.Core;
using Autofac.Features.AttributeFilters;
using System;
using System.Reflection;
using System.Threading.Tasks;
namespace Autofac.Issue
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = new ContainerBuilder();
            builder.RegisterType<Apple1>().AsSelf().InstancePerLifetimeScope().WithAttributeFiltering();
            builder.RegisterType<Apple2>().AsSelf().InstancePerLifetimeScope().WithAttributeFiltering();
            builder.RegisterType<Apple3>().AsSelf().InstancePerLifetimeScope().WithAttributeFiltering();
            
            var container = builder.Build();
            container.Resolve<Apple3>();
        }
    }
    public class Apple1
    {
    }
    public class Apple2
    {
        public Apple2(Apple1 apple1)
        {
        }
    }
    public class Apple3
    {
        public Apple3([Optional] Apple2 apple2)
        {
        }
    }
    [AttributeUsage(AttributeTargets.Parameter)]
    public class OptionalAttribute : ParameterFilterAttribute
    {
        public override object? ResolveParameter(ParameterInfo parameter, IComponentContext context)
        {
            var task = Task.Run(() =>
            {
                try
                {
                    Console.WriteLine("Start task");
                    if (context.TryResolve(parameter.ParameterType, out object? instance))
                    {
                        Console.WriteLine("TryResolve: true");
                        return instance;
                    }
                    Console.WriteLine("TryResolve: false");
                    return null;
                }
                catch (DependencyResolutionException)
                {
                    Console.WriteLine("Exception");
                    return null;
                }
            });
    
            return task.Result;
        }
        public override bool CanResolveParameter(ParameterInfo parameter, IComponentContext context)
        {
            return true;
        }
    }
}

It's like context.TryResole has conflicts with Task.Run(). Could anyone take a look?



Solution 1:[1]

I haven't debugged into this (and, honestly, don't have time) but you're likely getting deadlocks because things are registered InstancePerLifetimeScope. To ensure a given component is created once-and-only-once per lifetime scope, there is locking the first time something like that is created. And, of course, if that thing has its own dependencies that are also InstancePerLifetimeScope, that's going to cause locking down the chain. You're then throwing async operations into the middle of a resolve and that's going to be bad news.

Resolving an object is not an asynchronous operation. You can resolve two different things on two different threads from the same lifetime scope, sure, but you can't resolve part of a thing on one thread and part of the same thing on a different thread.

Think of a container resolve more like a constructor. Constructors are not asynchronous, right? Neither should resolve operations be async.

The answer here is to remove the Task.Run from the attribute. You won't actually get anything from it, and likely you're actually making the performance worse because instead of doing the fast operation of resolving something on the single thread, you're adding the overhead of spawning a new thread and re-joining later.

If you have some sort of factory that creates objects async, then you need to inject the factory into your constructor and call the async factory method yourself in your constructor. Trying to mix the sync and async in a fully synchronous object construction is a recipe for bad times.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Travis Illig