0

I am attempting to implement some generic config types and have bumped into an issue with conversions. Ideally what I'd like is a strongly typed instance of configuration values that can be assigned to our different Exporters.

What I have is:

  • IConfigurableExport: Interface to mark exporter as configurable and contains a ICollection<IExportConfiguration<Object>> Configurations property
  • IExportConfiguration<T>: Interface to define common properties (Name, Description, etc.)
  • ExportConfiguration<T>: Abstract class that implements IExportConfiguration<T>, sets the interface properties as Abstract, adds some common functionality like setting the value of itself from a list of configuration values and whatnot
  • ConfigurationInstance<bool>: Inherits ExporterConfiguration<Boolean>

The UI to configure the exporters is dynamically built based on the values from IConfigurableExport.Configurations. When I try to call IConfigurableExport.Configurations I get a casting error. Here is some sample code to illustrate the problem:

using System;
using System.Linq;
using System.Collections.Generic;
                    
public class Program
{
    public static void Main()
    {
        var t = new MyExporter();
        var configs = t.Configurations;
        foreach(var conf in configs){
            Console.WriteLine(conf.Name);
        }
    }
}

public interface IExportConfiguration<T>{ 
    string Description {get;}
    Guid Id {get;}
    string Name {get;}
    T Value {get; set;}
}


public abstract class ExportConfiguration<T>
    : IExportConfiguration<T>
{
    public abstract string Description { get; }
    public abstract string Name { get; }
    public abstract Guid Id { get; }
    public abstract T Value { get; set; }
    public abstract T DefaultValue { get; set; }
    
    public virtual void SetValue(ICollection<KeyValuePair<Guid, object>> values)
    {
        if (values.Any(kvp => kvp.Key == this.Id))
            Value = (T)values.First(kvp => kvp.Key == this.Id).Value;
        else
            Value = this.DefaultValue == null ? this.DefaultValue : default(T);
    }
}

public interface IConfigurableExport {
    ICollection<IExportConfiguration<object>> Configurations {get;}
}

public class MyConfig : ExportConfiguration<bool> {
    public override string Description { get { return "This is my description"; }}
    public override string Name { get{ return "My Config Name"; }}
    public override Guid Id { get { return Guid.Parse("b6a9b81d-412d-4aa8-9090-37c9deb1a9f4"); }}
    public override bool Value { get; set;}
    public override bool DefaultValue { get; set;}
}

public class MyExporter : IConfigurableExport {
    MyConfig conf = new MyConfig();
    public ICollection<IExportConfiguration<object>> Configurations {
        get {
            return new List<IExportConfiguration<object>> { (IExportConfiguration<object>)conf };
        }
    }
}

Here is a dontnetfiddle to illustrate the problem

Adam H
  • 1,750
  • 1
  • 9
  • 24
  • Perhaps you can have another abstract class `class ExportConfigurationBase` which declares all the non-generic properties, then declare the list `IReadOnlyCollection Configurations {get;}` see fiddle for example https://dotnetfiddle.net/lRaL2S. Ultimately `IConfigurableExport` needs to also be generic for it to have a strongly-typed reference to `IExportConfiguration` – Charlieface Nov 09 '21 at 22:00
  • I may be way off in my understanding, but you have four references to the type `object` when the concrete type used in the `MyConfig` declaration is `bool`. If you change the declaration of the function `SetValue` in the `abstract class ExportConfiguration` to be of type T and the other references to be `bool`, it should work. But I cannot be certain it will work for you in practice. [This dotnetfiddle](https://dotnetfiddle.net/aKCjqz) shows what I did and how it works – JayV Nov 09 '21 at 22:17
  • @JayV, thanks for the suggestion but the idea is to support ints and strings at a later date. For a stopgap, I could implement it this way but we would be faced with the same problem later when we wanted to support integers for example. – Adam H Nov 09 '21 at 22:20
  • @Charlieface, Thanks for the idea but the problem I see with this implementation is getting the value back out of the configuration item at runtime which is why Value is currently defined on the interface itself. – Adam H Nov 09 '21 at 22:20
  • I'd assumed you wanted to be able to get and set, at which point covariance is not an option anyway – Charlieface Nov 09 '21 at 22:57
  • @Charlieface no worries, I still appreciate your time. – Adam H Nov 09 '21 at 23:14

1 Answers1

2

The main problem is that IExportConfiguration<T> defines a T Value {get; set;}

If you have an IExportConfiguration<bool>, you can't cast IExportConfiguration<bool> to an IExportConfiguration<object> because you can't set Value with a type of object, it has to be set with a type of bool in order for it to be type safe.

To be able to make that kind of cast you would need to use a covariant interface like below

public interface IExportConfiguration<T> : IReadOnlyExportConfiguration<T>{
    new T Value {get; set;}
}

//this interface is covariant, and if you have a IReadOnlyExportConfiguration<string>
//it can be cast to an IReadOnlyExportConfiguration<object>
public interface IReadOnlyExportConfiguration<out T>{ 
    string Description {get;}
    Guid Id {get;}
    string Name {get;}
    T Value {get;}
}

and then you can redefine your configurable export as

public interface IConfigurableExport {
    ICollection<IReadOnlyExportConfiguration<object>> Configurations {get;}
}

But then you can't use MyConfig as a bool config because IReadOnlyExportConfiguration<bool> will not cast to IReadOnlyExportConfiguration<object> because bool is a simple data type. The type of T has to be a class type in order for it to be covariant with object.

public class MyConfig : ExportConfiguration<string> {
    public override string Description { get { return "This is my description"; }}
    public override string Name { get{ return "My Config Name"; }}
    public override Guid Id { get { return Guid.Parse("b6a9b81d-412d-4aa8-9090-37c9deb1a9f4"); }}
    public override string Value { get; set;}
    public override string DefaultValue { get; set;}
}

And then finally your exporter becomes

public class MyExporter : IConfigurableExport {
    MyConfig conf = new MyConfig();
    public ICollection<IReadOnlyExportConfiguration<object>> Configurations {
        get {
            return new List<IReadOnlyExportConfiguration<object>> { (IReadOnlyExportConfiguration<object>)conf };
        }
    }
}

But at this point you won't be able to set values on your configurations, and if that's needed, then you will need to restructure your solution.

TJ Rockefeller
  • 3,178
  • 17
  • 43
  • This is great! I've tweaked it a little in a dotnetfiddle to get it to work exactly how I want! [Here is a fiddle](https://dotnetfiddle.net/Ty9p0J) with the changes you suggested implemented. I will mark this as the accepted answer and update my question with the new solution. Thank you a bunch for your help. – Adam H Nov 09 '21 at 22:52
  • So I was doing some additional testing with this and this only seems to work with strings, do you have any idea why I can't use this with bools or ints? – Adam H Nov 10 '21 at 16:06
  • @AdamH I specifically say in my answer that you can't use `bool` or other simple data types because they are not covariant with object (basically you can't implicitly convert from a simple value type to a type of `object`) a more thorough answer is here https://stackoverflow.com/questions/12454794/why-covariance-and-contravariance-do-not-support-value-type – TJ Rockefeller Nov 10 '21 at 16:09
  • Thanks for the info, I appreciate your time. – Adam H Nov 10 '21 at 16:18