1

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;
        }
    }
}
Laurence
  • 980
  • 12
  • 31
  • 1
    Thank you for including a link to (potentially) runnable code, but please include a [mre] in the post itself. – gunr2171 Jul 06 '22 at 21:17
  • @gunr2171 - the fiddle is a much reduced sample compared to the actual project. I did try to get it down to the minimum to show the issue. And DotNetFiddle does helpfully show the compile error, so I thought it better than pasting code into the post that users then have to copy to VS to see the error. Does the code really need to be inside the post? – Laurence Jul 06 '22 at 21:28
  • It does. It's about persistence. If dotnetfiddle goes belly-up, the relevant details of the question must still be visible. – madreflection Jul 06 '22 at 21:35
  • @madreflection - OK I think I understand why .... but my question still is - is there something I can do to satisfy my requirements? If this is a much asked question, then can you point me to an answer? – Laurence Jul 06 '22 at 21:36
  • The linked duplicate is wrong. It's the same variance problem, but you don't have list elements you can simply convert so that solution doesn't apply. The answer when it's not a matter of creating a new list of cast elements, the answer, as I recall, is you can't. – madreflection Jul 06 '22 at 21:39
  • OK well at least I understand now, thanks – Laurence Jul 06 '22 at 21:48
  • 1
    @madreflection I picked duplicate as there was the answer that says just that - you can't and never will be do that cast... I've added more generic one so Laurence have some *light* reading - https://stackoverflow.com/questions/6797759/covariance-contravariance-in-c-sharp to figure out what exactly they want (using searchable terms) and if that is possible. – Alexei Levenkov Jul 06 '22 at 22:04
  • The compiler is telling you that the generic constraint `.BusinessInputFieldTypeBase`, doesn't work. Instead you need the constraint `.BusinessInputFieldTypeBase where F : Contracts.IBusinessInputFieldType` so that any `F` (eg `SkuCodeField`) can be supplied. – Jeremy Lakeman Jul 07 '22 at 01:03
  • @JeremyLakeman - sorry, I am not clear what you mean ... can you say where to make this change? – Laurence Jul 07 '22 at 06:42
  • Where your `cannot convert from 'X' to 'Y'` error appears, the thing you are trying to call should be changed. But that probably means that you don't actually want to change that bit. – Jeremy Lakeman Jul 07 '22 at 07:16
  • To solve the problem `BusinessInputFieldTypeBase` needs a "curiously recursive" generic constraint like `BusinessInputFieldTypeBase ... where T:BusinessInputFieldTypeBase` then `SkuCodeField : Contracts.BusinessInputFieldTypeBase`. This way the base type can enforce that the validator can process, not itself or any other `string` field, but `SkuCodeField` specifically. – Jeremy Lakeman Jul 07 '22 at 07:27
  • @JeremyLakeman - I appreciate you offering the suggestion, and I think I have implemented the "curiously recursive" constraint, but the compile error remains. I have updated the fiddle https://dotnetfiddle.net/GpbYIP with my latest code ... would you be able to take a look? – Laurence Jul 07 '22 at 12:30
  • `protected BusinessInputFieldTypeBase(IBusinessFieldValidator ...` the rest should then be obvious. That's how to solve the variance problem with more generics, though they will creep into other types in unexpected ways. You can also solve the deadlock by introducing a new type with less generic constraints. `protected BusinessInputFieldTypeBase(ISimpleValidator ...` `interface ISimpleValidator {bool ValidateValue(IBusinessInputFieldType field);}` ... etc. – Jeremy Lakeman Jul 08 '22 at 00:55
  • @JeremyLakeman - thanks so much - this is now compiling, runs correctly, and I can also see that it satisfies my requirement that a field can only be validated by validators that are declared using that field type. Now can I convince the SO moderators to re-open the question and let an answer be posted? :) ... – Laurence Jul 08 '22 at 10:39
  • @AlexeiLevenkov - Jeremy has shown me that my issue can be resolved with minor modifications to the code, using a "curiously recursive" generic constraint. I think it would be valuable to post an answer demonstrating this, because it is clearly not impossible. It is also not a duplicate of the List conversion problem, and not solved using covariance/contravariance. Please re-open so I or Jeremy can post an answer. Thanks. – Laurence Jul 08 '22 at 10:47
  • @Laurence please review https://meta.stackoverflow.com/questions/252252/this-question-already-has-answers-here-but-it-does-not-what-can-i-do-when-i - in particular the question is still full of statements you presumably no longer agree with and missing explanation why it is not solved by `in` or `out` on the type parameter. Please [edit] the question to clean that up (and ideally make sure regular https://stackoverflow.com/questions/1327568/curiously-recurring-template-pattern-and-generics-constraints-c is not helpful either). – Alexei Levenkov Jul 08 '22 at 16:29
  • 1
    @AlexeiLevenkov I have now substantially edited the question, and you are correct - I needed to to throw out most of what I originally wrote to explain the underlying problem I was REALLY trying to solve. – Laurence Jul 12 '22 at 10:58
  • 1
    @AlexeiLevenkov - I did also look at https://stackoverflow.com/questions/1327568/curiously-recurring-template-pattern-and-generics-constraints-c and although the answer to this also uses a "curiously recursive" generic constraint, the use cases are rather different IMHO. – Laurence Jul 12 '22 at 11:05

1 Answers1

0

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;
        }
    }
}
Laurence
  • 980
  • 12
  • 31
  • Off-topic, but your `SetValue` method may be flawed, as it allows for a scenario in which the `SelectedValue` persists even when invalid if `ValidateValue` throws an exception. To fix you'd need to modify the `ValidateValue` to also accept the proposed new value as an argument and validate that. – Boris B. Jul 12 '22 at 21:57