简体   繁体   中英

Converting AttributeData into a known attribute type Roslyn

So I have a known attribute type from my code, say, something like this:

[AttributeUsage(AttributeTargets.Method)]
public class AliasAttribute : Attribute
{
    public string Alias;
    public AliasAttribute(string alias)
    {
        Alias = alias;
    }
}

I also have a method decorated with this attibute, which I'm getting as an IMethodSymbol in Roslyn:

// E.g. code which Roslyn gets to analyze
[Alias("Hello")] public void World() {}

// Actual Roslyn code
var method = (IMethodSymbol) compilation.GetSymbolsWithName("World").Single();

I am able to retrieve the attribute data using code similar to this:

var attributeData = method.GetAttributes().Single();

Now I want to use some simple built-in method of casting this AttributeData to the known type, eg like this:

AliasAttribute alias = attributeData.MapToType<AliasAttribute>();

The way I'm doing it currently is complicated: it involves instantiating the attribute type using reflection. However, I did not account for a whole lot of edge-cases, like params parameters in a generic way. See the code in the answer below. However, it is too complicated and I wonder if there is a simpler way of doing this.

As noted in the question, here is my solution, using reflection.

public static T MapToType<T>(this AttributeData attributeData) where T : Attribute
{
    T attribute;
    if (attributeData.AttributeConstructor != null && attributeData.ConstructorArguments.Length > 0)
    {
        attribute = (T) Activator.CreateInstance(typeof(T), attributeData.GetActualConstuctorParams().ToArray());
    }
    else
    {
        attribute = (T) Activator.CreateInstance(typeof(T));
    }
    foreach (var p in attributeData.NamedArguments)
    {
        typeof(T).GetField(p.Key).SetValue(attribute, p.Value.Value);
    }
    return attribute;
}

public static IEnumerable<object> GetActualConstuctorParams(this AttributeData attributeData)
{
    foreach (var arg in attributeData.ConstructorArguments)
    {
        if (arg.Kind == TypedConstantKind.Array)
        {
            // Assume they are strings, but the array that we get from this
            // should actually be of type of the objects within it, be it strings or ints
            // This is definitely possible with reflection, I just don't know how exactly. 
            yield return arg.Values.Select(a => a.Value).OfType<string>().ToArray();
        }
        else
        {
            yield return arg.Value;
        }
    }
}

More helper methods

If anybody finds the code from above useful, I have also defined a couple of convenient methods:

public static bool TryGetAttributeData(this ISymbol symbol, ISymbol attributeType, out AttributeData attributeData)
{
    foreach (var a in symbol.GetAttributes())
    {
        if (SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType))
        {
            attributeData = a;
            return true;
        }
    }
    attributeData = default;
    return false;
}

public struct AttributeSymbolWrapper<T>
{
    public INamedTypeSymbol symbol;

    private static INamedTypeSymbol GetKnownSymbol(Compilation compilation, System.Type t)
    {
        return (INamedTypeSymbol) compilation.GetTypeByMetadataName(t.FullName);
    }

    public void Init(Compilation compilation)
    {
        symbol = GetKnownSymbol(compilation, typeof(T));
    }
}

public static bool TryGetAttribute<T>(this ISymbol symbol, AttributeSymbolWrapper<T> attributeSymbolWrapper, out T attribute) where T : System.Attribute
{
    if (TryGetAttributeData(symbol, attributeSymbolWrapper.symbol, out var attributeData))
    {
        attribute = attributeData.MapToType<T>();
        return true;
    }
    attribute = default;
    return false;
}

public static bool HasAttribute(this ISymbol symbol, ISymbol attributeType)
{
    foreach (var a in symbol.GetAttributes())
    {
        if (SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType))
        {
            return true;
        }
    }
    return false;
}

Then, somewhere global at the start of the program, save the types that you'll want to cast into:

public static class RelevantSymbols
{
    public static AttributeSymbolWrapper<AliasAttribute> AliasAttribute;

    public static void Init(Compilation compilation)
    {
        AliasAttribute.Init(compilation);
    }
}

Finally, you can use it like this, without repeating the attribute type in the type argument:

var method = (IMethodSymbol) compilation.GetSymbolsWithName("World").Single();

if (method.TryGetAttribute(RelevantSymbols.AliasAttribute, out var attribute))
{
    // do something with the attibute data
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM