Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nullable open generic arguments in C# class hierarchy

Consider this generic class hierarchy of EF Core entities:

public abstract class Parent<TUserKey>
  where TUserKey : IEquatable<TUserKey>  // same signature as IdentityUser<TKey>
{
  protected Parent() { }                 // non-public ctor for use by EF

  protected Parent(TUserKey? userId) { UserId = userId; }

  public long Id { get; }
  public TUserKey? UserId { get; }      // nullable because optional relationship
  // other properties...
}


public sealed class Child : Parent<long>
{
  private Child() : base() { }          // non-public ctor for use by EF

  public Child(long? userId) : base(userId) { }       // <----- PROBLEM HERE
  // other properties...
}

That won't compile:

cannot convert from 'long?' to 'long'

Ideas:

  1. I could declare the parent class with where TUserKey : struct, IEquatable<TUserKey>. However that isn't suitable because some subclasses use string as the type.
  2. I could change the subclass' ctor to public Child(long userId) : base(userId) { }. However many call sites pass a long?, so I'd need to use userId! everywhere.

I suspect I'm stuck with (2), but maybe I've overlooked something. Is there a better way, without a major rewrite?

(BTW: the purpose of TUserKey : IEquatable<TUserKey> is to match the signature of IdentityUser<TKey>.)

like image 887
lonix Avatar asked Mar 03 '26 22:03

lonix


1 Answers

As explained in the comments by @LasseV.Karlsen, the nullable open generic argument (in the base class) is the source of the problem.

This code is actually contained in a helper assembly used by our main systems, so it needs to be flexible. Some consuming assemblies use value types for the TUserKey primary key (e.g. int, long, Guid) and others use reference types (e.g. string).

So another approach is to split the hierarchy in two: one for value and one for reference types.

The base classes:

public abstract class Parent
{
  protected Parent() { }
  public long Id { get; }
  // other properties...
}


public abstract class ParentValue<TUserKey> : Parent
  where TUserKey : struct, IEquatable<TUserKey>      // <-----
{
  protected ParentValue() { }
  protected ParentValue(TUserKey? userId) { UserId = userId; }
  public TUserKey? UserId { get; }
}


public abstract class ParentReference<TUserKey> : Parent
  where TUserKey : class, IEquatable<TUserKey>       // <-----
{
  protected ParentReference() { }
  protected ParentReference(TUserKey? userId) { UserId = userId; }
  public TUserKey? UserId { get; }
}

The consumer would define a subclass. For value types:

public sealed class Child : ParentValue<long>
{
  private Child() : base() { }
  public Child(long? userId) : base(userId) { }
}

Or reference types:

public sealed class Child : ParentReference<string>
{
  private Child() : base() { }
  public Child(string? userId) : base(userId) { }
}
like image 173
lonix Avatar answered Mar 06 '26 16:03

lonix



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!