Home > Software engineering >  Prevent multicasting in generic method
Prevent multicasting in generic method

Time:08-24

I am making generic validators for checking input:

Interface:

public interface IInputValidator
{
    bool CanHandle<T>();
    bool Validate<T>(string? input, out T result);
}

Implementation:

public class IntegerValidator : IInputValidator
{
    public bool CanHandle<T>()
    {
        return typeof(T) == typeof(int);
    }

    public bool Validate<T>(string? input, out T result)
    {
        var isValid = int.TryParse(input, out var res);
        result = (T)(object)res;
        return isValid;
    }
}

Then I grab all the validators I have and inject like so: (it feels convenient that the interface itself is not generic thus I don't have to inject them one by one and able to group them in a single collection)

private readonly IEnumerable<IInputValidator> _inputValidators;

public CallerClass(IEnumerable<IInputValidator> inputValidators)
{
   _inputValidators = inputValidators;
}

And call it like:

var validator = _inputValidators.First(r => r.CanHandle<int>());
var isInputValid = validator.Validate(userInput, out int id);

It all looks fine except for this line in implementation

result = (T)(object)res;

I feel like something is wrong here but can't figure out how to make it better. It works like this though.

CodePudding user response:

The core issue is that you are trying to combine the resolution of the appropriate validator and the action of that validator into the same generic interface.

If you are willing to separate the resolver and validator functionality into two interfaces:

public interface IInputValidatorResolver
{
    bool CanHandle<T>();
    IInputValidator<T> GetValidator<T>();
}

public interface IInputValidator<T>
{
    bool Validate(string? input, out T result);
}

you can work with IInputValidatorResolver instances in your CallerClass contract to instead resolve the appropriate validator and strongly-type you call to the validator without an object cast. The resolver implementations can create a cache that casts your Validator to a generic IInputValidator<T> instance.

public class IntegerValidatorResolver : IInputValidatorResolver
{
    public bool CanHandle<T>() => typeof(T) == typeof(int);
    public IInputValidator<T> GetValidator<T>() => Cache<T>.Validator;
    
    private static class Cache<T>
    {
        public static readonly IInputValidator<T> Validator = BuildValidator();
        private static IInputValidator<T> BuildValidator() => ((IInputValidator<T>)new IntegerValidator());
    }
}

public class LongValidatorResolver : IInputValidatorResolver
{
    public bool CanHandle<T>() => typeof(T) == typeof(long);
    public IInputValidator<T> GetValidator<T>() => Cache<T>.Validator;

    private static class Cache<T>
    {
        public static readonly IInputValidator<T> Validator = BuildValidator();
        private static IInputValidator<T> BuildValidator() => ((IInputValidator<T>)new LongValidator());
    }
}

public class IntegerValidator : IInputValidator<int>
{
    public bool Validate(string? input, out int result) => int.TryParse(input, out result);
}

public class LongValidator : IInputValidator<long>
{
    public bool Validate(string? input, out long result) => long.TryParse(input, out result);
}

and you can test it with the following:

IEnumerable<IInputValidatorResolver> validatorResolvers = new List<IInputValidatorResolver> { new IntegerValidatorResolver(), new LongValidatorResolver() };
var intValidator = validatorResolvers.First(x => x.CanHandle<int>()).GetValidator<int>();

var isIntValid = intValidator.Validate(long.MaxValue.ToString(), out int intResult);
Console.WriteLine(isIntValid);
Console.WriteLine(intResult);

var longValidator = validatorResolvers.First(x => x.CanHandle<long>()).GetValidator<long>();

var isLongValid = longValidator.Validate(long.MaxValue.ToString(), out long longResult);
Console.WriteLine(isLongValid);
Console.WriteLine(longResult);

That said, this creates an awkward contract where if you do NOT perform a check with CanHandle<T> first, your call to GetValidator<T> can throw an exception. In addition, in either this implementation or your current implementation, you have to loop through resolvers/validators to find the appropriate instance, which is unnecessarily wasteful.

As a result, it may make more sense to have a single IInputValidatorResolver instance that knows how to resolve the appropriate validator based on the type of T, without a CanHandle<T>() check.

public interface IInputValidatorResolver
{
    IInputValidator<T> GetValidator<T>();
}

public class ValidatorResolver : IInputValidatorResolver
{
    public IInputValidator<T> GetValidator<T>() => Cache<T>.Validator;

    private static class Cache<T>
    {
        public static readonly IInputValidator<T> Validator = BuildValidator();
        private static IInputValidator<T> BuildValidator()
        {
            if (typeof(T) == typeof(int))
            {
                return ((IInputValidator<T>)new IntegerValidator());
            }
            else if (typeof(T) == typeof(long))
            {
                return ((IInputValidator<T>)new LongValidator());
            }
            else
            {
                throw new ArgumentException($"{typeof(T).FullName} does not have a registered validator.");
            }
        }
    }
}

public interface IInputValidator<T>
{
    bool Validate(string? input, out T result);
}

public class IntegerValidator : IInputValidator<int>
{
    public bool Validate(string? input, out int result) => int.TryParse(input, out result);
}

public class LongValidator : IInputValidator<long>
{
    public bool Validate(string? input, out long result) => long.TryParse(input, out result);
}

This allows for a much cleaner API and far less registration and enumeration:

IInputValidatorResolver resolver = new ValidatorResolver();
var intValidator = resolver.GetValidator<int>();

var isIntValid = intValidator.Validate(long.MaxValue.ToString(), out int intResult);
Console.WriteLine(isIntValid);
Console.WriteLine(intResult);

var longValidator = resolver.GetValidator<long>();

var isLongValid = longValidator.Validate(long.MaxValue.ToString(), out long longResult);
Console.WriteLine(isLongValid);
Console.WriteLine(longResult);

UPDATE It looks like you want a purely constructor-injection driven solution. You can accomplish this by registering a new interface as IEnumerable<IInputValidator> and injecting it into the resolver instance.

The IInputValidator interface is responsible for the CanHandle<T>() method that is checked prior to casting to IInputValidator<T>.

public interface IInputValidatorResolver
{
    IInputValidator<T> GetValidator<T>();
}

public class ValidatorResolver : IInputValidatorResolver
{
    private IEnumerable<IInputValidator?> _validators;
    
    public ValidatorResolver(IEnumerable<IInputValidator?> validators)
    {
        _validators = validators;
    }
    
    public IInputValidator<T> GetValidator<T>()
    {
        foreach (var validator in _validators)
        {
            if (validator!.CanHandle<T>())
            {
                return (IInputValidator<T>)validator;
            }
        }
        
        throw new ArgumentException($"{typeof(T).FullName} does not have a registered validator.");
    }
}

public interface IInputValidator 
{
    bool CanHandle<TInput>();   
}

public interface IInputValidator<T> : IInputValidator
{
    bool Validate(string? input, out T result);
}

public class IntegerValidator : IInputValidator<int>
{
    public bool CanHandle<T>() => typeof(T) == typeof(int);
    public bool Validate(string? input, out int result) => int.TryParse(input, out result);
}

public class LongValidator : IInputValidator<long>
{
    public bool CanHandle<T>() => typeof(T) == typeof(long);
    public bool Validate(string? input, out long result) => long.TryParse(input, out result);
}

You can test this behavior with the following:

var servicesCollection = new ServiceCollection();
servicesCollection.AddTransient(typeof(IEnumerable<IInputValidator>), s =>
{
    return new List<IInputValidator>
    {
            new IntegerValidator(),
            new LongValidator()
    };
});
servicesCollection.AddTransient<IInputValidatorResolver, ValidatorResolver>();

var serviceProvider = servicesCollection.BuildServiceProvider();

var resolver = serviceProvider.GetService<IInputValidatorResolver>();
var intValidator = resolver.GetValidator<int>();

var isIntValid = intValidator.Validate(long.MaxValue.ToString(), out int intResult);
Console.WriteLine(isIntValid);
Console.WriteLine(intResult);

var longValidator = resolver.GetValidator<long>();

var isLongValid = longValidator.Validate(long.MaxValue.ToString(), out long longResult);
Console.WriteLine(isLongValid);
Console.WriteLine(longResult);

With this approach, you no longer need the ValidationResolver. It is simply a way to encapsulate the logic of GetValidator<T>. You could just as easily inject IEnumerable<IInputValidator> into your consuming classes and perform the cast each time you need to use it.

CodePudding user response:

Another option is to use Autofac's IComponentContext:

using Autofac;
using TransactionStorage.Interface;

namespace TransactionStorage.Core
{
    public class InputResolver : IInputResolver 
    {
        private readonly IComponentContext _context;

        public InputResolver (IComponentContext context)
        {
            _context = context;
        }

        public bool Validate<T>(string? userInput, out T result) where T : struct
        {
            var validator = _context.Resolve<IInputValidator<T>>();
            return validator.Validate(userInput, out result);
        }
    }
}

  • Related