Tuesday 12 February 2008

C# generics - parameter variance, its constraints and how it affects WCF

CLR generics are great and there is no doubt about that. Unfortunately, C# doesn't expose the whole beauty of it because all generic type parameters in C# are nonvariant though from CLR point of view they can be marked as nonvariant, covariant or contravariant. You can find more details about that topic here and here. In short the "nonvariant" word means that even though type B is a subtype of type A then SomeType<B> is not a subtype of SomeType<A> and therefore the following code won't compile:

List <String> stringList = null;
List <object> objectList = stringList; //this line causes a compilation error

Error 1 Cannot implicitly convert type 'System.Collections.Generic.List<string> to 'System.Collections.Generic.List<object>. An explicit conversion exists (are you missing a cast?)

The generics are all over the place in WCF and you would think that this is always beneficial to all of us. Well, it depends. One of the problems I noticed is that you can not easily handle generic types in a generic way. I know it does not sound good :) but that's what I wanted to say. The best example is ClientBase<T> that is the base class for auto generated proxies. VS.NET generates a proxy type per contract(interface) which might lead to a situation where you need to manage quite a few many different proxies. Let's assume that we use username and password as our authentication method and we want to have a single place where the credentials are set. The method might look like the one below: public void ConfigureProxy(ClientBase<Object> proxy) {     proxy.ClientCredentials.UserName.UserName = "u";     proxy.ClientCredentials.UserName.Password = "p"; } Unfortunately we can't pass to that method a proxy of type ClientBase<IMyContract> because of nonvariant nature of C# generics. I can see at least two options how to get around that issue. The first one requires you to clutter the method with a generic parameter despite the fact that there is no use of it.

public void ConfigureProxy <T>(ClientBase <T> proxy) where T : class   
{
proxy.ClientCredentials.UserName.UserName = "u";
proxy.ClientCredentials.UserName.Password = "p";
}
You can imagine I'm not big fun of this solution. The second one is based on the idea that the non-generic part of the public interface of ClientBase class is exposed as either a non-generic ClientBase class or a non-generic interface IClientBase. Approach based on a non-generic class:

public abstract class ClientBase : ICommunicationObject, IDisposable   
{
public ClientCredentials ClientCredentials
{
//some code goes here
}
}

public abstract class ClientBase <T> : ClientBase where T : class
{
}

Approach based on a non-generic interface:

public interface IClientBase : ICommunicationObject, IDisposable 
{
ClientCredentials ClientCredentials { get; }
}

public abstract class ClientBase <T> : IClientBase where T : class
{
}

Having that hierarchy in place we could define our method in the following way:


public void ConfigureProxy(ClientBase/IClientBase proxy)
{
proxy.ClientCredentials.UserName.UserName = "u";
proxy.ClientCredentials.UserName.Password = "p";
}

Unfortunately WCF architects haven't thought about that and a non-generic ClientBase/IClientBase class/interface doesn't exist. The interesting part of this story is that the FaultException<T> class does not suffer from the same problem because there is a non-generic FaultException class that exposes all the non-generic members. The FaultException<T> class basically adds a single property that returns the detail of the fault and that's it. I can find more classes that are implemented in exactly the same way FaultException<T> is implemented. It looks like ClientBase<T> is the only one widely used class that breaks that rule. I would love to see this inconvenience fixed as an extension of C# parameter variance.

3 comments:

  1. C# generics - parameter variance, its constraints and how it affects WCF

    ReplyDelete
  2. I agree. I wanted to do a similar thing, except in my case I wanted to provide a common function to close or abort the channel based on state. Sounds like the WCF guys missed this one. Thanks for your ideas though.

    ReplyDelete