Reflection and Attributes in .Net Core refuse to cooperate - c#

I have class:
public class Domain
{
public static Assembly[] GetAssemblies()
{
var assemblies = new List<Assembly>();
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
{
try
{
var assemblyName = AssemblyLoadContext.GetAssemblyName(module.FileName);
var assembly = Assembly.Load(assemblyName);
assemblies.Add(assembly);
}
catch (BadImageFormatException)
{
// ignore native modules
}
}
return assemblies.ToArray();
}
}
My main class looks like:
class Program
{
public static Dictionary<String, Type> animals;
static void Main(string[] args)
{
var assTab = Domain.GetAssemblies();
foreach (var assembly in assTab)
{
var m = assembly.GetCustomAttribute<Method>();
if (m != null)
{
animals.Add(m.Name, assembly.GetType());
}
}
Where Method is a MethodAttribute class. In Animal.dll I have class like Dog, Cat etc. with Attribute [Method("cat")] and so on. To my dictionary I want to add this attribute name as string and type as Type (dog, Dog) and so on. My problem is that my program do not do that. After running program in variable animal I have 0 score. What should I change to achieve what I want?

The problem is in this line:
var m = assembly.GetCustomAttribute<Method>();
This line is getting attributes on the assembly. So if the attribute is not applied to the assembly then you will get null back.
What you need to do is get the types in the assembly first and then check each type for whether it has the attribute on it. Something more like this:
foreach(Type type in assembly.GetTypes())
{
var attr = assembly.GetCustomAttribute<Method>();
if (attr!=null)
{
animals.Add(attr.Name, type);
}
}

Related

Can't access arguments of attribute from system library using source generator

I am trying to make a source generator for mapping columns from the google bigquery api client to class properties. I'm having trouble getting custom column names from a ColumnAttribute on the properties. ConstructorArguments is always empty and columnAttribute.AttributeClass in this sample is always an ErrorTypeSymbol. If I try to load that type using compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.Schema.ColumnAttribute") the result is always null.
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace BigQueryMapping;
[Generator]
public class BigQueryMapperGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// add marker attribute
context.RegisterPostInitializationOutput(ctx =>
ctx.AddSource("BigQueryMappedAttribute.g.cs", SourceText.From(Attribute, Encoding.UTF8)));
// add static interface
context.RegisterPostInitializationOutput(ctx =>
ctx.AddSource("BigQueryMappedInterface.g.cs", SourceText.From(Interface, Encoding.UTF8)));
// get classes
IncrementalValuesProvider<ClassDeclarationSyntax> classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => s is ClassDeclarationSyntax c && c.AttributeLists.Any(),
transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx)
)
.Where(static m => m is not null)!;
IncrementalValueProvider<(Compilation Compilation, ImmutableArray<ClassDeclarationSyntax>Syntaxes)>
compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());
context.RegisterSourceOutput(compilationAndClasses,
static (spc, source) => Execute(source.Compilation, source.Syntaxes, spc));
static ClassDeclarationSyntax? GetSemanticTargetForGeneration(GeneratorSyntaxContext context)
{
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
foreach (var attributeListSyntax in classDeclarationSyntax.AttributeLists)
{
foreach (var attributeSyntax in attributeListSyntax.Attributes)
{
var fullName = context.SemanticModel.GetTypeInfo(attributeSyntax).Type?.ToDisplayString();
if (fullName == "BigQueryMapping.BigQueryMappedAttribute")
return classDeclarationSyntax;
}
}
return null;
}
static void Execute(Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes,
SourceProductionContext context)
{
try
{
if (classes.IsDefaultOrEmpty)
return;
var distinctClasses = classes.Distinct();
var classesToGenerate = GetTypesToGenerate(compilation, distinctClasses, context.CancellationToken);
foreach (var classToGenerate in classesToGenerate)
{
var result = GeneratePartialClass(classToGenerate);
context.AddSource($"{classToGenerate.RowClass.Name}.g.cs", SourceText.From(result, Encoding.UTF8));
}
}
catch (Exception e)
{
var descriptor = new DiagnosticDescriptor(id: "BQD001",
title: "Error creating bigquery mapper",
messageFormat: "{0} {1}",
category: "BigQueryMapperGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
context.ReportDiagnostic(Diagnostic.Create(descriptor, null, e.Message, e.StackTrace));
}
}
}
static IEnumerable<ClassToGenerate> GetTypesToGenerate(Compilation compilation,
IEnumerable<ClassDeclarationSyntax> classes,
CancellationToken ct)
{
var columnAttributeSymbol =
compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.Schema.ColumnAttribute");
foreach (var #class in classes)
{
Debug.WriteLine($"Checking class {#class}");
ct.ThrowIfCancellationRequested();
var semanticModel = compilation.GetSemanticModel(#class.SyntaxTree);
if (semanticModel.GetDeclaredSymbol(#class) is not INamedTypeSymbol classSymbol)
continue;
var info = new ClassToGenerate(classSymbol, new());
foreach (var member in classSymbol.GetMembers())
{
if (member is IPropertySymbol propertySymbol)
{
if (propertySymbol.DeclaredAccessibility == Accessibility.Public)
{
if (propertySymbol.SetMethod is not null)
{
var columnName = propertySymbol.Name;
var columnAttribute = propertySymbol.GetAttributes().FirstOrDefault(a =>
a.AttributeClass!.ToDisplayString() == "Column");
if (columnAttribute is not null)
{
if (!columnAttribute.ConstructorArguments.IsDefaultOrEmpty)
{
var nameArg = columnAttribute.ConstructorArguments.First();
if (nameArg.Value is string name)
{
columnName = name;
}
}
}
info.Properties.Add((columnName, propertySymbol));
}
}
}
}
yield return info;
}
}
static string GeneratePartialClass(ClassToGenerate c)
{
var sb = new StringBuilder();
sb.Append($#"// <auto-generated/>
namespace {c.RowClass.ContainingNamespace.ToDisplayString()}
{{
public partial class {c.RowClass.Name} : BigQueryMapping.IBigQueryGenerated<{c.RowClass.Name}>
{{
public static {c.RowClass.Name} FromBigQueryRow(Google.Cloud.BigQuery.V2.BigQueryRow row)
{{
return new {c.RowClass.Name}
{{");
foreach (var (columnName, property) in c.Properties)
{
// would like to check if key exists but don't see any sort of ContainsKey implemented on BigQueryRow
var tempName = $"___{property.Name}";
var basePropertyType = property.Type.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString();
if (basePropertyType.EndsWith("?"))
{
basePropertyType = basePropertyType.Substring(default, basePropertyType.Length - 1);
}
sb.Append($#"
{property.Name} = row[""{columnName}""] is {basePropertyType} {tempName} ? {tempName} : default,");
}
sb.Append($#"
}};
}}
}}
}}");
return sb.ToString();
}
private record struct ClassToGenerate(INamedTypeSymbol RowClass,
List<(string ColumnName, IPropertySymbol Property)> Properties);
public const string Attribute = /* lang=csharp */ #"// <auto-generated/>
namespace BigQueryMapping {
[System.AttributeUsage(System.AttributeTargets.Class)]
public class BigQueryMappedAttribute : System.Attribute
{
}
}";
public const string Interface = /* lang=csharp */ #"// <auto-generated/>
namespace BigQueryMapping {
public interface IBigQueryGenerated<TRow> {
static TRow FromBigQueryRow(Google.Cloud.BigQuery.V2.BigQueryRow row) => throw new System.NotImplementedException();
}
}";
}
I have tried this with both System.ComponentModel.DataAnnotations.Schema.ColumnAttribute and a custom attribute injected via context.RegisterPostInitializationOutput to similar results. I have also tried rewriting this to use ISourceGenerator instead of IIncrementalGenerator and gotten the same behavior. Am wondering what I need to do to get columnAttribute loading correctly.
Thanks for any help in advance
It's hard to psychic debug the code but this does jump out:
var columnAttribute = propertySymbol.GetAttributes().FirstOrDefault(a =>
a.AttributeClass!.ToDisplayString() == "Column");
I'd guess the class here would be the fully qualified name. You already fetched the ColumnAttribute type earlier, so what'd be even better is to compare the AttributeClass to that type rather than doing string checks like this.
As a semi-related comment, if you're looking for types/members that are annotated with a specific attribute, rather than doing it yourself we have SyntaxValueProvider.ForAttributeWithMetadataName which is pretty heavily optimized to reduce the performance impact on your Visual Studio. It requires 17.3 or higher, but as long as you're OK with that it'll generally help performance.

Getting assembly attributes with Roslyn

I want to be able to read some assembly attributes from the AssemblyInfo.cs file in an assembly using the Roslyn code analysis.
So given the following sample:
using System;
using System.Collections.Generic;
using System.Text;
[assembly: Helloworld.TestAttribute1("Test1")]
[assembly: Helloworld.TestAttribute1(TheValue = "Test1", IgnoreThis = "I dont want this one!")]
namespace Helloworld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public class TestAttribute1 : Attribute
{
public TestAttribute1()
{
}
public TestAttribute1(string theValue)
{
this.TheValue = theValue;
}
public string TheValue { get; set; }
public string IgnoreThis { get; set; }
}
}
I want to be able to extract the attribute of type TestAttribute1 and the value of defined property named TheValue.
This is defined twice in the example - first using the constructor parameter, and the other using a named parameter.
I have the following code:
static void Main(string[] args)
{
string cs = GetFile();
SyntaxTree tree = CSharpSyntaxTree.ParseText(cs);
var root = (CompilationUnitSyntax)tree.GetRoot();
var compilation = CSharpCompilation.Create("").AddSyntaxTrees(tree);
var model = compilation.GetSemanticModel(tree);
// get the attributes
AttributeSyntax attr1 = root.DescendantNodes()
.OfType<AttributeSyntax>().ToArray()[0];
AttributeSyntax attr2 = root.DescendantNodes()
.OfType<AttributeSyntax>().ToArray()[1];
var ex1 = attr1.ArgumentList.Arguments.FirstOrDefault().Expression as LiteralExpressionSyntax;
var str1 = ex1.GetText().ToString();
var ex2 = attr2.ArgumentList.Arguments.FirstOrDefault().Expression as LiteralExpressionSyntax;
var str2 = ex2.GetText().ToString();
}
Currently I am cheating a little by just hard coding locating the assembly attributes. Again hard coding the ArgumentList to get the first expression in there. This gets me the result for str1 and str2 to be \"Test1\"
Is there a way just to say, give me the attributes of type TestAttribute1 then say, give me the value of the property named TheValue?
You can achive that just try to get attributes from IAssemblySymbol,this symbol can be retrived from Compilation:
var attribute = compilation.Assembly.GetAttributes().FirstOrDefault(x => x.AttributeClass.ToString() == "Helloworld.TestAttribute1");
if(!(attribute is null))
{
var ctorArgs = attribute.ConstructorArguments;
var propArgs = attribute.NamedArguments;
}
ctorArgs and propArgs is a collection (propArgs is dictionary) of TypedConstant items and TypedConstant has property Value (or Values when it's array) that keeps the passed value as ctor argument or as property value. And finally, you just need to filter arguments that you are interesting using TypedConstant.Type.
This should look like the following:
SyntaxTree tree = CSharpSyntaxTree.ParseText(cs);
var root = (CompilationUnitSyntax)tree.GetRoot();
var compilation = CSharpCompilation.Create("test").AddSyntaxTrees(tree);
// get references to add
compilation = compilation.AddReferences(GetGlobalReferences());
var model = compilation.GetSemanticModel(tree);
var attrs = compilation.Assembly.GetAttributes().Where(x => x.AttributeClass.ToString() == "Helloworld.TestAttribute1");
foreach (var attr in attrs)
{
var ctorArgs = attr.ConstructorArguments;
var propArgs = attr.NamedArguments;
}
private static IEnumerable<MetadataReference> GetGlobalReferences()
{
var assemblies = new[]
{
typeof(System.Object).Assembly, //mscorlib
};
var refs = from a in assemblies
select MetadataReference.CreateFromFile(a.Location);
return refs.ToList();
}
In order to get the attributes of a given type, the following code will work:
Func<AttributeSyntax, bool> findAttribute = (a) =>
{
var typeInfo = model.GetTypeInfo(a).ConvertedType;
return typeInfo.Name == "TestAttribute1" && typeInfo.ContainingNamespace.Name == "Helloworld";
};
AttributeSyntax[] attrs = root.DescendantNodes()
.OfType<AttributeSyntax>()
.Where(findAttribute)
.ToArray();

How to find uses of a method in a second assembly?

I am trying to map out the dependency matrix for a collection of assemblies including what dependency methods are used where. The basic DLL dependency matrix was easy but I am finding it difficult to get the method mapping. The tool I have been using is jbevain MethodBaseRocks.cs.
The dependencies of the assembly I want to parse are being loaded according to AppDomain.CurrentDomain.GetAssemblies() but I am getting FileNotFoundException and ReflectionTypeLoadExceptions.
Is there a correct way to load referenced assemblies?
I have tried LoadFile, LoadFrom and ReflectionOnlyLoadFrom all with the same result.
How do I get the types for methods that use a references Type?
I can step around the ReflectionTypeLoadExceptions error with the answer from here but these methods are the exact ones I want to map
TestLibraryA.dll
namespace TestLibraryA
{
public class TestClassA
{
public int DoStuff(int a, int b)
{
return a + b;
}
}
}
TestLibaryB.dll
using TestLibraryA;
namespace TestLibraryB
{
public class TestClassB
{
public int DoStuffAgain()
{
TestClassA obj = new TestClassA();
int ans = obj.DoStuff(3, 5);
return ans;
}
public TestClassA DoOtherStuff()
{
TestClassA result = new TestClassA();
return result;
}
}
}
Parser Code Application
public List<string> GetMethods()
{
List<string> result = new List<string> { };
Assembly dependencyAssembly = Assembly.LoadFile("TestLibraryA.dll");
Assembly targetAssembly = Assembly.LoadFile("TestLibaryB.dll");
Type[] types = targetAssembly.GetTypes();
// ReflectionTypeLoadExceptions thrown if a dependency type is used
// NB Not demo'ed in this example
foreach(var type in types)
{
foreach(var method in type.GetMethods())
{
// With the above DoStuffAgain() method is returned but DoOtherStuff() is not
var instructions = MethodBodyReader.GetInstructions(method);
// FileNotFoundException thrown saying TestLibraryA.dll not loaded
// the line throwing the error is
// MethodBodyReader(method)
// this.body = method.GetMethodBody();
foreach (var instruction in instructions)
{
MethodInfo methodInfo = instruction.Operand as MethodInfo;
if (methodInfo != null)
{
result.Add(methodInfo.DeclaringType.FullName + "." + methodInfo.Name);
}
}
}
}
return result;
}
To avoid the exception you need to add the code below before you load TestLibaryB
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
if (args.Name == "TestLibaryA...")
{
return Assembly.LoadFrom("TestLibaryA's Path");
}
return null;
}

Loading of plugin from dll

I've started on a simple plugin loader that monitors a directory and loads plugins if the dll(s) in it contain the IPlugin interface.
public class PluginLoader : Dictionary<string, IPlugin>
{
private FileSystemWatcher watcher;
private string pluginPath;
public PluginLoader()
: base()
{
pluginPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "plugins");
if (!Directory.Exists(pluginPath))
Directory.CreateDirectory(pluginPath);
watcher = new FileSystemWatcher(pluginPath, "*.dll");
watcher.IncludeSubdirectories = true;
watcher.Created += watcher_Created;
watcher.EnableRaisingEvents = true;
}
private void watcher_Created(object sender, FileSystemEventArgs e)
{
LoadPlugin(e.FullPath);
}
private void LoadPlugin(string path)
{
IPlugin plugin = null;
Assembly assembly = Assembly.LoadFrom(path);
foreach (Type type in assembly.GetExportedTypes())
{
if (type.IsClass && type.GetInterfaces().Count(iType => iType == typeof(IPlugin)) == 1)
{
ConstructorInfo constructor = type.GetConstructor(new Type[] { });
object instance = constructor.Invoke(new object[] { });
plugin = instance as IPlugin;
// plugin is now not null
}
}
if (plugin != null && !this.ContainsKey(plugin.PluginName))
{
this[plugin.PluginName] = plugin;
}
}
}
This version of LoadPlugin() works, the plugin variable ends up being != null. This one, however, does not work:
private void LoadPlugin(string path)
{
IPlugin plugin = null;
Assembly assembly = Assembly.LoadFrom(path);
foreach (Type type in assembly.GetExportedTypes())
{
if (type.IsClass && type.GetInterface(typeof(IPlugin).FullName) != null)
{
ConstructorInfo constructor = type.GetConstructor(new Type[] { });
object instance = constructor.Invoke(new object[] { });
plugin = instance as IPlugin;
// plugin is still null
}
}
if (plugin != null && !this.ContainsKey(plugin.PluginName))
{
this[plugin.PluginName] = plugin;
}
}
I just don't understand why. So my question is: Why does plugin end up being null in the second example?
Solution:
Problem was that I had two different IPlugin types because its assembly existed in two different locations. So deleting the Framework.Lib.dll in the plugin directory solved it.
The only reason I can think of is that the type implements an IPlugin with the same namespace but from a different assembly. The typeof(IPlugin).FullName would then match between your plugin loader and the plugin, but the implemented type still does not equal the expected type.
The first example does match the exact same type, in the second example you're matching by the FullName which only includes the namespace, not the assembly a type is loaded from.
To determine whether this is the case, try log the following value:
bool matches = typeof(IPlugin).IsAssignableFrom(type);
string expected = typeof(IPlugin).AssemblyQualifiedName;
string actual = type.GetInterface(typeof(IPlugin).FullName).AssemblyQualifiedName;
typeof(IPlugin).IsAssignableFrom(type) is probably what you were looking for in the first place.

Creating an AppDomain and calling a method from an assembly in a subfolder

I have an example application that has a number of endpoints (c# classes) which use an interface which defines some methods. These endpoints are in their own class libraries.
In an assembly called "EndPoints"
namespace EndPoints
{
public interface IEndPoint
{
void Initialize(XmlDocument message);
bool Validate();
void Execute();
}
}
In an assembly called "EndPoints.EndPoint1"
namespace EndPoints
{
public class EndPoint1 : IEndPoint
{
private XmlDocument _message;
public void Initialize(XmlDocument message)
{
_message = message;
Console.WriteLine("Initialize EndPoint1");
}
public bool Validate()
{
Console.WriteLine("Validate EndPoint1");
return true;
}
public void Execute()
{
Console.WriteLine("Execute EndPoint1");
}
}
}
The application will "choose" an endpoint to use and then find the appropriate class, create an instance of it and then call the methods in turn.
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
// Generate "random" endpoint name
string endPointName = GetEndPointName();
// Get the class name from the namespaced class
string className = GetClassName(endPointName);
// Dummy xmldocument that used to pass into the end point
XmlDocument dummyXmlDocument = new XmlDocument();
// Load appropriate endpoint assembly because the application has no reference to it so the assembly would not have been loaded yet
LoadEndPointAssembly(endPointName);
// search currently loaded assemblies for that class
var classTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => p.FullName == endPointName)
.ToList();
// cycle through any found types (should be 1 only)
for (int i = 0; i < classTypes.Count; i++)
{
var classType = classTypes[i];
IEndPoint classInstance = Activator.CreateInstance(classType) as IEndPoint;
classInstance.Initialize(dummyXmlDocument);
if (classInstance.Validate())
{
classInstance.Execute();
}
}
}
private static void LoadEndPointAssembly(string endPointName)
{
using (StreamReader reader = new StreamReader(endPointName + ".dll", System.Text.Encoding.GetEncoding(1252), false))
{
byte[] b = new byte[reader.BaseStream.Length];
reader.BaseStream.Read(b, 0, System.Convert.ToInt32(reader.BaseStream.Length));
reader.Close();
AppDomain.CurrentDomain.Load(b);
}
}
private static string GetEndPointName()
{
// Code to create "random" endpoint class name
Random rand = new Random();
int randomEndPoint = rand.Next(1, 4);
return string.Format("EndPoints.EndPoint{0}", randomEndPoint);
}
private static string GetClassName(string namespacedClassName)
{
string className = null;
string[] components = namespacedClassName.Split('.');
if (components.Length > 0)
{
className = components[components.Length - 1];
}
return className;
}
}
}
I want to change the application to achieve the following;
- each endpoint assembly (and any config files and/or other assemblies that it uses) are contained in a subfolder below the application folder. the subfolder name would be the name of the endpoint class e.g. "EndPoint1"
- each endpoint runs in its own appdomain.
However, so far I've been unable to achieve this. I keep getting an exception stating its failed to load the appropriate assembly even though when I create the appdomain I specify the subfolder to be used by setting the ApplicationBase and PrivateBinPath properties of the AppDomainSetup; e.g.
AppDomain appDomain = null;
AppDomain root = AppDomain.CurrentDomain;
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = root.SetupInformation.ApplicationBase + className + #"\";
setup.PrivateBinPath = root.SetupInformation.ApplicationBase + className + #"\";
appDomain = AppDomain.CreateDomain(className, null, setup);
I've then been trying to use the Load method on the newly created appDomain to load the assembly. That's when I get the error.
Please does anyone have any thoughts about how I can load the appropriate assembly and call the methods defined in the interface? Many thanks.
I would do it in the following way. Firstly you need a class derived from MarshalByRef. It will be responsible for loading EndPoints and executing them in separate application domains. Here, I assume that it is defined in ConsoleApplication1 but it can be moved somewhere else:
public class EndPointLoader : MarshalByRefObject
{
public void Load(string path, string endPointName)
{
Assembly.LoadFrom(path);
var classTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => p.FullName == endPointName)
.ToList();
for (int i = 0; i < classTypes.Count; i++)
{
....
}
}
}
Here is a code that uses this class. You can put in in your LoadEndPointAssembly method.
var appDomain = AppDomain.CreateDomain(endPointName);
var loader = (EndPointLoader)appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(EndPointLoader).FullName);
loader.Load(assemblyPath, endPointName);

Categories