您能否将扩展方法的范围限制为具有特定属性的 类?

Can you restrict the scope of an Extension Method to classes with a specific attribute?

我们有一个自定义 FileExtensionAttribute,我们用它装饰我们的模型 类,它基于文件持久性。定义如下:

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

    public readonly string FileExtension;
}

我们还创建了以下扩展方法,以便更方便地检索这些扩展:

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();
    }
}

在上面,对于没有指定属性的类型,两种方法return分别是空枚举或null。但是,我们希望更主动地首先阻止此类调用。

如果在指定类型上找不到此类属性,我们可以很容易地抛出异常,但我想知道是否有办法将扩展方法的调用限制为仅支持在第一名,所以这是一个编译时错误,而不是必须在 运行 时处理的问题。

那么是否可以将扩展方法限制为仅支持具有给定属性的类型?如果可以,怎么做?

注意:我认为这在纯 C# 中可能是不可能的,但也许可以使用类似 PostSharp 的东西。

目前不支持。扩展方法是有限的,但可以非常强大。我很好奇为什么返回一个空列表是一个问题,我认为这将是理想的。如果它为空或 null 则什么也不做,没什么大不了的——生活还要继续。

为了更直接地回答你的问题,不。对于编译时错误,您不能通过属性限制扩展方法。

PostSharp 确实可以帮到你。

大纲:

  • 创建 AssemblyLevelAspect,它将使用 ReflectionSearch 搜索程序集中扩展方法的所有使用。这将给出调用这些扩展方法的方法列表。
  • 对于所有这些方法,使用 ISyntaxReflectionService 获取语法树。它是 IL 语法树而不是源代码本身。
  • 搜索 typeof(X).GetFileExtensions()variable.GetType.GetFileExtensions() 等模式并验证传递的类型是否具有 FileExtension 属性。
  • 如果发现不正确的用法,写一个编译时错误。

来源:

[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
                        )
                    );
            }
        }
    }
}

用法:

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

备注:

  • [MulticastAttributeUsage(PersistMetaData = true)]AttributeInheritance = MulticastInheritance.Multicast 都需要保留程序集上的属性,以便对引用声明项目的项目也执行分析。
  • 可能需要更深入的分析才能正确处理派生的 类 和其他特殊情况。
  • 需要 PostSharp Professional 许可证。