How to inject an attribute using a PostSharp attribute?
Asked Answered
B

1

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
    {
    }
}
Bullnecked answered 21/10, 2011 at 15:3 Comment(4)
Found solution here: doc.sharpcrafters.com/postsharp-2.1/…Bullnecked
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.Kersey
@DustinDavis I have removed the reference to CopyCustomAttribute so we don't confuse anyone.Bullnecked
Does it have to be done with PostSharp? it is simple to do with Mono Cecil.Buddhism
K
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);
    }
}
Kersey answered 21/10, 2011 at 16:6 Comment(6)
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.Bullnecked
I have an example of that, gotta find it real quickKersey
@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 tryKersey
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?Bullnecked
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...Bullnecked
@DistinDavis It worked! I just posted the solution as an update to my original question. Thanks for help out here, much appreciated.Bullnecked

© 2022 - 2024 — McMap. All rights reserved.