3

We have a custom FileExtensionAttribute which we decorate our model classes which are based on file persistence with. It is defined as follows:

[AttributeUsage(AttributeTargets.Class, AllowMultiple=true, Inherited=true)]
public class FileExtensionAttribute : Attribute
{
    public FileExtensionAttribute(string fileExtension)
    {
        FileExtension = fileExtension;
    }

    public readonly string FileExtension;
}

We've also created the following extension methods to make retrieving those extensions more convenient:

public static class FileExtensionAttributeHelper
{
    public static IEnumerable<string> GetFileExtensions(this Type type)
    {
        return type.CustomAttributes
            .OfType<FileExtensionAttribute>()
            .Select(fileExtensionAttribute => fileExtensionAttribute.FileExtension);
    }

    public static string GetPrimaryFileExtension(this Type type)
    {
        return GetFileExtensions(type).FirstOrDefault();
    }
}

In the above, for types which don't have the attribute specified, the two methods return an empty enumeration or null respectively. However, we would like to be more proactive in stopping such calls in the first place.

While we can easily throw an exception if no such attributes are found on the specified type, I'm wondering if there's a way to restrict the calling of the extension methods to only support types which have that attribute set in the first place so it's a compile-time error and not something that has to be dealt with at run-time.

So is it possible to restrict extension methods to only support types with a given attribute? If so, how?

Note: I'm thinking this may not be possible in pure C#, but perhaps something like PostSharp can be used for this.

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • These should just not be extension methods. Extending the Type class for such a highly specific usage is plain wrong. – Hans Passant Nov 14 '15 at 03:24
  • Hence my reason to restrict it. That's the entire point of my question. – Mark A. Donohoe Nov 14 '15 at 03:25
  • Two wrongs don't make a right, the C# designers could always be counted on getting that detail correct. Just write a plain static method that takes the Type object as an argument. – Hans Passant Nov 14 '15 at 03:27
  • I have that. And while I appreciate your commentary, a) that is not what my question was, and b) sorry, but I just disagree with your assessment. Instead of focusing on my example, focus on the question and it's intent. That's the point of Stack. – Mark A. Donohoe Nov 14 '15 at 03:28

2 Answers2

1

This is not currently supported. Extension methods are limiting, but can be extremely powerful. I am most curious why getting an empty list back is a problem, I would assume that would be ideal. If it is empty or null then do nothing, not a big deal -- life goes on.

To more directly answer your question, no. You cannot restrict extension methods by attribute for compile time errors.

David Pine
  • 23,787
  • 10
  • 79
  • 107
  • Yeah, I didn't think so. I have a feeling PostSharp is the only way to achieve this. Oh... and I actually disagree with your 'not a big deal' comment. It is a big deal because that means someone's attempting to incorrectly use our file IO framework. Checking for null, then doing nothing if it's not supported is ignoring a coding problem. I'm trying to address the problem, not mute the symptom if that makes sense. Think about it being the same as calling a substring function on a null string. You wouldn't simply ignore it because it means there's a problem. You throw an exception. – Mark A. Donohoe Nov 14 '15 at 00:54
  • To me, we don't resort to throwing exceptions if it is not exceptional. But I'm not familiar enough with your framework. Those kinds of decisions are yours, I was trying to communicate my experiences. Also, in doing. A once over on the PostSharp documentation I'm not adding that it supports this either. I hope you'll accept my answer! – David Pine Nov 14 '15 at 00:58
  • I will, but not just yet. I want to give it some more time to get some other eyes on this, especially about the PostSharp aspect. And about that, PostSharp allows you to write code that can validate such things, so it can support it. You just have to code that support yourself. – Mark A. Donohoe Nov 14 '15 at 01:01
  • I'm familiar with PostSharp I have used t before, it is awesome! But conditionally hiding an extension method based on an attribute isn't possible. Sounds more like a Roslyn feature to implement, but who knows. You could just throw when the call is made. You asked "So is it possible to restrict extension methods to only support types with a given attribute? If so, how?" -- I am thinking that you could just throw in your extension method of the attribute isn't found? – David Pine Nov 14 '15 at 01:08
  • No, that's not how you would design it. It would be caught as part of the PostSharp post-compile process where it is updating/injecting into the IL. There you have the ability to inspect the post-compiled code since that's where you're injecting your stuff. You simply see if someone is using that API when they shouldn't be and abort if they are. That's what I'm thinking anyway. Of course I haven't actually tried it yet. – Mark A. Donohoe Nov 14 '15 at 01:12
  • Well @David, I hate to do this, but as I suspected, PostSharp looks like it can do this thanks to the answer below. Unfortunately, that means I have to change the answer. Sorry! (I really do hate that because when it happens to me, I hate seeing a red negative in my daily cred. But I did vote your answer up so you should get some back, and technically the solution does rely on a third-party tool. – Mark A. Donohoe Nov 20 '15 at 16:55
  • Sadness ensues, on the bright side -- we all learned something today. Thanks – David Pine Nov 20 '15 at 17:20
0

PostSharp can indeed help you.

Outline:

  • Create AssemblyLevelAspect that would search using ReflectionSearch for all uses of your extension methods in the assembly. This will give a list of methods that call those extension methods.
  • For all these methods, get the syntax tree using ISyntaxReflectionService. It is IL syntax tree not the source code itself.
  • Search for patterns like typeof(X).GetFileExtensions() and variable.GetType.GetFileExtensions() and validate that the passed type has FileExtension attribute.
  • Write a compile time error if incorrect usage is found.

Source:

[MulticastAttributeUsage(PersistMetaData = true)]
public class FileExtensionValidationPolicy : AssemblyLevelAspect
{
    public override bool CompileTimeValidate( Assembly assembly )
    {
        ISyntaxReflectionService reflectionService = PostSharpEnvironment.CurrentProject.GetService<ISyntaxReflectionService>();

        MethodInfo[] validatedMethods = new[]
        {
            typeof(FileExtensionAttributeHelper).GetMethod( "GetFileExtensions", BindingFlags.Public | BindingFlags.Static ),
            typeof(FileExtensionAttributeHelper).GetMethod( "GetPrimaryFileExtension", BindingFlags.Public | BindingFlags.Static )
        };

        MethodBase[] referencingMethods =
            validatedMethods
                .SelectMany( ReflectionSearch.GetMethodsUsingDeclaration )
                .Select( r => r.UsingMethod )
                .Where( m => !validatedMethods.Contains( m ) )
                .Distinct()
                .ToArray();

        foreach ( MethodBase userMethod in referencingMethods )
        {
            ISyntaxMethodBody body = reflectionService.GetMethodBody( userMethod, SyntaxAbstractionLevel.ExpressionTree );

            ValidateMethodBody(body, userMethod, validatedMethods);
        }

        return false;
    }

    private void ValidateMethodBody(ISyntaxMethodBody methodBody, MethodBase userMethod, MethodInfo[] validatedMethods)
    {
        MethodBodyValidator validator = new MethodBodyValidator(userMethod, validatedMethods);

        validator.VisitMethodBody(methodBody);
    }

    private class MethodBodyValidator : SyntaxTreeVisitor
    {
        private MethodBase userMethod;
        private MethodInfo[] validatedMethods;

        public MethodBodyValidator( MethodBase userMethod, MethodInfo[] validatedMethods )
        {
            this.userMethod = userMethod;
            this.validatedMethods = validatedMethods;
        }

        public override object VisitMethodCallExpression( IMethodCallExpression expression )
        {
            foreach ( MethodInfo validatedMethod in this.validatedMethods )
            {
                if ( validatedMethod != expression.Method )
                    continue;

                this.ValidateTypeOfExpression(validatedMethod, expression.Arguments[0]);
                this.ValidateGetTypeExpression(validatedMethod, expression.Arguments[0]);
            }

            return base.VisitMethodCallExpression( expression );
        }

        private void ValidateTypeOfExpression(MethodInfo validatedMethod, IExpression expression)
        {
            IMethodCallExpression callExpression = expression as IMethodCallExpression;

            if (callExpression == null)
                return;

            if (callExpression.Method != typeof(Type).GetMethod("GetTypeFromHandle"))
                return;

            IMetadataExpression metadataExpression = callExpression.Arguments[0] as IMetadataExpression;

            if (metadataExpression == null)
                return;

            Type type = metadataExpression.Declaration as Type;

            if (type == null)
                return;

            if (!type.GetCustomAttributes(typeof(FileExtensionAttribute)).Any())
            {
                MessageSource.MessageSink.Write(
                    new Message(
                        MessageLocation.Of( this.userMethod ),
                        SeverityType.Error, "MYERR1",
                        String.Format( "Calling method {0} on type {1} is not allowed.", validatedMethod, type ),
                        null, null, null
                        )
                    );
            }
        }

        private void ValidateGetTypeExpression(MethodInfo validatedMethod, IExpression expression)
        {
            IMethodCallExpression callExpression = expression as IMethodCallExpression;

            if (callExpression == null)
                return;

            if (callExpression.Method != typeof(object).GetMethod("GetType"))
                return;

            IExpression instanceExpression = callExpression.Instance;

            Type type = instanceExpression.ReturnType;

            if (type == null)
                return;

            if (!type.GetCustomAttributes(typeof(FileExtensionAttribute)).Any())
            {
                MessageSource.MessageSink.Write(
                    new Message(
                        MessageLocation.Of(this.userMethod),
                        SeverityType.Error, "MYERR1",
                        String.Format("Calling method {0} on type {1} is not allowed.", validatedMethod, type),
                        null, null, null
                        )
                    );
            }
        }
    }
}

Usage:

[assembly: FileExtensionValidationPolicy(
               AttributeInheritance = MulticastInheritance.Multicast
               )]

Notes:

  • [MulticastAttributeUsage(PersistMetaData = true)] and AttributeInheritance = MulticastInheritance.Multicast are both needed to preserve the attribute on the assembly so that the analysis is performed also on projects that reference the declaring project.
  • More deep analysis may be needed to correctly handle derived classes and other special cases.
  • PostSharp Professional license is needed.
Daniel Balas
  • 1,805
  • 1
  • 15
  • 20