Preserving Attributes in Auto-Generated Classes

The method by which Code Effects component allows the use of custom attributes for source object customization is very convenient, but this feature could potentially create a problem if you use attributes to decorate members of a source object that is auto-generated by a framework such as Entity Framework.

For example, suppose that we use Code Effects to filter records in an Entity Framework database. Here is an example of a table class auto-generated by Entity Framework (this is a part of the Product class of Microsoft's AdventureWorks database generated by Entity Framework 4):

namespace TestProject
{
	public System.Data.Linq.Table<Product> Products
	{
		get
		{
			return this.GetTable<Product>();
		}
	}

	[global::System.Data.Linq.Mapping.TableAttribute(Name="Production.Product")]
	public partial class Product : INotifyPropertyChangingINotifyPropertyChanged
	{
		private static PropertyChangingEventArgs emptyChangingEventArgs =
			new PropertyChangingEventArgs(String.Empty);
		
		private int _ProductID;

		// Some code is omitted here

		[global::System.Data.Linq.Mapping.ColumnAttribute(
			Storage="_ProductID", AutoSync=AutoSync.OnInsert,
			DbType="Int NOT NULL IDENTITY",
			IsPrimaryKey=true, IsDbGenerated=true)]
		public int ProductID
		{
			get
			{
				return this._ProductID;
			}
			set
			{
				if ((this._ProductID != value))
				{
					this.OnProductIDChanging(value);
					this.SendPropertyChanging();
					this._ProductID = value;
					this.SendPropertyChanged("ProductID");
					this.OnProductIDChanged();
				}
			}
		}

		// The rest of the class is omitted
	}
}

Because this class is auto-generated, anything that we might add to it will be lost the next time we reload the database. So, if we'd like to use the Product table as our source object and customize its members with Code Effects' attributes, we either need to keep adding the same attributes after each reload, over and over again, or come up with something less tedious. Thankfully, .NET is sophisticated enough to offer an elegant solution that requires almost no effort. The solution is based on interfaces and partial classes.

First, we need to declare an interface that implements and decorates all the properties that we'd like to use in our rules/filters and excludes all the unwanted ones:

namespace TestProject
{
	public interface IProduct
	{
		[Field(AllowCalculations = false, Min = 0)]
		int ProductID { getset; }
 
		[ExcludeFromEvaluation]
		string ProductNumber { getset; }
	}
}

Obviously, we need to create a separate file for this interface. Notice the use of the Field and ExcludeFromEvaluation attributes to decorate interface members. That will become the main feature of the whole exercise when we are done - this is how we can include or exclude any auto-generated member of the class into/from our rules or filters.

In Entity Framework all table classes are declared as partial. Let's use this fact and declare the other part of our Product table in a separate file. The main purpose of that other part of the Product class is to implement our new IProduct interface. It's also the place to decorate the Product class with the optional Source attribute, as well as to declare any other members that don't get auto-generated by Entity Framework, such as in-rule methods and rule actions. So, let's do that:

namespace TestProject
{
	[Source(DeclaredMembersOnly = true)]
	[ExternalAction(typeof(Helper), "SomeAction")]
	public partial class Product : IProduct
	{
		[Field(DisplayName="Invoice", Max = 10)]
		public string InvoiceNumber { getset; }

		[Method(DisplayName = "Now")]
		[return: Return(DateTimeFormat = "MM/dd/yyyy")]
		public DateTime GetNow()
		{
			return DateTime.Now;
		}
	}
 
	public class Helper
	{
		[Action("Some Action")]
		public static void SomeAction()
		{
			// Action's logic goes here
		}
	}
}

Here is what's going on:

  • We extended the auto-generated Product class by declaring its other part. It is vital to declare that "extension" part in a separate file. Otherwise, it'll be erased the next time we reload our database.
  • The optional Source attribute is applied in order to demonstrate where it should be used, if needed. The value of its DeclaredMembersOnly property is set to True. This will prevent Code Effects component from loading members of Product's base class, should we decide to implement one later. The default value of this property is False.
  • We also declared the Helper class with a static action method just to demonstrate the use of other class-level attributes, such as the ExternalAction attribute, on partial source objects.
  • This part of the class declares a property called InvoiceNumber, decorated with the Field attribute, and an in-rule method called GetNow, decorated with the Method and Return attributes. These members don't exist in the database and don't get auto-generated but they will show up in Rule Editor. This demonstrates how to add fields and methods to auto-generated source objects.
  • Obviously, both parts of the Product class are declared in the same namespace, even though they are located in different files.

So, now if (or when) we reload our database, all our attributes and non-database members will survive re-generation due to the existence of the new part of the Products table class. Fields that do get re-generated won't lose their custom attributes because those attributes are declared in the interface the class implements, not the class itself.

It's important to remember that the Entity Framework database, including the auto-generated part of the Product class, must already exist in the project before we declare the other part of the class. Otherwise, the project won't compile because our new part doesn't implement all members of the IProduct interface - the auto-generated part does.

And, of course, if we add new columns to the Product table, we also need to add corresponding members to the IProduct interface if we'd like to use those columns as fields in our business rules or LINQ filters.

Post your support requests on Stackoverflow.com. You can also post your comments and product feedback using the form at the bottom of this page.
Comments: 0
Name (optional):
Comment (URLs are allowed and must start with http:// or https://; all tags will be encoded):
Remaining character count: