Once
we have decided upon an interface definition for a service, we are able
to move forward with the service which implements this interface. For
those of you who have previously built .NET interface classes, and then
realized those interfaces in subsequent concrete classes, the WCF model
is quite natural. In fact, it's the same. We build a concrete service
class, and choose to implement the WCF service contract defined earlier.
For this example, we take the previously-built interface (which has
since had its Insert operations replaced by a single operation that takes a data contract parameter) and implement the service logic.
Consider creating
distinct Visual Studio.NET projects to house the service contract and
the service implementation. This allows you to share the contract
project with service consumers without sharing details of the service
that realizes the contract.
public class VendorService : IVendorContract
{
public void InsertVendor(VendorType newVendor)
{
System.Console.WriteLine("Vendor {0} inserted by service ...", newVendor.VendorId);
}
public bool DeleteVendor(string vendorId)
{
System.Console.WriteLine("Vendor {0} deleted ...", vendorId);
return true;
}
}
A WCF service may
have metadata attributes applied to it in order to influence or dictate
behavior. For instance, WCF has very robust support for creating and
consuming transactional services. While specific attributes are applied
directly to the service contract to affect how the service respects
transactions, the attributes on the concrete service itself establish
the way the service processes those transactions. In order to identify
whether a service will accept transactions or not, an attribute is added
to the service contract.
[ServiceContract(Name="VendorService", Namespace="http://Seroter.BizTalkSOA/Contracts")]
public interface IVendorContract
{
[OperationContract(Name="DeleteVendor")]
[TransactionFlow(TransactionFlowOption.Allowed)]
bool DeleteVendor(string vendorId);
}
However, this doesn't dictate the implementation details. That is left for attributes on the service itself. The ServiceBehavior attribute has numerous available properties used to shape the activities of the service. Likewise, an OperationBehavior
applied to the implemented contract operations enables us to further
refine the actions of the operation. In the following code snippet, I've
instructed the service to put a tight rein on the transaction locks via
the Serializable isolation level. Next, I commanded the DeleteVendor
operation to either enlist in the flowed transaction or create a new
one (TransactionScopeRequired), and to automatically commit the
transaction upon operation conclusion (TransactionAutoComplete).
[ServiceBehavior(TransactionIsolationLevel= System.Transactions.IsolationLevel.Serializable)]
public class VendorService : IVendorContract
{
[OperationBehavior(TransactionAutoComplete=true, TransactionScopeRequired=true)]
public bool DeleteVendor(string vendorId)
{
System.Console.WriteLine("Vendor {0} deleted ...", vendorId);
return true;
}
}
Be aware of the nuances of
where WCF attributes may be applied (e.g. service contract, concrete
service, service operation) and the rich capabilities that these
metadata tags can offer you.
Critical point
While this example
showed how to attach transactions to services, but you need to be
extremely cautious and judicious with the usage of transactions across
service boundaries. While WCF makes this seem transparent, try to make
your services as encapsulated as possible so that they have few explicit
or implied dependencies on other services.
Throwing custom service faults
Whenever possible,
you should avoid returning the full exception stack back to the service
caller. You may inadvertently reveal security or implementation details
that allow a malicious user to engage in mischief. Within WCF, a fault contract
is a custom data contract that allows you to shape the exception being
returned to the service consumer. Let's say we defined a controlled
fault contract that looks like this:
[DataContract(Name = "InsertFault")]
contractfault contract, definingpublic class InsertFaultType
{
private string friendlyMessage;
[DataMember()]
public string FriendlyMessage
{
get { return friendlyMessage; }
set { friendlyMessage = value; }
}
}
So far, that looks like any
old data contract. And in reality, that's all it is. However, we
associate this fault contract with a particular operation by adding the attribute to the operation in the service contract. FaultContract
[OperationContract(Name="InsertVendor")]
[FaultContract(typeof(InsertFaultType))]
void InsertVendor(VendorType newVendor);
While implementing
the service, we explicitly produce and throw these custom exception
types back to the service consumer. This is done by catching the .NET
exception, and creating a new fault object from the custom fault we
created earlier. Then, we throw a new FaultException typed to our custom fault definition.
public void InsertVendor(VendorType newVendor)
{
try
{
//do complex database update ...
}
catch (System.Data.SqlClient.SqlException sqlEx)
{
//log actual fault to admin log
//throwing SQL exception back to caller is bad.
//Create new exception out of custom fault contract
InsertFaultType insertFault = new InsertFaultType();
insertFault.FriendlyMessage = "Insert operation failed";
//throw custom fault
contractfault contract, throwingthrow new FaultException<InsertFaultType>(
insertFault,
"illegal insert");
}
}
By defining and throwing
custom service faults, you achieve better control over how your service
communicates to the outside world and better insulate yourself from
critical implementation leakage.