1

Generic Attributes in C#

 1 year ago
source link: https://code-maze.com/csharp-generic-attributes/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Generic Attributes in C#

Posted by Code Maze | Updated Date Jan 11, 2023 | 0

Code Maze Book Collection

Want to build great APIs? Or become even better at it? Check our Ultimate ASP.NET Core Web API program and learn how to create a full production-ready ASP.NET Core API using only the latest .NET technologies. Bonus materials (Security book, Docker book, and other bonus files) are included in the Premium package!

In this article, we are going to learn how generic attributes help us implement better custom attributes through a practical example.

To download the source code for this article, you can visit our GitHub repository.

While we go in-depth in this article with generic attributes, we do have introductory articles on Generics and Custom Attributes if these are new topics to you.

Implementing Generic Attributes the Old Way

Before C#11, to implement attributes in a type-agnostic way, we used to take advantage of System.Type class to pass the type information via the constructor parameter. With this approach, the parameter could be of any type and we had no control over it.

So, let’s spin up a C# console application, and create a vehicle validation attribute, that can accept a type that implements validation for vehicles:

[AttributeUsage(AttributeTargets.Class)]
public class VehicleValidatorAttribute : Attribute
private Type _vehicleValidatorType;
public VehicleValidatorAttribute(Type vehicleValidatorType)
_vehicleValidatorType = vehicleValidatorType;
public Type VehicleValidatorType => _vehicleValidatorType;
[AttributeUsage(AttributeTargets.Class)]
public class VehicleValidatorAttribute : Attribute
{
    private Type _vehicleValidatorType;

    public VehicleValidatorAttribute(Type vehicleValidatorType)
    {
        _vehicleValidatorType = vehicleValidatorType;
    }

    public Type VehicleValidatorType => _vehicleValidatorType;
}

Here we accept the Type of the class that implements the validation as a constructor parameter and the VehicleValidatorType property is exposed to get the validator type. The issue with this type of implementation is that we can pass any type to the constructor.

Now, let’s assume that we use this attribute to create an instance of a VehicleValidatorType using reflection and call IsValid() method of IVehicleValidator interface:

public static IVehicleValidator<T>? GetVehicleValidator<T>() where T : class
var modelType = typeof(T);
var validatorAttr = modelType
.GetCustomAttribute(typeof(VehicleValidatorAttribute)) as VehicleValidatorAttribute;
if (validatorAttr is not null)
var validatorType = validatorAttr.VehicleValidatorType;
var vehicleValidator = Activator.CreateInstance(validatorType) as IVehicleValidator<T>;
return vehicleValidator;
return null;
public static IVehicleValidator<T>? GetVehicleValidator<T>() where T : class
{
    var modelType = typeof(T);

    var validatorAttr = modelType
        .GetCustomAttribute(typeof(VehicleValidatorAttribute)) as VehicleValidatorAttribute;

    if (validatorAttr is not null)
    {
        var validatorType = validatorAttr.VehicleValidatorType;
        var vehicleValidator = Activator.CreateInstance(validatorType) as IVehicleValidator<T>;

        return vehicleValidator;
    }

    return null;
}

If the passed-in vehicle validator type implements the IsValid() method of IVehicleValidator interface, all is well.

But what happens if we pass a type that doesn’t implement the IsValid() method? Yes, it will fail during runtime, trying to call the validation method on the null value returned.

Using C# 11 Generic Attributes

Let’s now use C# 11 generic attributes to implement the VehicleValidator attribute:

[AttributeUsage(AttributeTargets.Class)]
public class VehicleValidatorAttribute<T> : Attribute
where T : class
[AttributeUsage(AttributeTargets.Class)]
public class VehicleValidatorAttribute<T> : Attribute
    where T : class
{
}

Now, to get the type information, we can use the generic parameter. We don’t need to use unwanted properties and constructor parameters as we did earlier.

Advantages of Using Generic Attributes

Let’s have a look at the several benefits generic attributes bring.

C# Style Syntax

The attribute usage syntax is now more close to how C# syntax is. If we take the example of the VehicleValidator attribute, before C# 11 we would need to pass the type parameter using the typeof operator:

[VehicleValidator(typeof(CarValidator))]
[VehicleValidator(typeof(CarValidator))]

Compared to this, we now have the syntax in line with C# generic style. It’s arguably cleaner and more understandable to C# developers:

[VehicleValidator<CarValidator>]
[VehicleValidator<CarValidator>]
Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Type Restriction

The option to specify generic type constraints along with attributes prevents the unintended usage of types with the attributes. If we use types that don’t pass the generic constraint, we will get a compile-time error:

Type restriction on generic attributes

This is a major improvement to minimize the number of runtime errors with attributes.

Restrictions With Generic Attributes

It’s also important to keep in mind some of the restrictions with generic attributes as of C# 11.

Restriction on Type

Generic attributes restrict the usage of a few types as type parameters:

  • dynamic type
  • Nullable reference types like string?
  • Tuples using C# style syntax like (int x, int y)

The impermissible ones are the types that need metadata annotations. Although, we could use an alternative type:

  • object instead of dynamic
  • string for string?
  • ValueTuple<int, int> instead of (int x, int y)

Fully Closed Construction

When using a generic feature, the type we use needs to be fully specified and constructed. That means we cannot use the type parameter from the enclosing type if the enclosing type itself is generic:

public class GenericAttribute<T> : Attribute { }
public class GenericType<T>
[GenericAttribute<T>] // Invalid. Compile time error.
public string Method1() => default;
[GenericAttribute<string>] // Valid
public string Method2() => default;
public class GenericAttribute<T> : Attribute { }

public class GenericType<T>
{
    [GenericAttribute<T>] // Invalid. Compile time error.
    public string Method1() => default;

    [GenericAttribute<string>] // Valid
    public string Method2() => default;
}

We get a compile-time error when we use the type parameter from the class with the generic attribute of Method1. On the other hand, using an allowed type directly is valid.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

A Practical Example of Generic Attribute

Let’s implement a generic vehicle validator that validates vehicles by some logic. We will build up from the VehicleValidator attribute we created earlier.

First, let’s create a generic interface to define a contract for our validators:

public interface IVehicleValidator<T>
where T : class
bool IsValid(T entity);
public interface IVehicleValidator<T>
    where T : class
{
    bool IsValid(T entity);
}

Secondly, let’s add the models for the Vehicles:

public class Car
public bool IsConvertible { get; set; }
public class Truck
public string? LoadCapacity { get; set; }
public class Car
{
    public bool IsConvertible { get; set; }
}

public class Truck
{
    public string? LoadCapacity { get; set; }
}

After that, we are going to define a validator for a specific vehicle. For instance, a CarValidator deriving from IVehicleValidator:

public class CarValidator : IVehicleValidator<Car>
public bool IsValid(Car car) => car.IsConvertible;
public class CarValidator : IVehicleValidator<Car>
{
    public bool IsValid(Car car) => car.IsConvertible;
}

Here we define the validation to be successful if the Car is a convertible type car.

In the next step, let’s modify the vehicle validation attribute we created earlier to specify our restrictions. We can do that based on the IVehicleValidator interface:

[AttributeUsage(AttributeTargets.Class)]
public class VehicleValidatorAttribute<T, TEntity> : Attribute
where T : class, IVehicleValidator<TEntity>
where TEntity : class
[AttributeUsage(AttributeTargets.Class)]
public class VehicleValidatorAttribute<T, TEntity> : Attribute
where T : class, IVehicleValidator<TEntity>
where TEntity : class
{
}

We specify that the vehicle entities must be a class and the validator should be an IVehicleValidator of a specific vehicle type like a Car. This is where we take advantage of generic attributes in C#11 to restrict types using constraints.

Finally, we need a ValidationHelper class that exposes a method to get the validator for the type passed in:

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<
public static class ValidationHelper
public static IVehicleValidator<T>? GetValidator<T>()
where T : class
var modelType = typeof(T);
var validatorAttr = modelType
.GetCustomAttribute(typeof(VehicleValidatorAttribute<,>));
if (validatorAttr is not null)
var validatorType = validatorAttr
.GetType()
.GetGenericArguments()
.First();
return Activator.CreateInstance(validatorType) as IVehicleValidator<T>;
return null;
public static class ValidationHelper
{
    public static IVehicleValidator<T>? GetValidator<T>()
        where T : class
    {
        var modelType = typeof(T);

        var validatorAttr = modelType
            .GetCustomAttribute(typeof(VehicleValidatorAttribute<,>));

        if (validatorAttr is not null)
        {
            var validatorType = validatorAttr
                .GetType()
                .GetGenericArguments()
                .First();

            return Activator.CreateInstance(validatorType) as IVehicleValidator<T>;
        }

        return null;
    }
}

We use reflection code to get the model type like Car or Truck. Then, we find the attribute of the VehicleValidator type using that model. Afterward, we take the first generic type argument from the attribute for creating the vehicle validator instance.

To see our code in action, let’s fetch the validator for Car and perform the validation:

public static void Main()
var carValidator = ValidationHelper.GetValidator<Car>();
var car = new Car()
IsConvertible = true
Console.WriteLine(carValidator?.IsValid(car));
public static void Main()
{
    var carValidator = ValidationHelper.GetValidator<Car>();
    var car = new Car()
    {
        IsConvertible = true
    };

    Console.WriteLine(carValidator?.IsValid(car));
}

As we create a convertible Car, the validation will be successful and the console output will be true.

Conclusion

In this article, we’ve learned about generic attributes in C# along with the benefits and limitations. Also, we saw them in action with our hands-on exercise and how it helps enforce type-safe attribute implementations. Finally, we’ve used generic constraints to specify conditions for allowed types in a generic attribute.

Code Maze Book Collection

Want to build great APIs? Or become even better at it? Check our Ultimate ASP.NET Core Web API program and learn how to create a full production-ready ASP.NET Core API using only the latest .NET technologies. Bonus materials (Security book, Docker book, and other bonus files) are included in the Premium package!

Share:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK