Thursday, November 29, 2007

WCF: Contract-level FaultContract

The FaultContract attribute can only be applied to operations. This is great for requiring the programmer to be explicit when defining the contract. However, sometimes we want to indicate that every operation in a contract may raise a set of "standard" faults. It's a real pain to mark each operation with the appropriate fault contracts; furthermore it can make the contract very messy, and it's very easy to forget to specify the standard faults.

So what we really want is a contract-level fault attribute; something like "ContractFault(typeof(Message))". Or perhaps even better, a "StandardFaults" attribute.

It turns out that this can be done, but only in a way that according to the MSDN "may result in undefined behavior". Oh well. Undefined behavior, here we come.

The way to do it is to define an attribute that implements IContractBehavior. This attribute will allow us to insert behavior both when we're starting the service host, and when we're creating a client proxy. Handily, this approach also affects the WSDL metadata that is exported, so non-WCF clients can work with our services too. Here's what the StandardFaultsAttribute looks like:


[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = true)]
public class StandardFaultsAttribute : Attribute, IContractBehavior
{
// this is a list of our standard fault detail classes.
static Type[] Faults = new Type[]
{
typeof(AuthFailure),
typeof(UnexpectedException),
typeof(UserFriendlyError)
};

public void AddBindingParameters(
ContractDescription contractDescription,
ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters)
{
}

public void ApplyClientBehavior(
ContractDescription contractDescription,
ServiceEndpoint endpoint,
ClientRuntime clientRuntime)
{
}

public void ApplyDispatchBehavior(
ContractDescription contractDescription,
ServiceEndpoint endpoint,
DispatchRuntime dispatchRuntime)
{
}

public void Validate(
ContractDescription contractDescription,
ServiceEndpoint endpoint)
{
foreach (OperationDescription op in contractDescription.Operations)
{
foreach (Type fault in Faults)
{
op.Faults.Add(MakeFault(fault));
}
}
}

private FaultDescription MakeFault(Type detailType)
{
string action = detailType.Name;
DescriptionAttribute description = (DescriptionAttribute)
Attribute.GetCustomAttribute(detailType, typeof(DescriptionAttribute));
if (description != null)
action = description.Description;
FaultDescription fd = new FaultDescription(action);
fd.DetailType = detailType;
fd.Name = detailType.Name;
return fd;
}
}


There are a couple of things to note about this implementation. First, we actually have to instantiate a new FaultDescription object for each fault on each operation. There's no way to share FaultDescriptions between operations. Second, we use the DescriptionAttribute of the detail class (if it's there) to set the Action for the fault. This gives relatively friendly FaultException<> messages. Third, the "undefined behavior" I mentioned is the act of modifying the contractDescription in the Validate method. We're technically not allowed to do that. But it works.

To use our new attribute, we simply apply it to the service contract:


[ServiceContract, StandardFaults]
public interface IMyService
{
// ....
}


And we're done.

Labels:

4 Comments:

Blogger Author said...

Great code

2:07 PM  
Anonymous Anonymous said...

Awesome, just what I wanted.

6:35 PM  
Anonymous Anonymous said...

u are the man.

2:13 PM  
Blogger Unknown said...

rockstar!!!

7:14 AM  

Post a Comment

<< Home