A Best Practice for Designing Interfaces in .NET C#
Tags: csharp, architecture, dotnet, programming
Published: 2021-12-29 10:32:12

### Is it enough to define `IMyInterface<T>`? do I need `IMyInterface` as well?
<br>
While working on your masterpiece software system, you define your own interfaces to act as contracts between your different system modules and to expose some contracts to the outside world. This is not rocket science to any software developer.

However, what I learned throughout my years as a Software Engineer is that we should put more care about how we design our interfaces. If you search the internet, you would find tons of resources discussing the best practices to follow when designing interfaces, some of these are actually good resources.

But, I have one more best practice to tell you about which I couldn’t find online except for rare coincidences.

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

![Photo by [Susan Q Yin](https://unsplash.com/@syinq?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/10368/1*SQ6hIAZwJcTNxcEY1EEmsg.jpeg)

## Is it enough to define `IMyInterface<T>`? do I need `IMyInterface` as well?

To answer this question, let’s walk through the example below step by step, so bear with me and don’t rush things out.

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

![Photo by [Goh Rhy Yan](https://unsplash.com/@gohrhyyan?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/11358/1*IBfTo5s9hFUjdTk6oJth8w.jpeg)

### Disclaimer

We would not follow a lot of the best practices here for simplicity and to drive the main focus to the topic we are discussing. So, no unnecessary abstractions, no care for immutability,….and so on.

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

### Main Entity Classes

These are the main entity classes we are going to use.

```cs
// This class is representing the simple entity we are going to use
// in our example. 
public class Data
{
    public int DataId { get; set; }
    public string DataDescription { get; set; }
}

// This class is the first kind of the parent Data class.
public class EmployeeData : Data
{
    public string EmployeeName { get; set; }
}

// This class is the second kind of the parent Data class.
public class AssetData : Data
{
    public int AssetId { get; set; }
    public string AssetName { get; set; }
}
```

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

### Generic Interface

This is the generic interface we are going to use and we should focus on.

```cs
// This is the interface we are going to focus on.
// It is an interface for something that would be able
// to read and write data to something that is ready
// to be used this way.
public interface IReaderWriter<TData> where TData : Data
{
    void Initialize();
    TData Read(int dataId);
    void Write(TData data);
}
```

We notice on this interface the following:

 1. It is a generic interface which accepts a generic parameter of type `Data`.

 2. It has an Initialize method.

 3. It has a `Read(int dataId)` method which expects an integer parameter and returns a `TData`.

 4. It has `Write(TData data)` method which expects a `TData` parameter.

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

### Generic Interface Implementer

This is the class implementing our generic interface. The class itself is generic.

```cs
// This is the class implementing our IReaderWriter<TData>
// but now we know that it is going to save and retrieve
// data to and from a file. We would not care about the
// implementation so don't give it too much thought.
public class FileReaderWriter<TData> : IReaderWriter<TData> where TData : Data
{
    public void Initialize() { throw new NotImplementedException(); }

    public TData Read(int dataId) { throw new NotImplementedException(); }

    public void Write(TData data) { throw new NotImplementedException(); }
}
```

We notice on this class the following:

 1. It is a generic class.

 2. The methods are throwing exceptions and this is intentionally done to drive the main focus to where it belongs.

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

### Inside the Employee Module

Now, in your system, you have a module dedicated to managing **Employee** data, let’s call this module; **Employee Module**.

Inside the **Employee Module**, you are sure of the type of the data you are dealing with, it is obviously of type `EmployeeData`.

That’s why you can write code like the one below without having any problems.

```cs
var employeeDataFileReaderWriter = new FileReaderWriter<EmployeeData>();
employeeDataFileReaderWriter.Initialize();

employeeDataFileReaderWriter.Write(new EmployeeData
    { DataId = 1, DataDescription = "Some description.", EmployeeName = "Ahmed" });

var ahmed = employeeDataFileReaderWriter.Read(1);
```

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

### Inside the Asset Module

Now, in your system, you have a module dedicated to managing **Asset** data, let’s call this module; **Asset Module**.

Inside the **Asset Module**, you are sure of the type of the data you are dealing with, it is obviously of type `AssetData`.

That’s why you can write code like the one below without having any problems.

```cs
var assetDataFileReaderWriter = new FileReaderWriter<AssetData>();
assetDataFileReaderWriter.Initialize();

assetDataFileReaderWriter.Write(new AssetData
{ DataId = 2, DataDescription = "Some description.", AssetId = 5, AssetName = "Asset 5."});

var asset5 = assetDataFileReaderWriter.Read(2);
```

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

### What About a Common Module

Now let’s assume that you have a common module that has a `Run(IReaderWriter readerWriter)` method. Inside this method you want to call the `Initialize` method of the passed in `readerWriter` parameter.

You would try to write something like this:

![Image by [Ahmed Tarek](https://medium.com/@eng_ahmed.tarek)](https://cdn-images-1.medium.com/max/2000/1*NO9BgF8GPnjUC-LQNDi08g.png)

It is clear now that you can’t do it as you don’t have a non-generic definition of the `IReaderWriter` interface. In other words, we only have `IReaderWriter<TData>`, not `IReaderWriter`.

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

![Photo by [Brett Jordan](https://unsplash.com/@brett_jordan?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/10368/1*i3BDvzg5_VxUFhFWTAbnYg.jpeg)

### Wrong Expectations

Now, I can hear someone shouting from a far distance saying:
>  Huh, it is a piece of cake. Let’s use `IReaderWriter<object>`. Every class is a child of Object, right?….. genius.

My answer to him is that he should do his homework as this is not going to work. If either you or him don’t trust me on this, just give it a try and you would see the following:

![Image by [Ahmed Tarek](https://medium.com/@eng_ahmed.tarek)](https://cdn-images-1.medium.com/max/2000/1*uFS0BJERZdJJU7vdL02oCg.png)

You need more explanation right, the short answer is because your interface is **Invariant;** you can’t call a method expecting `IReaderWriter<SomeClass>` and pass in an instance of `IReaderWriter<AnyOtherClass>`. The only acceptable call would be with passing `IReaderWriter<SomeClass>`, nothing else.

If you want to understand more you can check my story [Covariance and Contravariance in .NET C#. Have hard time understanding it? Let me simplify it for you.](https://dev.to/ahmedtarekhasan/covariance-and-contravariance-in-net-c-20nl)

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

![Photo by [Brett Jordan](https://unsplash.com/@brett_jordan?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/10368/1*p1ea1_zCuEYSs0cfY3S-Ig.jpeg)

### This Is the Way

Now you understand why we need to define a non-generic `IReaderWriter` interface.

Therefore, moving on to the implementation, we can end up with this code:

```cs
public interface IReaderWriter
{
    void Initialize();
}

public interface IReaderWriter<TData> : IReaderWriter where TData : Data
{
    TData Read(int dataId);
    void Write(TData data);
}
```

Now, we can notice the following:

 1. We defined a new interface but this time it is a non-generic interface.

 2. This interface would define only the `Initialize()` method as this is what we actually need in the common module or even the new ones if they come in the future.

 3. Now the other generic interface can safely extend the non-generic one with the `Read` and `Write` methods without changing anything on the signature.

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

### Let’s Give It a Test Drive

So, now let’s go back to our common module and see if it is going to work or not.

![Image by [Ahmed Tarek](https://medium.com/@eng_ahmed.tarek)](https://cdn-images-1.medium.com/max/2000/1*dR4BCI8sXZUzPSTOijQGag.png)

Finally, it is working. Let’s celebrate and get something to eat and drink, what a trip 🙂

*I don’t want to be a bearer of bad news here, but, I have bad news…*

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

![Photo by [Nik Shuliahin](https://unsplash.com/@tjump?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/10008/1*kNiQOJ1bd-qSEYpzj4-ynw.jpeg)

### Why So Sad

You have **new requirements** coming in and the common module needs some modifications. The common module now should be able to **store and retrieve data in and from a Blob storage**. The Blob storage can store any kind of data.

So, based on this input, you would try to do something like this:

![Image by [Ahmed Tarek](https://medium.com/@eng_ahmed.tarek)](https://cdn-images-1.medium.com/max/2000/1*2cG2du_xg3uyuzPxSqBcOQ.png)

It is clear now that it is not going to work as `IReaderWriter` interface doesn’t define the `Read` and `Write` methods. They are defined in the `IReaderWriter<TData>`. However, on the common module and at the moment of calling `StoreInBlob` and `RetrieveFromBlob` methods, we don’t know the type of data. So, what to do!!!

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

![Photo by [NeONBRAND](https://unsplash.com/@neonbrand?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/9792/1*cjoTe1ydGwUA0i0ULI4tLw.jpeg)

### The Wrong Way To Go

You might now lose hope and sadly decide to drop the whole generic interface thing. You would change the code to the following:

```cs
public interface IReaderWriter
{
    void Initialize();
    Data Read(int dataId);
    void Write(Data data);
}

public class FileReaderWriter : IReaderWriter
{
    public void Initialize() { throw new NotImplementedException(); }

    public Data Read(int dataId) { throw new NotImplementedException(); }

    public void Write(Data data) { throw new NotImplementedException(); }
}
```

So, now the common module would be fine as follows:

![Image by [Ahmed Tarek](https://medium.com/@eng_ahmed.tarek)](https://cdn-images-1.medium.com/max/2000/1*txWiRigg1sGii5SqADQhKg.png)

However, you lost the edge of dealing with strong-typed objects as follows:

![Image by [Ahmed Tarek](https://medium.com/@eng_ahmed.tarek)](https://cdn-images-1.medium.com/max/2000/1*V9D_z1fieSgqLvGUPriRFQ.png)

Now you have to cast your **Employee** object so that you can access its unique members, like `EmployeeName` as in the image.

![Image by [Ahmed Tarek](https://medium.com/@eng_ahmed.tarek)](https://cdn-images-1.medium.com/max/2000/1*EgRCXGc4is8capm9_McvSQ.png)

Similarly, you have to cast your **Asset **object so that you can access its unique members, like `AssetName` as in the image.

So, now what????

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

![Photo by [Michael Carruth](https://unsplash.com/@michaelcarruth?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/14720/1*AXhwXXSNN1Cr9Ca_AbXeIw.jpeg)

### Moment of Truth

The keyword for the best solution here is the word `new`. Let me break it down for you.

```cs
public interface IReaderWriter
{
    void Initialize();
    Data Read(int dataId);
    void Write(Data data);
}

public interface IReaderWriter<TData> : IReaderWriter where TData : Data
{
    new TData Read(int dataId);
}
```

We can notice the following:

 1. The `IReaderWriter` interface now defines are the required methods.

 2. However, for the `Read` and `Write` methods, they are now using the parent `Data` entity type.

 3. The `IReaderWriter<TData>` interface now extends the `IReaderWriter` interface.

 4. This means that it also indirectly defines the three methods we know about.

 5. However, inside the `IReaderWriter<TData>` interface, we need to use the generic type `TData`, not the parent `Data`.

 6. To do so, we need to add `TData Read(int dataId);` and `void Write(TData data);` methods to the `IReaderWriter<TData>` interface.

 7. For the read method, you can’t do this because the parent interface, the non-generic one, already defines the same exact method in terms of name and input parameters, but only different return type.

 8. This would confuse the compiler at run time as it would not know which method to call, the one returning `Data` or the other one returning `TData`.

 9. That’s why the compiler would not allow you to do so unless you add the `new` keyword at the start of the method definition as in the code above.

 10. This instructs the compiler to hide the `Read` method inherited from the parent and replace it with the one defined after the `new` keyword.

 11. Now, you might ask, why didn’t we do the same with the `Write` method?

 12. The answer is simply because we don’t need to do so. In the parent interface, we already have a method called `Write` which expects a parameter of type `Data` which is the parent of all types that could be passed in to the `Write` method.

 13. This means that this method could be called passing any `TData` could come.

 14. Another thing, if you try to use the `new` keyword with the `Write` method, you would get a warning that you are actually not hiding anything from the parent interface. This is logical as the two `Write` methods have different input parameter types, so, it is sound and clear to the compiler that they are two different methods.

```cs
public class FileReaderWriter<TData> : IReaderWriter<TData> where TData : Data
{
    public void Initialize() { throw new NotImplementedException(); }

    public TData Read(int dataId) { throw new NotImplementedException(); }

    public void Write(TData data) { throw new NotImplementedException(); }

    Data IReaderWriter.Read(int dataId) { return Read(dataId); }

    void IReaderWriter.Write(Data data) { Write((TData)data); }
}
```

We can notice the following:

 1. The old three methods are the same.

 2. Now we have two more methods implemented.

 3. The first method is `Data IReaderWriter.Read(int dataId) { return Read(dataId); }`.

 4. This method is an **explicit implementation** of the `Data Read(int dataId);` method defined in the parent `IReaderWriter` interface.

 5. This means that whenever an object of the `FileReaderWriter<TData>` class is casted, implicitly or explicitly, as the non-generic interface `IReaderWriter`, this `Read` method implementation would be used.

 6. The second method is `void IReaderWriter.Write(Data data) { Write((TData)data); }`.

 7. This method is an **explicit implementation** of the `void Write(Data data);` method defined in the parent `IReaderWriter` interface.

 8. This means that whenever an object of the `FileReaderWriter<TData>` class is casted, implicitly or explicitly, as the non-generic interface `IReaderWriter`, this `Write` method implementation would be used.

This now leads to the following:

![Image by [Ahmed Tarek](https://medium.com/@eng_ahmed.tarek)](https://cdn-images-1.medium.com/max/2000/1*iUjkPAgr655LVhWt1R-uZg.png)

And

![Image by [Ahmed Tarek](https://medium.com/@eng_ahmed.tarek)](https://cdn-images-1.medium.com/max/2000/1*n3cVGfL6TEDZ73OrLUXlsQ.png)

Finally, everything is working as it should 🙂

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

![Photo by [Nick Fewings](https://unsplash.com/@jannerboy62?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/6064/1*WqEtbutQR9C4vWMYD7KMPQ.jpeg)

### It Is What It Is

This design technique -afraid of even calling it a pattern- is already used in .NET classes you are using daily. Did you notice that in .NET we have `IEnumerable` and `IEnumerable<T>`? Could you imagine if we don’t have `IEnumerable` what would life be 🙂

This would mean that you can’t write code which loops on an enumerable, just an enumerable. You would always need to know first the type of items inside the enumerable.

You can argue that you still can write a method which accepts `<T>` and then it would pass it to the `IEnumerable<T>`, but my friend, this would keep bubbling up till you eventually would have to choose an entity type. This entity type is not always defined on all layers or levels of code as we proved above.

Therefore, my final advice to you, don’t try to beat around the bushes, it is what it is…
<br>
That’s it, hope you found reading this story as interesting as I found writing it.
<br><br>
This article was originally published [here](https://www.developmentsimplyput.com/post/a-best-practice-for-designing-interfaces-in-net-c).

![A Best Practice for Designing Interfaces in DotNet (.NET) CSharp (C#). When to define a non-generic interface in addition to the generic interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22vftqh632cvcqkmbzhd.png)

### Resources

These are resources you might find useful.
[Covariance and Contravariance in .NET C#](https://dev.to/ahmedtarekhasan/covariance-and-contravariance-in-net-c-20nl)