7

How can I write a PostSharp aspect to apply an attribute to a class? The scenario I'm considering is a WCF entity (or domain object) that needs to be decorated with the DataContract attribute. It should also have a Namespace property. Like this:

using System.Runtime.Serialization;

namespace MWS.Contracts.Search.V1
{
    namespace Domain
    {
        [DataContract(Namespace = XmlNamespaces.SchemaNamespace)]
        public class PagingContext
        {
            [DataMember]
            public int Page { get; set; }

            [DataMember]
            public int ResultsPerPage { get; set; }

            [DataMember]
            public int MaxResults { get; set; }
        }
    }
}

In the above example you can see what I want the output to look like. It has the DataContract attribute applied to the class. Doing this by hand is tedious and not unique. I'd really just like to write a single aspect that can be applied a my "Domain" namespace. It would then apply the serialization related attributes for me. This way I can just focus on developing entity objects, and not worry about the serialization pluming details.

I have found documentation on PostSharp's website for injecting code before, after, and instead of methods. However what I'm looking for is a way to inject an Attribute onto a type.


Here is the solution!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using PostSharp.Aspects;
using PostSharp.Extensibility;
using PostSharp.Reflection;

namespace MWS.Contracts.Aspects
{
    // We set up multicast inheritance so  the aspect is automatically added to children types.
    [MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)]
    [Serializable]
    public sealed class AutoDataContractAttribute : TypeLevelAspect, IAspectProvider
    {
        private readonly string xmlNamespace;

        public AutoDataContractAttribute(string xmlNamespace)
        {
            this.xmlNamespace = xmlNamespace;
        }

        // This method is called at build time and should just provide other aspects.
        public IEnumerable<AspectInstance> ProvideAspects(object targetElement)
        {
            var targetType = (Type) targetElement;

            var introduceDataContractAspect =
                new CustomAttributeIntroductionAspect(
                    new ObjectConstruction(typeof (DataContractAttribute).GetConstructor(Type.EmptyTypes)));

            introduceDataContractAspect.CustomAttribute.NamedArguments.Add("Namespace", xmlNamespace);

            var introduceDataMemberAspect =
                new CustomAttributeIntroductionAspect(
                    new ObjectConstruction(typeof (DataMemberAttribute).GetConstructor(Type.EmptyTypes)));

            // Add the DataContract attribute to the type.
            yield return new AspectInstance(targetType, introduceDataContractAspect);

            // Add a DataMember attribute to every relevant property.)))
            foreach (var property in
                targetType.GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance)
                    .Where(property =>
                           property.CanWrite &&
                           !property.IsDefined(typeof (NotDataMemberAttribute), false)))
                yield return new AspectInstance(property, introduceDataMemberAspect);
        }
    }

    [AttributeUsage(AttributeTargets.Property)]
    public sealed class NotDataMemberAttribute : Attribute
    {
    }
}
Paul Fryer
  • 9,268
  • 14
  • 61
  • 93
  • Found solution here: http://doc.sharpcrafters.com/postsharp-2.1/Default.aspx##PostSharp-2.1.chm/html/fa546b6c-9107-4d5f-b921-b93ead7c42a7.htm – Paul Fryer Oct 21 '11 at 15:30
  • CopyCustomAttribute does not do what you're wanting. It only applies custom attributes to members that you are introducing into the target type. It will not apply attributes to existing members. – Dustin Davis Oct 21 '11 at 16:11
  • @DustinDavis I have removed the reference to CopyCustomAttribute so we don't confuse anyone. – Paul Fryer Oct 21 '11 at 16:13
  • Does it have to be done with PostSharp? it is simple to do with Mono Cecil. – Simon Oct 22 '11 at 09:03

1 Answers1

2

See http://www.sharpcrafters.com/blog/post/PostSharp-Principals-Day-12-e28093-Aspect-Providers-e28093-Part-1.aspx

Here is a working example. Applying this aspect to a class will apply the XmlIgnore attribute to any public property that does not already have XmlElement or XmlAttribute applied to it. the trick is using the CustomAttributeIntroductioinAspect that is built in to Postsharp. You just need to instantiate an instance specifying the attribute type and contructor details, then create a provider to apply it to the target(s).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PostSharp.Extensibility;
using PostSharp.Aspects;
using PostSharp.Reflection;
using System.Xml.Serialization;

namespace ApplyingAttributes
{
    [MulticastAttributeUsage(MulticastTargets.Field | MulticastTargets.Property,
                            TargetMemberAttributes = MulticastAttributes.Public | MulticastAttributes.Instance)]
    public sealed class AddXmlIgnoreAttribute : LocationLevelAspect, IAspectProvider
    {
        private static readonly CustomAttributeIntroductionAspect customAttributeIntroductionAspect =
            new CustomAttributeIntroductionAspect(
                new ObjectConstruction(typeof(XmlIgnoreAttribute).GetConstructor(Type.EmptyTypes)));

        public IEnumerable<AspectInstance> ProvideAspects(object targetElement)
        {
            LocationInfo memberInfo = (LocationInfo)targetElement;

            if (memberInfo.PropertyInfo.IsDefined(typeof(XmlElementAttribute), false) ||
                memberInfo.PropertyInfo.IsDefined(typeof(XmlAttributeAttribute), false))
                yield break;

            yield return new AspectInstance(memberInfo.PropertyInfo, customAttributeIntroductionAspect);
        }
    }

}

To use attributes, specifying parameters, I use

 public class MyAspect : TypeLevelAspect, IAspectProvider
{
    public IEnumerable<AspectInstance> ProvideAspects(object targetElement)
    {
        yield return Create<MethodInfo>(mi, "Value");

    }

    private AspectInstance Create<T>(T target, string newName)
    {
        var x = new CustomAttributeIntroductionAspect(
            new ObjectConstruction(typeof(NewMethodName).GetConstructor(new Type[] { typeof(string) }), new object[] { newName })
            );

        return new AspectInstance(target, x);
    }
}
Dustin Davis
  • 14,482
  • 13
  • 63
  • 119
  • Thanks for the example. How do I set properties to the CustomAttributeIntroductionAspect ? For example on the DataContractAttribute there is a property called "Namespace" which I'd like to set. – Paul Fryer Oct 21 '11 at 16:10
  • I have an example of that, gotta find it real quick – Dustin Davis Oct 21 '11 at 16:13
  • @Paul I've updated my answer with an example I use for applying attributes with constructor parameters. I know not all attributes use constructors but instead use properties, but it's a start. Gvie it a try – Dustin Davis Oct 21 '11 at 16:24
  • The DataContractAttribute only has one constructor and it takes in zero parameters. So I can't set the parameters as part of the constructor parameters. I'm wondering if the PostSharp API simply doesn't have the capability to set attribute properties from the CustomAttributeIntroductionAspect type? – Paul Fryer Oct 21 '11 at 16:32
  • Ok, I think I found what I'm looking for. There is a property called NamedArguments. This is where you can provide a name / value pair and it will set the property, by name. Still testing... – Paul Fryer Oct 21 '11 at 16:42
  • 1
    @DistinDavis It worked! I just posted the solution as an update to my original question. Thanks for help out here, much appreciated. – Paul Fryer Oct 21 '11 at 20:11