.NET RIA Services relies heavily on metadata annotations for expressing intent beyond what can be inferrd via convention. For example, validation rules on entities and members can be declared as annotations, which then enable a variety of consumption scenarios. We also have metadata for describing model aspects in DAL-agnostic fashion, and hints for automatic UI-generation. What we have today is just a first step.
The general design we're enabling is actually quite flexible. For example, a number of developers want to have metadata specified external to their code, for example in XML files or in a database. Some don't like attributes, and have asked for a fluent interface instead. In RIA Services, we wanted to create a consistent API for consumers to lookup metadata. In short the CLR metadata API, or the TypeDescriptor API from component model. And we want to let producers or specifiers of metadata choose a persistence store or specification mechanism that meets the needs of their scenario, as long as they can surface attribute instances when needed.
In fact, we have an over-arching vision for a metadata pipeline (which I'll get to later in the post).
However, the first concrete experience developers have with our metadata model when adding validation rules and UI hints, is unfortunately not very pretty. The out-of-the-box approach is based on an associated metadata class (or buddy class) mechanism that we share with ASP.NET dynamic data. Some folks have called them “ugly buddies” and that name has caught on in terms of how the feature is described! The ugly buddies mechanism suffers from usability issues (eg. lots of repetition, and room for typos, or names getting out of sync) and discoverability issues. At the end of the day, it is no more than a workaround for the missing language feature that would enable adding metadata to members via a partial class.
Using the extensibility mechanisms in the bits today, we've published a sample that demonstrates specifying metadata in XML. In this post, I'll show you my first stab at providing a fluent-API-based metadata approach. I'd obviously love to see this in the product out-of-the-box, but it is another feature, and there is the realities of a product cycle. However, there is always a v-next. In the meantime, do provide feedback via comments - it will definitely feed into the design process.
To set some concrete context for this post, lets first see what you'd need to do today using ugly buddies. For example, say I have the following generated class in my model:
public partial class Product { public int ID { get; set; } public string Name { get; set; } public double UnitPrice { get; set; } public int CategoryID { get; set; } public Category Category { get; set; } }
Typically I can't modify the generated class, so I am forced to create an alternate class, with fields that match on names, place metadata on those fields, and then associate the two classes with an additional bit of metadata, as shown below:
// Metadata to point to the buddy class [MetadataType(typeof(ProductMetadata))] public partial class Product { internal static class ProductMetadata { // Don't show ID values as columns in an auto-generated DataGrid [Display(AutoGenerateField = false)] public static readonly object ID = null; [Display(AutoGenerateField = false)] public static readonly object CategoryID = null; // Name is required [Required] public static readonly object Name = null; // Price is required and must be within the specified range [Required] [Range(1, 1000)] [Display(Name = "Unit Price", Description = "The price shown in the catalog.")] public static readonly object UnitPrice = null; // When sending Product's from server to client, include the // Name of every category as a projected CategoryName property // on Product itself. [Include("Name", "CategoryName")] public static readonly object Category = null; } }
Clearly this is not ideal. A somewhat better alternative comes from using LINQ expressions to specify which members that you're attaching metadata to, rather than having to do name matching. Consequently, you get intellisense help, so you don't have to memorize member names, as well as refactoring support.
public class ProductMetadata : MetadataClass<Product> { public ProductMetadata() { // UI Hints AddMemberMetadata(p => p.ID, new DisplayAttribute() { AutoGenerateField = false }); // Validation Rules AddMemberMetadata(p => p.UnitPrice, new RequiredAttribute()); AddMemberMetadata(p => p.UnitPrice, new RangeAttribute(1, 1000)); ... } } [AssociateMetadataClasses] public class CatalogService : LinqToSqlDomainService<NorthwindDataContext> { }
The [AssociateMetadataClasses] attribute on the domain service uses the extensibility mechanism present in a domain service to plug in one or more metadata providers into the metadata pipeline. This metadata provider uses a convention (appending the “Metadata” suffix) to find the associated metadata class.
While this is a little better, it has a couple of problems itself. You now have to keep repeating the expression that identifies the member. Having to instantiate the attributes looses the benefits of a declarative attribute syntax (for which they were primarily designed).
The MetadataClass<TModel> that I derived from also supports a fluent interface, that goes the next step. It specifically introduces friendly and more readable scenario-based APIs to express intent. Behind the scenes, those APIs create the same metadata attributes, and the end consumers of the metadata don't need to know where and how the metadata was specified. Here is an example:
ppublic class ProductMetadata : MetadataClass<Product> { public ProductMetadata() { this.UIHints(p => p.ID).HideField(); this.UIHints(p => p.CategoryID).HideField(); this.UIHints(p => p.UnitPrice).Label("Unit Price").Describe("The price shown in the catalog"); this.Validation(p => p.UnitPrice).Required().Range(1, 1000); this.Projection(p => p.Category).IncludeMember("Name", "CategoryName"); } }
With C# 4 just around the corner, this will get even better with named and optional arguments where appropriate.
The Metadata Pipeline
The metadata pipeline is a big name for a couple of simple goals:
Some of this is implemented today, esp. the server-side capabilities, using TypeDescriptor and its provider model. This functionality doesn't yet exist on the Silverlight client, and so we rely on having the metadata burned into CLR types on the client (for now).
Validation is a great example to see this metadata pipeline in action. Let's say I have a string column in the database.
At the storage level, the constraints defined in the database are that the column is non-nullable, and that the length is limited to 256 characters. These types of schema-level metadata is picked up by typical ORMs and is captured within the model so the DAL can perform constraint enforcement before even trying to send a change down to the database. Ideally you want this enforcement all the way down in the user interface where you can present the error to the user in the context of their task as soon as possible.
So the first thing RIA Services does is translate DAL-specific metadata into DAL-agnostic (or even technology-neutral) set of constraints metadata defined in System.ComponentModel.DataAnnotations. For example, the LinqToSqlDomainService and LinqToEntitiesDomainService translate string length constraints into a StringLengthAttribute, and non-nullability into RequiredAttribute. The developer can add additional constraints or further constrain by overriding/replacing the inherited metadata at the middle tier, using either the buddy class, an xml file, or the fluent-API calls, or some other mechanism. The aggregated metadata is then enforced at the service boundary so any incoming HTTP request is validated before any application logic processes the requests to insert or update data. An example of technology-neutral is the consumption of the same attributes by ASP.NET Dynamic Data… it consumes those same attributes to perform page validation by virtue of validation controls.
The next thing RIA Services does is to reflect on the model, understand the attributes that are present, and propagate those attributes down to the client by virtue of code-generation. Today this happens for Silverlight. On the client, the generated DomainContext enforces these constraints when application code submits changes to the server. This prevents wasted HTTP requests, when something invalid can be detected on the client. Further, within the presentation, controls like DataForm and DataGrid can inspect and invoke those validation constraints to present the user with errors even before the form is committed.
Another related concept is to apply the intent as close to the end-user as possible, for the best experience, but enforce at each level, and most importantly before application logic or storage come into the picture.
The metadata pipeline is not just about validation. We also have metadata attributes for UI hints that can guide generation of forms. In the future I'd like to see metadata around formatting, authorization, navigation across the model, etc.
And finally, the use of attributes allows plugging in existing systems, such as an existing rules engine, by virtue of creating a derived attribute.
The sample
You can download the sample, and check out the code for additional uses. You can also use the resulting framework binary (System.Web.DomainServices.Metadata.dll) in your own projects. Do you like this approach? I'd love to hear any and all feedback on your thoughts and suggestions, so we can factor them in for the product.