views:

136

answers:

2

Basically here's the issue. All entities in my system are identified by their type and their id.

new Customer() { Id = 1} == new Customer() {Id = 1};
new Customer() { Id = 1} != new Customer() {Id = 2};
new Customer() { Id = 1} != new Product() {Id = 1};

Pretty standard scenario. Since all Entities have an Id I define an interface for all entities.

public interface IEntity {
  int Id { get; set;}
}

And to simplify creation of entities I make

public abstract class BaseEntity<T> : where T : IEntity {
  int Id { get; set;}
  public static bool operator ==(BaseEntity<T> e1, BaseEntity<T> e2) {
    if (object.ReferenceEquals(null, e1)) return false;
      return e1.Equals(e2);
  }
  public static bool operator !=(BaseEntity<T> e1, BaseEntity<T> e2) { 
    return !(e1 == e2);
  }
}

where Customer and Product are something like

public class Customer : BaseEntity<Customer>, IEntity {}
public class Product : BaseEntity<Product>, IEntity {}

I think this is hunky dory. I think all I have to do is override Equals in each entity (if I'm super clever, I can even override it only once in the BaseEntity) and everything with work.

So now I'm expanding my test coverage and find that its not quite so simple! First of all , when downcasting to IEntity and using == the BaseEntity<> override is not used.

So what's the solution? Is there something else I can do? If not, this is seriously annoying.

Upadate It would seem that there is something wrong with my tests - or rather with comparing on generics. Check this out

[Test] public void when_created_manually_non_generic() {
    // PASSES!
    var e1 = new Terminal() {Id = 1};
    var e2 = new Terminal() {Id = 1};
    Assert.IsTrue(e1 == e2);
}
[Test] public void when_created_manually_generic() {
    // FAILS!
    GenericCompare(new Terminal() { Id = 1 }, new Terminal() { Id = 1 });
}
private void GenericCompare<T>(T e1, T e2) where T : class, IEntity {
    Assert.IsTrue(e1 == e2);            
}

Whats going on here? This is not as big a problem as I was afraid, but is still quite annoying and a completely unintuitive way for the language to behave.

Update Update Ah I get it, the generic implicitly downcasts to IEntity for some reason. I stand by this being unintuitive and potentially problematic for my Domain's consumers as they need to remember that anything happening within a generic method or class needs to be compared with Equals()

+1  A: 

Ok, took me a minute... but here's your problem.

You're probably doing something like this, right?

class Customer : BaseEntity<Customer>{}

class Product : BaseEntity<Product>{}

See, the problem is that BaseEntity<Customer> and BaseEntity<Product> are two entirely different classes. With templates a new class gets generated by the compiler for each templated type. In other words, what the compiler will kick out is something like BaseEntity_Customer and BaseEntity_Product.

Really, I don't think you even need the interface or the templates at all do you? If you just put ID in the base class, it will automatically be there for anything that derives from BaseEntity. If you mark it abstract, each base class will still need to create their own implementation of it... which is what it looks like you're trying to do, but would actually work.

Telos
The interface is there because I don't want to force all my entities to inherit from BaseEntity<> I want to keep my hierarchy as simple as possible for the consumer, without repeating myself. I actually wish there was a way to mark BaseEntity as invisible outside the library.
George Mauer
As for the generic, that is there simply because I'm considering doing some reflection stuff in the base entity to set up some conventions. I don't see why BaseEntity_Customer and BaseEntity_Product would give me the current results
George Mauer
They give you the current results because they are different classes. Not the same class. In other words Customer inherits from BE_Customer, not from BaseEntity. There is, in fact, no such thing as BaseEntity.
Telos
Ok, I get it. I removed that generic and the situation gets a bit better. Still can't compare IEntities using == though and the comparison failure when using generics still continues.
George Mauer
A: 

I think the problem in the update with comparisons of generics has to do with the fact that static methods and variables are quite different from instance methods and variables.

I don't know how the CLR treats them but conceptually they're almost like two different classes. So just like you wouldn't be able to access any static methods on T, an operator on T would not be applied.

That's my understanding of the issue as it is. I'd love a more technical explanation if someone has it.

Furthermore, at least on one front the issue is moot. If IEntity is the value of the generic parameter T the compiler will not allow you to compare two instances of type T using the == operator. I believe this is because of what I have said above.

However, the problem still persists if the generic parameter is of type class,IEntity or if IEntity is an instance parameter. For example

[Test]
public void are_equal_when_passed_as_parameters_downcast_to_interfaces() {
    //FAILS!
    CompareTwoEntities(new Terminal() { Id = 1 }, new Terminal() { Id = 1 });
}
private void CompareTwoEntities(IEntity e1, IEntity e2) {
    Assert.IsTrue(e1 == e2);
}
George Mauer