1

I'm working with WebAPI, my API needs to call an external API to get data back for processing.

I designed a BaseResponse class as below:

public interface IResponseData
{
}

public class BaseResponse<T> where T : IResponseData
{
    public int ResponseId { get; set; }
    public T? Data { get; set; }
}

Since Data must be some kind of response data, the T parameter must implement IResponseData.

But then I realize I can just do this instead and get rid of the entire generic part.

public interface IResponseData
{
}

public class BaseResponse
{
    public int ResponseId { get; set; }
    public IResponseData? Data { get; set; }
}

What is the point of using where T : IResponseData here? Are there any cases the first example is better than the second one, and vice versa?

For more context (this is quite long text), currently, I have 2 types of response, SingleResponse and MultipleResponse, both of which implement IResponseData.

I was trying to do this:

BaseResponse<IResponseData> baseResponse = new BaseResponse<SingleResponse>();

And I get an error saying that I can not convert SingleResponse to IResponseData, despite the fact that SingleResponse implements IResponseData.

I did some research about covariant, but still don't understand how to make the code above work. Can someone show me how to make this work? (more detail context info below.)

The reason why I write the code above is because:

  1. I have a method that processes response based on the input parameter count. If count is 1, SingleRequest and SingleResponse, if count > 1, MultipleRequest and MultipleResponse.
  2. I have to prepare a request, call API, validate response, and process response data for either case.
  3. Request classes don't have anything in common. Only responses have, which is why I create base class BaseResponse.

So before, you can imaging the code to be like this:

if (input.Count == 1)
{
  SingleRequest singleRequest = new SingleRequest();
  BaseResponse<SingleResponse> singleResponse = await getSingleAPI();
  Validate(singleResponse);
  Process(singleResponse);
} 
else if (input.Count > 1)
{
  MultipleRequest multipleRequest = new MultipleRequest();
  BaseResponse<MultipleResponse> multipleResponse = await getMultipleAPI();
  Validate(multipleResponse);
  Process(multipleResponse);
}

As you can see, only the request is different. The follow-up process is the same for both cases. I don't want code duplication. So, I want to create a BaseResponse<T> with T implements IResponseData.

Now, I can do this outside the if else

BaseResponse<MultipleResponse> response;
if (input.Count == 1)
{
  SingleRequest singleRequest = new SingleRequest();
  response = await getSingleAPI();
} 
else if (input.Count > 1)
{
  MultipleRequest multipleRequest = new MultipleRequest();
  response = await getMultipleAPI();
}

Validate(response)
Process(response)

But response = await getSingleAPI(); and response = await getMultipleAPI(); will not compile since getSingleAPI() returns BaseResponse<SingleResponse>() and getMultipleAPI() returns BaseResponse<MultipleResponse>().

They produce the same error as BaseResponse<IResponseData> baseResponse = new BaseResponse<SingleResponse>();.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
  • 1
    Hi and welcome to SO. It depends. I think the answer may lie on how you actually intend to consume these objects. What do you do with the `baseResponse` object after you get it? Do you care about the type (single/multi)? Will you treat it differently based on it and how do make that determination? – JuanR Apr 05 '23 at 14:24
  • I'm not entirely sure this is "opinion-based" given that one can provide concrete non-opinionated code examples showing why some code would require the interface type constraint `where T : IResponseData` – Matthew Watson Apr 05 '23 at 15:38
  • @MatthewWatson answers clearly demonstrate that the question is opinion-based - just random reasoning why generics are useful. None of the reason actually apply to OP's case because their use-case does not require generics at all... – Alexei Levenkov Apr 05 '23 at 18:01
  • Is my question too broad? Should I create a new question? – haitrieu-yare Apr 05 '23 at 18:14

4 Answers4

3

Here's one reason: Code using your BaseResponse might want to use a class that implements IResponseData which contains items that are not part of IResponseData.

For example, given:

public class BaseResponse<T> where T : IResponseData
{
    public int ResponseId { get; set; }
    public T?  Data       { get; set; }
}

public interface IResponseData
{
}

public sealed class MyResponseData: IResponseData
{
    public int ImportantMethod() => 42;
}

You might have code like this:

var baseResponse = new BaseResponse<MyResponseData>
{
    Data = new MyResponseData()
};

MyResponseData result = baseResponse.Data;

Console.WriteLine(result.ImportantMethod());

But if you change the BaseResponse class definition to

public class BaseResponse
{
    public int            ResponseId { get; set; }
    public IResponseData? Data       { get; set; }
}

then the code will no longer compile.


If you can limit the usage of the type parameter to just the return type of items in the interface (rather than argument types for items in the interface) then you can allow some conversions by using the out keyword (generic modifier).

An example will make this clearer:

You would invent a new IBaseResponse<T> interface, and declare its type parameter as out like so:

public interface IBaseResponse<out T> where T: class, IResponseData
{
    int ResponseId { get; set; }
    T? Data { get; }
}

Note that this interface does not have a setter for Data. If it did, the code would not compile because you're not allowed to use an out generic type as an input.

Anyway, after creating that interface your class can implement it like so:

public class BaseResponse<T> : IBaseResponse<T> where T: class, IResponseData
{
    public int ResponseId { get; set; }
    public T?  Data       { get; set; }
}

Assuming the previous definition of MyResponseData, now the following code will compile OK:

var baseResponse = new BaseResponse<MyResponseData>
{
    Data = new MyResponseData()
};

IBaseResponse<IResponseData> test = baseResponse;
IResponseData? data = test.Data;

It's not quite what you want, because IBaseResponse<IResponseData>.Data does not have a setter, of course.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
1

One reason is performance. A method with a generic constraint will be specialized for each type. This can be relevant for things like generic math, consider:

public T Add(T a, T b) where T : INumber<T>{
    return a + b
}

If this is called once with double and once with decimal the compiler will generate two version of the method, each fully optimized for the specific type. A non generic Add-method would cause the parameters to be boxed, as well as virtual calls to get the correct add method. This overhead can become significant for math heavy code.

That said, there are absolutely cases where generic constraints are overused, and a non generic variant would be better.

JonasH
  • 28,608
  • 2
  • 10
  • 23
1

It seems to me that you're asking three questions:

  1. Why BaseResponse<IResponseData> baseResponse = new BaseResponse<SingleResponse>(); is not working
  2. How to make it work
  3. When and where to use genereics.

Here are the answers:

  1. Because BaseResponse<IResponseData> could not guarantee that your object which is BaseResponse<SingleResponse>() will set SingleResponse in Data property.
  2. To make it work you should remove setter of the property. But you could use it in implementation.
Ex.:

interface IData {}

class Data : IData {}

interface IDataHolder<out T> where T : IData
{
    T Data { get; }
}

class DataHolder<T> : IDataHolder<T>
    where T : IData
{
    T Data { get; set; }
}

// Then this will work
IDataHolder<IData> dataHolder = new DataHolder<Data>();
  1. To use some class specific methods\properties\etc. For example you have interface ICar with method void Drive() and implementation Truck : ICar also have method UnhookTrailer(). To get access to UnhookTrailer() method you need to cast or hold an object with exactly this type. Warehouse working only with Truck should implement methods only for this class but not for all the ICar
-1

In general.

One way I use "Generics Constraints".. it is more of the "high level variety".

See:

https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters

Below is a quote from the article, it is NOT formatting well, so go to the URL above instead. But I will leave the quote below in case the URL dies in the future.

Constraint Description

where T : struct The type argument must be a non-nullable value type. For information about nullable value types, see Nullable value types. Because all value types have an accessible parameterless constructor, the struct constraint implies the new() constraint and can't be combined with the new() constraint. You can't combine the struct constraint with the unmanaged constraint.

where T : class The type argument must be a reference type. This constraint applies also to any class, interface, delegate, or array type. In a nullable context, T must be a non-nullable reference type.

where T : class? The type argument must be a reference type, either nullable or non-nullable. This constraint applies also to any class, interface, delegate, or array type.

where T : notnull The type argument must be a non-nullable type. The argument can be a non-nullable reference type or a non-nullable value type.

where T : default This constraint resolves the ambiguity when you need to specify an unconstrained type parameter when you override a method or provide an explicit interface implementation. The default constraint implies the base method without either the class or struct constraint. For more information, see the default constraint spec proposal.

where T : unmanaged The type argument must be a non-nullable unmanaged type. The unmanaged constraint implies the struct constraint and can't be combined with either the struct or new() constraints.

where T : new() The type argument must have a public parameterless constructor. When used together with other constraints, the new() constraint must be specified last. The new() constraint can't be combined with the struct and unmanaged constraints.

where T : < base class name > The type argument must be or derive from the specified base class. In a nullable context, T must be a non-nullable reference type derived from the specified base class.

where T : < base class name >? The type argument must be or derive from the specified base class. In a nullable context, T may be either a nullable or non-nullable type derived from the specified base class.

where T : < interface name > The type argument must be or implement the specified interface. Multiple interface constraints can be specified. The constraining interface can also be generic. In a nullable context, T must be a non-nullable type that implements the specified interface.

where T : < interface name >? The type argument must be or implement the specified interface. Multiple interface constraints can be specified. The constraining interface can also be generic. In a nullable context, T may be a nullable reference type, a non-nullable reference type, or a value type. T may not be a nullable value type.

where T : U The type argument supplied for T must be or derive from the argument supplied for U. In a nullable context, if U is a non-nullable reference type, T must be non-nullable reference type. If U is a nullable reference type, T may be either nullable or non-nullable.

In your case, you have a "very specific constraint", and so you are correct, that may be overkill.

and I would prefer your next option of

public class BaseResponse
{
    public int ResponseId { get; set; }
    public IResponseData? Data { get; set; }
}

But to your question....there are "more interesting" choices for generics constraints" than a single ISomething.

Also (as per the article). you can have multiple constraints:

class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
    // ...
}

...

Most recently, I have created a generic constraint on an Enum.

public class PageResponse<T, E> where E : Enum
{
    public int PageNumber { get; set; }
    public int PageSize { get; set; }
    public int TotalCount { get; set; }
    public int TotalPages { get; set; }
    public IEnumerable<T> Items { get; set; }
    
    public E OrderByEnum { get; set; }
}

Here I am capturing the results of a "single page of paging" (as in a REST service).

Most of the properties are "for any page request" (the ints above), BUT .. the other things are more fluid.

The "Items" (the "T" above) could be any POCO object. Imagine "Department" in one case and "Employee" in another.

And the "OrderByEnum" could be different.

public Enum DepartmentSortBy
{
  DepartmentNameAsc,
  DepartmentNameDesc,
  DepartmentCreateDateAsc,
  DepartmentCreateDateDesc
}


public Enum EmployeeSortBy
{
  LastNameAsc,
  LastNameDesc,
  HireDateAsc,
  HireDateDesc
}

so I want to keep the "order by" enum "generic".. so it can be reused.

so I can do the two below things:

PageResponse<Department, DepartmentSortBy> pr = new PageResponse<Department, DepartmentSortBy>();

and

PageResponse<Employee, EmployeeSortBy> pr = new PageResponse<Employee, EmployeeSortBy>();

but I cannot do

PageResponse<Employee, ArithmeticException> bogus = new PageResponse<Employee, ArithmeticException>();

because ArithmeticException is not an Enum.

granadaCoder
  • 26,328
  • 10
  • 113
  • 146