Reputation: 379
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, e.g. 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.
Upvotes: 2
Views: 812
Reputation: 379
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;
}
}
}
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
}
Upvotes: 6