You can achive this with Mono.Cecil library
In your main project
Define a common interface for all your validators:
public interface IArgumentValidator
{
void Validate(object argument);
}
Now, rewrite your ValidateMetaFieldsAttribute
like this:
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class ValidateMetaFieldsAttribute : Attribute, IArgumentValidator
{
public void Validate(object argument)
{
//todo: put your validation logic here
}
}
Create your own IL-Rewriter
Create another console application, add Mono.Cecil as nuget.
Open your main project:
var assembly = AssemblyDefinition.ReadAssembly(@"ClassLibrary1.dll"); // your project assembly
var module = assembly.MainModule;
Locate IArgumentValidator
and all descendant validators:
var validatorInterface = module.Types
.FirstOrDefault(t => t.IsInterface && t.Name == "IArgumentValidator");
var validators = module.Types
.Where(t => t.Interfaces.Contains(validatorInterface)).ToArray();
Then you need to find all types, where validators are used:
var typesToPatch = module.Types.Select(t => new
{
Type = t,
Validators =
t.Methods.SelectMany(
m => m.Parameters.SelectMany(
p => p.CustomAttributes.Select(a => a.AttributeType)))
.Distinct()
.ToArray()
})
.Where(x => x.Validators.Any(v => validators.Contains(v)))
.ToArray();
Now in each found type you need to add all validators used in this type (as fields)
foreach (var typeAndValidators in typesToPatch)
{
var type = typeAndValidators.Type;
var newFields = new Dictionary<TypeReference, FieldDefinition>();
const string namePrefix = "e47bc57b_"; // part of guid
foreach (var validator in typeAndValidators.Validators)
{
var fieldName = $"{namePrefix}{validator.Name}";
var fieldDefinition = new FieldDefinition(fieldName, FieldAttributes.Private, validator);
type.Fields.Add(fieldDefinition);
newFields.Add(validator, fieldDefinition);
}
At the moment all new fields are null, so they should be initialized. I've put initialization in a new method:
var initFields = new MethodDefinition($"{namePrefix}InitFields", MethodAttributes.Private, module.TypeSystem.Void);
foreach (var field in newFields)
{
initFields.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
initFields.Body.Instructions.Add(Instruction.Create(OpCodes.Newobj, field.Key.Resolve().GetConstructors().First()));
initFields.Body.Instructions.Add(Instruction.Create(OpCodes.Stfld, field.Value));
}
initFields.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
type.Methods.Add(initFields);
But this is not enough because this method never called. To fix this you also need to patch all constructors of current type:
var ctors = type.GetConstructors().ToArray();
var rootCtors = ctors.Where(c =>
!c.Body.Instructions.Any(i => i.OpCode == OpCodes.Call
&& ctors.Except(new []{c}).Any(c2 => c2.Equals(i.Operand)))).ToArray();
foreach (var ctor in rootCtors)
{
var retIdx = ctor.Body.Instructions.Count - 1;
ctor.Body.Instructions.Insert(retIdx, Instruction.Create(OpCodes.Ldarg_0));
ctor.Body.Instructions.Insert(retIdx + 1, Instruction.Create(OpCodes.Call, initFields));
}
(Some tricky part here is rootCtors
. As I say before you can patch all constructors, however it's not nessesary, because some constructors may call other)
Last thing we need to do with current type is to patch each method that have our validators
foreach (var method in type.Methods)
{
foreach (var parameter in method.Parameters)
{
foreach (var attribute in parameter.CustomAttributes)
{
if (!validators.Contains(attribute.AttributeType))
continue;
var field = newFields[attribute.AttributeType];
var validationMethod = field.FieldType.Resolve().Methods.First(m => m.Name == "Validate");
method.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Ldarg_0));
method.Body.Instructions.Insert(1, Instruction.Create(OpCodes.Ldfld, field));
method.Body.Instructions.Insert(2, Instruction.Create(OpCodes.Ldarg_S, parameter));
method.Body.Instructions.Insert(3, Instruction.Create(OpCodes.Callvirt, validationMethod));
}
}
}
After all types are patched you can save modified assembly
assembly.Write("PatchedAssembly.dll");
You can find all this code as single file here
Example of work (from dotPeek)
Source class:
public class Demo
{
public Component SaveComponent([ValidateMetaFields] Component componentToSave)
{
return componentToSave;
}
}
Patched class:
public class Demo
{
private ValidateMetaFieldsAttribute e47bc57b_ValidateMetaFieldsAttribute;
public Component SaveComponent([ValidateMetaFields] Component componentToSave)
{
this.e47bc57b_ValidateMetaFieldsAttribute.Validate((object) componentToSave);
return componentToSave;
}
public Demo()
{
this.e47bc57b_InitFields();
}
private void e47bc57b_InitFields()
{
this.e47bc57b_ValidateMetaFieldsAttribute = new ValidateMetaFieldsAttribute();
}
}