Home > OS >  Cannot constrain C# base class constructor to only accept injected object of a type that uses the de
Cannot constrain C# base class constructor to only accept injected object of a type that uses the de

Time:07-15

The title is quite a mouthful, so here is the problem spelled out:

(please note - my question is how to implement point 6, and if you look at my code attempting to do this, it might look like a duplicate of an existing frequently-asked question for which the answer is "this conversion is impossible - this can't be done", however I know this already, so what I am looking for is some other way to solve point 6)

  1. I am creating a framework for “business input fields” … this means fields in an application that can accept user inputted values which then require some validation. For example, a field that accepts SKU codes (the EAN, UPC, barcodes etc used to identify products).
  2. Each field type must have a related .NET value/string type that the field value must convert to (e.g. DateTime, int, string etc)
  3. To accomplish the above, the framework defines an interface IBusinessInputFieldType<TV>, and an abstract base class BusinessInputFieldTypeBase<TV> : IBusinessInputFieldType<TV> (TV is the .NET value/string type)
  4. To be constructed, each field type requires a "validator" object to be injected. These validators contain the logic to validate the value entered into the field. The idea here is to define the validation logic outside the field types themselves, because a single field type might need different validation logic under different circumstances. For example, if the app requires that all codes entered into the SKU Code field are "EAN 13" format then an Ean13CodeValidator will be injected.
  5. Each validator type must specify the field type it works with, e.g. SKU Code field validators must declare that they only validate SKU Code fields
  6. Each field type must only accept validators for that specific field type. For example, it must not be possible to inject a "store code validator".

Everything is working, except my implementation of point #6. My attempt is below, but fails to compile. The error is in the SkuCodeField constructor's call to the base constructor. The error is:

cannot convert from 'Contracts.IBusinessFieldValidator<Implementations.SkuCodeField, string>' to 'Contracts.IBusinessFieldValidator<Contracts.BusinessInputFieldTypeBase<Implementations.SkuCodeField, string>, string>'

Please note (as I mention above) I do know that my attempted conversion to the type required by the base constructor cannot succeed (as noted in other questions). This code is here because I need to show you a minimal workable example (mwe).

My question is - is it possible to define (in an abstract base class) a constraint that refers to the derived type?

using System;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Example usage
            var field = new Implementations.SkuCodeField(new Implementations.Ean13CodeValidator());
            var input = Console.ReadLine();
            var inputOk = field.SetValue(input);
            Console.WriteLine(inputOk);
        }
    }
}

namespace Contracts
{
    public interface IBusinessInputFieldType<TV>
        where TV : IComparable, IConvertible, IEquatable<TV>      // this is a way to limit TV to value types and strings
    {
        TV SelectedValue { get; }
        bool SetValue(TV value);
    }

    public abstract class BusinessInputFieldTypeBase<TV> : IBusinessInputFieldType<TV>
        where TV : IComparable, IConvertible, IEquatable<TV>
    {
        protected BusinessInputFieldTypeBase(IBusinessFieldValidator<IBusinessInputFieldType<TV>, TV> validator)
        {
            _validator = validator;
        }

        private readonly IBusinessFieldValidator<IBusinessInputFieldType<TV>, TV> _validator;

        public TV SelectedValue { get; private set; }

        public bool SetValue(TV value)
        {
            var priorValue = SelectedValue;
            SelectedValue = value;

            if (_validator != null)
            {
                var valueOk = _validator.ValidateValue(this);
                if (!valueOk) SelectedValue = priorValue;
                return valueOk;
            }

            return true;
        }
    }

    public interface IBusinessFieldValidator<TF, TV>
        where TF : IBusinessInputFieldType<TV>
        where TV : IComparable, IConvertible, IEquatable<TV>
    {
        bool ValidateValue(TF field);
    }
    }

namespace Implementations
{
    public class SkuCodeField : Contracts.BusinessInputFieldTypeBase<string>
    {
        public SkuCodeField(Contracts.IBusinessFieldValidator<SkuCodeField, string> validator)
            : base(validator)
            // the below complies but the validator is passed through as null
            //: base(validator as Contracts.IBusinessFieldValidator<Contracts.IBusinessInputFieldType<string>, string>)
        {
        }
    }

    public class Ean13CodeValidator : Contracts.IBusinessFieldValidator<SkuCodeField, string>
    {
        public bool ValidateValue(SkuCodeField field)
        {
            return field != null && field.SelectedValue != null && field.SelectedValue.Length == 13;
        }
    }

    public class Ean8CodeValidator : Contracts.IBusinessFieldValidator<SkuCodeField, string>
    {
        public bool ValidateValue(SkuCodeField field)
        {
            return field != null && field.SelectedValue != null && field.SelectedValue.Length == 8;
        }
    }
}

CodePudding user response:

Credit for this answer is due to @JeremyLakeman who pointed me to the solution below (see comments in my post).

He said - To solve the problem BusinessInputFieldTypeBase needs a "curiously recursive" generic constraint like BusinessInputFieldTypeBase<TF,TV> ... where T:BusinessInputFieldTypeBase<TF,TV> then SkuCodeField : Contracts.BusinessInputFieldTypeBase<SkuCodeField, string>. This way the base type can enforce that the validator can process, not itself or any other string field, but SkuCodeField specifically.

The next part was to modify the base constructor to require a slightly different type: protected BusinessInputFieldTypeBase(IBusinessFieldValidator<TF, TV> validator)

The complete working code sample is:

using System;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Example usage
            var field = new Implementations.SkuCodeField(new Implementations.Ean13CodeValidator());
            var input = Console.ReadLine();
            var inputOk = field.SetValue(input);
            Console.WriteLine(inputOk);
        }
    }
}

namespace Contracts
{
    public interface IBusinessInputFieldType<TV>
        where TV : IComparable, IConvertible, IEquatable<TV>      // this is a way to limit TV to value types and strings
    {
        TV SelectedValue { get; }
        bool SetValue(TV value);
    }

    public abstract class BusinessInputFieldTypeBase<TF, TV> : IBusinessInputFieldType<TV>
        where TF : BusinessInputFieldTypeBase<TF, TV>           // "curiously recursive" generic constraint
        where TV : IComparable, IConvertible, IEquatable<TV>
    {
        protected BusinessInputFieldTypeBase(IBusinessFieldValidator<TF, TV> validator)
        {
            _validator = validator;
        }

        private readonly IBusinessFieldValidator<TF, TV> _validator;

        public TV SelectedValue { get; private set; }

        public bool SetValue(TV value)
        {
            var priorValue = SelectedValue;
            SelectedValue = value;

            if (_validator != null)
            {
                var valueOk = _validator.ValidateValue(this as TF);
                if (!valueOk) SelectedValue = priorValue;
                return valueOk;
            }

            return true;
        }
    }

    public interface IBusinessFieldValidator<TF, TV>
        where TF : IBusinessInputFieldType<TV>
        where TV : IComparable, IConvertible, IEquatable<TV>
    {
        bool ValidateValue(TF field);
    }
}

namespace Implementations
{
    public class SkuCodeField : Contracts.BusinessInputFieldTypeBase<SkuCodeField, string>
    {
        public SkuCodeField(Contracts.IBusinessFieldValidator<SkuCodeField, string> validator)
            : base(validator)
        {
        }
    }

    public class Ean13CodeValidator : Contracts.IBusinessFieldValidator<SkuCodeField, string>
    {
        public bool ValidateValue(SkuCodeField field)
        {
            return field != null && field.SelectedValue != null && field.SelectedValue.Length == 13;
        }
    }

    public class Ean8CodeValidator : Contracts.IBusinessFieldValidator<SkuCodeField, string>
    {
        public bool ValidateValue(SkuCodeField field)
        {
            return field != null && field.SelectedValue != null && field.SelectedValue.Length == 8;
        }
    }
}
  • Related