2

I ran into an issue with generics which I believed can be solved with covariance, but don't fully understand how covarience works and how it can be declared properly. Let's say I have the following interfaces and classes:

public interface IOwnedObject<TUser>
where TUser : IBaseUser
{
    string UserId { get; set; }
    TUser User { get; set; }
}

public interface IBaseUser
{
    string Id { get; set; }
}

public class User : IBaseUser
{
    public string Id { get; set; }
}

public class SomeOwnedObject : IOwnedObject<User>
{
    public string UserId { get; set; }
    public User User { get; set; }
}

Then consider the following code:

var obj = new SomeOwnedObject();
            if(obj is IOwnedObject<IBaseUser> o)
                Console.WriteLine("Success"); // This never executes

The above if statement does not evaluate to true. However, in IOwnedObject it's only ever possible for TUser to be IBaseUser.

The following code evaluates to true:

var obj = new SomeOwnedObject();
            if(obj is IOwnedObject<User> o)
                Console.WriteLine("Success"); // This executes

since User implements IBaseUser, shouldn't IOwnedObject<IBaseUser> technically be a base class of IOwnedObject<User>. Is it possible to make that first statement evaluate to true without referencing the concrete implementation User?

Brad
  • 10,015
  • 17
  • 54
  • 77
  • c# doesn't have templates. it has generics. – Daniel A. White Apr 11 '19 at 18:58
  • "shouldn't IOwnedObject technically be a base class of IOwnedObject" - Just making sure you've seen - https://stackoverflow.com/questions/41179199/cast-genericderived-to-genericbase (or any other https://www.bing.com/search?q=c%23+generic+base+derived) – Alexei Levenkov Apr 11 '19 at 19:09

2 Answers2

5

You can get the first statement to evaluate to true with a few modifications to your interface. We'll want to mark the TUser type as covariant with the out keyword, and in doing so we'll have to remove the property setter.

public interface IOwnedObject<out TUser> where TUser : IBaseUser
{
    string UserId { get; set; }
    TUser User { get; }
}

Now evaluating

var obj = new SomeOwnedObject();
if (obj is IOwnedObject<IBaseUser> o)
    Console.WriteLine("Success");

will result in "Success" being printed to the console.

Jonathon Chase
  • 9,396
  • 21
  • 39
  • Thank you for mentioning removing the property setter. I couldn't figure out what the errors meant when I added out or in keyworks on the type parameter. Works great! – Brad Apr 11 '19 at 19:09
  • 1
    @Brad Yep, `get` only for covariant, `set` only for contravariant (`in`), and both for invariant. The keywords being `out` and `in` mark the generic variance, as well as describe how the types can be used. A generic parameter marked `in` could use the `in` type as a method parameter's type, but not as a method's return type. A generic parameter marked `out` could use the type as a method's return type, but not as one of the method's parameters. Since getters and setters are just a sugar for methods that will generated, the rules apply to them as well. – Jonathon Chase Apr 11 '19 at 19:14
0

Either do @Jonathon's suggestion or the below. Making it covariant you need to use an invariant type, which is IBaseUser.

public static void Main()
{
    var obj = new SomeOwnedObject();
        if(obj is IOwnedObject<IBaseUser> o)
            Console.WriteLine("Success"); // This never executes
}

public interface IOwnedObject<out TUser> where TUser : IBaseUser
{
    string UserId { get; set; }
    IBaseUser User { get; set; }
}

public interface IBaseUser
{
    string Id { get; set; }
}

public class User : IBaseUser
{
    public string Id { get; set; }
}

public class SomeOwnedObject : IOwnedObject<User>
{
    public string UserId { get; set; }
    public IBaseUser User { get; set; }
}

https://dotnetfiddle.net/PAE3CW

Michael
  • 3,350
  • 2
  • 21
  • 35
  • This removes the generic type parameter from being used entirely, since the parameter isn't utilized. It ends up being a constraint on declaration, but doesn't restrict the property type to the provided type as is likely intended. – Jonathon Chase Apr 11 '19 at 19:19