This is my method
public Component SaveComponent([ValidateMetaFields] Component componentToSave) {
...
}
This is my custom attribute:
[AttributeUsage(AttributeTargets.Parameter)]
public class ValidateMetaFieldsAttribute : Attribute
{
public ValidateMetaFieldsAttribute()
{
// how to access `componentToSave` here?
}
}
I am wondering is there a way to access componentToSave
object from ValidateMetaFieldsAttribute
? I could not find any sample code or examples.
No, attribute instances don't have any notion of the target they're applied to.
Note that normally you fetch attributes from a target, so whatever's doing that fetching could potentially supply the information to whatever comes next. Potentially slightly annoying, but hopefully not infeasible.
One small exception to all of this is caller info attributes - if you use something like
[AttributeUsage(AttributeTargets.Parameter)]
public class ValidateMetaFieldsAttribute : Attribute
{
public ValidateMetaFieldsAttribute([CallerMemberName] string member = null)
{
...
}
}
... then the compiler will fill in the method name (SaveComponent
) in this case, even though the attribute is applied to the parameter. Likewise you can get at the file path and line number.
Given this comment about the purpose, however, I think you've got a bigger problem:
To validate componentToSave and throw an exception before method body even runs.
The code in the attribute constructor will only be executed if the attribute is fetched. It's not executed on each method call, for example. This may well make whatever you're expecting infeasible.
You may want to look into AOP instead, e.g. with PostSharp.
You can achive this with Mono.Cecil library
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 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
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();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With