0

Given the code example at the end of this question, how would you configure a Microsoft DI container to resolve the types?

For example, I would like to be able to have something like :

var services = new ServiceCollection();
services.AddTransient<IHeader, Header>();
services.AddTransient<IParagraph, Paragraph>();
services.AddTransient<IFooter, Footer>();
services.AddTransient<IDocument, Document>();
services.AddTransient<IUndoRedo, UndoRedo>();

where I could for example swap out the implementation of IParagraph for something else, without having to make the change in lots of places, like this :

services.AddTransient<IParagraph, ParagraphVersion2>();

Here is the code I am trying to convert to use DI :

public enum ObjectType
{
    Header,
    Footer,
    Paragraph
}

public interface IObject
{
    IUndoRedo UndoRedo { get; }
}

public interface IHeader : IObject { }
public class Header : IHeader
{
    public IUndoRedo UndoRedo { get; }

    public Header(IUndoRedo undoRedo)
    {
        UndoRedo = undoRedo;
    }
}

public interface IParagraph : IObject { }
public class Paragraph : IParagraph
{
    public IUndoRedo UndoRedo { get; }

    public Paragraph(IUndoRedo undoRedo)
    {
        UndoRedo = undoRedo;
    }
}

public interface IFooter : IObject { }
public class Footer : IFooter
{
    public IUndoRedo UndoRedo { get; }

    public Footer(IUndoRedo undoRedo)
    {
        UndoRedo = undoRedo;
    }
}

public interface IDocument
{
    IUndoRedo UndoRedo { get; }
    List<IObject> Objects { get; }
}

public interface IUndoRedo { }
public class UndoRedo : IUndoRedo { }

public class Document : IDocument
{
    public IUndoRedo UndoRedo { get; }
    public List<IObject> Objects { get; } = new();

    public Document(IUndoRedo undoRedo)
    {
        UndoRedo = undoRedo;
    }
}

public class ObjectFactory
{
    public IObject Create(ObjectType objectType, IUndoRedo undoRedo)
    {
        switch (objectType)
        {
            case ObjectType.Header:
                return new Header(undoRedo);
            case ObjectType.Footer:
                return new Footer(undoRedo);
            case ObjectType.Paragraph:
                return new Paragraph(undoRedo);
            default:
                throw new NotSupportedException(nameof(objectType));
        }
    }
}

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

        // each new document gets a unique undoredo that is shared by its objects
        {
            var undoRedo1 = new UndoRedo();
            var document1 = new Document(undoRedo1);
            document1.Objects.Add(factory.Create(ObjectType.Header, undoRedo1));
            document1.Objects.Add(factory.Create(ObjectType.Footer, undoRedo1));
            document1.Objects.Add(factory.Create(ObjectType.Paragraph, undoRedo1));
            document1.Objects.Add(factory.Create(ObjectType.Paragraph, undoRedo1));
        }

        // each new document gets a unique undoredo that is shared by its objects
        {
            var undoRedo2 = new UndoRedo();
            var document2 = new Document(undoRedo2);
            document2.Objects.Add(factory.Create(ObjectType.Header, undoRedo2));
            document2.Objects.Add(factory.Create(ObjectType.Footer, undoRedo2));
            document2.Objects.Add(factory.Create(ObjectType.Paragraph, undoRedo2));
        }
    }
}

I can't quite figure out how to pass the constructor arguments. There should be one instance of IUndoRedo per document, and each object within the document should be passed the documents IUndoRedo instance.

The only way I have managed to do it, is to remove the constructor arguments and use properties instead, that are set after construction. But I don't want to do this, as it means you can accidently create partially valid objects if you forget to set the required properties.

Vastar
  • 9
  • 4
  • 2
    The container would do that for you, normally, except this looks like a document model and DI isn't suited to that because you have to figure out *which* thing to pass in each place. The container isn't going to have the document elements registered because they're going to vary from document to document, paragraph to paragraph, etc. You could register factories for creating each of the elements, inject and use the factories, but you yourself have to put the elements created with them together into a meaningful document. – madreflection Jul 03 '23 at 21:40
  • I wouldn't expect it to have the element instances registered, just the types. For example, there could be a document parser class that would read in an xml file, and if the xml element tag was 'Header' it would request an instance of 'IHeader' passing in the Header text content into the constructor. – Vastar Jul 03 '23 at 22:01
  • I was only talking about the types, not the instances. Again, this isn't suited to DI. Your Header, for example, might have content that needs to be parsed further. A Paragraph definitely would. The container needs to know about the context of the parsing in order to parse it and create that sub-graph before passing it to the constructor. The container can't know about the parsing context, nor should it. The document (object graph) isn't a *dependency* here, and that's the key thing to remember. However, factories that create the parts of the document *are* dependencies. – madreflection Jul 03 '23 at 22:21
  • 2
    You can create all these as scoped services instead of transient, then create a scope per document. But if feels like you are trying to use DI to track data, rather than services. You could register a factory for each type, but I wouldn't try to put the data objects themselves into DI. – Jeremy Lakeman Jul 04 '23 at 00:51
  • Ok, thanks for your comments. It would be a WPF application. Imagine an application like Microsoft word, where the user could edit multiple documents at a time, each document in a new tab. The objects would be for example them creating a image or hyperlink in their document. So is DI not used for these types of apps? – Vastar Jul 04 '23 at 05:37
  • @JeremyLakeman would you mind showing example code? As I'm really struggling to understand how to go about this. – Vastar Jul 04 '23 at 05:41
  • IMHO your `Document` class should implement helper methods to create the other classes, passing the `UndoRedo` instance to each constructor, and append them to the `Objects` list. Encapsulating all those details so the caller doesn't need to worry about them. – Jeremy Lakeman Jul 04 '23 at 05:54

1 Answers1

3

A paragraph is not suitable for AddTransient<TType>, because documents can have more than one paragraph and presumably you don't want the same paragraph every time. Instead, you need a "Paragraph Factory,", which you can add with this prototype.

services.AddTransient( x => new Paragraph(x.GetService<IDocument>().UndoRedo ) );

This code declares a small function which returns a paragraph. The function accepts the service collection as an argument, so you can use it to get the IDocument instance, then use the document's UndoRedo instance to pass to the constructor for the paragraph.

You can use the same technique for your other document objects.

John Wu
  • 50,556
  • 8
  • 44
  • 80
  • But there are one or more documents open at a time, wouldn't this return a new document each time? – Vastar Jul 04 '23 at 08:42
  • It depends how you register `IDocument`. Would you like it to? – John Wu Jul 04 '23 at 08:53
  • I gave an example of how I would like it to behave in the question. – Vastar Jul 04 '23 at 08:55
  • Also, why would AddTransient return the same paragraph each time? Isn't that the whole point of transient? In that it returns a new instance each time? – Vastar Jul 04 '23 at 08:59
  • can you explain @John? – Vastar Jul 05 '23 at 17:22
  • Hard to explain without seeing your new code. But it depends if you are using the dependency injection or service locator pattern (see [this question](https://stackoverflow.com/questions/1557781/whats-the-difference-between-the-dependency-injection-and-service-locator-patte#:~:text=Service%20Locator%20is%20used%20when,while%20DI%20is%20global%20level.)). If you are using service locator, yes, you can pull many instances. If you are using DI, you always get only one instance pushed to to. So that instance needs to be a factory, so you can create multiple different paragraphs. – John Wu Jul 05 '23 at 18:07
  • Note that service locator is not recommended, see [this](https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/) and [this](https://stackoverflow.com/questions/22795459/is-servicelocator-an-anti-pattern). – John Wu Jul 05 '23 at 18:09