11
\$\begingroup\$

I find myself frequently converting between formats, so I have come up with the following conversion framework. The converter interface surfaces a method to convert from a source type to a target type.

public interface IConverter<in TSource, out TTarget>
{
    TTarget Convert(TSource source);
}

The factory is for producing concrete converters:

public interface IConverterFactory
{
    IConverter<TSource, TTarget> GetConverter<TSource, TTarget>();
}

This next interface allows for the implementation of a generic converter that you can inject into a class and reuse for many conversions of different types (you can inject the factory instead but the code just looks cleaner with the generic converter):

public interface IGenericConverter
{
    TTarget Convert<TSource, TTarget>(TSource source);
}

Here's a simple example to illustrate how these interfaces are implemented. First I will look at the factory as we can provide a pretty generic factory implementation.

public class ConverterFactory : IConverterFactory
{
    private readonly Dictionary<Tuple<Type, Type>, Func<object>> _converters =
        new Dictionary<Tuple<Type, Type>, Func<object>>();

    public void RegisterConverter<TSource, TTarget>(Func<object> constructor)
    {
        _converters.Add(new Tuple<Type, Type>(typeof(TSource), typeof(TTarget)), constructor);
    }

    public IConverter<TSource, TTarget> GetConverter<TSource, TTarget>()
    {
        var constructor = _converters[new Tuple<Type, Type>(typeof (TSource), typeof (TTarget))];
        return (IConverter<TSource, TTarget>) constructor();
    }
}

The factory implementation introduces a dictionary to keep track of registered converters and the associated constructors for the converters. A registration method is added to allow registration of converters. And finally GetConverter is implemented to look up the converter from the dictionary.

The generic converter can be implemented with a dependency on this factory like so:

public class GenericConverter : IGenericConverter
{
    private readonly ConverterFactory _factory;

    public GenericConverter(ConverterFactory factory)
    {
        _factory = factory;
    }

    public TTarget Convert<TSource, TTarget>(TSource source) 
    {
        var converter = _factory.GetConverter<TSource, TTarget>();
        return converter.Convert(source);
    }
}

Inject a factory into the constructor and then Convert can be called for any conversion and the class will get a converter from the factory, run the converter and return the result of the conversion.

Here are some models. Note the POCO models, absence of dependencies, completely different signatures and cleanliness.

public class Dog
{
    public string Name { get; set; }
}

public class DogDescriptor
{
    public string Name { get; set; }
}

public class Cat
{
    public int Number { get; set; }
}

public class CatDescriptor
{
    public int Number { get; set; }
}

So the plan is to convert from DogDescriptor to Dog and from CatDescriptor to Cat. The converters need to implement IConverter.

public class DogConverter : IConverter<DogDescriptor, Dog>
{
    public Dog Convert(DogDescriptor descriptor)
    {
        return new Dog {Name = descriptor.Name};
    }
}

public class CatConverter : IConverter<CatDescriptor, Cat>
{
    public Cat Convert(CatDescriptor descriptor)
    {
        return new Cat {Number = descriptor.Number};
    }
}

Now we have all the bits and pieces we need to do a conversion:

var factory = new ConverterFactory();

// register converters
factory.RegisterConverter<DogDescriptor, Dog>(() => new DogConverter());
factory.RegisterConverter<CatDescriptor, Cat>(() => new CatConverter());


var converter = new GenericConverter(factory);
var dog = converter.Convert<DogDescriptor, Dog>(new DogDescriptor{Name = "Spot"});
var cat = converter.Convert<CatDescriptor, Cat>(new CatDescriptor{Number = 666});

Ideally you will use dependency injection to inject the generic converter into any classes that need to do conversions. Your dependency injection solution can then be configured to run the generic converter as a singleton.

Extending conversion support is fairly easy, assuming the two new models are introduced as Duck and DuckDescriptor. All the developer needs to do is implement a DuckDescriptor-to-Duck converter and then register the converter with the factory.

The factory has no dependencies and can easily be tested. The generic converter needs a mock factory injected for testing. Converters only have dependencies on the models they convert which makes testing converters extremely easy.

Questions

  1. Am I reinventing the wheel? I couldn't find anything like it in the .NET framework but I also did not look very hard.

  2. Many conversion implementations extend the model classes with ToType style methods. Does this approach offer any advantages?

While I can see the attraction of this approach, it requires an extension method for every conversion which starts getting ugly when you have to do vendor specific conversions of the same models. Using the factory solution, you would simply implement vendor specific factories and inject a generic converter constructed with the vendor specific factory.

\$\endgroup\$
5
\$\begingroup\$

I only scan read the very well written question (sorry it was late!) but:

Am I reinventing the wheel? I couldn't find anything like it in the .NET framework but I also did not look very hard.

As I understand it, yes.

I believe all you're trying to do here is to register a 'mapping' between types. Provide an instance of one type, get back another one.

Libraries such as Automapper solve this problem, by allowing you to define a mapping between types.

In your example you call converter.Convert, whereas a typical automapper configuration might call something like Mapper.Map<T>(myType) and perform a similar role. Let me know if I've missed something, but I think that's what you're after.

\$\endgroup\$
  • 1
    \$\begingroup\$ Automapper seems to provide a framework very similar to the one I described. My IConverter type is the ITypeConverter type in AutoMapper. Registering the converter with AutoMapper is similar to how I register converters with the factory "Mapper.CreateMap<string, DateTime>().ConvertUsing(new DateTimeTypeConverter());" \$\endgroup\$ – Hawk Jun 4 '14 at 23:42

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.