2

I've created a custom property in C#:

[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public class ExampleAttribute : Attribute
{
}

I would like this to ONLY be applied to properties, but more specifically, properties that are not classes (just base types).

Example:

public class ExamplePoco
{
    // These should be fine...
    [Example]
    public string Prop1 { get; set; }
    [Example]
    public bool Prop2 { get; set; }
    [Example]
    public int Prop3 { get; set; }

    // But this shouldn't be allowed because it's a class, rather than base type
    [Example]
    public OtherExample Prop4 { get; set; }
}

public class OtherExample
{
    public string Other1 { get; set; }
}

Does anyone know how I'd further restrict this custom attribute at compile time? If this can only be done at runtime, what is the best way to approach this?

Thanks in advance!

Rob
  • 6,819
  • 17
  • 71
  • 131

1 Answers1

1

There is a limit to how much you can enforce at compile time with straight C#. As has been pointed out in the comments, you can write a custom Roslyn extension to look for invalid usage. That's a fairly heavy solution - most people tend to implement validation of these sorts of things at runtime, not at compile time.

Some examples that come to mind are Newtonsoft's JsonConverter attribute, which requires that the type in the constructor implements a specific interface, or Asp.Net's Route attribute, which has constraints on syntax and ambiguity. These things could be implemented as a Roslyn extension, but the most common convention is to validate at runtime.

Eager Validation is where you declare all the the types (or a convention) that need to be validated, and the validation is done straight away. Some ways to declare the types for this include:

  • Pass in an explicit list of Types to the Validate method
  • Make an interface, pass in an assembly, and scan the assembly for types that implement the interface
  • Make an attribute, pass in an assembly, and scan the assembly for types that are annotated with it

Lazy validation is where you only validate a type when it is used. This has the advantage of faster startup times, but it also means any types that have invalid attribute usage will not be detected until they are used.

Validation uses reflection, and can be quite a performance hit, so if you do decide to do lazy validation, you should definitely cache the results.

In order to validate if a type has incorrect attribute usage, you could make a method like the following:

private static readonly Type[] PrimitiveTypes = new Type[]
{
    typeof(string),
    typeof(int),
    typeof(bool),
    typeof(int?), // Are nullable primitive types allowed? You decide
}

public static void Validate(Type type)
{
    var properties = type.GetProperties();
    foreach (var property in properties)
    {
        var attribute = property.GetCustomAttribute<ExampleAttribute>();
        if (attribute == null)
            continue;

        if (!Array.Contains(PrimitiveTypes, property.PropertyType))
            throw new Exception("Make a custom exception type and message for this scenario");
    }
}

In this example, I'm throwing an exception if the validation fails, just to keep it simple. It can be helpful for debugging if instead you make a list of validation errors, and return them, so the user can see all the errors instead of just the first one.

Andrew Williamson
  • 8,299
  • 3
  • 34
  • 62