container icon indicating copy to clipboard operation
container copied to clipboard

Upgrade to 4.x+ - breaking changes to IResolverPolicy

Open DoctorVanGogh opened this issue 6 years ago • 6 comments

I tried upgrading a project to the latest release and noticed some breaking changes. Most of the issues were documented in unitycontainer/abstractions#97, but I've come about one issues where I'm somewhat stumped. While the functionality is not missing completely, a workaround is extremely brittle.

Previous behavior

Old versions allowed declaring DependencyResolutionAttribute sub-classes where you could supply IResolverPolicy (through the CreateResolver override) which were automatically picked up & used in the build up.

This seems to be gone(ish) in 4.x+.

Possible workaround

I think I found the usable-ish functionality replacement in MemberProcessor's AttributeFactories where you can use ~~Add~~(latest nuget) AddFactories (HEAD) to once again inject your own custom resolver logic for specifically attributed dependencies.

    class FooResolutionExtension : UnityContainerExtension {

        protected override void Initialize() {
            var strategies = (IEnumerable<MemberProcessor>) Context.BuildPlanStrategies;
            var processor = (ConstructorProcessor) strategies.First(s => s is ConstructorProcessor);
            processor.Add(typeof(FooResolutionAttribute), FooResolutionFactory);
        }

        private ResolveDelegate<BuilderContext> FooResolutionFactory(Attribute attribute, object info, object value) {
            var type = ((ParameterInfo)info).ParameterType;
            return (ref BuilderContext context) =>
                   {
                       try
                       {
                           // custom resolver logic here....
                           return context.Resolve(type, ((FooResolutionAttribute)attribute).Name);
                       }
                       catch (Exception ex)
                           when (ex.InnerException?.GetType().FullName != "Unity.Exceptions.CircularDependencyException")
                       {
                           return value;
                       }
                   };
        }
    }

    class FooResolutionAttribute : DependencyResolutionAttribute {

        public int Bar { get; set; }

        public FooResolutionAttribute() : this(null){            
        }

        public FooResolutionAttribute(string name) : base(name) {
        }
        
    }

    class Program
    {
        static void Main(string[] args) {
            IUnityContainer container = new UnityContainer();

            container.AddNewExtension<FooResolutionExtension>();

            var x = container.Resolve<Baz>();
        }

        public class Baz {
            public Baz([FooResolution(Bar = 12)] string baz) {                
            }
        }
    }

Issue with the workaround

It's ~~near~~ impossible to get the MemberProcessor cleanly. Even if you add an Extension to implement your logic, the ExtensionContext will only return the BuildPlanStrategies as an IStagedStrategyChain<MemberProcessor, BuilderStage> which you then have to explicitly cast to an IEnumerable<MemberProcessor> to find the processor(s) to modify.

That cast is only possible, because the returned object is actually a StagedStrategyChain<MemberProcessor, BuilderStage> which also implements IEnumerable<MemberProcessor>. But this is not guaranteed at all and may fail at any time. Also the exported interface is explicitly not the internal Unity class.

Funnily enough the "official" Legacy extension needs to make the very same unsafe cast 😉 Bad extension!

Suggested solution

Have IStagedStrategyChain<in TStrategyType, in TStageEnum> officialy derive from IEnumerable<TStrategyType>.

It's only implementor already implements both interfaces, and the name explicitly tells people it's a chain, so being able to enumerate the links in that chain is not unreasonable.

DoctorVanGogh avatar May 02 '19 18:05 DoctorVanGogh

I am not sure if this is a right approach but I'll do research and see if this could be done with current engine.

ENikS avatar May 09 '19 22:05 ENikS

It's explicitly aligned with how Unity resolves the other predefined DependencyResolver Attributes (Dependency & OptionalDependency). So sticking any "custom" attribute into the same lookup dictionaries as the predefined attributes seems the right place.

(Although currently even with the recent commit it's still cumbersome to catch all the different MemberProcessor derivatives since the actual AddFactories method is anchored on the specific generic type).

Maybe adding a IAttributeFactoryHolder (?) interface onto MemberProcessor<,> might be helpful if

interface IAttributeFactoryHolder {
    void AddFactories(IEnumerable<AttributeFactory> factories);
    AttributeFactory[] AttributeFactories { get; }
}
...
    public abstract partial class MemberProcessor<TMemberInfo, TData> : MemberProcessor,
                                                                        ISelect<TMemberInfo>,
                                                                        IAttributeFactoryHolder 
                                                                        where TMemberInfo : MemberInfo
...

That way library consumers could do

class FooResolutionExtension : UnityContainerExtension {

        protected override void Initialize() {
            foreach (var holder in Context.BuildPlanStrategies.OfType<IAttributeFactoryHolder>) {
                holder .AddFactories(....)
            }
        }
...

instead of having to manually go through all known concrete MemberProcessor types.

DoctorVanGogh avatar May 10 '19 00:05 DoctorVanGogh

This whole thing needs to be simplified a bit. If you think you could elegantly solve it, by all means, send a PR.

ENikS avatar May 10 '19 00:05 ENikS

Since I don't have vs 2019 available (yet), it'll just be a conceptual implementation:

  • Add new method(s) to IUnityContainer similar to the "new" RegisterFactory methods. Call them RegisterResolverAttribute (or somesuch) and make it
void RegisterResolverAttribute(Type attributeType, params AttributeFactory[] handlers) {
  if (!typeof(DependencyResolutionAttribute).IsAssignableFrom(attributeType))
    throw new InvalidArgumentException();
  // TODO: iterate pipeline(s), iterate member processors, call AddFactories(handlers) for each
}

void RegisterResolverAttribute<TAttribute>(params AttributeFactory[] handlers) 
  where TAttribute : DependencyResolutionAttribute
  => RegisterResolverAttribute(typeof(TAttribute), handlers);
  • Basically do all the wiring up under the hood

DoctorVanGogh avatar May 10 '19 17:05 DoctorVanGogh

https://weblogs.asp.net/ricardoperes/unity-part-5-injecting-values

ENikS avatar Dec 11 '19 01:12 ENikS

Added following virtual methods for customization:

        /// <summary>
        /// <see cref="ParameterInfo"/> resolver factory
        /// </summary>
        /// <<remarks>
        /// <see cref="ParameterInfo"/> resolver will attempt to resolve value for the parameter. 
        /// <para>If resolution succeeds, parameter will be initialized with resolved value. 
        /// If resolution fails, but parameter has default value, that value will be used to 
        /// initialize the parameter.</para>
        /// <para>If no default value is provided, container will either throw 
        /// <see cref="ResolutionFailedException"/> if dependency is required, or apply null 
        /// value if parameter is optional.</para>
        /// </remarks>
        /// <typeparam name="TContext">The context is implicitly determined based on calling method</typeparam>
        /// <param name="info"><see cref="ParameterInfo"/> of the injected parameter</param>
        /// <returns>Returns <see cref="ResolveDelegate{TContext}"/> that resolves dependency</returns>
        public virtual ResolveDelegate<TContext> GetResolver<TContext>(ParameterInfo info) 
            where TContext : IResolveContext
        {
         . . .
        }

        /// <summary>
        /// <see cref="PropertyInfo"/> resolver factory
        /// </summary>
        /// <typeparam name="TContext">The context is implicitly determined based on calling method</typeparam>
        /// <param name="info"><see cref="PropertyInfo"/> of the injected property</param>
        /// <returns>Returns <see cref="ResolveDelegate{TContext}"/> that resolves dependency</returns>
        public virtual ResolveDelegate<TContext> GetResolver<TContext>(PropertyInfo info)
            where TContext : IResolveContext => GetResolver<TContext>(info.PropertyType);

        /// <summary>
        /// <see cref="FieldInfo"/> resolver factory
        /// </summary>
        /// <typeparam name="TContext">The context is implicitly determined based on calling method</typeparam>
        /// <param name="info"><see cref="FieldInfo"/> of the injected field</param>
        /// <returns>Returns <see cref="ResolveDelegate{TContext}"/> that resolves dependency</returns>
        public virtual ResolveDelegate<TContext> GetResolver<TContext>(FieldInfo info)
            where TContext : IResolveContext => GetResolver<TContext>(info.FieldType);

        /// <summary>
        /// <see cref="Type"/> resolver factory
        /// </summary>
        /// <typeparam name="TContext">The context is implicitly determined based on calling method</typeparam>
        /// <param name="type"><see cref="Type"/> to resolve</param>
        /// <returns>Returns <see cref="ResolveDelegate{TContext}"/> that resolves dependency</returns>
        public abstract ResolveDelegate<TContext> GetResolver<TContext>(Type type) where TContext : IResolveContext;

ENikS avatar Dec 28 '19 01:12 ENikS