(2009-05-02 Important update below!)
Scenario:
You have a lot of tables in your database with columns such as EditDate and UpdateByUserId and you're using Linq as your data access method.
Problem:
Instead of updating these columns all over the place in your code, you want a centralized solution to make sure that these columns are updated whenever an entity is changed.
Discussion and Solution:
Disclaimer: This is what I've come up with, please comment on it should you see any problems with it. This article will assume experience with working with Linq and object oriented programming.
To keep this example lean and simple we will have only one common column between our entities. I will use the Northwind database, however I have edited the Customers and Products table, adding an EditDate column of type datetime.
This of course means that when the Linq to SQL classes are generated (the .dbml file) both the Product and the Customer class will have an EditDate property. My first take on this was to create a superclass and force the Product and Customer classes to inherit from it through a custom partial class definition, like so:
public abstract class Entity { //Property definition copied from Product //class in Northwind.designer.cs, “abstract” added: public abstract System.Nullable<System.DateTime> EditDate { get; set; } } public partial class Product : Entity { } public partial class Customer : Entity { }
However, this gives some errors and warnings:
As you can see in the warnings, a solution to this would be to go in to the dbml-file and change the EditDate property in each class to be an override;
But I don’t really like this solution since this would require me to do this whenever I’ve changed something in the Customer table and regenerates this class… It seems to me that leaving the .dbml file (and it’s designer.cs code behind) as is is more maintainable. This is also the reason why I inherit from my Entity superclass in partial class definitions (of Product and Customer) instead of editing the Northwind.designer.cs file directly. This is possible thanks to the fact that the Linq generated classes are declared as partial.
So, my next approach was to create an interface with the shared property definition and then implement that interface through my partial class definitions;
interface IEntity { //Property definition copied from Product //class in Northwind.designer.cs: System.Nullable<System.DateTime> EditDate { get; set; } } public partial class Product : IEntity { } public partial class Customer : IEntity { }
And this actually compiles just fine. As you can see, my partial class definitions does not implement the EditDate property, but the partial classes in the designer.cs file does.
But hey now, I wanted to share code between my entities. There is no way to share code via an interface… So, we actually have to combine this with a superclass that requires it’s inheritors to implement IEntity. That way we can work with the common column in code inside of that class. Let’s first have a look at the new Entity class;
public abstract class Entity { public Entity() { if (!(this is IEntity)) { throw new Exception("Class that inherits from Entity must also implement IEntity."); } } }
I don’t know any programmatic way of enforcing the combination of class inheritance with a specific interface, so as you can see above I use the default constructor to check if the current object (this) is an IEntity. If not the exception message will hopefully give the developer running this code some hint on what to do. With this validation in place, the Entity will now be able to access members through the IEntity interface.
The Linq entity classes (Product and Customer in the designer.cs file) exposes an event of particular interest, the PropertyChanged event. This event is raised whenever a property value of such an object is changed. Thankfully, the event has the exact same name in all (both) classes, so let’s add this to our IEntity interface so that we can access it in the Entity superclass;
interface IEntity { event PropertyChangedEventHandler PropertyChanged; System.Nullable<System.DateTime> EditDate { get; set; } }
Now, we can add some code to the Entity class;
public abstract class Entity { private IEntity _entity; public Entity() { if (!(this is IEntity)) { /.../ } _entity = (IEntity)this; _entity.PropertyChanged += new PropertyChangedEventHandler(_entity_PropertyChanged); } void _entity_PropertyChanged(object sender, PropertyChangedEventArgs e) { _entity.EditDate = DateTime.Now; } }
First of all I have added a private class member of type IEntity, this is assigned in the constructor and now I can easily access all the members of the IEntity interface (which actually will run code in the generated designer.cs-file!).
To achieve my goal I hook up a method to the PropertyChanged event and in that method i update the EditDate property.
UPDATE 090502: When debugging the code today I found that the above eventhandler method causes an infinite loop. This is not strange since the change of EditDate also will trigger the PropertyChanged event. To handle this, the following if statement is added to the method;
void _entity_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName != "EditDate") { _entity.EditDate = DateTime.Now; } }
The PropertyName property and the PropertyChangedEventArgs gives me the name of the property that got changed causing the PropertyChanged event to trigger. By checking that this isn’t equal to “EditDate” I can update the value of EditDate. This will cause the event to trigger again, but this time e.PropertyName will have the value of “EditDate” so the if statement will be skipped. Should you add code that also updates an UpdatedByUserId field, this too has to be checked in the if statement.
Before we can test this, we must update the partial class definitions so that they inherit from the Entity class;
public partial class Product : Entity, IEntity { } public partial class Customer : Entity, IEntity { }
With this in place we can run some simple code to see that it works;
NorthwindDataContext dc = new NorthwindDataContext(); dc.Products.First().ProductName = "Altered Product Name"; dc.Customers.First().ContactName = "Altered Contact Name"; dc.SubmitChanges();
After this code is run, we can have a look in the database and find that the EditDate column of both tables also has been updated on these posts;
Conclusion:
Using this approach I have achieved a solution that allows me to share code between classes generated from the .dbml-file without creating any work when a regeneration of the file is needed. The only thing I have to remember to do is when I add a new entity (table) to the .dbml file that also has these common columns, then I have to make sure I create a partial class definition that makes the class inherit from the Entity class and implement IEntity, and that’s it;
public partial class Order : Entity, IEntity { /* An empty class */ }
No code is needed in this class defintion, the requirements of the IEntity interface should be fulfilled in the generated part of the class (that is based on your db design). (However, these class definitions might come in handy for other purposes.)
Here is a class diagram giving you an overview of the pattern;
The three bottom classes are the ones generated from the .dbml file, remember though that the Product and Customer class are complemented with partial class definitions that makes them inherit from Entity and implement IEntity.
No comments :
Post a Comment