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 = () => 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"); }
