Search code examples
c#.netcustom-attributes

Limit property to specific values


There are some properties in a class where there are only certain values that are acceptable. The corresponding field in the database, however, is of type string (nvarchar). This means that it's possible the field will have an unacceptable value, in which case the actual field should be assigned a pre-selected value instead of what was read.

(The database's schema or contents cannot be changed at this point.)

I'm trying to figure out a way to have a property that can be assigned a string but, if that string isn't one of the acceptable ones, will store a pre-selected value.

One approach that basically works is to use something that works as a string-based enum as the inner (private) field, and only expose (set as public) a property that follows this logic, as in the following example (in this case, an unacceptable value is being treated as the value "Real"):

public class Item
{
    private sealed class Reality
    {
        public readonly string Value;

        private Reality(string value)
        {
            Value = value;
        }

        public static readonly Reality Real = new Reality("Real");
        public static readonly Reality Imaginary = new Reality("Imaginary");
        public static readonly Reality BasedOnReality = new Reality("Based on reality");
    }

    private Reality _realityValue;

    public string RealityStr
    {
        get { return _realityValue.Value; }
        set
        {
            if (value == Reality.Imaginary.Value)
            {
                _realityValue = Reality.Imaginary;
            }
            else if (value == Reality.BasedOnReality.Value)
            {
                _realityValue = Reality.BasedOnReality;
            }
            else
            {
                _realityValue = Reality.Real;
            }
        }
    }
}

The above works correctly:

Item item = new Item();

item.RealityStr = "Real";
Console.WriteLine(item.RealityStr); //Output: Real

item.RealityStr = "Imaginary";
Console.WriteLine(item.RealityStr); //Output: Imaginary

item.RealityStr = "Existing";
Console.WriteLine(item.RealityStr); //Output: Real

The problem with this approach is that such a class, and the corresponding setter, will have to be defined for each field separately. Ideally, the logic would be defined only once, and the list of acceptable values would be given at the field's definition, like below (not valid C# syntax):

public class Item
{
    public LimitedString<"Real", "Imaginary", "Based on reality"> Reality;

    public LimitedString<"Other", "Blue", "Red", "Green"> Colour;
}

(In this example, LimitedString is a type that implements the logic I'm looking for, for the given list of strings. The default value would be the first one.)

I think it may be possible to do this with a custom attribute that's given a list of strings, and a special class, where the syntax would be like below:

public class Item
{
    [AcceptableValues("Real", "Imaginary", "Based on reality")]
    public LimitedString Reality;

    [AcceptableValues("Other", "Blue", "Red", "Green")]
    public LimitedString Colour;
}

However I wasn't able to find a way that a class could read an attribute that is applied to a certain instance of itself, which again means that some logic would have to be duplicated for each property that has acceptable values.
Also, assignment would have to be done by a function as it's not possible to overload the assignment operator and using a public implicit operator LimitedString(string value) would return a new LimitedString object, meaning that I'd again need a different class for each property.

Is this at all possible to do in C# without duplicating logic for each specific field/property? I hope I've described the problem well.


Solution

  • I guess it's not possible to do it in a single line like I imagined.

    I'm going with the following, which I think is the way to do it with the least possible duplication of code:

    public class Item
    {
        private readonly LimitedString _reality = new LimitedString("Real", 
            "Imaginary", "Based on reality");
        public string Reality
        {
            get { return _reality.Value; }
            set { _reality.Value = value; }
        }
    
        private readonly LimitedString _colour = new LimitedString("Other", 
            "Blue", "Red", "Green");
        public string Colour
        {
            get { return _colour.Value; }
            set { _colour.Value = value; }
        }
    }
    

    A C++-style macro would be useful here, maybe.