Reputation: 13716
I have a whole series of classes which look like
public class ReadVersionCommand : Command
{
public ReadVersionCommand() : base(0x00, 0x01, 0x00, null) { }
}
public class DisplayTextCommand : Command
{
public DisplayTextCommand(byte[] data) : base(0x05, 0x00, 0x00, data) { }
}
public class ReadKeyCommand : Command
{
public ReadKeyCommand(byte p3, byte[] data) : base(0x09, 0x00, p3, data) { }
}
I want to iterate over all these classes, and generate information based on the four parameters to the base Command
class (which I don't have control over). Ideally, I'd do this at runtime, so that we can add more subclasses to Command
and have them automatically show up the next time we run the code.
I know how to use reflection iterate all the classes in question.
I know how to take each Type
object and get the constructor.
I know how to take each ConstructorInfo
object and get the parameters passed to the constructor, both the types and the names. I need to differentiate between a constructor which has one byte p2
parameter and one which has one byte p3
, and I can do that.
I know how to get the base Command
class's constructor, and list the types and names (byte p1, byte p2, byte p3, byte[] data
).
If there were any code in the body of each constructor, I know how to get it with GetMethodBody()
.
However, I can't find any way to tell that each constructor is actually calling the base(byte, byte, byte, byte[])
constructor, and I can't find any way to see what the static values which are being passed to are. The values themselves are "magic" values which mean things to the underlying class, but only in combination. (i.e. 0x00, 0x01, 0x00
means one thing, and 0x01, 0x00, 0x00
means something very different.)
How can I get the values passed to the base constructor using reflection?
Upvotes: 1
Views: 432
Reputation: 13716
As was suggested in the comments, I ended up just creating an instance of the object then querying the set parameters.
Effectively:
ctor = type.GetConstructor();
parameters = ctor.GetParameters();
foreach (p in parameters)
{
// Mark that we have this parameter
}
// Construct array of parameters, using garbage values.
ctor.Invoke(callingParameters);
// For each parameter we didn't have, read the value.
It's ugly, and I'm not proud of it, but it works for my purposes.
Note: Even though this is the answer I went with this time, I'm going to accept M.Stramm's answer instead. It's definitely a better answer than this, and if I ever need to do this again, I will be using that solution instead.
Upvotes: 1
Reputation: 1309
First off, the obvious answer is that you are asking for the wrong thing. You should use attributes on your derived classes and query for their contents. Something like
[Magic(0xDE, 0xAD, 0xBE, 0xEF)]
public class ReadVersionCommand {}
Now that that's out of the way, to answer your stated problem 100%** you can use the Nuget Package ICSharpCode.Decompiler
which powers ILSpy to do some runtime decompilation and give you exactly what you asked for.
Because it's a fiddly bit of work to make it run, I did that for you.
Output:
public ReadVersionCommand ();
If you use this ReadVersionCommand constructor, then the first parameter to Command's constructor will be 19
public ReadVersionCommand (byte b2);
If you use this ReadVersionCommand constructor, then the first parameter to Command's constructor will be 5
public ReadVersionCommand (byte b1, byte b2);
If you use this ReadVersionCommand constructor, then the first parameter to Command's constructor will be b1
Code:
namespace ConsoleApp1 {
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Ast;
using ICSharpCode.NRefactory.CSharp;
using Mono.Cecil;
class Program {
static void Main(string[] args) {
var path = Assembly.GetExecutingAssembly().Location;
var assembly = AssemblyDefinition.ReadAssembly(path);
var types = assembly.Modules.SelectMany(x => x.Types).ToList();
var baseType = types.FirstOrDefault(x => x.FullName == typeof(Command).FullName);
var derivedTypes = types.Where(x => x.BaseType == baseType);
var ctx = new DecompilerContext(assembly.MainModule);
foreach (var type in derivedTypes) {
var astBuilder = new AstBuilder(ctx);
astBuilder.AddType(type);
var ast = astBuilder.SyntaxTree;
var ctorDecls = ast.Descendants.OfType<ConstructorDeclaration>();
var descriptors = ctorDecls.Select(ctor => Describe(type, ctor));
foreach (var desc in descriptors) {
var firstParameter = desc.BaseCallParameters.FirstOrDefault();
Console.WriteLine(desc.Signature);
Console.WriteLine($"If you use this {desc.Type.Name} constructor, then the first parameter to {baseType.Name}'s constructor will be {firstParameter}");
Console.WriteLine();
}
Console.ReadLine();
}
}
private static string GetPrettyCtorName(ConstructorDeclaration ctor) {
var copy = ctor.Clone();
var blocks = copy.Children.OfType<BlockStatement>().ToList();
foreach (var block in blocks) {
block.Remove();
}
return copy.ToString().Replace(Environment.NewLine, "");
}
private static ConstructorDescriptor Describe(TypeDefinition type, ConstructorDeclaration ctor) {
return new ConstructorDescriptor {
Type = type,
Signature = GetPrettyCtorName(ctor),
BaseCallParameters =
ctor
.Descendants
.OfType<MemberReferenceExpression>()
.Where(y => y.ToString() == "base..ctor")
.Select(y => y.Parent)
.FirstOrDefault()
?.Children
.Skip(1)
};
}
}
public class ConstructorDescriptor {
public TypeDefinition Type { get; set; }
public string Signature { get; set; }
public IEnumerable<AstNode> BaseCallParameters { get; set; }
}
public class Command {
public Command(byte b1, byte b2, byte b3, byte[] data) { }
}
public class ReadVersionCommand : Command {
public ReadVersionCommand() : base(0x13, 0x37, 0x48, null) { }
public ReadVersionCommand(byte b2) : base(0x05, b2, 0x00, null) { }
public ReadVersionCommand(byte b1, byte b2) : base(b1, b2, 0x00, null) { }
}
}
** Well, more like 90% since the code does not use Reflection. You could however achieve the same by parsing the IL in the MethodBody of the ctor to get the parameters.
Upvotes: 2
Reputation: 1501
Reflection doesn't let you inspect the actual code inside a method in a helpful way - see this answer regarding reflection: Can I use reflection to inspect the code in a method?.
But here's an approach that should work. It uses reflection to find all the subclasses, and then retrieves the data you want from a "Prototype" instance of each class.
public abstract class Command
{
// define a public property for each element you want to query
public byte Data { get; }
public Command(byte data)
{
Data = data;
}
}
public class Command1 : Command
{
// Require each subclass to define a static "prototype" instance,
// calling the constructor with default values for any args
public static Command Prototype = new Command1();
public Command1() : base(0x12)
{
}
}
[TestFixture]
public class ReflectionTest
{
[Test]
public static void ListPrototypes()
{
// find all loaded subclasses of Command
var subclasses =
from assembly in AppDomain.CurrentDomain.GetAssemblies()
from type in assembly.GetTypes()
where type.IsSubclassOf(typeof(Command))
select type;
foreach (var subclass in subclasses)
{
// get the prototype instance of each class
var prototype =
subclass.GetField("Prototype", BindingFlags.Public | BindingFlags.Static)?.GetValue(null) as Command;
if (prototype != null)
{
// emit the data from the prototype
Console.WriteLine($"{subclass.Name}, Data={prototype.Data}");
}
}
}
}
Upvotes: 1