0

I'm writing a client and service using WCF, however I suspect that there are multiple issues at the moment.

As you can see from the code below, the process is as follows: The client asks for some data, which the service generates and returns in a DTO-object. The client then attempts to do a look-up in a returned dictionary, but this throws a KeyNotFoundException.

In addition, a test on the server fails before this (if left uncommented) because the input parameter list allBranches no longer contains Branch currentBranch, which it did on the client side of the method call.

Can someone enlighten me as to what happens in this code, and why it blows up first on the server side and later on the client side?

Shared class definitions
------------------------

[DataContract(IsReference = true)]
public class Branch
{
    public Branch(int branchId, string name)
    {
        BranchId = branchId;
        Name = name;
    }

    [DataMember]
    public int BranchId { get; set; }

    [DataMember]
    public string Name { get; set; }
}

[DataContract]
public class Department
{
    public string Name { get; set; }

    // a few other properties, both primitives and complex objects
}

[DataContract]
public class MyDto
{
    [DataMember]
    public IDictionary<Branch, List<Department>> DepartmentsByBranch { get; set; }

    [DataMember]
    public Branch CurrentBranch { get; set; }

    // lots of other properties, both primitives and complex objects
}

Server-side
--------------------------------
public CreateData(List<Branch> allBranches, Branch currentBranch)
{
    // BOOM: On the server side, currentBranch is no longer contained in allBranches (presumably due to serialization and deserialization)
    if (!branches.Contains(branchToOpen))
    {
        throw new ArgumentException("allBranches no longer contain currentBranch!");
    }

    // Therefore, I should probably not do the following, expecting to use currentBranch as a key in departmentsByBranch later on
    var departmentsByBranch = branches.ToDictionary(branch => branch, branch => new List<Department>());

    return new MyDto
    {
        DepartmentsByBranch = departmentsByBranch,
        CurrentBranch = departmentsByBranch,
    };
}

Client-side (relevant code only)
--------------------------------
var service = new ServiceProxy();   // using a binding defined in app.config

var allBranches = new List<Branch>
{
    new Branch(0, "First branch"),
    new Branch(1, "Second branch"),
    // etc...
};
var currentBranch = allBranches[0];

MyDto dto = service.CreateData(allBranches, currentBranch);

var currentDepartments = dto.DepartmentsByBranch[currentBranch];    // BOOM: Generates KeyNotFoundException

EDIT: I followed Jon's excellent answer below and did the following (which fixed all problems):

  • Made Branch immutable by giving every property a private setter.

    • Every class used as a key in a dictionary should be immutable, or at least have its hash code computed from immutable properties.
  • Implemented IEquatable + overrides of Object.Equals and GetHashCode, the latter as per this SO-answer (link)

Implementing IEquatable is done simply by testing for equal property values,

public bool Equals(Branch other)
    {
        return other != null && ((BranchId == other.BranchId) && (Name == other.Name));
    }
Community
  • 1
  • 1
larslovlie
  • 189
  • 1
  • 2
  • 10
  • The client and the server run on different machines, and they do not share memory. A reference on the client will always be different from any reference on the server. – John Saunders Jan 27 '15 at 17:16
  • @JohnSaunders Yes, I realized that, but I was (naively) expecting that when execution entered CreateData on the server, WCF would make sure that currentBranch was still the same object as one of the elements in allBranches... Silly I suppose. Jons answer below makes sure that Branch.Equals no longer tests reference equality, but my own version of Equals (which simply tests for property equality). – larslovlie Jan 27 '15 at 18:04

1 Answers1

0

The reason your code fails is that you have two separate Branch instances in your client: the one you create locally (currentBranch) and the one that gets received from the server and created implicitly by WCF (inside dto.DepartmentsByBranch). You have not specified that these two instances are "the same thing", so as far as the dictionary is concerned it has never seen that currentBranch you are talking about.

You need to give Branch a proper implementation of IEquatable<Branch> -- and the same goes for all classes you use as dictionary keys.

Note that "proper implementation" means

If you implement IEquatable<T>, you should also override the base class implementations of Object.Equals(Object) and GetHashCode so that their behavior is consistent with that of the IEquatable<T>.Equals method.

Jon
  • 428,835
  • 81
  • 738
  • 806
  • Thanks for the tip! Why doesn't simply using IsReference in the DataContract attribute avoid the ArgumentException in CreateData on the server though? – larslovlie Jan 27 '15 at 17:20
  • @larslovlie: That attribute determines what the serializer does (it will create a single instance of an object per serialized graph). It doesn't (and it cannot) make the serializer realize that an equivalent instance already exists in your app's memory and reconcile with that. – Jon Jan 27 '15 at 17:42