Fluent API for .NET RIA Services Metadata

A first stab at creating a fluent API for specifying metadata to declare validation rules and UI hints, rathen than "ugly" buddy classes, using the .NET RIA Services metadata pipeline extensibility.

.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 and Metadata Flow

The metadata pipeline is a big name for a couple of simple goals:

  • Allow developers to specify intent once, where it makes sense, and have it flow downstream so it is consumable by various components. As data and models flow downstreams, that intent should not be lost.
  • Allow developers to override at any level. For example, the middle tier should be able to further constrain a model, or the developer should be able to further customize or override auto-generated UI. One should be able to do so incrementally.
  • Have a common representation for various concerns like data model concepts (keys, associations etc), validation rules, UI scaffolding hints, etc. so they can be consumed for different scenarios – server-side HTML rendering, services, client-side UI whether its Ajax or Silverlight, etc.

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.

Posted on Sunday, 9/20/2009 @ 9:34 PM | #Silverlight


Comments

28 comments have been posted.

Kevin Isom

Posted on 9/20/2009 @ 11:50 PM
Nikhil,
Awesome. I would say it would be better to do the mapping more along these lines:
Map(p => p.UnitPrice).
UIHints().
Label("Unit Price").
Describe("The price shown in the catalog").
Validation().
Required().
Range(1, 1000);
That way you only have to define the property in one place and all the meta info is in that one place.

It would also be good to have some conventions, like keys being hidden and that sort of thing so you don't have to map them outside of the key mapping. The more conventions the better.
Again that's just awesome effort.
Cheers

Nikhil Kothari

Posted on 9/21/2009 @ 12:21 AM
@Kevin,
Taking in reverse order. I'd love to see [Key] members hidden by default. This is something that could be achieved by tuning the logic in things like DataGrid/DataForm that do auto-generation, i.e. nothing new needed at the metadata level.

I did think of Map(member) as being the root, and ten having UIHints and Validation as part of the same sequence. For some reason, I felt the value in having root be Validation(member) or UIHints(member) was more interesting, as in the future its possible you have one metadata class for specifying validation rules and another one for specifying UI hints. Anyway, its a very valid point - we have to understand what is the more common usage, and optimize for that.

Marian Kostal

Posted on 9/21/2009 @ 2:30 AM
Interesting alternative solution, but I think that code with buddy class is more elegant and more readable than fluent API. I feel it as fight between declarative and imperative/functional approach.

Jos van der Til

Posted on 9/21/2009 @ 5:00 AM
Seems like we are both working with the same idea.

I posted my project here: http://fluentmetadata.codeplex.com/

Steve

Posted on 9/21/2009 @ 5:18 AM
I prefer this - good stuff.

How would you override this , ie. for a customer with different product metadata on the product class ?

Antti

Posted on 9/21/2009 @ 5:51 AM
Any estimate when RIA Services version with this Fluent-API is coming? This seems very interesting and more preferable than the current buddy class solution but it would be nice to know a rough estimate when we can expect to see this as out-of-the-box feature. This year? Q1 next year? Q2 next year?, ... ?

Nikhil Kothari

Posted on 9/21/2009 @ 8:42 AM
@Antii
Working on that question with the team. As of right now, this isn't a planned feature for v1 (as mentioned in the post). Also, as mentioned, I'd really like to see it happen, but there are other more critical v1 features (especially things that can't be added externally) that I'll argue for and prioritize first.

@Steve
Not sure I understand your question. Can you clarify?

Alexey Zakharov

Posted on 9/21/2009 @ 8:57 AM
Hi,

Silverlight really need technology like RIA Services. But I'm afraid of the way it is evolving... I mean a code generation here.

WCF code generated proxy was ugly... And it takes a lot of workarounds to work without it. EF code generation make us totally ignore it.

RIA services have even more monstrous code generation garbage and it is not something like T4 that we can to fix.

Please provide alternative non code generation way to use RIA services.

Thanks,
Alexey Zakharov.

Nikhil Kothari

Posted on 9/21/2009 @ 1:47 PM
@Alexey
What specifically in the code-gen do you dislike? Sounds like you're ok with code-gen, because you're ok with T4. If its ability to customize, that is one thing (its a feature we need to have). But if it is something specific about what happens today, then it would be good to hear the specifics.

You can of course use RIA services without code-gen. The client-side framework doesn't care how the entities or the domain context were defined. You could write your own code-gen, or manually author them. In fact, if you look at my Bing sample (http://www.nikhilk.net/BLinq-Silverlight-RIAServices.aspx), the entities and domain context were hand-written.

Alexey Zakharov

Posted on 9/22/2009 @ 2:13 AM
Fist and perhaps the main reason: all microsoft code gen solutions (services references, EF and LTS, RIA Services) uses CodeDom, so there is no way to customize generated code. If all this solutions would be based for example on T4 templates everything will become much better... but in that case you should provide rich toolset for T4, which is now available only from other vendors.

But in many cases we can do things without codegen. For example if we are using WCF we could just use Microsoft Project Linker to share DTO objects and Service contract and then use service with the channel factory. That is what I'm talking about.

Nikhil Kothari

Posted on 9/22/2009 @ 11:15 AM
@Alexey
I am not arguing against T4 - it is absolutely wonderful. There are competing requirements though - extensibility and customizability. T4 is great at the latter IMO. However for extensibility you need a model for the code that multiple pieces of code can post-process. CodeDom provides a model (yes, I have my gripes with its API, but it at least exists as a standard) For example, in RIA Services, the Authentication infrastructure starts with code generated for a regular DomainService, and adds authentication-specific semantics.

At the end of the day, most people use what is available out-of-the-box rather than everyone writing a set of T4 templates for themselves. This is why it is imperative we get the default code-gen reasonable and acceptable by most. As said before, if there are specific gripes with what exists today (until the day where we do have T4)?

I agree where you can use shared code, that should be an option. In RIA Services, for example, we let you write shared code that allows you to share business rules such as validation, computed properties in entities, and general purpose utility code, rather than relying on code generation for those.

spartan.the

Posted on 10/10/2009 @ 5:45 AM
Microsoft is trying to catch MDA train?

IBM tried that trip with no great success. MDA hype is dead, now people try to write "agile crap", later there will be other hype when we'll reflect mistakes we did.

spartan.the

Posted on 10/10/2009 @ 5:47 AM
PS. Nikhilk, look inside time formatter for "Posted on" label. I think it inverts AM and PM labels.

Umair

Posted on 11/13/2009 @ 10:27 AM
Looks great !! Will try it out. I have a question, may be similar to what Steve had asked earlier (from above comments)

Is there a way to decide which attributes to apply to a Domain class at runtime. For ex., Decide at runtime, when to use [include] on domain class for a domain service call ? Is it possible with this Fluent API or my understanding of RIA Services is not right ? Thanks

Jack Ukleja

Posted on 12/8/2009 @ 6:14 AM
Very interesting. Was just thinking something along these lines today (how ugly buddy classes are, limitations of using attributes etc)

I wonder if there is an opportunity for OSLO in this area? With the OSLO modelling framework surely you could provide such a metadata framework?

AKS

Posted on 1/5/2010 @ 10:12 AM
Nikhil, thanks for the useful post! However, I've encountered a problem trying to use the API to annotate a key field (KeyAttribute) in a result collection from a stored procedure defined in a Linq2Sql datacontext. I'm getting the following error:

The entity 'xxxResult' does not have a key defined. Entities exposed by DomainService operations must have at least one property marked with the KeyAttribute.

The only way that I've been able to get this to work is to embed a Metadata class in a partial class in the datacontext which is precisely what your API is trying to avoid, no? Other metadata works just fine. Any suggestions? TIA.

Dewy

Posted on 1/13/2010 @ 6:47 AM
Hi Nikhil,

I am trying to figure out of .Net RIA Services is a fit for the framework that we currently use for our business apps.
We currently don't allow public setters on our BOs. I have just created a sample app, but all the generated fields are marked as ReadOnly. I haven't used any metadata classes. Is there a way to use the above approach (or anyother way) to override the ReadOnly attribute being applied to the generated proxy?

Buddy

Posted on 4/12/2010 @ 5:49 AM
I am using the latest bits for VS2010, Silverlight 4 and RIA services (as released at Mix10). My database has all "many to many" relationships with resolution tables. When the Entity Model is created, it correctly identifies the many-to-many and eliminates the connecting tables.
When I setup the [Includes] in the metadatfile and the domain, the parent child relationships seem to work fine...however, I just get ONE record for each of the child collections.
When debugging, the entire child tables are loaded, just not displayed.
The UI controls were created from drag-drop using the DataView.

Steve Naughton

Posted on 5/15/2010 @ 9:51 AM
Hope this will become the standard way to add metadata for Dynamic Data also :)

Steve

Marcel

Posted on 5/17/2010 @ 8:43 AM
Hi Nikhil,
is there an update for the Fluent API for the NET 4.0 Framework?
The MetadataProviderAttribute is missing in the System.ComponentModel.DataAnnotations :-/ . Do you have a hint how the class is called now?

Thanks,
Marcel

Uwascan

Posted on 6/3/2010 @ 12:25 PM
First I like to thank you for all you are doing for the Community. it is simply awesome. Please post an update on Fluent API for .NET RIA Services Metadata for the NET 4.0 Framework. The old version does not work with latest bits.

Thanks
Uwascan

Uwascan

Posted on 6/4/2010 @ 4:24 AM
I found the answer to my last post.
change the content of the file AssociateMetadataClassesAttribute.cs to the following

// AssociateMetadataClassesAttribute.cs
//

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Web.DomainServices.Metadata;
using System.ServiceModel.DomainServices.Server;

namespace System.Web.DomainServices {

public sealed class AssociateMetadataClassesAttribute : DomainServiceDescriptionProviderAttribute
{

public AssociateMetadataClassesAttribute()
: base(typeof(AssociatedMetadataClassTypeDescriptionProvider)) {
}

public override DomainServiceDescriptionProvider CreateProvider(Type domainServiceType, DomainServiceDescriptionProvider parent)
{
return base.CreateProvider(domainServiceType, parent);
}

private sealed class AssociatedMetadataClassTypeDescriptionProvider : DomainServiceDescriptionProvider
{

public AssociatedMetadataClassTypeDescriptionProvider(Type domainServiceType, DomainServiceDescriptionProvider parent)
: base(domainServiceType, parent)
{

}

public override ICustomTypeDescriptor GetTypeDescriptor(Type type, ICustomTypeDescriptor parent)
{
ICustomTypeDescriptor baseDescriptor = base.GetTypeDescriptor(type, parent);

string metadataTypeName = type.Namespace + "." + type.Name + "Metadata";
Type metadataType = type.Assembly.GetType(metadataTypeName,/* throwOnError */ false, /* ignoreCase */ false);

if ((metadataType != null) && typeof(MetadataClass).IsAssignableFrom(metadataType))
{
MetadataClass metadata = (MetadataClass)Activator.CreateInstance(metadataType);
return new AssociatedMetadataClassTypeDescriptor(type, metadata, baseDescriptor);
}

return baseDescriptor;
}
}

private sealed class AssociatedMetadataClassTypeDescriptor : CustomTypeDescriptor {

private MetadataClass _metadata;

public AssociatedMetadataClassTypeDescriptor(Type objectType, MetadataClass metadata, ICustomTypeDescriptor parentDescriptor)
: base(parentDescriptor) {
_metadata = metadata;
}

public override AttributeCollection GetAttributes() {
AttributeCollection baseAttributes = base.GetAttributes();

if (_metadata.HasModelMetadata) {
List<Attribute> modelAttributes = _metadata.GetModelMetadata();
return AttributeCollection.FromExisting(baseAttributes, modelAttributes.ToArray());
}

return baseAttributes;
}

public override PropertyDescriptorCollection GetProperties() {
PropertyDescriptorCollection propDescs = base.GetProperties();

if (_metadata.HasModelMemberMetadata) {
List<PropertyDescriptor> newPropDescs = new List<PropertyDescriptor>(propDescs.Count);

foreach (PropertyDescriptor propDesc in propDescs) {
List<Attribute> memberAttributes = _metadata.GetMemberMetadata(propDesc.Name);
if (memberAttributes == null) {
newPropDescs.Add(propDesc);
}
else {
newPropDescs.Add(new WrappedPropertyDescriptor(propDesc, memberAttributes.ToArray()));
}
}

propDescs = new PropertyDescriptorCollection(newPropDescs.ToArray());
}

return propDescs;
}
}

private sealed class WrappedPropertyDescriptor : PropertyDescriptor {

private PropertyDescriptor _parentPropDesc;

public WrappedPropertyDescriptor(PropertyDescriptor propDesc, Attribute[] attributes)
: base(propDesc, attributes) {
_parentPropDesc = propDesc;
}

public override Type ComponentType {
get {
return _parentPropDesc.ComponentType;
}
}

public override bool IsReadOnly {
get {
return _parentPropDesc.IsReadOnly;
}
}

public override Type PropertyType {
get {
return _parentPropDesc.PropertyType;
}
}

public override bool CanResetValue(object component) {
return _parentPropDesc.CanResetValue(component);
}

public override object GetValue(object component) {
return _parentPropDesc.GetValue(component);
}

public override void ResetValue(object component) {
_parentPropDesc.ResetValue(component);
}

public override void SetValue(object component, object value) {
_parentPropDesc.SetValue(component, value);
}

public override bool ShouldSerializeValue(object component) {
return _parentPropDesc.ShouldSerializeValue(component);
}
}
}
}

recompile and you are good to go. I found the key in a word document i downloaded online "WCF RIA SERVICES
BREAKING CHANGES" I cannot remember the url for now but you can google it too. the key section of that document reads;

"DomainServiceDescriptionProvider and MetadataProvider combined into a single class
The DomainServiceDescriptionProvider class and the MetadataProvider class have been combined into the DomainServiceDescriptionProvider class. Anyone who has created a class that derives from DomainServiceDescriptionProvider or MetadataProvider will have to adapt the derived class for this change. As a result of this change, the following classes were also removed:
• MetadataProviderAttribute
• LinqToEntitiesTypeDescriptionProvider
• LinqToSqlTypeDescriptionProvider
If you applied the LinqToEntitiesMetadataProvider or LinqToSqlMetadataProvider attribute, you must now use either the LinqToEntitiesDomainServiceDescriptionProvider or LinqToSqlDomainServiceDescriptionProvider attribute."

the solution is still not perfect because it can only pick metadata for the product class and ignore metadata for Category. Hope this helps.

thanks
Uwascan

Uwascan

Posted on 6/4/2010 @ 10:53 AM
Yeah it is me again. I could not give up on this awesome work.
I found the problem outlined in the last post. metadata for the iategory class was ignored because
my Category metadata class ended with MetaData instead of Metadata. so i refactored AssociatedMetadataClassTypeDescriptionProvider class so that it will accept the following suffixes metadata, Metadata, MetaData and METADATA. the class is shown below.

private sealed class AssociatedMetadataClassTypeDescriptionProvider : DomainServiceDescriptionProvider
{

public AssociatedMetadataClassTypeDescriptionProvider(Type domainServiceType, DomainServiceDescriptionProvider parent)
: base(domainServiceType, parent)
{

}

public override ICustomTypeDescriptor GetTypeDescriptor(Type type, ICustomTypeDescriptor parent)
{
ICustomTypeDescriptor baseDescriptor = base.GetTypeDescriptor(type, parent);

Type metadataType = this.GetMetaDataType(type);

if ((metadataType != null) && typeof(MetadataClass).IsAssignableFrom(metadataType))
{
MetadataClass metadata = (MetadataClass)Activator.CreateInstance(metadataType);
return new AssociatedMetadataClassTypeDescriptor(type, metadata, baseDescriptor);
}

return baseDescriptor;
}

private Type GetMetaDataType(Type type)
{
string metadataTypeName = type.Namespace + "." + type.Name + "metadata";
Type metadataType = type.Assembly.GetType(metadataTypeName,/* throwOnError */ false, /* ignoreCase */ false);

if ((metadataType != null) && typeof(MetadataClass).IsAssignableFrom(metadataType))
{
return metadataType;
}
else
{
metadataTypeName = type.Namespace + "." + type.Name + "Metadata";
metadataType = type.Assembly.GetType(metadataTypeName,/* throwOnError */ false, /* ignoreCase */ false);

if ((metadataType != null) && typeof(MetadataClass).IsAssignableFrom(metadataType))
{
return metadataType;
}
else
{
metadataTypeName = type.Namespace + "." + type.Name + "MetaData";
metadataType = type.Assembly.GetType(metadataTypeName,/* throwOnError */ false, /* ignoreCase */ false);

if ((metadataType != null) && typeof(MetadataClass).IsAssignableFrom(metadataType))
{
return metadataType;
}
else
{
metadataTypeName = type.Namespace + "." + type.Name + "METADATA";
metadataType = type.Assembly.GetType(metadataTypeName,/* throwOnError */ false, /* ignoreCase */ false);

if ((metadataType != null) && typeof(MetadataClass).IsAssignableFrom(metadataType))
{
return metadataType;
}
}
}
}

return null;
}
}

Once again I like to thank Nikhil Kothari for this wonderful job.

Uwascan

prasannakumar

Posted on 4/25/2011 @ 10:37 PM
nicee more use full

prasannakumar

Posted on 4/25/2011 @ 10:38 PM
nicee more usefull

Water Damage Restoration

Posted on 8/18/2011 @ 12:32 PM
What is the significance of "Name" and "Category Name"?

Magnus

Posted on 10/1/2011 @ 4:00 PM
Loving this solution. I have not tested it yet but it looks as if will solve my problem. In my case I want to be able to
decorate added members (on classes exposed by RIA) in another assembly with attributes like DataMember (for Order,
unfortunately currently ignored in the endpoint factory) and RoundtripOriginal. This to have a forward-compatible data
contract exposed on the SOAP-endpoint (SoapXmlEndpointFactory). Now I can put away Association & IncludeAttributes
as well. I'm still going for annotations on the the actual members, like for client/server-side validation, but this makes it
possible to separate these service related attributes from my models.

Is this, or something similar, planned to be a part of a future release of RIA? Your original post is more than 2 years ago.

I hope the source code works as well as it looks :)

Magnus

Posted on 10/2/2011 @ 4:11 AM
Hi, I've tried the code now and made some modifications to be able to use metadataclasses from a different assembly than the models.

First of, I added a type-parameter to the constructor (pointing to one of the metadataclasses):
public sealed class AssociateMetadataClassesAttribute : DomainServiceDescriptionProviderAttribute
{
private Assembly _metadataAssembly = null;

public AssociateMetadataClassesAttribute()
: base(typeof(AssociatedMetadataClassTypeDescriptionProvider))
{

}

public AssociateMetadataClassesAttribute(Type metadataClass) : this()
{
if (metadataClass == null)
throw new ArgumentNullException("metadataClass");
_metadataAssembly = metadataClass.Assembly;
}

public override DomainServiceDescriptionProvider CreateProvider(Type domainServiceType, DomainServiceDescriptionProvider parent)
{
var provider = base.CreateProvider(domainServiceType, parent) as AssociatedMetadataClassTypeDescriptionProvider;
provider.MetadataLookupAssembly = _metadataAssembly;
return provider;
}

The attribute is now used in the following way AssociateMetadataClasses(typeof(ExternDataMetadata)) where ExternDataMetadata
is in a different assembly than the class it extends, ExternData.

As you can see I also added a public property (MetadataLookupAssembly ) on the AssociatedMetadataClassTypeDescriptionProvider to be able to set the lookup assembly.
This is the fully modified AssociatedMetadataClassTypeDescriptionProvider:
private sealed class AssociatedMetadataClassTypeDescriptionProvider : DomainServiceDescriptionProvider
{
public AssociatedMetadataClassTypeDescriptionProvider(Type domainServiceType, DomainServiceDescriptionProvider parent)
: base(domainServiceType, parent)
{

}

public override ICustomTypeDescriptor GetTypeDescriptor(Type type, ICustomTypeDescriptor parent)
{
ICustomTypeDescriptor baseDescriptor = base.GetTypeDescriptor(type, parent);

// Tries to get metadataclass from lookup assembly
// with a fallback to the same assembly as the types
var metadataType =
TryGetMetadataClass(MetadataLookupAssembly, type) ??
TryGetMetadataClass(type);

if ((metadataType != null) && typeof(MetadataClass).IsAssignableFrom(metadataType))
{
MetadataClass metadata = (MetadataClass)Activator.CreateInstance(metadataType);
return new AssociatedMetadataClassTypeDescriptor(type, metadata, baseDescriptor);
}

return baseDescriptor;
}

public Assembly MetadataLookupAssembly { get; set; }

#region Private Methods

private Type TryGetMetadataClass(Type type)
{
return TryGetMetadataClass(type.Assembly, type);
}

private Type TryGetMetadataClass(Assembly assembly, Type type)
{
if (assembly == null) return null;

var metadataClassName = String.Concat(type.Name, "Metadata");

return assembly.GetTypes().FirstOrDefault(
t => typeof(MetadataClass).IsAssignableFrom(t) &&
String.Compare(t.Name, metadataClassName, /* ignoreCase */ false) == 0);
}

#endregion
}

Hope someone else has use of these modifications.
Post your comment and continue the discussion.