Search code examples
asp.net-coregenerics.net-8.0nullable-reference-typesc#-12.0

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>.)


Solution

  • 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) { }
    }