Custom Data exchanged with WCF services is defined through DataContracts using DataContract and DataMember attributes to the data definition classes. Please refer to the msdn help for more information:
http://msdn.microsoft.com/en-us/library/ms733127.aspx
The recommended way of doing so is to apply the DataMember attributes to properties
namespace MyTypes
{
[DataContract]
public class PurchaseOrder
{
private int poId_value;
// Apply the DataMemberAttribute to the property.
[DataMember]
public int PurchaseOrderId
{
get { return poId_value; }
set { poId_value = value; }
}
}
}
However there are cases where you want to apply the DataMember attributes to the fields instead of the properties. I will describe here one of these cases.
The Case
The case is: you want to develop an N-tier application, exposing business data through a WCF service facade. To do so, you define a custom data structure, representing your business, you expose it as a DataContract and you share the DataContract assembly between the WCF service and the client application.
Let's take the following definition of a DataContract representing a contact:
[DataContract]
public class Contact : INotifyPropertyChanged
{
#region fields
private string _firstName;
private string _lastName;
private bool _isDirty = false;
#endregion
#region properties
[DataMember]
public string FirstName
{
get
{
return _firstName;
}
set
{
if (_firstName == value)
return;
_firstName = value;
OnPropertyChanged("FirstName");
}
}
[DataMember]
public string LastName
{
get
{
return _lastName;
}
set
{
if (_lastName == value)
return;
_lastName = value;
OnPropertyChanged("LastName");
}
}
[DataMember]
public bool IsDirty
{
get
{
return _isDirty;
}
set
{
if (_isDirty == value)
return;
_isDirty = value;
OnPropertyChanged("IsDirty");
}
}
#endregion
#region methods
public Contact(string firstName, string lastName)
{
_firstName = firstName;
_lastName = lastName;
}
public override string ToString()
{
return string.Format("FirstName: '{0}' LastName: '{1}' IsDirty: {2}", FirstName, LastName, IsDirty);
}
#endregion
#region INotifyPropertyChanged implementation
protected void OnPropertyChanged(string propertyName)
{
if (!ReferenceEquals(null, PropertyChanged))
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
if (propertyName != "IsDirty")
IsDirty = true;
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
The only addition to the first simple case is the support for the INotifyPropertyChanged interface. Indeed, if you want to develop a proper application, you probably want to handle self-tracking entities, to make sure the service at server level can easily figure out what happened to the data in order to efficiently process it. First step to handling self-tracking entities is to be able to determine whether data has been changed since last retrieval. This is handled through the IPropertyChanged interface.
Here starts our problems.
Let's take following service definition:
[ServiceContract]
public interface IContactService
{
[OperationContract]
Contact PassContactThrough(Contact theContactToPassThrough);
}
And service implementation:
public class ContactService : IContactService
{
public Contact PassContactThrough(Contact theContactToPassThrough)
{
return theContactToPassThrough;
}
}
All this operation is doing is passing the Contact through. We therefore expect to get back the same Contact information as the one we initially sent.
To test this, let's create a client, add a reference to the service and to the data contract assembly and do the following implementation:
static void Main(string[] args)
{
ContactServiceClient service = new ContactServiceClient();
var contactToPassThrough = new Contact("Walter", "Almeida");
Console.WriteLine("Contact Before Service Call: " + contactToPassThrough.ToString());
Contact returnedContact = service.PassContactThrough(contactToPassThrough);
Console.WriteLine("Contact After Service Call: " + returnedContact.ToString());
}
We send through a Contact data for Walter Almeida. The Contact is clean, marked with IsDirty = false.
However, the returned Contact contains the proper "Walter Almeida" information but is marked ad IsDirty = true!
The explanation is the following: when sent to the service, the Contact data is serialized, accordingly to the DataMember attributes information. When received by the service, the received data is deserialized into a new Contact instance. When deserializing, the service will assign values to the Contact instance properties, because it is the properties that are marked as DataMembers. When doing so: the OnPropertyChanged method is called and IsDirty is set to true.
We could have been lucky here if IsDirty (also marked as DataMember) would be the last one to be deserialized thus compensating the previous calls to IsDirty = true. However when no specified otherwise, the DataMembers are serialized in alphabetical order. Therefore IsDirty is deserialized before LastName...
Here are candidate solutions to this problem with benefits and drawbacks:
Solution 1: Do not share the exact same DataContract assembly
Instead of sharing the exact same data contract assembly, the server should use a assembly that has the datacontract definition without the INotifyPropertyChanged interface implementation: just data, no behaviour:
[DataContract]
public class Contact : INotifyPropertyChanged
{
#region fields
private string _firstName;
private string _lastName;
private bool _isDirty = false;
#endregion
#region properties
[DataMember]
public string FirstName
{
get
{
return _firstName;
}
set
{
_firstName = value;
}
}
[DataMember]
public string LastName
{
get
{
return _lastName;
}
set
{
_lastName = value;
}
}
[DataMember]
public bool IsDirty
{
get
{
return _isDirty;
}
set
{
_isDirty = value;
}
}
#endregion
}
However this requires two different versions of the data definition, leading to the associated maitainability issues.
Moreover we also loose all the tracking capabilities at server side, which is wrong: it is possible for the service to perform actions on the data and therefore these actions should be also tracked.
Solution 2: Change order of properties serialization
By default DataMembers are serialized in alphabetical order. You can redefine this order the following way:
[DataContract]
public class Contact : INotifyPropertyChanged
{
#region fields
private string _firstName;
private string _lastName;
private bool _isDirty = false;
#endregion
#region properties
[DataMember(Order = 0)]
public string FirstName
{
get
{
return _firstName;
}
set
{
if (_firstName == value)
return;
_firstName = value;
OnPropertyChanged("FirstName");
}
}
[DataMember(Order = 1)]
public string LastName
{
get
{
return _lastName;
}
set
{
if (_lastName == value)
return;
_lastName = value;
OnPropertyChanged("LastName");
}
}
[DataMember(Order = 2)]
public bool IsDirty
{
get
{
return _isDirty;
}
set
{
if (_isDirty == value)
return;
_isDirty = value;
OnPropertyChanged("IsDirty");
}
}
#endregion
#region methods
public Contact(string firstName, string lastName)
{
_firstName = firstName;
_lastName = lastName;
}
public override string ToString()
{
return string.Format("FirstName: '{0}' LastName: '{1}' IsDirty: {2}", FirstName, LastName, IsDirty);
}
#endregion
#region INotifyPropertyChanged implementation
protected void OnPropertyChanged(string propertyName)
{
if (!ReferenceEquals(null, PropertyChanged))
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
if (propertyName != "IsDirty")
IsDirty = true;
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
The problem is now solved: IsDirty is not altered by the deserialization. Or better said: the IsDirty value is altered during deserialization but restored to the proper value at the end of deserialization when the IsDirty value is deserialized.
This solution is working but not perfect: there are still unecessary calls to the property changed event handler. And it can become cumbersome if we further complexify the Contact class. Moreover this solution does not work if we define a hierarchy of data classes and define the IsDirty property on the base class:
public class BaseContact : INotifyPropertyChanged
{
#region fields
private bool _isDirty = false;
#endregion
#region properties
[DataMember]
public bool IsDirty
{
get
{
return _isDirty;
}
set
{
if (_isDirty == value)
return;
_isDirty = value;
OnPropertyChanged("IsDirty");
}
}
#endregion
#region INotifyPropertyChanged implementation
protected void OnPropertyChanged(string propertyName)
{
if (!ReferenceEquals(null, PropertyChanged))
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
if (propertyName != "IsDirty")
IsDirty = true;
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
[DataContract]
public class Contact : BaseContact
{
#region fields
private string _firstName;
private string _lastName;
#endregion
#region properties
[DataMember]
public string FirstName
{
get
{
return _firstName;
}
set
{
if (_firstName == value)
return;
_firstName = value;
OnPropertyChanged("FirstName");
}
}
[DataMember]
public string LastName
{
get
{
return _lastName;
}
set
{
if (_lastName == value)
return;
_lastName = value;
OnPropertyChanged("LastName");
}
}
#endregion
#region methods
public Contact(string firstName, string lastName)
{
_firstName = firstName;
_lastName = lastName;
}
public override string ToString()
{
return string.Format("FirstName: '{0}' LastName: '{1}' IsDirty: {2}", FirstName, LastName, IsDirty);
}
#endregion
}
In this case setting orders on DataMembers will not solve the problem because DataMembers of base classes (and therefore IsDirty) are always serialized/deserialized first.
Solution 3: Mark fields as DataMember instead of properties
Third solution is to mark the fields with the DataMember attribute, rather than the properties. This way, the deserialization step will set fields rather than properties and therefore OnPropertyChanged will never be called.
The limitation is that serialization of private fields is not allowed in partial trust scenarios (and this is the case for instance when developping a SilverLight client). Everything works fine when running in a full trust environment. To overcome this issue, the fields must be set as internal rather than private and you need to mark the assembly defining the data contracts with the following attribute (in assemblyinfo.cs):
[assembly: InternalsVisibleTo("System.Runtime.Serialization")]
This gives access right to the serialization assmebly to access your internal members. Not doing so would result in the following exception at runtime, when running in partial trust environments:
The data contract type 'Contact' cannot be deserialized because the property '_firstName' does not have a public setter. Adding a public setter will fix this error. Alternatively, you can make it internal, and use the InternalsVisibleToAttribute attribute on your assembly in order to enable serialization of internal members - see documentation for more details. Be aware that doing so has certain security implications
So here is the final version of the Contact data contract:
[DataContract]
public class Contact : INotifyPropertyChanged
{
#region fields
[DataMember( Name = "FirstName")]
internal string _firstName;
[DataMember( Name = "LastName")]
internal string _lastName;
[DataMember( Name = "IsDirty")]
internal bool _isDirty = false;
#endregion
#region properties
public string FirstName
{
get
{
return _firstName;
}
set
{
if (_firstName == value)
return;
_firstName = value;
OnPropertyChanged("FirstName");
}
}
public string LastName
{
get
{
return _lastName;
}
set
{
if (_lastName == value)
return;
_lastName = value;
OnPropertyChanged("LastName");
}
}
public bool IsDirty
{
get
{
return _isDirty;
}
set
{
if (_isDirty == value)
return;
_isDirty = value;
OnPropertyChanged("IsDirty");
}
}
#endregion
#region methods
public Contact(string firstName, string lastName)
{
_firstName = firstName;
_lastName = lastName;
}
public override string ToString()
{
return string.Format("FirstName: '{0}' LastName: '{1}' IsDirty: {2}", FirstName, LastName, IsDirty);
}
#endregion
#region INotifyPropertyChanged implementation
private void OnPropertyChanged(string propertyName)
{
if (!ReferenceEquals(null, PropertyChanged))
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
if (propertyName != "IsDirty")
IsDirty = true;
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
Please note the extra Name = xx setting of the DataMembers. This is to ensure we get a equivalent data schema as the one when marking the Properties as DataMembers.
This implementation solves our issue and works in both full trust and partial trust environment. That's it for now!
Recent Comments