2
\$\begingroup\$

This is the second version of my type converter framework.

The the previous one can be found here: Type creator service & framework

In this version I mostly implemented what @Dmitry Nogin suggested in his great reviews

I also extended it so that each converter has access to all the registered converters in case it needs them like for creating collections.

After giving it another thougt I changed my mind again it went back to name it just converters :-) neither activator, nor factory nor anything else seem to be ok.

The framework's goals are:

  • super easy to use - the user needs to override only one method
  • super easy to extend - the user can create any converter he wants and also use any registered converter
  • type safe
  • can be used without generics - types are resolved dynamicaly from properties of other classes at runtime

Base class for all converters:

public abstract class TypeConverter
{
    public CompositeConverter CompositeConverter { get; internal set; }

    public abstract bool TryConvert(Type type, object arg, CultureInfo culture, out object instance);
}

Composite converter to hold multiple converters:

public class CompositeConverter : TypeConverter
{
    private CompositeConverter() { }

    private CompositeConverter(CompositeConverter compositeConverter, TypeConverter typeConverter)
    {
        Converters = 
            compositeConverter.Converters
            .Concat(new TypeConverter[] { typeConverter}).ToArray();

        // update the composite converter 
        foreach (var activator in Converters)
        {
            typeConverter.CompositeConverter = this;
        }
    }

    public TypeConverter[] Converters { get; private set; } = 
        Enumerable.Empty<TypeConverter>().ToArray();

    public CompositeConverter Register<TConverter>() where TConverter : TypeConverter, new()
    {
        return (this + new TConverter());
    }

    public override bool TryConvert(Type type, object arg, CultureInfo culture, out object instance)
    {
        foreach (var activator in Converters)
        {
            if (activator.TryConvert(type, arg, culture, out instance))
            {
                return true;
            }
        }
        instance = null;
        return false;
    }

    public object Convert(Type type, object arg, CultureInfo culture)
    {
        object instance;
        if (!TryConvert(type, arg, culture, out instance))
            throw new NotSupportedException();

        return instance;
    }

    public static CompositeConverter Create() => 
        return new CompositeConverter();

    public static CompositeConverter operator +(CompositeConverter left, TypeConverter right) =>
        new CompositeConverter(left, right);
}

Base converter for user converters:

public abstract class TypeConverter<TArg, TResult> : TypeConverter
{
    public override bool TryConvert(Type type, object arg, CultureInfo culture, out object instance)
    {
        instance = 
            type.IsAssignableFrom(typeof(TResult)) && arg is TArg 
            ? Convert((TArg)arg, culture) 
            : (object)null;
        return instance != null;
    }

    public abstract TResult Convert(TArg value, CultureInfo culture);
}

Several converters that I wrote to test the new architecture:

public class StringToInt32Converter : TypeConverter<string, int>
{
    public override int Convert(string value, CultureInfo culture)
    {
        return int.Parse(value);
    }
}

public class Int32ToStringConverter : TypeConverter<int, string>
{
    public override string Convert(int value, CultureInfo culture)
    {
        return value.ToString(culture);
    }
}

public class SingleToStringConverter : TypeConverter<float, string>
{
    public override string Convert(float value, CultureInfo culture)
    {
        return value.ToString(culture);
    }
}

public class StringToEnumConverter<TEnum> : TypeConverter<string, TEnum>
{
    public StringToEnumConverter()
    {
        // enum constraint on T are not supported so we need to check the type here
        if (typeof(TEnum).BaseType != typeof(Enum)) 
        {
            throw new InvalidOperationException("TEnum must by of Enum type."); 
        }
    }

    public override TEnum Convert(string value, CultureInfo culture)
    {
        return (TEnum)Enum.Parse(typeof(TEnum), value);
    }
}

public class StringToListConverter<T> : TypeConverter<IEnumerable<string>, List<T>>
{
    public override List<T> Convert(IEnumerable<string> values, CultureInfo culture)
    {
        // use other converters to create a list of T
        return values.Select(v => 
            (T)CompositeConverter.Convert(typeof(T), v, culture)).ToList();
    }
}

Usage:

var compositeTypeConverter =
    CompositeConverter
    .Create()
    .Register<StringToInt32Converter>()
    .Register<Int32ToStringConverter>()
    .Register<StringToEnumConverter<ConsoleColor>>()
    .Register<StringToListConverter<int>>()
    .Register<SingleToStringConverter>();

var text1 = compositeTypeConverter.Convert(
    typeof(int), "123", CultureInfo.InvariantCulture);
var int1 = compositeTypeConverter.Convert(
    typeof(string), 123, CultureInfo.InvariantCulture);
var float1 = compositeTypeConverter.Convert(
    typeof(string), 123.456f, CultureInfo.InvariantCulture);
var consoleColor1 = compositeTypeConverter.Convert(
    typeof(ConsoleColor), "Black", CultureInfo.InvariantCulture);
var list1 = compositeTypeConverter.Convert(typeof(
    List<int>), new[] { "1", "3", "7" }, CultureInfo.InvariantCulture);

These examples include only a StringToSomething converters but in the final project there will also be ByteToSomething converters that's why one of the requirements is to not only support strings.

In the final project I will add the other helpers suggested in the first reviews because I first wanted to build a proof-of-concept and the add the nice-to-haves. I'll also make the culture parameter optional and default to the invariant culture.

\$\endgroup\$
2
\$\begingroup\$

Thanks for sharing – it is an interesting problem :)

  1. I would go with mutable state only when it is required for performance optimization. Immutability almost always makes code a way cleaner and maintainable.
  2. We should not depend on implementation – let’s prefer abstraction where possible. I mean we should work with TypeConverter, not CompositeConverter to define a service.

Here is an example. Demo code:

class Program
{
    static void Main(string[] args)
    {
        TypeConverter converter = TypeConverter.Default
            .Register<StringToInt32Converter>();

        Console.WriteLine(
            converter.Convert(
                typeof(int), "42", CultureInfo.InvariantCulture));
    }
}

Where:

public class StringToInt32Converter : TypeConverter<string, int>
{
    protected override int Convert(string arg, ConversionContext context) =>
        int.Parse(arg, context.Culture);
}

Now library. Let’s define our abstraction as clean as possible:

    public abstract class TypeConverter
    {
         public static readonly TypeConverter Default = CompositeConverter.Empty;
         protected internal abstract bool TryConvert(
             ConversionContext context, object arg, out object instance);
    }

And all the API with multiple overloads will come as two sets of extension methods – I really like this trick :)

public static class Composition
{
    public static TypeConverter Register<TConverter>(this TypeConverter that)
        where TConverter : TypeConverter, new() =>
        that.Register(new TConverter());

    // etc…

    // base method to be used above
    public static TypeConverter Register(this TypeConverter that, TypeConverter converter) =>
        new CompositeConverter(that, converter);
}

And:

public static class Conversions
{
    public static object Convert(this TypeConverter converter, Type type, object arg, CultureInfo culture)
    {
        object instance;
        if (!converter.TryConvert(type, arg, culture, out instance))
            throw new NotSupportedException();

        return instance;
    }

    // etc

    // base method to be used above
    public static bool TryConvert(this TypeConverter converter, Type type, object arg, CultureInfo culture, out object instance) =>
        converter.TryConvert(new ConversionContext(converter, type, culture), arg, out instance);
}

As you see – API references TypeConverter abstraction only.

Helper class (to reduce amount of arguments):

public class ConversionContext
{
    public ConversionContext(TypeConverter service, Type type, CultureInfo culture)
    {
        Service = service;
        Type = type;
        Culture = culture;
    }

    public TypeConverter Service { get; }
    public Type Type { get; }
    public CultureInfo Culture { get; }
}

Now simple immutable composite converter:

public class CompositeConverter : TypeConverter
{
    public static readonly TypeConverter Empty = new CompositeConverter();

    public CompositeConverter(params TypeConverter[] converters)
    {
        Converters = converters;
    }

    protected internal sealed override bool TryConvert(ConversionContext context, object arg, out object instance)
    {
        instance = null;
        foreach (var converter in Converters)
            if (converter.TryConvert(context, arg, out instance))
                return true;

        return false;
    }

    TypeConverter[] Converters { get; }
}

And a super-type for your business objects:

public abstract class TypeConverter<TArg, TResult> : TypeConverter
{
    protected internal sealed override bool TryConvert(ConversionContext context, object arg, out object instance)
    {
        var match = context.Type.IsAssignableFrom(typeof(TResult)) && arg is TArg;
        instance = match ? (object)Convert((TArg)arg, context) : null;
        return match;
    }

    protected abstract TResult Convert(TArg arg, ConversionContext context);
}

UPDATE - using context.Service property

        TypeConverter converter = TypeConverter.Default
            .Register<StringToInt32Converter>()
            .Register<StringArrayToInt32ArrayConverter>();

        Console.WriteLine(
            converter.Convert(
                typeof(int[]), new[] { "42" }, CultureInfo.InvariantCulture));

where:

public class StringToInt32Converter : TypeConverter<string, int>
{
    protected override int Convert(string arg, ConversionContext context) =>
        int.Parse(arg, context.Culture);
}

public class StringArrayToInt32ArrayConverter : TypeConverter<string[], int[]>
{
    protected override int[] Convert(string[] arg, ConversionContext context) =>
        arg
            .Select(s => (int)context.Service.Convert(typeof(int), s, context.Culture))
            .ToArray();
}
\$\endgroup\$
  • \$\begingroup\$ Thank you for another great review ; -) If I get it right you mean I shouldn't work with the CompositeConverter.Create()? I somehow agree to that but I'm also not very fond of the Default property. It allows to only use one converter collection for the entire application because it's static. This makes testing harder as each test uses the same converter collection and there might be situations where the user wants to have more then one. What I do like is the coversion context. I think I'll make the converters array internal/private and pass it via the context too. \$\endgroup\$ – t3chb0t Jun 12 '16 at 16:20
  • \$\begingroup\$ Regarding 1 - the CompositeConverter it is immutable - I guess. I've implemented your suggestion here ;-] I create a new composite converter on each registration. \$\endgroup\$ – t3chb0t Jun 12 '16 at 16:22
  • \$\begingroup\$ 1) Default is just a Null pattern - an immutable converter singleton which converts nothing, so you you can start expanding it many times independently. Yes, one place will not influence another and you can reuse semi-constructed sequences - immutability rules :) \$\endgroup\$ – Dmitry Nogin Jun 12 '16 at 16:26
  • 1
    \$\begingroup\$ Yep, I wish I could develop frameworks around the clock. Framework Design Guidelines is my bible :) \$\endgroup\$ – Dmitry Nogin Jun 12 '16 at 17:49
  • 1
    \$\begingroup\$ This book might be exacly what I needed... and I've just bought one ;-D \$\endgroup\$ – t3chb0t Jun 12 '16 at 21:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.