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 aICollection<IExportConfiguration<Object>> Configurations
propertyIExportConfiguration<T>
: Interface to define common properties (Name, Description, etc.)ExportConfiguration<T>
: Abstract class that implementsIExportConfiguration<T>
, sets the interface properties as Abstract, adds some common functionality like setting the value of itself from a list of configuration values and whatnotConfigurationInstance<bool>
: InheritsExporterConfiguration<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
CodePudding user response:
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.