autofac / Documentation

Usage and API documentation for Autofac and integration libraries
http://autofac.readthedocs.org
MIT License
71 stars 121 forks source link

Service resolution algorithm should be documented #138

Closed zvrba closed 3 years ago

zvrba commented 3 years ago

Problem Statement

The documentation section on "Controlling Scope and Lifetime" refers to Nicholas Blumhardt's article. Therein one can find the following sentence "When the resolve operation has moved from a child scope to the parent, any further dependencies will be resolved in the parent scope."

This sentence made me believe that the following test would fail, i.e., I expected that resolving ComponentUser from innerScope would return ScopedComponent named "OUTER" because the resolution had resolve ComponentUser from the parent scope, where it was registered.

        [Fact]
        public void OuterComponentResolvesInnermostInstance() {
            var cb = new ContainerBuilder();

            var outerscoped = new ScopedComponent("OUTER");
            cb.RegisterInstance(outerscoped);
            cb.RegisterType(typeof(ComponentUser));

            using var outer = cb.Build();

            {
                var cu = outer.Resolve<ComponentUser>();
                Assert.Equal("OUTER", cu.SC.Name);
                Assert.True(ReferenceEquals(outerscoped, cu.SC));
            }

            {
                var innerscoped = new ScopedComponent("INNER");
                using var inner = outer.BeginLifetimeScope("InnerScope", cb2 => {
                    cb2.RegisterInstance(innerscoped);
                });

                var cu = inner.Resolve<ComponentUser>();
                Assert.Equal("INNER", cu.SC.Name);
                Assert.True(ReferenceEquals(cu.SC, innerscoped));

                var innerscoped2 = new ScopedComponent("INNER2");
                using (var inner2 = inner.BeginLifetimeScope("InnerScope2", cb2 => {
                    cb2.RegisterInstance(innerscoped2);
                })) {
                    var cu2 = inner2.Resolve<ComponentUser>();
                    Assert.Equal("INNER2", cu2.SC.Name);
                    Assert.True(ReferenceEquals(cu2.SC, innerscoped2));
                }

                var cu1 = inner.Resolve<ComponentUser>();
                Assert.Equal("INNER", cu.SC.Name);
                Assert.True(ReferenceEquals(cu1.SC, innerscoped));
            }

            {
                var cu = outer.Resolve<ComponentUser>();
                Assert.Equal("OUTER", cu.SC.Name);
                Assert.True(ReferenceEquals(outerscoped, cu.SC));
            }
        }

        class ScopedComponent
        {
            public readonly string Name;
            public ScopedComponent(string name) { Name = name; }
        }

        class ComponentUser
        {
            public ScopedComponent SC;
            public ComponentUser(ScopedComponent sc) { SC = sc; }
        }
    }

However, the test passes (actually the desired behavior).

Desired Solution

Documentation should contain an algorithmic description of the resolution process. The way things are now, I have no idea whether the above works by accident (by way of being an implementation detail) or by design.

zvrba commented 3 years ago

I can propose a terse summary of my understanding for the documentation (also confirmed by more tests):

"Lifetime scopes can be thought of being organized in a tree of nodes (parent-child relationship), container being the tree root. When resolving a service and its dependencies, Autofac considers the union of all registrations along the (single) path starting from the current lifetime scope and up to the root."

(Now, of course, the devil is in the details: multiple registrations for the same service [last one wins], etc.)

tillig commented 3 years ago

That "devil is in the details" part is why we haven't got super detailed doc on all of this and, instead, somewhat spread it out. For example, if you wanted to know how lifetime scopes work with decorators, it's on that page instead of in one central location. (It's also easier to maintain all the stuff about decorators on the decorators page instead of trying to find all the places spread out where it might be.) Same for everything else - composite services, instance-per-request, application integrations, etc.

I think you're right, that we could add more info and some analogies to help folks understand it better. I don't know if it will necessarily be a full "algorithmic description" because, as you're discovering, it's not that easy. There's also this interesting dichotomy where if the docs are too long folks won't read them; if they're too detailed then it begs the nitpickers to come find errors and try to include every edge case, which leads to them being too long; if they're too detailed then folks won't actually look beyond the one doc to see if there's more info, so they get confused when things don't fit their mental model; but if they're not long enough then the concept clarity is missing. We need to find the right balance - convey the information so the concepts are understood, with enough detail (maybe some examples) to explain it, but likely not at a pseudocode or flowchart level because we won't be able to capture everything in one go.

zvrba commented 3 years ago

I get your point about the documentation, but I've spent almost the whole day today trying to figure out how resolution works across lifetime scopes, both from the docs and by searching the net. The only article I found is the one I cited, and that one contains a single sentence that is, at best, misleading, if not straight out wrong. So I resorted to writing tests, but the problem with that approach is that I don't know whether this behavior can be relied upon in the future. (I have a C++ background where undefined, unspecified and implementation-defined behaviors all exist. A test can confirm that Autofac version x.y.z behaves "as such", but it can never tell me whether the behavior is part of the "official contract" or... just an implementation artifact. From the previous ticket, I have realized that you take compatibility seriously, but relying just on tests without any kind of semi-formal contract (think unix man pages for syscalls) makes me... uneasy. On the other hand, maybe I'm just too "damaged" by C++ :D)

PS: I'd ignore nitpickers. They can fork the docs and write them as they'd themselves like them to be. As you wrote before, you're working on this for free and you don't owe anyone anything. So when I open a ticket, it's more of like asking for a favor rather than expecting you to obey to my wishes.

tillig commented 3 years ago

I'm going to combine this with #89 since they're roughly the same thing.