Using AutoMapper to help you map FSharpOption<> types

Why?

Because your model is structured this way, and you have realised you need this, otherwise this doesn’t apply to you.

Scenario

When you get used to using AutoMapper to help you everywhere, you begin to demand it helps you everywhere by default. In this scenario you have to configure it to help you map from an F# type that has option (Guid is just an example).

In our event sourcing setup, we have commands that now change to have an additional property (not option), but the event now needs to have option (as that data was not always present).

We end up using those types/classes (events) that have the optional value to map to C# classes that are used for persistence (in this case RavenDB), and they are reference type fields so a null value is acceptable for persistence.

Here’s the Source and Destination classes, hopefully seeing that makes this scenario clearer.

public class SourceWithOption
{
    public string Standard { get; set; }
    public FSharpOption<Guid> PropertyUnderTest { get; set; }
}

public class DestinationWithNoOption
{
    public string Standard { get; set; }
    public Guid PropertyUnderTest { get; set; }
}

Note: the DestinationWithNoOption is the equivalent C# class that we get our of the F# types, so the F# code is really this trivial (SubItemId is the optional one):

type JobCreatedEvent = {
    Id : Guid
    Name: string
    SubItemId : option<Guid>
}

Solution

Where you do all your AutoMapper configuration you’re going to make use of the MapperRegistry and add your own.

(Note: all this code is up as a gist.

var allMappers = AutoMapper.Mappers.MapperRegistry.AllMappers;

AutoMapper.Mappers.MapperRegistry.AllMappers = () 
    => allMappers().Concat(new List<IObjectMapper>
    {
            new FSharpOptionObjectMapper()
    });

And the logic for FSharpOptionObjectMapper is:

public class FSharpOptionObjectMapper : IObjectMapper
{
    public object Map(ResolutionContext context, IMappingEngineRunner mapper)
    {
        var sourceValue = ((dynamic) context.SourceValue);

        return (sourceValue == null || OptionModule.IsNone(sourceValue)) 
		? null : 
		sourceValue.Value;
    }

    public bool IsMatch(ResolutionContext context)
    {
        var isMatch = 
		    context.SourceType.IsGenericType &&
		    context.SourceType.GetGenericTypeDefinition() 
				== typeof (FSharpOption<>);

        if (context.DestinationType.IsGenericType)
        {
            isMatch &= 
			    context.DestinationType.GetGenericTypeDefinition() 
			    != typeof(FSharpOption<>);
        }

        return isMatch;
    }
}

Tests to prove it

Here’s a test you can run to show that this works, I started using Custom Type Coverters (ITypeConverter) but found that would not work in a generic fashion across all variations of FSharpOption<>.

[Test]
public void FSharpOptionObjectMapperTest()
{
    Mapper.CreateMap();
    
    var allMappers = AutoMapper.Mappers.MapperRegistry.AllMappers;
    AutoMapper.Mappers.MapperRegistry.AllMappers = () =&gt; allMappers().Concat(new List
        {
            new DustAutomapper.FSharpOptionObjectMapper()
        });

    var id = Guid.NewGuid();
    var source1 = new SourceWithOption
    {
        Standard = "test",
        PropertyUnderTest = new FSharpOption(id)
    };

    var source2 = new SourceWithOption
    {
        Standard = "test"
        //PropertyUnderTest is null
    };

    var result1 = Mapper.Map(source1);
    
    Assert.AreEqual("test", result1.Standard, "basic property failed to map");
    Assert.AreEqual(id, result1.PropertyUnderTest, "'FSharpOptionObjectMapper : IObjectMapper' on Guid didn't work as expected");

    var result2 = Mapper.Map(source2);
    
    Assert.AreEqual("test", result1.Standard, "basic property failed to map");
    Assert.IsNull(result2.PropertyUnderTest, "'FSharpOptionObjectMapper : IObjectMapper' for null failed");
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s