-1

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!

Vastar
  • 9
  • 4
  • You seem to have a circular dependency on implementation not explicit by interfaces; an `IObject` need an `IDocument` and IDocument needs a `void createObject(int type)` method but it seems like an IObject from implementation. Try to describe what is your real problem. – Max Jul 02 '23 at 14:37
  • @Max yes, a Document holds one or more objects. But an object also holds a reference to the document it is contained in. – Vastar Jul 02 '23 at 14:47
  • Just to point out, I'm not resolving the document from the container when creating an object, I already have access to document reference. So it's not a circular dependency – Vastar Jul 02 '23 at 14:59

2 Answers2

0

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));
        }
    }

codeninja.sj
  • 3,452
  • 1
  • 20
  • 37
  • The first option doesn't really help though, as you can't change the implementation of IObject1 and IObject2 from the container configuration. The second option exposes the IServiceProvider which I think is bad practise, and also means I can't test it as my test now needs to create a IServiceProvider. The ```.With``` also breaks encapsulation of the object class. So you end up with a poor implementation of the object class just for the sake of DI. It's weird that you can't simply pass constructor arguments when you resolve from the DI container – Vastar Jul 03 '23 at 08:54
0

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.

Max
  • 6,821
  • 3
  • 43
  • 59
  • I don't understand why having the Object have a reference back to the document is such an issue? A simpler example is an XmlDocument. Where you add an XmlElement to the the Document, each XmlElement in the document keeps a reference to the XmlDocument. – Vastar Jul 03 '23 at 14:24