-1
\$\begingroup\$

I'm working on a framework which builds on .net core Identity. Let's say I wanted to extend the IdentityUser<TKey> class with a ParentUserId property while the developer should still be able to decide which type to use for the TKey primary key. This property would be a foreign key to another user, but could be null if the user doesn't have a parent.

Using string for TKey would work without problems because strings are nullable and don't conflict with the where TKey : IEquatable<TKey> constraint (although I don't know why). But if the developer decides to use int, I'm screwed.

Currently I'm thinking about a solution like this:

public class FrameworkUser<TKey, TNullableKey> : IdentityUser<TKey> 
       where TKey : IEquatable<TKey>
{
    public virtual TNullableKey ParentUserId { get; set; }

    // for better understanding
    public virtual FrameworkUser<TKey, TNullableKey> ParentUser { get; set; }
}

But I'm very unsure and don't think it's beautiful.

  1. The developer could do something like new FrameworkUser<string, int?>() by mistake. Would be great if something like where TNullableKey : Nullable<TKey> were possible.

  2. I can't constraint TNullableKey. IEquatable doesn't allow nullable value types (i.e. int?), although I don't know if this will result in worse performance when used as a foreign key anyway. struct won't allow strings.

  3. Getting the value of ParentUserId will differ depending on the type. For instance to securely get the value of an int?, I would check for int?.HasValue to be true and then get it's value from int?.Value. The same procedure is quite different for a string which could be null, empty, or have some chars. This would make switches from one type to another without extra work difficult/impossible.

Another solution could be having two implementations, one for string and one for struct, but that's not feasible because then I would also need to have two IdentityStore<TUser>s and so on.

So, what would you do?

\$\endgroup\$

closed as off-topic by t3chb0t, yuri, pacmaninbw, Mast, 1201ProgramAlarm May 26 at 18:17

This question appears to be off-topic. The users who voted to close gave this specific reason:

  • "Lacks concrete context: Code Review requires concrete code from a project, with sufficient context for reviewers to understand how that code is used. Pseudocode, stub code, hypothetical code, obfuscated code, and generic best practices are outside the scope of this site." – t3chb0t, yuri, pacmaninbw, Mast, 1201ProgramAlarm
If this question can be reworded to fit the rules in the help center, please edit the question.

  • \$\begingroup\$ new FrameworkUser<string, int?>() -> int? can be null, so why would it be a mistake? I thought new FrameworkUser<string, int>() would be the mistake. \$\endgroup\$ – dfhwze May 25 at 18:33
  • \$\begingroup\$ We will need more code to see the big picture. Can you post the rest of it? I'm afraid we won't be able to do much here with the current code. If this is only hypothetical then I suggest trying Software Engineering that could be better in this case. Code Review requires real and working code. \$\endgroup\$ – t3chb0t May 25 at 18:33
  • \$\begingroup\$ @dfhwze It's a reference to another user, so TKey and TNullableKey must have the same type, but the second needs to be nullable. \$\endgroup\$ – Linus Caldwell May 25 at 18:37
  • \$\begingroup\$ @t3chb0t Well, maybe you are right and I should have posted it there. Thanks for the hint! \$\endgroup\$ – Linus Caldwell May 25 at 18:38
  • 1
    \$\begingroup\$ @LinusCaldwell It is a very interesting question nonetheless. I get what you want to do now. It cannot be done with generic constraints. \$\endgroup\$ – dfhwze May 25 at 18:40
1
\$\begingroup\$

I don't think it can be done at design time given generic constraints. Second best option would be at runtime. There are 2 runtime constraints:

  1. TNullableKey must be null-assignable (reference type or nullable type)
  2. The underluying type of TNullableKey must be of type TKey

code

public class FrameworkUser<TKey, TNullableKey> : IdentityUser<TKey>
           where TKey : IEquatable<TKey>
  {
      public virtual TNullableKey ParentUserId { get; set; }
      public FrameworkUser() 
      {
          var sourceType = typeof(TKey);
          var nullAssignableType = typeof(TNullableKey);

          if (!IsNullAssignable(nullAssignableType)) {
              throw new InvalidOperationException();
          }

          var targetType = nullAssignableType;
          if (IsNullable(targetType)) {
              targetType = Nullable.GetUnderlyingType(targetType);
          }

          if (!targetType.Equals(sourceType)) {
              throw new InvalidOperationException();
          }
      }

      private static bool IsNullable(Type t) {
          return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>);
      }

      private static bool IsNullAssignable(Type t) {
          if (IsNullable(t) || !t.IsValueType) {
              return true;
          }
          return false;
      }
 }

Test case

public static void Main() {

        // OK
        var a = new FrameworkUser<string, string>();
        var b = new FrameworkUser<int, int?>();

        // Errors
        //var c = new FrameworkUser<string, int>();
        //var d = new FrameworkUser<string, int?>();
        //var e = new FrameworkUser<int, int>();

        Console.ReadKey();
    }
\$\endgroup\$
  • 1
    \$\begingroup\$ If the solution with two generic parameters is the best I can go, this would at least prevent mistakes, so thanks for your input! \$\endgroup\$ – Linus Caldwell May 25 at 19:11
3
\$\begingroup\$

Just an idea:

Define FrameworkUser as:

  public class FrameworkUser<TKey> : IdentityUser<TKey> where TKey : IEquatable<TKey>
  {
    // Either you could do this, if ParentUserId is relying on ParentUser
    public virtual UserId<TKey>? ParentUserId => ParentUser != null ? ParentUser.Id : (UserId<TKey>?)null;

    //.. or this, if it is an independent value (Name changed for testing):
    public virtual UserId<TKey>? ParentId { get; set; }


    // for better understanding
    public virtual FrameworkUser<TKey> ParentUser { get; set; }
  }

where UserId<TKey> is defined as:

  public struct UserId<TKey>
  {
    public UserId(TKey id)
    {
      Id = id;
    }

    public TKey Id { get; }

    public override string ToString()
    {
      return Id.ToString();
    }

    public static implicit operator TKey(UserId<TKey> userId) => userId.Id;
    public static implicit operator UserId<TKey>(TKey id) => new UserId<TKey>(id);
  }

Test cases:

    FrameworkUser<string> stringUser = new FrameworkUser<string>();
    Console.WriteLine($"Witout Parent: {stringUser.ParentUserId}");
    Console.WriteLine($"Witout ParentId: {stringUser.ParentId}");
    stringUser.ParentUser = new FrameworkUser<string> { Id = "ParentId" };
    stringUser.ParentId = "Independent ParentId";
    Console.WriteLine($"With Parent: {stringUser.ParentUserId}");
    if (stringUser.ParentId != null)
      Console.WriteLine($"With ParentId: {stringUser.ParentId}");

    Console.WriteLine();
    FrameworkUser<int> intUser = new FrameworkUser<int>();
    Console.WriteLine($"Witout Parent: {intUser.ParentUserId}");
    Console.WriteLine($"Witout ParentId: {intUser.ParentId}");
    intUser.ParentUser = new FrameworkUser<int> { Id = 1234 };
    intUser.ParentId = 1234;
    Console.WriteLine($"With Parent: {intUser.ParentUserId}");
    if (intUser.ParentId != null)
      Console.WriteLine($"With ParentId: {intUser.ParentId}");
\$\endgroup\$
  • \$\begingroup\$ This is a refreshing idea. +1 for thinking outside the box. \$\endgroup\$ – dfhwze May 25 at 21:26
  • \$\begingroup\$ This would be wonderful, but when I try to configure Entity Framework and set HasConversion() for the ParentId property, it seems to be ignored at the foreign key and EFcore complains about incompatibility with the primary key, or if I let EF do the foreign key config itself, it creates an additional ParentId1 column for that. I know this should be another question, but maybe you already have a solution for this too. Thanks anyway! \$\endgroup\$ – Linus Caldwell May 26 at 6:34
  • \$\begingroup\$ @LinusCaldwell I would make a new question. EF compatibility was never a requirement in this question :-) \$\endgroup\$ – dfhwze May 26 at 7:33
  • \$\begingroup\$ @LinusCaldwell: See my update \$\endgroup\$ – Henrik Hansen May 26 at 7:50
  • \$\begingroup\$ I already tried option 2 (ParentUser is a navigation property for ParentUserId), but this creates an additional useless column ParentUserId1 or complains the foreign key and the primary key are not compatible if I config the foreign key manually. This question addresses this issue but has no answers. Anyhow, thank you for your effort! It's a great solution. I'll mark it as accepted if I manage to solve the EF issue. \$\endgroup\$ – Linus Caldwell May 26 at 8:17

Not the answer you're looking for? Browse other questions tagged or ask your own question.