2

Let's say I have a data model consisting of 2 classes, both implementing the same interface:

    interface IEntity { }
    class EntityTypeA : IEntity { }
    class EntityTypeB : IEntity { }

I have a generic service which contains a list of these entities and does something with them. There are multiple different implementations of the Service, all inheriting from IService, but let's say for now there's just one, "Service".

    interface IService<T> where T : class, IEntity {
        // stuff
        T GetEntity(Func<T, bool> linq);
    }

    class Service<T> : IService<T> where T : class, IEntity {
        // stuff
        IEnumerable<T> _entities;
    }

At this point I can easily create new services for various entities and work with them. Add new entities of a specific type to them, call methods, get them back without having to cast anything manually.

    IService<EntityTypeA> serviceA = new Service<EntityTypeA>();
    IService<EntityTypeB> serviceB = new Service<EntityTypeB>();

All fine, but now I want to have all these services stored in one place so I can easily fetch the one I want later, without having to keep them all in a separate variable.

Ultimately I wanted to be able to do sth like this:

    _manager = new ServiceManager();
    _manager.AddService("A", serviceA);
    _manager.AddService("B", serviceB);
    IService<EntityTypeA> serviceA = _manager.GetService<EntityTypeA>("A");

So I tried something like this:

class ServiceManager {
        IDictionary<string, IService<IEntity>> _services;

        public void AddService<T>(string key, IService<T> manager) where T : class, IEntity {
            _services[key] = (IService<IEntity>)manager;
        }

        public IService<T> GetService<T>(string key) where T : class, IEntity {
            return (IService<T>)_services[key];
        }
    }

The issue here is the "invalid cast exception" when calling the AddService (and probably GetService as well) methods, I cannot cast and store an Service<EntityTypeA> into IService<IEntity>. Which was a bit of a surprise to me, since EntityTypeA implements IEntity and Service implements IService...

So my question is: How can I store all these generic services in a single variable so I can easily get them with one method from the manager? I'd like this manager to be a single instance responsible for managing all these services but I have no idea how to hold all these generic classes in it.

Rob1n
  • 23
  • 3
  • Any reason why you are using an interface and not an abstract class? Regards – Adrian Efford May 13 '20 at 08:01
  • I thought this is a good way to use interfaces, interfaces define methods that will be used in services but each service has its own implementation of them depending on what it's supposed to do with IEntities within. How would you change this to abstract? – Rob1n May 13 '20 at 08:13
  • Hello Rob1n, welcome to StackOverflow. What you describe it's similar to what an IoC does (in order to register and retrieve a service). Could you please add the full exception in order to see if there is any hint there. I haven't reproduced your code but the idea looks ok for me. – Sebastian Inones May 13 '20 at 08:15
  • You could declare an abstract class with the same methods (abstract) as in your interface. Your services inherite from the abstract class (overriding the methods of corse). Now you can declare your dicctionary from the typ //AbstractClassName, and store your services in it. Your mehtod "GetService" returns a type //AbstractClassName. If this seems like a valid solution for you let me know and I will post a solution for you ! – Adrian Efford May 13 '20 at 08:19
  • Does this answer your question? [C# generic handlers, what am I misunderstanding?](https://stackoverflow.com/questions/15723317/c-sharp-generic-handlers-what-am-i-misunderstanding) – Pavel Anikhouski May 13 '20 at 08:26
  • @AdrianEfford But that abstract class would still have to be generic so it can have methods in it that take a generic parameter, so I still have the same issue in the end? Unless I'm missing something – Rob1n May 13 '20 at 08:36
  • In my opinion the service you are now passing to the method would not be generic rather from type Entity. There you can pass eighter EntityA or EntityB because both inherit from Entity. Maybe I'm missing something but I think this should work – Adrian Efford May 13 '20 at 08:40
  • @PavelAnikhouski Thanks for the link. It seems similar but it is 7 years old and it mostly goes into why it doesn't work rather than what a good solution would be. But it seems to me that there still isn't a good solution in either case... – Rob1n May 13 '20 at 08:42

1 Answers1

3

You can't store an Service<EntityTypeA> into IService<IEntity> because IService is invariant on T. Generic type parameters are by default invariant, unless you have declared them otherwise. Foo<Derived> is not assignable to Foo<Base> in general. See this post for why.

Depending on what the // stuff is in IService, you can potentially make T covariant, allowing you to assign a value of type IService<EntityTypeA> (and hence Service<EntityTypeA>) to a variable of type IService<IEntity>.

You do this by adding an out modifier on T:

interface IService<out T> where T : class, IEntity {
    // stuff
    T GetEntity(Func<T, bool> linq);
}

This will not work if IService has methods that take in a T (among other things):

interface IService<out T> where T : class, IEntity {
    // stuff
    T GetEntity(Func<T, bool> linq);
    void Foo(T t); // compiler error
}

as this will break type safety:

IService<IEntity> s = new Service<EntityTypeA>();
s.Foo(new EntityTypeB()); // breaks type safety! I can now give Service<EntityTypeA> a EntityTypeB object!
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • I can give it a try, but do you consider this as something I should do, or should I maybe restructure my code in a different way? Whenever I have to "work around the code" I feel like I'm doing something wrong, even though my use case seems somewhat sensible. – Rob1n May 13 '20 at 08:38
  • @Rob1n You should do this, if `Service` doesn't "have methods that take in a `T` (among other things)". Basically, if `Service` _can_ be covariant, it _should_ be. – Sweeper May 13 '20 at 08:44
  • It can take in a generic Func linq query which filters and returns the entities within. From the example: `T GetEntity(Func linq);` So this means I can't make it covariant then, right? – Rob1n May 13 '20 at 08:48
  • 1
    @Rob1n That's fine. Basically, if the compiler allows it, your class can be covariant. – Sweeper May 13 '20 at 08:49