Breaking SOA with WCF

6 בפברואר 2008

11 תגובות

Service Oriented Architecture contains 4 tenets:



  1. Boundaries are Explicit

  2. Services Are Autonomous

  3. Services share schema and contract, not class

  4. 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:



  1. WCFServices1

  2. WCFServices2

  3. Contracts

  4. Common

  5. Host

  6. Client

image


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:


image



  • I need to add both services:


image


Here is the result:


image 


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:


image


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:



  1. Create a mapper class that will map from one type to the other.

  2. 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:


image


Clicking on it will open the advanced options dialog:


image


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:




  1. 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.

  2. 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:


image


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.

kick it on DotNetKicks.com

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. (*) שדות חובה מסומנים

11 תגובות

  1. ekampf6 בפברואר 2008 ב 18:27

    Hi Guy,
    I don't understand why the approach you describe (using Common) breaks "Services share schema and contract, not class".

    Your Customer class is not a class per-se but a simple representation of the data contract.
    The same way you're using an interface to describe your service contract.

    Defining your data contracts in a 3rd assembly that both service contracts use is not different than defining a single schema file imported by two different WSDLs (if you were to write WSDLs by hand).

    As long as you keep your data contract class strictly used to describe the data and you do not add any behavior logic to it you're not breaking that SOA tenet.

    You're still advertising a standard contract describing the messages your service can send\receive and you're not imposing any marshal-by-value or a common execution environment.

    The only thing you're doing with Common is optimizing the way proxy will be generated when used with .NET (which isn't different than what you could have done by manually editing the generated references)

    Regards,
    Eran

    להגיב
  2. kolbis6 בפברואר 2008 ב 19:29

    Hi Eran. Thanks for the response.
    I have to disagree…

    While we are in Microsoft world everything is OK but lets assume that you are not using a .NET client. and you need to consume the service. You will not get a definition for the customer.

    Basically I am not using the DataContract and it can be moved.

    להגיב
  3. ekampf6 בפברואר 2008 ב 19:55

    Ofcourse you will.
    Add a new console application to your solution, do not reference Contract or Common and add a service reference.

    Then go to the generated Reference.cs and you'll see Customer there.

    The only way you can break tenent #3 with your example is if, for example you'll add a GetNameString() method to Customer that will return, say, FirstName + " " + LastName.

    You'll then have both your services and client, which reference Common assembly, use GetNameString() logic.

    Since logic is not part of the service metadata non .NET clients (or clients who donot have Common assembly) will be crippled since some necessary logic will be missing.

    Relying on shared schema and contract means passing data messages in an agreed schema (which can be implemented using a data contract class if you're in .NET) and not relying on marshaling actual objects with logic between services (like you can do with Remoting for example)

    להגיב
  4. kolbis6 בפברואר 2008 ב 20:07

    Adding logic to the Customer is not relevent in my opinion because it does not include in the operation contracts.

    להגיב
  5. ekampf6 בפברואר 2008 ב 20:27

    Thats exactly the point.

    If you add some logic to Customer, it will not be part of the service's contract.
    Yet, your .NET clients and services implementation, since they reference Common assembly – will have access to this logic and be able to run it, while non .NET clients (or clients without access to Commmon) will not have access to it and will not be able to properly use the service (assuming that functionality is necessary to use the service) – thats why tenent 3 as you defined it forbids passing around objects and makes you adhere to data schemas.

    The class, without logic, is just another way of writing a schema.

    Lets assume for a second you have a real LOB application (SAP ERP written in ABAP – a whole different world than .NET).
    The ERP defines the schema for a Customer object and have several services – Service1 and Service2 – exposing a Customer object.

    In .NET, I can still hand-code (or use WSDL.exe) to generate the .NET data contract representing the ERP's Customer schema. I can then take this class, put it in a shared library\namespace and generate two proxies to Service 1 and 2 that will use that same class. It does not mean I'm breaking the ERP's SOA implementation, I'm just optimizing the proxy generation in my environemnt (and I can do the same in Java etc. too)

    להגיב
  6. ekampf6 בפברואר 2008 ב 20:30

    In fact, the entire concept of ESOA is based on having a semantic layer of shared schemas (Customer, Employee, Invoice…) that it used accross all services.

    Anyone writing a service that passes around a Customer object uses the same standard shared schema and thus you get services talking with each other on the same language without the need for complex transformations and orchestrations.

    להגיב
  7. Ido Flatow7 בפברואר 2008 ב 10:33

    In the "old" days, when we used ASP.NET Web Services , before the days of WCF, we could have used the wsdl.exe with the /sharetypes parameter – if giving the wsdl.exe a list of services, it would try to match identical schemas in the services,
    Identical not only by schema content but also by the same namespace – which is what we see in WCF services when sharing data contracts between services.

    In WCF, for some reason microsoft removed this option from the svcutil.exe, but suggested the /r option to reference an assembly – which makes us reference the data contract assembly that is a part of the service instead of creating a new data structure that resembles the original data contract – which exactly contradicts the tenet "share schema, not class".

    As both of you mentioned, the data contract class can hold more that the data members – it can also add "hidden" members (not mark to be serialized), it can hold logic (if one wishes to combine logic with data) and it sometimes represents newer versions of the services, versions we might not want to use right now, so referencing the "inner" logic of a service is something that should be considered as taboo.

    Should we really break the tenets because svcutil.exe wasn't designed properly ? The problem is with how the proxy is built, so the answer should be towards solving the type sharing svcutil.exe has suppressed.

    Focus your solution on how to build a proxy that understand that two data contracts that have the same name, same content and most of all – same namespace, do mean the same schema !

    In web services, when we needed to change the way the proxy runs, we could have created a class that derives from SchemaImportedExtension and supply it to the wsdl.exe as a filter class. In that class we captured the incoming wsdl and changed the way the generated code is created. I wonder if there is a similiar way to do it with svcutil.exe, maybe i'll have a look at it when I have some time.

    For conclusion – just because MS decides to give us a messed up proxy generator, it doesn't mean we need to break the architecture – Fix the problem, not the side effects (in hebrew it sounds better) !

    Ido Flatow.

    להגיב
  8. kolbis8 בפברואר 2008 ב 8:00

    Hi Ido…Thanks for the long response. I agree with you.

    להגיב
  9. Fred25 בפברואר 2009 ב 18:54

    Great article, we should not restricted ourself by the technology available. In fact, if we blindly respect the these SOA tenets, and forget our true business needs, you are finding trouble for yourself. SOA is great, but it is not universal solution for all problem. But programming by interface should be always respected. Interface is xsd schema in SOA, but in lots of case, we need .net interface but not xsd schema. That is where SOA fails.

    להגיב
  10. Jeff Burke9 באפריל 2009 ב 0:03

    Completely agree with Ido.

    So… anyone got a proxy generator that allows one to include, exclude and share datacontracts by name instead of by assembly?

    להגיב
  11. Jeff Burke14 באפריל 2009 ב 20:25

    http://www.codeplex.com/ULinqGen

    Here is a solution. It allows you to create 2 assemblies from the dbml… 1 for entities and 1 for the datacontext/data access layer. With this, you can acomplish a clean split between the entity types and the data access layer. Use the entity types assembly, and the /r option to cleanly import datatypes w/o referencing/distributing server implementation code.

    להגיב