DI : Resolve dynamic type whilst also passing in constructor args

171 Views Asked by At

I have a Document class that has a list of Objects. To create and add an Object, you pass in a numeric object type id to Document.CreateObject(). Then depending on the type id, it will create the appropriate object instance.

If the id is 0, it will create an instance of Object1, if it is 1, it will create an instance of Object2.

How can I set this up with a DI container that allows me to specify the implementation for IObject1 and IObject2?

This is what I have so far, but don't know how to implement the commented out bit.

namespace ConsoleApp9
{
    public interface IObjectFactory
    {
        IObject Create(IDocument document, int type);
    }

    public class ObjectFactory : IObjectFactory
    {
        private readonly Func<IDocument, Type, IObject> _factory;

        public ObjectFactory(Func<IDocument, Type, IObject> factory)
        {
            _factory = factory;
        }

        public IObject Create(IDocument document, int type)
        {
            if (type == 0) return _factory(document, typeof(IObject1));
            if (type == 1) return _factory(document, typeof(IObject2));

            throw new NotSupportedException(nameof(type));
        }
    }

    public interface IDocument
    {
        void CreateObject(int type);
    }

    public class Document : IDocument
    {
        private readonly IObjectFactory _objectFactory;

        public List<IObject> Objects { get; } = new();

        public Document(IObjectFactory objectFactory)
        {
            _objectFactory = objectFactory;
        }

        public void CreateObject(int type)
        {
            var obj = _objectFactory.Create(this, type);
            Objects.Add(obj);
        }
    }

    public interface IObject
    {
        IDocument Document { get; }
    }

    public interface IObject1 : IObject { }

    public class Object1 : IObject1
    {
        public IDocument Document { get; }

        public Object1(IDocument document)
        {
            Document = document;
        }
    }

    public interface IObject2 : IObject { }

    public class Object2 : IObject2
    {
        public IDocument Document { get; }

        public Object2(IDocument document)
        {
            Document = document;
        }
    }

    internal class Program
    {
        static void Main(string[] args)
        {
            var services = new ServiceCollection();

            services.AddTransient<IObject1, Object1>();
            services.AddTransient<IObject2, Object2>();
            services.AddSingleton(typeof(IObjectFactory), (x) =>
            {
                return new ObjectFactory((IDocument document, Type type) =>
                {
                    // type will be either typeof(IObject1) or typeof(IObject2)
                    // I want to resolve by this type and also pass the constructor arg (document)
                    // x.GetRequiredService(type, document);

                    // I don't want to do this.
                    return new Object1(document);
                });
            });

            services.AddTransient<IDocument, Document>();
            
            var serviceProvider = services.BuildServiceProvider();

            var document = serviceProvider.GetRequiredService<IDocument>();
            document.CreateObject(0);
            document.CreateObject(1);
        }
    }
}

As mentioned in the comment. I want to resolve the object to create by using the type returned from the factory.

I'm probably going about this the complete wrong way!

2

There are 2 best solutions below

1
On

First and foremost, as per your current design, The IObject instances can only be created by passing the IDocument object in the constructor, so instantiating IObject instance types explicitly using the new keyword inside the Create method is a good approach, and this way, we can avoid circular dependency.

    public class ObjectFactory : IObjectFactory
    {
        public IObject Create(int type, IDocument document)
        {
            if (type == 0)
            {
                return new Object1(document);
            }

            if (type == 1)
            {
                return new Object2(document);
            }

            throw new NotSupportedException(nameof(type));
        }
    }

In case, you don't like to create instances explicitly using the new keyword, then don't pass the IDocument object in the constructor, instead create an object using the service provider, and then configure the IDocument object by calling an initialization method.

    public interface IObject1: IObject
    {
        IObject1 With(IDocument document);
    }

    public class Object1: IObject1
    {
        public IDocument Document { get; private set; }
   
        public IObject1 With(IDocument document)
        {
            Document = document;
            return this;
        }
    }

    public class ObjectFactory : IObjectFactory
    {
        private readonly IServiceProvider _serviceProvider;

        public ObjectFactory(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public IObject Create(int type, IDocument document)
        {
            if (type == 0)
            {
                return _serviceProvider.GetService<IObject1>().With(document);
            }

            if (type == 1)
            {
                return _serviceProvider.GetService<IObject2>().With(document);
            }

            throw new NotSupportedException(nameof(type));
        }
    }

1
On

From my point of view, reading only interfaces definitions, you have mixed what to do with how to do it.

Get only IDocument and IObject interfaces, others are only factories

public interface IDocument
{
    void CreateObject(int type);
}

public interface IObject
{
    IDocument Document { get; }
}

The IObject references IDocument, and IDocument has only a method to create Object.

Problem: from the definition I cannot understand if it are an IObject.

Try with this solution

public interface IDocument
{
    void AddObject(IObject object);
}

public interface IObject
{
    IDocument Document { get; }
}

Now I can better express the relation from interfaces, BUT I create a circular dependency; I try to refactor again. Most probably I don't need the entire IDocument but only the key(?).

public interface IDocument
{
    public int Id {get;}

    void AddObject(IObject object);
}

public interface IObject
{
    public int DocumentId { get; }
}

Another refactor to better express the relation could be

public interface IDocument
{
    public int Id { get; }

    public IReadOnlyCollection<IObject> Objects { get; }

    void AddObject(IObject object);
}

public interface IObject
{
    public int DocumentId { get; }
}

And now the int type problem. You cannot resolve an interface by int, there are some IoC frameworks that can do that like windsor castle, but, for better understand the problem, the questions are:

What are the differences between Object1 and ObjectN ? How does the int Type impact the instances? Can I delegate to another service injected on a generic IObject class ? I think you must better analyze the problem.

If you are required to resolve the IObject by an int you must use the factory for the IObject.

public interface IObjectFactory
{
    IObject Create(int documentId, int type);
}

I hope this answer can be useful to think about interfaces in another point of view.