Home > Enterprise >  Is it possible to contraint a generic parameter type to this?
Is it possible to contraint a generic parameter type to this?

Time:05-18

Short version

How do I force the BaseClass's TModel generic parameter to be of the same type as the class that derives from it?

public class BaseClass<TModel, TValidator> where TValidator : IValidator<TModel> { }

public class Person : BaseClass<Person, PersonValidator> { }

In this example, how do I force the BaseClass's TModel to be of type Person and not something else?

This is invalid syntax, but it's what I'm imagining:

public class BaseClass<TValidator> where TValidator : IValidator<this> { }

public class Person : BaseClass<PersonValidator> { }

Is this somehow possible or should I use a totally different solution to achieve this?

Long version

I'm trying to extract some validation logic into a base class, but I don't know how to constraint the generic types so the resulting base class is fully fool-proof.

Here's an example of what all the validation logic looks like without a base class. I'm using FluentValidation to validate the object and I'm exposing that validation result via the IDataErrorInfo interface so it can be used by the WPF UI.

Original solution

public class User : IDataErrorInfo 
{
    private readonly IValidator<Person> _validator = new();

    public string Username { get; set; }
    public string Password { get; set; }

    private string ValidateAndGetErrorForProperty(string propertyName)
    {
        var result = _validator.Validate(this);

        if (result.IsValid)
        {
            return string.Empty;
        }

        return result.Errors.FirstOrDefault(a => a.PropertyName == propertyName)?.ErrorMessage ?? string.Empty;
    }

    //IDataErrorInfo implementation
    public string Error => string.Empty;
    public string this[string columnName] => ValidateAndGetErrorForProperty(columnName);
}

public class UserValidator : AbstractValidator<User>
{
    public UserValidator()
    {
        RuleFor(a => a.Username)
            .EmailAddress();

        RuleFor(a => a.Password)
            .MinimumLength(12);
    }
}

Validation implementation separated into a base class

I'd like to separate the validation logic and IDataErrorInfo implementation into a base class so this boilerplate doesn't have to be repeated in every model class. Here's what I have.

public abstract class ValidationBase<TModel, TValidator> : IDataErrorInfo where TValidator : IValidator<TModel>, new()
{
    private readonly TValidator _validator;

    public ValidationBase()
    {
        _validator = Activator.CreateInstance<TValidator>();
    }

    private string ValidateAndGetErrorForProperty(string propertyName)
    {
        //I have to check if this is of type TModel since the TModel isn't constraint to this
        if (this is not TModel model)
        {
            throw new InvalidOperationException($"Instance is not of the supported type: {typeof(TModel)}. Type of {GetType()} found instead");
        }

        var result = _validator.Validate(model);

        if (result.IsValid)
        {
            return string.Empty;
        }

        return result.Errors.FirstOrDefault(a => a.PropertyName == propertyName)?.ErrorMessage ?? string.Empty;
    }

    //IDataErrorInfo implementation
    public string Error => string.Empty;
    public string this[string columnName] => ValidateAndGetErrorForProperty(columnName);
}

And here's how I'm using it:

public class User : ValidationBase<User, UserValidator>
{
    public string Username { get; set; }
    public string Password { get; set; }
}

The problem

The problem I have with this solution is that you can write this invalid code:

public class InvalidClass : ValidationBase<User, UserValidator>
{
    
}

CodePudding user response:

Is this what you are looking for?

public interface IValidator<TModel>
{
}

public class BaseClass<TModel, TValidator> 
    where TModel : BaseClass<TModel, TValidator> 
    where TValidator 
    : IValidator<TModel> { }

// Only classes derived from BaseClass can be instantiated
public class Person 
    : BaseClass<Person, PersonValidator> { }

public class PersonValidator 
    : IValidator<Person>
{
}

This is a classic pattern where a generic parameter is constrained to the derived class.

  • Related