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.