Passing the context around in a C# class library, looking for an "easy" way without using static
Asked Answered
S

1

6

For a library (.NET Standard 2.0), I designed some classes that look roughly like this:

public class MyContext
{
    // wraps something important. 
    // It can be a native resource, a connection to an external system, you name it.
}

public abstract class Base
{
    public Base(MyContext context)
    {
        Context = context;
    }

    protected MyContext Context { get; private set; }

    // methods
}

public class C1 : Base
{
    public C1(MyContext context, string x)
        : base(context)
    {
        // do something with x
    }

    // methods
}

public class C2 : Base
{
    public C2(MyContext context, long k, IEnumerable<C1> list)
        : base(context)
    {
        // do something with the list of C1s.
    }

    // methods
}

public class C3 : Base
{
    public C3(MyContext context, int y, double z)
        : base(context)
    {
        // do something with y and z.
    }

    public void DoSomething()
    {
        var something = new C2(this.Context, 2036854775807, new[]
            {
            new C1(this.Context, "blah"),
            new C1(this.Context, "blahblah"),
            new C1(this.Context, "blahblahblah"),
            }
            );
    }

    // other methods
}

// other similar classes

The classes were criticized because of that "MyContext" parameter that every constructor requires. Some people say that it clutters the code.

I said that the parameter is needed to propagate the context, so that e.g. when you call the DoSomething method of C3, the contained instances of C1 and C2 all share the same context.

A potential alternative is defining the parameter as Base instead of MyContext, e.g.:

public abstract class Base
{
    public Base(Base b) // LOOKS like a copy constructor, but it isn't!
    {
        Context = b.Context;
    }

    protected MyContext Context { get; private set; }

    // methods
}

public class C1 : Base
{
    public C1(Base b, string x)
        : base(b)
    {
        // do something with x
    }

    // methods
}

public class C2 : Base
{
    public C2(Base b, long k, IEnumerable<C1> list)
        : base(b)
    {
        // do something with the list of C1s.
    }

    // methods
}

public class C3 : Base
{
    public C3(Base b, int y, double z)
        : base(b)
    {
    }

    public void DoSomething()
    {
        var something = new C2(this, 2036854775807, new[]
            {
            new C1(this, "blah"),
            new C1(this, "blahblah"),
            new C1(this, "blahblahblah"),
            }
            );
    }

    // other methods
}

// other similar classes

Now the parameter you must pass is shorter, but it still raised some eyebrows.

(And I don't like it very much, because logically a C1/C2/C3/etc object does not require a Base, it requires a MyContext. Not to mention that constructor that seems a copy constructor, but it is not! Oh, and now how can I initialize C1, C2 or C3 outside a Base-derived class, since all of them want a Base, and Base is abstract? I need to "bootstrap" the system somewhere... I guess all classes may have two constructors, one with Base and one with MyContext ... but having two constructors for each class may be a nightmare for future maintainers...).

Some people said "Why don't you turn MyContext into a singleton, like in Project X?". Project X has a similar class structure but, "thanks" to the singleton, classes don't have a MyContext parameter: they "just work". It's magic! I don't want to use a singleton (or static data in general), because:

  • singletons are actually global variables, global variables are a bad programming practice. I used global variables many times, and many times I regretted my decision, so now I prefer to avoid global variables if possible.
  • with a singleton, classes share a context, but only ONE context: you cannot have MULTIPLE contexts. I find it too limiting for a library that will be used by an unspecified number of people in the company.

Some people said that we should not be too anchored to "academic theory", useless ideas of "good" and "bad" programming practices, that we should not overengineer classes in the name of abstract "principles". Some people said that we should not fall into the trap of "passing around ginormous context objects". Some people said YAGNI: they're pretty sure that 99% of the usage will involve exactly one context.

I don't want to anger that 1% by disallowing their use case, but I don't want to frustrate the other 99% with classes that are difficult to use.

So I thought about a way to have the cake and eat it. For example: MyContext is concrete but it ALSO has a static instance of itself. All classes (Base, C1, C2, C3, etc) have two constructors: one with a MyContext concrete parameter, the other one without it (and it reads from the static MyContext.GlobalInstance). I like it... and at the same time I don't, because again it requires to maintain two constructors for each class, and I'm afraid it can be too error-prone (use the incorrect overload just once and the entire structure collapses, and you find out only at runtime). So forget for the moment the "static AND non static" idea.

I tried to imagine something like this:

public abstract class Base
{
    public Base(MyContext context)
    {
        Context = context;
    }

    protected MyContext Context { get; private set; }


    public T Make<T>(params object[] args) where T : Base
    {
        object[] arrayWithTheContext = new[] { this.Context };
        T instance = (T)Activator.CreateInstance(typeof(T),
            arrayWithTheContext.Concat(args).ToArray());
        return instance;
    }

    // other methods
}

public class C1 : Base
{
    public C1(MyContext context, string x)
        : base(context)
    {
        // do something with x
    }

    // methods
}

public class C2 : Base
{
    public C2(MyContext context, long k, IEnumerable<C1> list)
        : base(context)
    {
        // do something with the list of C1s.
    }

    // methods
}

public class C3 : Base
{
    public C3(MyContext context, int y, double z)
        : base(context)
    {
        // do something with y and z.
    }

    public void DoSomething()
    {
        var something = Make<C2>(2036854775807, new[]
            {
            Make<C1>("blah"),
            Make<C1>("blahblah"),
            Make<C1>("blahblahblah"),
            }
            );
    }

    // other methods
}

// other similar classes

The calls to Make LOOK better, but they are even more error-prone, because the signature of Make is not strongly-typed. My goal is to simplify doing stuff for the users. If I opened a parenthesis and Intellisense proposed me ... an array of System.Object, I'd be VERY frustrated. I could pass incorrect parameter and I would find out only at runtime (Since good old .NET Framework 2.0, I hoped to forget about arrays of System.Object...)

So what? A factory with a dedicated, strongly-typed method for each Base-derived class would be type-safe, but maybe it would also be "ugly" to see (and a textbook violation of the open-close principle. Yes, "principles", again):

public class KnowItAllFactory
{
    private MyContext _context;

    public KnowItAllFactory(MyContext context) { _context = context; }

    public C1 MakeC1(string x) { return new C1(_context, x); }
    public C2 MakeC2(long k, IEnumerable<C1> list) { return new C2(_context, k, list); }
    public C3 MakeC3(int y, double z) { return new C3(_context, y, z); }
    // and so on
}

public abstract class Base
{
    public Base(MyContext context)
    {
        Factory = new KnowItAllFactory(context);
    }

    protected MyContext Context { get; private set; }
    protected KnowItAllFactory Factory { get; private set; }

    // other methods
}

public class C1 : Base
{
    public C1(MyContext context, string x)
        : base(context)
    {
        // do something with x
    }

    // methods
}

public class C2 : Base
{
    public C2(MyContext context, long k, IEnumerable<C1> list)
        : base(context)
    {
        // do something with the list of C1s.
    }

    // methods
}

public class C3 : Base
{
    public C3(MyContext context, int y, double z)
        : base(context)
    {
        // do something with y and z.
    }

    public void DoSomething()
    {
        var something = Factory.MakeC2(2036854775807, new[]
            {
            Factory.MakeC1("blah"),
            Factory.MakeC1("blahblah"),
            Factory.MakeC1("blahblahblah"),
            }
            );
    }

    // other methods
}

// other similar classes

EDIT (after the comments): I forgot to mention that yes, someone suggested me about using a DI/IoC framework like in "Project Y". But apparently "Project Y" uses it only to decide if instantiate MyConcreteSomething or MyMockSomething when ISomething is needed (please consider mocking and unit tests out of scope for this question, because reasons), and it passes around the DI/IOC framework objects exactly like I pass around my context...

Stammer answered 18/11, 2020 at 7:40 Comment(5)
In my opinion, there is nothing wrong with your very first code snippet (the one with context in the constructors). It make the code much clearer and maintainable. This is called inversion of control. Your project can use a Dependency Injection framework to automate the process of injecting an instance in every constructor. But it is very good that your classes explicitly specify what they need in their constructors.Lightish
I'd stay with the first snippet approach. You can't make everyone happy, nor should you care much about that. They provided some "arguments", you found them unconstructive, so just move on.Wortman
First snippet is standard constructor injection. I dont see a problem either. Another approach would be using DbContextScope. It has its pros and cons, likewise your pattern. You have a lot of control on how and when you create a context. You'd still have parameters in repositories constructors, i.e public MyRepository(IAmbientDbContextLocator contextLocator) - you don't have to create it in every service method, but you could, if needed.Selfreliance
My bad, above one is not .net standard, there is a fork thoughSelfreliance
thanks to all of you people! Yes, I forgot to mention, someone suggested me about using a DI/IOC framework like in "Project Y", but apparently "Project Y" uses it only to decide if instantiate MyConcreteSomething or MyMockSomething when ISomething is needed (please consider mocking and unit tests out of scope for this question, because reasons), and it passes around the DI/IOC framework objects exactly like I pass around my context...Stammer
N
3

Of the alternatives listed in OP, the original one is the best. As the Zen of Python states, explicit is better than implicit.

Using a Singleton would make an explicit dependency implicit. That doesn't improve usability; it makes things more unclear. It makes the API harder to use because there's more things a user of the API would have to learn before he or she could use it successfully (as opposed to just looking at the constructor signature and supplying the arguments that makes the code compile).

It doesn't really matter whether the API requires a few more keystrokes to use. In programming, typing isn't a bottleneck.

Passing MyContext via the constructor is just plain old Constructor Injection, even if it's a concrete dependency. Don't let the Dependency Injection (DI) jargon fool you, though. You don't need a DI Container to use those patterns.

What you might consider as an improvement is to apply the Interface Segregation Principle (ISP). Does the base class need an entire MyContext object? Could it implement its methods with only a subset of MyContext's API?

Does the base class need MyContext at all, or is it only there for the benefit of derived classes?

Do you even need a base class?

In my experience, you can always come up with a better design that doesn't use inheritance. I don't know what it'd be in this case, but how about something like the following?

public class C1
{
    public C1(MyContext context, string x)
    {
        // Save context and x in class fields for later use
    }

    // methods
}

public class C2
{
    public C2(MyContext context, long k, IEnumerable<C1> list)
    {
        // Save context, k, and the list of C1s in class fields for later use
    }

    // methods
}

public class C3
{
    public C3(MyContext context, int y, double z)
    {
        Context = contex;
        // do something with y and z.
    }

    public MyContext Context { get; }

    public void DoSomething()
    {
        var something = new C2(this.Context, 2036854775807, new[]
            {
            new C1(this.Context, "blah"),
            new C1(this.Context, "blahblah"),
            new C1(this.Context, "blahblahblah"),
            }
            );
    }

    // other methods
}

Whether or not you have a base class makes no difference when it comes to reusing a single instance of MyContext. You can still do this even if these three classes have no common supertype.

Nikos answered 18/11, 2020 at 9:26 Comment(1)
Thank you for your answer (I'll wait some days and then I'll mark it green if there are no other answers). Very interesting ideas, worth exploring and experimenting. But I have a hunch that it's more likely for certain people (er... including me???) to accept avoiding global variables... than to admit that a class library can exist without inheritance :D Blame some university teachers ;)Stammer

© 2022 - 2024 — McMap. All rights reserved.