Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Access object value from parameter attribute in C#

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.

like image 795
Node.JS Avatar asked Dec 01 '22 14:12

Node.JS


2 Answers

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.

like image 67
Jon Skeet Avatar answered Dec 10 '22 04:12

Jon Skeet


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();
  }
}
like image 42
Aleks Andreev Avatar answered Dec 10 '22 04:12

Aleks Andreev