- Please visit this project site for the latest releases and source code.
Article Series
This article is the second part of a series on developing a Silverlight business application using Self-tracking Entities, WCF Services, WIF, MVVM Light Toolkit, MEF, and T4 Templates.
Contents
Introduction
In this second part, we will go over the topic of how to implement client-side change tracking using self-tracking entities. In the current version of ADO.NET Self-Tracking Entity Generator from VS2010, there is already a method called AcceptChanges()
, but there is no implementation of either the method RejectChanges()
or the property HasChanges
. We will explore how to add these functionalities into our enhanced version of Self-Tracking Entity Generator. After that, we will also go over several topics on how other parts of this sample can seamlessly work with this new data access layer.
Background
Let us first inspect the auto-generated entity classes from the ADO.NET Self-Tracking Entity Generator out of VS2010. Basically, each entity class is a POCO (Plain Old CLR Object) along with two additional interfaces: IObjectWithChangeTracker
and INotifyPropertyChanged
.
The interface of interest here is IObjectWithChangeTracker
, which adds a new property ChangeTracker
into each auto-generated entity class. This property keeps all the change tracking information for the subgraph of any given object, and it is of type ObjectChangeTracker
.
ObjectChangeTracker
is the class where we will find most of the client-side change tracking logic. Let us briefly go over some of the existing methods and properties we may use later:
ChangeTrackingEnabled
, as its name suggests, stores a Boolean value indicating whether this self-tracking entity object enables change tracking or not.
State
stores a value of either Unchanged
, Added
, Modified
, or Deleted
, which keeps track of the different states of a self-tracking entity object.
OriginalValues
stores the original values for properties that were changed.
ObjectsAddedToCollectionProperties
stores the added objects to collection valued properties that were changed.
ObjectsRemovedFromCollectionProperties
stores the removed objects to collection valued properties that were changed.
In addition to the properties mentioned above, the ObjectChangeTracker
class also implements the AcceptChanges()
method which we will discuss later.
Change Tracking Infrastructure
After a brief overview of the ObjectChangeTracker
class, we are now ready to visit what additional logic we are going to add for a full client-side change tracking infrastructure. Let us first discuss any existing and new events for the ObjectChangeTracker
class.
Events ObjectStateChanging, ObjectStateChanged, and UpdateHasChanges
There are three events defined in our new ObjectChangeTracker
class, and they are ObjectStateChanging
, ObjectStateChanged
, and UpdateHasChanges
. The ObjectStateChanging
event exists in the original version, while the other two are new additions.
public event EventHandler<ObjectStateChangingEventArgs> ObjectStateChanging;
public event EventHandler<ObjectStateChangedEventArgs> ObjectStateChanged;
public event EventHandler UpdateHasChanges;
protected virtual void OnObjectStateChanging(ObjectState newState)
{
if (ObjectStateChanging != null)
{
ObjectStateChanging(this,
new ObjectStateChangingEventArgs() { NewState = newState });
}
}
protected virtual void OnObjectStateChanged(ObjectState newState)
{
if (ObjectStateChanged != null)
{
ObjectStateChanged(this,
new ObjectStateChangedEventArgs() { NewState = newState });
}
}
protected virtual void OnUpdateHasChanges()
{
if (UpdateHasChanges != null)
{
UpdateHasChanges(this, new EventArgs());
}
}
As their names suggest, the ObjectStateChanging
event fires every time before the State
property changes, and the ObjectStateChanged
event gets triggered every time after the State
property changes. The other event UpdateHasChanges
is also self-explanatory. It gets fired in places where we want to update the HasChanges
property.
Method AcceptChanges()
Next, let us examine the AcceptChanges()
method:
public void AcceptChanges()
{
OnObjectStateChanging(ObjectState.Unchanged);
OriginalValues.Clear();
ObjectsAddedToCollectionProperties.Clear();
ObjectsRemovedFromCollectionProperties.Clear();
ChangeTrackingEnabled = true;
_objectState = ObjectState.Unchanged;
OnObjectStateChanged(ObjectState.Unchanged);
}
As the comment above states, the AcceptChanges()
clears the property OriginalValues
as well as the properties ObjectsAddedToCollectionProperties
and ObjectsRemovedFromCollectionProperties
. It then resets ObjectChangeTracker
back to the Unchanged
state, thus accepting all the changes made to the entity object. This method also fires the ObjectStateChanging
event before any change takes place, and fires the ObjectStateChanged
event immediately after.
Method RejectChanges()
We will discuss RejectChanges()
next, but before that, let us briefly go over another simple new method in the class, ObjectChangeTracker
.
public void SetParentObject(object parent)
{
this._parentObject = parent;
}
And, SetParentObject()
is used inside the ChangeTracker
property of every auto-generated entity class, as follows:
[DataMember]
public ObjectChangeTracker ChangeTracker
{
get
{
if (_changeTracker == null)
{
_changeTracker = new ObjectChangeTracker();
_changeTracker.SetParentObject(this);
_changeTracker.ObjectStateChanging += HandleObjectStateChanging;
_changeTracker.ObjectStateChanged += HandleObjectStateChanged;
_changeTracker.UpdateHasChanges += HandleUpdateHasChanges;
}
return _changeTracker;
}
set
{
if(_changeTracker != null)
{
_changeTracker.ObjectStateChanging -= HandleObjectStateChanging;
_changeTracker.ObjectStateChanged -= HandleObjectStateChanged;
_changeTracker.UpdateHasChanges -= HandleUpdateHasChanges;
}
_changeTracker = value;
_changeTracker.SetParentObject(this);
if(_changeTracker != null)
{
_changeTracker.ObjectStateChanging += HandleObjectStateChanging;
_changeTracker.ObjectStateChanged += HandleObjectStateChanged;
_changeTracker.UpdateHasChanges += HandleUpdateHasChanges;
}
}
}
As we can see from the lines of code above, every time ChangeTracker
changes, it updates a reference (inside the _parentObject
field) to its containing entity object. This _parentObject
field is needed by the RejectChanges()
method as shown below:
public void RejectChanges()
{
OnObjectStateChanging(ObjectState.Unchanged);
Type type = _parentObject.GetType();
foreach (var originalValue in OriginalValues.ToList())
type.GetProperty(originalValue.Key).SetValue(
_parentObject, originalValue.Value, null);
Dictionary<string, ObjectList> removeCollection =
ObjectsAddedToCollectionProperties.ToDictionary(n => n.Key, n => n.Value);
Dictionary<string, ObjectList> addCollection =
ObjectsRemovedFromCollectionProperties.ToDictionary(n => n.Key, n => n.Value);
if (removeCollection.Count > 0)
{
foreach (KeyValuePair<string, ObjectList> entry in removeCollection)
{
PropertyInfo collectionProperty = type.GetProperty(entry.Key);
IList collectionObject = (IList)collectionProperty.GetValue(_parentObject, null);
foreach (object obj in entry.Value.ToList())
{
collectionObject.Remove(obj);
}
}
}
if (addCollection.Count > 0)
{
foreach (KeyValuePair<string, ObjectList> entry in addCollection)
{
PropertyInfo collectionProperty = type.GetProperty(entry.Key);
IList collectionObject = (IList)collectionProperty.GetValue(_parentObject, null);
foreach (object obj in entry.Value.ToList())
{
collectionObject.Add(obj);
}
}
}
OriginalValues.Clear();
ObjectsAddedToCollectionProperties.Clear();
ObjectsRemovedFromCollectionProperties.Clear();
_objectState = ObjectState.Unchanged;
OnObjectStateChanged(ObjectState.Unchanged);
}
RejectChanges()
is a bit similar to AcceptChanges()
. But in stead of accepting changes, it applies all the original values back with the help of _parentObject
and a little magic of .NET reflection. From the code snippet above, we know that it first rolls back all the original values stored in OriginalValues
, makes a copy of both ObjectsAddedToCollectionProperties
and ObjectsRemovedFromCollectionProperties
, and then uses the copies to roll back all those values too. Just like AcceptChanges()
, RejectChanges()
also fires the ObjectStateChanging
event before any changes take place, and fires the ObjectStateChanged
event immediately after setting the State
back to Unchanged
.
Next, we will move on to talk about the new HasChanges
property.
Property HasChanges
In WCF RIA Services, the DomainContext
class has a property called HasChanges which indicates whether this context has any pending changes. Since we are using self-tracking entities for client-side change tracking, it is logical that we add this new property on each entity class, and we only need to add it on the client-side. In our sample application, this new property is generated by the T4 template IssueVisionClientModel.tt inside the project IssueVision.Data.
public Boolean HasChanges
{
get { return _hasChanges; }
private set
{
if (_hasChanges != value)
{
_hasChanges = value;
if (_propertyChanged != null)
{
_propertyChanged(this,
new PropertyChangedEventArgs("HasChanges"));
}
}
}
}
private Boolean _hasChanges = true;
Please note that it is important to set the initial value of this property to true
. This is because whenever we create a new entity object, its initial State
is set as Added
. Since any entity object in Added
state always has changes to save, the initial value of HasChanges
should be true
.
Next, let us try to figure out where the HasChanges
property needs to get updated. As we have already discussed above, there are three events defined inside the ObjectChangeTracker
class: ObjectStateChanging
, ObjectStateChanged
, and UpdateHasChanges
. It is easy to figure out that we need to update HasChanges
whenever either the ObjectStateChanged
or UpdateHasChanges
event gets triggered.
private void HandleObjectStateChanged(object sender, ObjectStateChangedEventArgs e)
{
#if SILVERLIGHT
HasChanges = (this.ChangeTracker.State == ObjectState.Added) ||
(this.ChangeTracker.ChangeTrackingEnabled &&
(this.ChangeTracker.State != ObjectState.Unchanged ||
this.ChangeTracker.ObjectsAddedToCollectionProperties.Count != 0 ||
this.ChangeTracker.ObjectsRemovedFromCollectionProperties.Count != 0));
#endif
}
private void HandleUpdateHasChanges(object sender, EventArgs e)
{
#if SILVERLIGHT
HasChanges = (this.ChangeTracker.State == ObjectState.Added) ||
(this.ChangeTracker.ChangeTrackingEnabled &&
(this.ChangeTracker.State != ObjectState.Unchanged ||
this.ChangeTracker.ObjectsAddedToCollectionProperties.Count != 0 ||
this.ChangeTracker.ObjectsRemovedFromCollectionProperties.Count != 0));
#endif
}
The logic to determine whether there is any pending change for a specific self-tracking entity object is as follows: first, if the State
of ChangeTracker
is Added
, the entity object has pending changes. Second, if change tracking is enabled, and its State
is not Unchanged
or either ObjectsAddedToCollectionProperties
or ObjectsRemovedFromCollectionProperties
is not empty, the entity object also has pending changes.
The ObjectStateChanged
event is triggered by the OnObjectStateChanged()
method, and this method is called in both AcceptChanges()
and RejectChanges()
, as we have already seen above. The UpdateHasChanges
event is triggered by the OnUpdateHasChanges()
method, and it is called in places like the following:
public bool ChangeTrackingEnabled
{
get { return _changeTrackingEnabled; }
set
{
if (_changeTrackingEnabled != value)
{
_changeTrackingEnabled = value;
OnUpdateHasChanges();
}
}
}
So far, we have completed our discussion about this new change tracking infrastructure. Next, we will move on to talk about how to use these new features.
Class IssueVisionModel Implementation
Let us first take a look at how the IssueVisionModel
class is built. The IssueVisionModel
class implements the IIssueVisionModel
interface in the project IssueVision.Common, and if you are familiar with the previous sample built with WCF RIA Services, you will notice that this interface class looks very similar in both samples. For example, they both have methods like SaveChangesAsync()
, RejectChanges()
, and both have properties like HasChanges
, IsBusy
, etc. Actually, this is a good thing. It means that even if these two samples use totally different data access layers, the model classes expose almost identical interfaces, thus making it possible that we can re-use most of the source code from the View and ViewModel classes.
Before we dig into the IssueVisionModel
class, let us first take a look at the IssueVision.WCFService project where we will find all the service references.
Project IssueVision.WCFService
Both the IssueVisionServiceClient
and PasswordResetServiceClient
classes derive from the base class ClientBase
, which provides the base implementation used to create client objects that can call services. For each of these two classes, we have implemented the Singleton pattern, and added a new property called ActiveCallCount
that keeps track of the number of concurrent active calls.
#region "Singleton"
private static readonly IssueVisionServiceClient instance =
new IssueVisionServiceClient("CustomBinding_IIssueVisionService");
public static IssueVisionServiceClient Instance
{
get { return instance; }
}
#endregion "Singleton"
#region "Active Call Count"
private int _activeCallCount;
public int ActiveCallCount
{
get { return this._activeCallCount; }
}
public void DecrementCallCount()
{
Interlocked.Decrement(ref this._activeCallCount);
if (this._activeCallCount == 0)
this.OnPropertyChanged("ActiveCallCount");
}
public void IncrementCallCount()
{
Interlocked.Increment(ref this._activeCallCount);
if (this._activeCallCount == 1)
this.OnPropertyChanged("ActiveCallCount");
}
#endregion "Active Call Count"
Because of the Singleton design pattern and this new property ActiveCallCount
, we are now able to implement the IsBusy
property.
Property IsBusy
IsBusy
is defined as follows:
public Boolean IsBusy
{
get { return this._isBusy; }
private set
{
if (this._isBusy != value)
{
this._isBusy = value;
this.OnPropertyChanged("IsBusy");
}
}
}
private Boolean _isBusy = false;
private void _proxy_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("ActiveCallCount"))
{
this.IsBusy = (this._proxy.ActiveCallCount != 0);
}
}
The value of IsBusy
is updated every time there is a PropertyChanged
event for ActiveCallCount
. If ActiveCallCount
is not equal to zero, IsBusy
is set to true
; otherwise, it is set to false
.
Property HasChanges
Next, let us check how to implement the HasChanges
property. Basically, this property is set to true
if there is any pending change for any self-tracking entity object the Model class is keeping track of. Based on the business logic of this sample application, we will only update either an Issue
object or a User
object. Therefore, we add two new properties called CurrentEditIssue
and CurrentEditUser
into the IssueVisionModel
class. Following is the code snippet for the CurrentEditUser
property:
public User CurrentEditUser
{
get { return _currentEditUser; }
set
{
if (!this.CurrentEditUserHasChanges())
{
if (!ReferenceEquals(_currentEditUser, value))
{
if (_currentEditUser != null)
{
((INotifyPropertyChanged)_currentEditUser).PropertyChanged -=
IssueVisionModel_PropertyChanged;
}
_currentEditUser = value;
if (_currentEditUser != null)
{
((INotifyPropertyChanged)_currentEditUser).PropertyChanged +=
IssueVisionModel_PropertyChanged;
}
ReCalculateHasChanges();
}
}
else
throw new InvalidOperationException(CommonResources.HasChangesIsTrue);
}
}
private User _currentEditUser;
private void IssueVisionModel_PropertyChanged(object sender,
PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("HasChanges"))
{
ReCalculateHasChanges();
}
}
As the code above shows, the CurrentEditUser
property subscribes to the PropertyChanged
event, and whenever this self-tracking entity object fires a PropertyChanged
event of its property HasChanges
, the HasChanges
property on the Model class gets re-calculated with the ReCalculateHasChanges()
method. Please do not get confused with the two different HasChanges
properties. One is defined on the entity class level, and the other is defined inside the Model class IssueVisionModel
. The ReCalculateHasChanges()
method is defined below:
private void ReCalculateHasChanges()
{
this.HasChanges = CurrentEditIssueHasChanges() || CurrentEditUserHasChanges();
}
private bool CurrentEditIssueHasChanges()
{
Boolean hasChanges = false;
if (_currentEditIssue != null)
{
hasChanges = hasChanges || _currentEditIssue.HasChanges ||
(_currentEditIssue.Platform == null ? false :
_currentEditIssue.Platform.HasChanges) ||
(_currentEditIssue.Attributes == null ? false :
_currentEditIssue.Attributes.Any(n => n.HasChanges)) ||
(_currentEditIssue.Files == null ? false :
_currentEditIssue.Files.Any(n => n.HasChanges));
}
return hasChanges;
}
private bool CurrentEditUserHasChanges()
{
Boolean hasChanges = false;
if (_currentEditUser != null)
{
hasChanges = hasChanges || _currentEditUser.HasChanges;
}
return hasChanges;
}
The HasChanges
property is set to true
if either the CurrentEditIssueHasChanges()
or CurrentEditUserHasChanges()
function returns true
. And, the CurrentEditIssueHasChanges()
function checks whether the entity object CurrentEditIssue
has any pending changes. This is accomplished by checking whether the object itself has any pending changes, as well as looping through all its navigation properties, namely Platform
, Attributes
, and Files
. The CurrentEditUserHasChanges()
function performs almost identical logic.
Method SaveChangesAsync()
As we now know how the HasChanges
property works, we can move on to talk about the SaveChangesAsync()
method.
public void SaveChangesAsync()
{
if (HasChanges)
{
if (_currentEditIssue != null && CurrentEditIssueHasChanges())
{
this._proxy.UpdateIssueAsync(_currentEditIssue);
this._proxy.IncrementCallCount();
this._updateIssueDone = false;
}
if (_currentEditUser != null && CurrentEditUserHasChanges())
{
this._proxy.UpdateUserAsync(_currentEditUser);
this._proxy.IncrementCallCount();
this._updateUserDone = false;
}
}
}
SaveChangesAsync()
first checks whether there are any pending changes. If that is true, the method will further verify whether there are any pending changes for the CurrentEditIssue
property. If that is also true, an actual call to UpdateIssueAsync()
is made passing in the object _currentEditIssue
. Next, the method takes a similar approach against the property CurrentEditUser
.
After either UpdateIssueAsync()
or UpdateUserAsync()
is done on the server side, the _proxy_UpdateIssueCompleted()
or _proxy_UpdateUserCompleted()
event handler will get called:
private void _proxy_UpdateIssueCompleted(object sender,
UpdateIssueCompletedEventArgs e)
{
this._proxy.DecrementCallCount();
this._updateIssueDone = true;
string warningMessage = string.Empty;
long updatedIssueID = 0;
if (e.Error == null)
{
if (e.Result.Count() == 2)
{
warningMessage = e.Result[0] as string;
updatedIssueID = Convert.ToInt64(e.Result[1]);
}
}
if (e.Error == null && string.IsNullOrEmpty(warningMessage))
{
if (_currentEditIssue.ChangeTracker.State == ObjectState.Added)
{
_currentEditIssue.IssueID = updatedIssueID;
}
_currentEditIssue.AcceptChanges();
if (_currentEditIssue.Platform != null)
{
_currentEditIssue.Platform.AcceptChanges();
}
if (_currentEditIssue.Attributes != null)
{
foreach (IssueVision.EntityModel.Attribute item in _currentEditIssue.Attributes)
item.AcceptChanges();
}
if (_currentEditIssue.Files != null)
{
foreach (IssueVision.EntityModel.File item in _currentEditIssue.Files)
item.AcceptChanges();
}
}
else
{
if (SaveChangesCompleted != null)
{
if (this._lastError == null || this.AllowMultipleErrors)
{
SaveChangesCompleted(this, new ResultArgs<string>(
warningMessage, e.Error, e.Cancelled, e.UserState));
}
}
this._lastError = e.Error;
}
if (this._updateIssueDone && this._updateUserDone)
{
if (SaveChangesCompleted != null && this._lastError ==
null && string.IsNullOrEmpty(warningMessage))
{
SaveChangesCompleted(this, new ResultArgs<string>(
string.Empty, e.Error, e.Cancelled, e.UserState));
}
}
}
The event handler for UpdateIssueCompleted
shown above first checks whether there is any error or warning message returned from the server. If everything works OK, it calls AcceptChanges()
on _currentEditIssue
as well as all its navigation properties, which sets the HasChanges
property of _currentEditIssue
back to false
. But if something goes wrong, AcceptChanges()
will not be called, and the error or warning message will be passed back to any ViewModel class through an event. And, if a user chooses to cancel any failed update operation, a RejectChanges()
will be made as we will discuss next.
Method RejectChanges()
It is relatively easy to understand RejectChanges()
because it is similar in logic to the SaveChangesAsync()
method.
public void RejectChanges()
{
if (_currentEditIssue != null)
{
_currentEditIssue.RejectChanges();
if (_currentEditIssue.Attributes != null)
{
foreach (IssueVision.EntityModel.Attribute item in _currentEditIssue.Attributes)
item.RejectChanges();
}
if (_currentEditIssue.Files != null)
{
foreach (IssueVision.EntityModel.File item in _currentEditIssue.Files)
item.RejectChanges();
}
}
if (_currentEditUser != null)
{
_currentEditUser.RejectChanges();
}
}
The RejectChanges()
method on the Model class will call RejectChanges()
on both CurrentEditIssue
and CurrentEditUser
. And, for each entity, RejectChanges()
gets called on the object itself as well as all its navigation properties that are collection objects.
Here we finish our discussion on how the Model class IssueVisionModel
is built; we will move on to a new topic about different categories of entity properties.
Three Different Categories of Entity Properties
In WCF RIA Services, we can tweak the accessibility of a certain entity property by using either metadata annotations or partial classes. Take the User
class as an example; it is defined in the EDM file like the following:
The auto-generated entity class is based on this EF Model, and it is a direct reflection of what is available in the database. But with WCF RIA Services, we can further tweak this class like the following:
[MetadataTypeAttribute(typeof(User.UserMetadata))]
public partial class User
{
internal class UserMetadata
{
protected UserMetadata()
{
}
......
[Exclude]
public string PasswordAnswerHash { get; set; }
[Exclude]
public string PasswordAnswerSalt { get; set; }
[Exclude]
public string PasswordHash { get; set; }
[Exclude]
public string PasswordSalt { get; set; }
[Exclude]
public Byte ProfileReset { get; set; }
}
......
[DataMember]
public string Password { get; set; }
......
[DataMember]
public string NewPassword { get; set; }
......
}
The code snippet above basically tells WCF RIA Services to exclude generating properties like PasswordSalt
and PasswordHash
on the client-side, and add two new properties Password
and NewPassword
into the entity class User
, and also generate those two properties on the client-side. Please note that these two new properties do not exist in our sample database. With this type of flexibility, we can classify entity properties into the following three categories:
- Properties that are only available on the client-side but not on the server-side.
- Properties that are available on both client and server sides, including properties that can be directly saved into a database field, as well as properties that cannot, such as
Password
and NewPassword
above.
- Properties that are only available on the server-side but never generated on the client-side, like
PasswordSalt
and PasswordHash
above.
Unfortunately, if we choose self-tracking entitles and WCF Services as our data access layer, most of these nice features as you see above are not available, and we have to do things a little bit differently.
Properties Available Only on Client-side
Let us start with an easy case first. For properties available only on client-side, we can simply take the same approach as we do with WCF RIA Services: adding new properties on client-side by using partial classes:
public partial class User
{
......
[Display(Name = "Confirm new password")]
[Required(ErrorMessage = "This field is required.")]
[CustomValidation(typeof(User), "CheckNewPasswordConfirmation")]
public string NewPasswordConfirmation
{
get
{
return this._newPasswordConfirmation;
}
set
{
PropertySetterEntry("NewPasswordConfirmation");
_newPasswordConfirmation = value;
PropertySetterExit("NewPasswordConfirmation", value);
OnPropertyChanged("NewPasswordConfirmation");
}
}
private string _newPasswordConfirmation;
......
}
Properties Available on Both Sides
It seems easy to handle properties available on both sides until we need to exclude properties like PasswordHash
and add new properties like NewPassword
so that they are available on both sides even though there is no such database field that ever existed.
Our approach is to use an advanced feature of Entity Framework called "virtual table". In case you are not familiar, here is a link to the MSDN documentation. Next, we will walk through how to add User
as a virtual table into our EDM file.
- First, we need to open the "IssueVision.edmx" file with an XML editor by right-clicking on the file, select Open With, then choose XML Editor, and click OK.
- Add an
EntitySet
and DefiningQuery
element into the SSDL section, as follows:
<EntitySet Name="Users"
EntityType="IssueVisionModel.Store.Users"
store:Type="Views">
<DefiningQuery>
<![CDATA[]]>
</DefiningQuery>
</EntitySet>
- Add an
EntityType
in the section where EntityType
items are defined within the SSDL section:
<EntityType Name="Users">
<Key>
<PropertyRef Name="Name" />
</Key>
<Property Name="Name" Type="nvarchar"
Nullable="false" MaxLength="50" />
<Property Name="FirstName" Type="nvarchar"
Nullable="false" MaxLength="50" />
<Property Name="LastName" Type="nvarchar"
Nullable="false" MaxLength="50" />
<Property Name="Email"
Type="nvarchar" MaxLength="100" />
<Property Name="Password" Type="nvarchar"
Nullable="false" MaxLength="50" />
<Property Name="NewPassword" Type="nvarchar"
Nullable="false" MaxLength="50" />
<Property Name="PasswordQuestion" Type="nvarchar"
Nullable="false" MaxLength="200" />
<Property Name="PasswordAnswer" Type="nvarchar"
Nullable="false" MaxLength="200" />
<Property Name="UserType" Type="char"
Nullable="false" MaxLength="1" />
<Property Name="ProfileReset"
Type="tinyint" Nullable="false" />
<Property Name="IsUserMaintenance"
Type="tinyint" Nullable="false" />
</EntityType>
- Add an
EntitySet
element into the CSDL section as follows:
<EntitySet Name="Users" EntityType="IssueVisionModel.User" />
- Add an
EntityType
in the section where EntityType
items are defined within the CSDL section:
<EntityType Name="User">
<Key>
<PropertyRef Name="Name" />
</Key>
<Property Name="Name" Type="String" Nullable="false"
MaxLength="50" Unicode="true" FixedLength="false" />
<Property Name="FirstName" Type="String" Unicode="true"
FixedLength="false" MaxLength="50" Nullable="false" />
<Property Name="LastName" Type="String" Unicode="true"
FixedLength="false" MaxLength="50" Nullable="false" />
<Property Name="Email" Type="String" Unicode="true"
FixedLength="false" MaxLength="100" />
<Property Name="Password" Type="String" Unicode="true"
FixedLength="false" MaxLength="50" Nullable="false" />
<Property Name="NewPassword" Type="String" Unicode="true"
FixedLength="false" MaxLength="50" Nullable="false" />
<Property Name="PasswordQuestion" Type="String"
Unicode="true" FixedLength="false"
MaxLength="200" Nullable="false" />
<Property Name="PasswordAnswer" Type="String"
Unicode="true" FixedLength="false"
MaxLength="200" Nullable="false" />
<Property Name="UserType" Type="String"
Unicode="false" FixedLength="true"
MaxLength="1" Nullable="false" />
<Property Type="Byte" Name="ProfileReset"
Nullable="false" />
<Property Type="Byte" Name="IsUserMaintenance"
Nullable="false" />
</EntityType>
- Save our changes and switch back to the Designer so that we can map the entity to the virtual table we just created, and it should look like the following:
- Next, we are going to add three custom functions into the SSDL section for the insert/delete/update operations of our newly created
User
entity. In case you need further information on how to define custom functions in the storage model, here is the link to the MSDN documentation. Following is one of the three custom functions:
<Function Name="UpdateUser" IsComposable="false">
<CommandText>
<![CDATA[]]>
</CommandText>
<Parameter Name="Name" Type="nvarchar"
MaxLength="50" Mode="In"/>
<Parameter Name="FirstName" Type="nvarchar"
MaxLength="50" Mode="In"/>
<Parameter Name="LastName" Type="nvarchar"
MaxLength="50" Mode="In"/>
<Parameter Name="Email" Type="nvarchar"
MaxLength="100" Mode="In"/>
<Parameter Name="PasswordQuestion" Type="nvarchar"
MaxLength="200" Mode="In"/>
<Parameter Name="UserType" Type="char"
MaxLength="1" Mode="In"/>
<Parameter Name="ProfileReset" Type="tinyint" Mode="In"/>
</Function>
- Again, save our changes after adding these three custom functions and switch back to the Designer so that we can map the entity and custom functions.
The advantage of using virtual tables is that the entity properties are not necessarily database fields, and even the entity class itself does not have to be based on one database table. It could be a joint of multiple tables. Also, there is no requirement to define matching insert/delete/update custom functions. Just like the entity class PasswordResetUser
in our sample application, it essentially becomes a read-only view.
Lastly, one cautionary note of using virtual tables is that none of these things actually exist in our sample database. If you run the Update Model Wizard, Entity Framework 4.0 Designer will wipe out any customizations of the SSDL. So, keep a copy of these changes if you need to update the EF model.
Properties Available Only on Server-side
Next, we are going to discuss how to implement the last of the three different categories of entity properties. In our sample application, the server-side only properties are PasswordSalt
, PasswordHash
, PasswordAnswerSalt
, and PasswordAnswerHash
. It is obvious that these four properties should stay on the server-side as transferring them to the client-side may become a security leak. One approach to keep properties on server-side is to take them out of the entity class User
and PasswordResetUser
of the EF Model, and use function imports to retrieve and update those database fields. Here is a link from MSDN documentation, in case you are not familiar with how to create a function import.
First, open the "IssueVision.edmx" file with the XML editor, and add all the related custom functions into the SSDL section. Following is just one of them:
<Function Name="GetPasswordAnswerHash" IsComposable="false">
<CommandText>
<![CDATA[]]>
</CommandText>
<Parameter Name="Name" Type="nvarchar"
MaxLength="50" Mode="In"/>
</Function>
After that, add function imports using the "Add Function Import" dialog box, as follows:
We need to add a total of six function imports as the following "Model Browser" shows:
Let us take a look at how to use these new function imports to retrieve and update server-side only properties. For example, context.GetPasswordAnswerHash(user.Name).First()
will retrieve the PasswordAnswerHash
field from the User table based on a certain user name. And, we can use context.ExecuteFunction
to call UpdatePasswordHashAndSalt
with the parameters of password salt and password hash values. The ResetPassword()
method in the class PasswordResetService
demonstrates how they are being used:
public void ResetPassword(PasswordResetUser user)
{
user.Validate();
using (IssueVisionEntities context = new IssueVisionEntities())
{
User foundUser = context.Users.FirstOrDefault(n => n.Name == user.Name);
if (foundUser != null)
{
string currentPasswordAnswerHash = context.GetPasswordAnswerHash(user.Name).First();
string currentPasswordAnswerSalt = context.GetPasswordAnswerSalt(user.Name).First();
string passwordAnswerHash = HashHelper.ComputeSaltedHash(user.PasswordAnswer,
currentPasswordAnswerSalt);
if (string.Equals(user.PasswordQuestion,
foundUser.PasswordQuestion,
StringComparison.Ordinal) &&
string.Equals(passwordAnswerHash, currentPasswordAnswerHash,
StringComparison.Ordinal))
{
string currentPasswordSalt = HashHelper.CreateRandomSalt();
string currentPasswordHash =
HashHelper.ComputeSaltedHash(user.NewPassword, currentPasswordSalt);
currentPasswordAnswerSalt = HashHelper.CreateRandomSalt();
currentPasswordAnswerHash =
HashHelper.ComputeSaltedHash(user.PasswordAnswer, currentPasswordAnswerSalt);
context.ExecuteFunction("UpdatePasswordHashAndSalt"
, new ObjectParameter("Name", user.Name)
, new ObjectParameter("PasswordHash", currentPasswordHash)
, new ObjectParameter("PasswordSalt", currentPasswordSalt));
context.ExecuteFunction("UpdatePasswordAnswerHashAndSalt"
, new ObjectParameter("Name", user.Name)
, new ObjectParameter("PasswordAnswerHash", currentPasswordAnswerHash)
, new ObjectParameter("PasswordAnswerSalt", currentPasswordAnswerSalt));
}
else
throw new UnauthorizedAccessException(ErrorResources.PasswordQuestionDoesNotMatch);
}
else
throw new UnauthorizedAccessException(ErrorResources.NoUserFound);
}
}
Retrieving and updating server-side only properties with function imports makes sure that none of these properties are exposed on the client-side. One problem I can think of is that this approach may not scale well if we have lots of server-side only properties. But, for most LOB applications, the number of server-side only properties should be relatively small, while the majority should be properties available on both sides. Therefore, scalability should not be a big concern here.
Server-side Update Logic
Before we finish this article, our last topic is how we actually do add/delete/update operations on the server-side. Following is how the UpdateIssue()
method of the IssueVisionService
class gets implemented.
public List<object> UpdateIssue(Issue issue)
{
List<object> returnList = new List<object>();
using (IssueVisionEntities context = new IssueVisionEntities())
{
if (issue.ChangeTracker.State == ObjectState.Added)
{
issue.Validate();
issue.OpenedDate = DateTime.Now;
issue.OpenedByID = HttpContext.Current.User.Identity.Name;
issue.LastChange = DateTime.Now;
issue.ChangedByID = HttpContext.Current.User.Identity.Name;
long newIssueID = context.Issues.Count() > 0 ?
(from iss in context.Issues select iss.IssueID).Max() + 1 : 1;
long newIssueHistoryID = context.IssueHistories.Count() > 0 ?
(from iss in context.IssueHistories select iss.IssueID).Max() + 1 : 1;
issue.IssueID = newIssueHistoryID > newIssueID ? newIssueHistoryID : newIssueID;
if (issue.StatusID == IssueVisionServiceConstant.OpenStatusID)
{
issue.AssignedToID = null;
}
if (issue.ResolutionID == null || issue.ResolutionID == 0)
{
issue.ResolutionDate = null;
issue.ResolvedByID = null;
}
else
{
issue.ResolutionDate = DateTime.Now;
issue.ResolvedByID = HttpContext.Current.User.Identity.Name;
}
context.Issues.ApplyChanges(issue);
context.SaveChanges();
returnList.Add(string.Empty);
returnList.Add(issue.IssueID);
return returnList;
}
else if (issue.ChangeTracker.State == ObjectState.Deleted)
{
if (issue.StatusID == IssueVisionServiceConstant.ActiveStatusID)
{
returnList.Add(ErrorResources.IssueWithActiveStatusID);
returnList.Add(0);
return returnList;
}
if (!HttpContext.Current.User.IsInRole(IssueVisionServiceConstant.UserTypeAdmin) &&
!(HttpContext.Current.User.Identity.Name.Equals(issue.AssignedToID)) &&
!(issue.AssignedToID == null &&
HttpContext.Current.User.Identity.Name.Equals(issue.OpenedByID)))
{
returnList.Add(ErrorResources.NoPermissionToDeleteIssue);
returnList.Add(0);
return returnList;
}
context.Issues.ApplyChanges(issue);
context.SaveChanges();
returnList.Add(string.Empty);
returnList.Add(0);
return returnList;
}
else
{
issue.Validate();
Issue originalIssue;
using (IssueVisionEntities otherContext = new IssueVisionEntities())
{
originalIssue = otherContext.Issues.First(n => n.IssueID == issue.IssueID);
}
if (!IssueIsReadOnly(originalIssue))
{
issue.LastChange = DateTime.Now;
issue.ChangedByID = HttpContext.Current.User.Identity.Name;
context.Issues.ApplyChanges(issue);
context.SaveChanges();
returnList.Add(string.Empty);
returnList.Add(issue.IssueID);
return returnList;
}
else
{
returnList.Add(ErrorResources.NoPermissionToUpdateIssue);
returnList.Add(0);
return returnList;
}
}
}
}
As the method above demonstrates, we determine whether to add, delete, or update an issue entity based on the ChangeTracker
's State
property. If the State
is Added
, we are going to add a new issue. If the State
is Deleted
, we should delete that issue from the database. And if the State
is either Unchanged
or Modified
, we will do an update operation. Also, no matter whether it is an add, delete, or update operation, we save changes by simply calling context.Issues.ApplyChanges(issue)
followed by context.SaveChanges()
.
Next Steps
We have covered quite a few topics in this second article. For our next part, we will move on to discuss the topics of client and server side validation logic with self-tracking entities. I hope you find this article useful, and please rate and/or leave feedback below. Thank you!
History
- January, 2011 - Initial release.
- March, 2011 - Update to fix multiple bugs including memory leak issues.