Service Oriented Architecture contains 4 tenets:
- Boundaries are Explicit
- Services Are Autonomous
- Services share schema and contract, not class
- Service compatibility Is based upon policy
When someone asks me if he should follow SOA tenets in his design I usually say:
"It depends...."
And next thing I find myself in, is a long discussion about the pros and cons for going with SOA. In the following post I will provide an argument why I think that SOA is not always the best way to go with.
To present my argument and result, I will use an example which most of you probably encountered at least once.
I have a solution that contains six projects as describes here:
- WCFServices1
- WCFServices2
- Contracts
- Common
- Host
- Client
The two WCF services Service1 and Service2 implement the same contract as defined in the Contract project. Here is a code snippet for that contact:
[ServiceContract(Namespace="http://blogs.microsoft.co.il/blogs/kolbis/06/02/08")]
public interface ICustomerService
{
[OperationContract]
Customer GetCustomer(int id);
}
The contract references an assembly named Common that contains a class named Customer which is returned as a result for the GetCustomer OperationContract.
The customer class is decorated with a DataContract attribute as shown in the following code snippet:
[DataContract(Namespace="http://blogs.microsoft.co.il/blogs/kolbis/06/02/08")]
public class Customer
{
[DataMember(IsRequired = true, Order = 0)]
public int Id { get; set; }
[DataMember(IsRequired = true, Order = 1)]
public string LastName { get; set; }
[DataMember(IsRequired=true,Order=2)]
public string FirstName { get; set; }
}
So far everything is simple.
Now, I have added a console host that will host the two services:
static void Main(string[] args)
{
ServiceHost host1 = new ServiceHost(typeof(WCFServices1.Service1));
ServiceHost host2 = new ServiceHost(typeof(WCFServices2.Service1));
host1.Open();
host2.Open();
Console.ReadLine();
host1.Close();
host2.Close();
Now I wanted to add the client that will consume the services.
Here is what I usually do when I need to add a service:
- Run the host in a non debug mode (Ctrl + F5).
- In the client application select to add a service reference:
- I need to add both services:
Here is the result:
Great! we have set our service proxies.
If we will look inside the generated proxy, there is an automated file that was generated for us named Reference.cs:
That file contains a definition for the Customer class that was defined in the Common project as you can see in the following code snippet:
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Name="Customer", Namespace="http://blogs.microsoft.co.il/blogs/kolbis/06/02/08
[System.SerializableAttribute()]
public partial class Customer : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged {
[System.NonSerializedAttribute()]
private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
private int IdField;
private string LastNameField;
private string FirstNameField;
[global::System.ComponentModel.BrowsableAttribute(false)]
public System.Runtime.Serialization.ExtensionDataObject ExtensionData {
get {
return this.extensionDataField;
}
set {
this.extensionDataField = value;
}
}
[System.Runtime.Serialization.DataMemberAttribute(IsRequired=true)]
public int Id {
get {
return this.IdField;
}
set {
if ((this.IdField.Equals(value) != true)) {
this.IdField = value;
this.RaisePropertyChanged("Id");
}
}
}
[System.Runtime.Serialization.DataMemberAttribute(IsRequired=true)]
public string LastName {
get {
return this.LastNameField;
}
set {
if ((object.ReferenceEquals(this.LastNameField, value) != true)) {
this.LastNameField = value;
this.RaisePropertyChanged("LastName");
}
}
}
[System.Runtime.Serialization.DataMemberAttribute(IsRequired=true, Order=2)]
public string FirstName {
get {
return this.FirstNameField;
}
set {
if ((object.ReferenceEquals(this.FirstNameField, value) != true)) {
this.FirstNameField = value;
this.RaisePropertyChanged("FirstName");
}
}
}
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName) {
System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
if ((propertyChanged != null)) {
propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
}
It is important to explain here that the default behavior for adding a service reference is to create a those referenced objects from the schema. This is a NEW definition for the class Customer. This way we implement the "Share schema, not classes" and the "Services are Autonomous" SOA tenets.
Here is where it gets tricky. Lets assume that we want to use the customer object we have got from the services:
Service1Reference.CustomerServiceClient proxy1 = new
Client.Service1Reference.CustomerServiceClient();
Service2Reference.CustomerServiceClient proxy2 = new
Client.Service2Reference.CustomerServiceClient();
Service1Reference.Customer cust1 = proxy1.GetCustomer(1);
Service2Reference.Customer cust2 = proxy1.GetCustomer(2);
I have highlighted the two "problematic" rows. We actually get two different objects, one from the definition in the Services1 and another from the Services2.
Which one should we use?
So, there are two options here:
- Create a mapper class that will map from one type to the other.
- Not share a schema rather share a class.
There are pros and cons to both methods, but you should be aware of both. Using the mapper will force you to write more code and mantain it but you will implement SOA tenets. If you select the second method you will surely write less code but you will ignore the SOA tenets.
Here is what you need to do to implement the seconds method:
When adding editing the service reference there is an Advanced button:
Clicking on it will open the advanced options dialog:
I am not going to talk about each option, rather focus on the Reuse types in referenced assemblies option. Basically it determines whether a WCF client will try to reuse that already exist in referenced assemblies instead of generating new types when a service is added or updated. By default, this option is checked.
There are two check boxes available:
-
Reuse types in all referenced assemblies - When selected, all types in the Referenced assemblies list will be reused if possible. By default, this option is selected.
- Reuse types in specified referenced assemblies - When selected, only the selected types in the Referenced assemblies list will be reused.
We will select the seconds option, but before continuing we will add a reference to the Common Project In order to use only a one definition of Customer. Now in the Advanced Dialog we will do the following:
Select the Reuse types in specified referenced assemblies and check the Common. The same goes with Service2. If you will look inside the Reference.cs class that was generated for us, you will see that the Customer definition was removed. The only definition we have is the one in the common:
Service1Reference.CustomerServiceClient proxy1 = new
Client.Service1Reference.CustomerServiceClient();
Service2Reference.CustomerServiceClient proxy2 = new
Client.Service2Reference.CustomerServiceClient();
Common.Customer cust1 = proxy1.GetCustomer(1);
Common.Customer cust2 = proxy1.GetCustomer(2);
Now, we have share a class and not the schema thus we have broke SOA but we wrote less code.
You can download the code I used here.
