1

BACKGROUND: We've an ASP.NET Core based web app using EF Core developed on top of DI & Generic Repository pattern. So, most of the things are done using interfaces. Now, we've reached master table maintenance module. We don't want to replicate the same service (backed by repository) class for all 10-20 master tables.

So, we've created a _ModelMasterBase class and derived all the master table classes from it. The CRUD for all master tables is the same. So, next we implemented things like MasterRepository<T>, MasterService<T> and their interfaces. Now everything has to use <T> where T is the type of the master table selected on the page to perform CRUD.

Initially, I expected that instance of IMasterService<_ModelMasterBase> can be converted to IMasterService<T> - again where T could be any child class derived from _ModelMasterBase - But it seems impossible! I've tried operators, casting, and almost everything I could google! Also due to repository pattern everything has to be strongly typed.

Now, we already use the trick to convert child obj to base class obj as per SO Post -

DerivedClass B = new DerivedClass();
BaseClass bc = JsonConvert.DeserializeObject<BaseClass>(JsonConvert.SerializeObject(B));

I know its a bit dirty trick but sometimes its handy to maintain the tradeoff between design and complexity. We use it with precaution. I wish there was something similar in case I wanted to cast MyService<Base> to MyService<Child>

Or you can forget all this and guide me to have a single point CRUD service for all my master tables - replicating the same thing 10-20 times seems irrational. Sorry, I couldn't explain in depth as it'd stretch the post.

Here's a v.basic sample of my code structure and at the end you'll see what we're trying to achieve. Hope it helps.


SOLUTION :

Based on mkArtak's suggestion, I was able to crack it by using 'Covariance' concept (example). Here's my updated code sample. Now there's a single controller and service layer for all master tables!

Hemant Tank
  • 1,724
  • 4
  • 28
  • 56
  • 1
    The usual example of why this doesn't work is `List`. If you could cast it to `List` then anyone would be able to add *anything* to the list, which would certainly confuse any code expecting that a `List` can only contain `string`s. – Damien_The_Unbeliever May 30 '17 at 09:00
  • Agreed but in my case we wanted to know if there was a way to achieve it somehow - knowing the exact boxing and unboxing scenarios. Like we can do - List<_ModelBaseMaster> result = data.Cast<_ModelBaseMaster>().ToList(); - where data can be a List and T is derived from _ModelBaseMaster. – Hemant Tank May 30 '17 at 11:07

2 Answers2

1

Inheritance problems?

This sort of thing smells like you should prefer composition over inheritance. As you're stating you're able to serialize/deserialize your specialized class to your base class, isn't this more like a data transfer object by definition? I wouldn't put logic into these but instead introduce another service that uses the base class logic without inheritance (e.g. passing an instance into the constructor) - that might solve the issue in the first place.

Not a solution

As of your particular question: you might be able to use a contravariant type parameter if this isn't too restrictive (e.g. you can only use the type parameter in parameters not in return types) and you own the interfaces in question. But the compiler only allows me to assign the services the other way around - which makes sense. Maybe you find a solution for this in a covariant manner instead?

using System;

public class Program
{
    internal interface IMasterService<in T> {
        void DoSomething(T table);
    }

    internal class BaseClass {}

    internal class DerivedClass : BaseClass {}

    internal class SpezializedService : IMasterService<BaseClass> {
        public void DoSomething(BaseClass table) {}
    }

    public static void Main()
    {
        // The compiler isn't happy about the other way around
        IMasterService<DerivedClass> baseService = new SpezializedService();
    }
}

Hope this helps.

mfeineis
  • 2,607
  • 19
  • 22
  • Thank you for your elaborated explanation, I think its close. Can you spare a few moments to take a look at my short code sample and help me understand how your solution will fix it. – Hemant Tank May 30 '17 at 12:16
1

Short answer: in the IMasterService definition prefix T with 'out' keyword.

public interface IMasterService<out T>
{
    // your existing methods' definitions here
}

It's called Covariance, which is described in MSDN.

Artak
  • 2,819
  • 20
  • 31
  • Thank you. Can you spare a few moments to take a look at my short code sample and help me understand how your solution will fix it. – Hemant Tank May 30 '17 at 12:15
  • @HemantTank The problem is that you can only use a covariant type as return types not for input parameters like the code sample suggests – mfeineis May 30 '17 at 12:39
  • @HemantTank, the fix is literally in adding the "out" for type parameter in the IMasterService definition. It will enforce the compatability between IMasterService and IMasterService implementations, so you'll be able to write something like:IMasterService baseMasterService = childMasterService; // whwere childMasterService is IMasterService type. In VB.Net you should use the following declaration of the interface: Interface IMasterService End Interface – Artak May 30 '17 at 17:33
  • You're a life saviour! I strived so hard that such a small fix seem impossible. My next is to address my CRUD routines like - void Add(T mObj); ERROR : Invalid variance : The type parameter 'T' must be contravariantly valid on 'IMasterService.Add(T)'. 'T' is covariant – Hemant Tank May 30 '17 at 19:38
  • I really advise you to go over the link I've shared. I'll try to get a sample here later. – Artak May 30 '17 at 19:50
  • Yes, I'm learning. Also found a more elaborated material - https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/covariance-contravariance/creating-variant-generic-interfaces - meanwhile, do share any example, I'm also trying. – Hemant Tank May 30 '17 at 20:21
  • @mkArtak you can't have both in this way with variance its one or the other - covariance and use of T as a return type XOR contravariance and use of T as parameters... maybe you can trick the compiler by using another contravariant type parameter and use the same class :-) `interface IMasterService {}` with using the same type on `T` and `U` – mfeineis May 31 '17 at 16:19
  • 1
    Hey @HemantTank, looks like you'll have to make the Add method generic as follows: void Add(V v) where V : BaseClass; – Artak May 31 '17 at 16:26
  • Done here's my sample code - https://pastebin.com/fJwBfF91 Also look for the unconventional ConvertParenttoChild(object parentInstance) where T will be child object type. – Hemant Tank Jun 05 '17 at 11:40