Question

I wanted to create a fluent interface that can be used like so:

void Main() {
    ModelStateMappings.MapDomainModel<Book>().MapViewModel<BookViewModel>()
        .Properties(book => book.Author, vm => vm.AuthorsName)
        .Properties(book => book.Price, vm => vm.BookPrice);

    ModelStateMappings.MapDomainModel<Store>().MapViewModel<StoreViewModel>()
        .Properties(store => store.Owner, vm => vm.OwnersName)
        .Properties(store => store.Location, vm => vm.Location);
}

I wanted end up with a collection that looked something like this:

static class ModelStateaMappings {
    private static IList<ModelMappings> mappings;
    // other methods in here to get it working
}

class ModelMappings {
    public Type DomainModelType {get;set;}
    public Type ViewModelType {get;set;}
    public IList<PropertyMapping> PropertyMappings {get;set;}
}

class PropertyMapping {
    public Expression<Func<object, object>> DomainProperty {get;set;}
    public Expression<Func<object, object>> ViewModelProperty {get;set;}
}

I was not able to get the above accomplished but I did create something similar which works in a similar fashion but I don't particularly like how I had to setup the fluent interfaces. I would rather have it read like the way I have it above.

Était-ce utile?

La solution 2

You can achieve it with following code

static class ModelStateMappings
{
    public static DomainModelMapping<TDomainModel> MapDomainModel<TDomainModel>()
    {
        // edit the constructor to pass more information here if needed.
        return new DomainModelMapping<TDomainModel>();
    }
}

public class DomainModelMapping<TDomainModel>
{
    public ViewModelMapping<TDomainModel, TViewModel> MapViewModel<TViewModel>()
    {
        // edit the constructor to pass more information here if needed.
        return new ViewModelMapping<TDomainModel, TViewModel>();
    }
}

public class ViewModelMapping<TDomainModel, TViewModel>
{
    public ViewModelMapping<TDomainModel, TViewModel>
        Properties<TDomainPropertyType, TViewModelPropertyType>(
            Expression<Func<TDomainModel, TDomainPropertyType>> domainExpr,
            Expression<Func<TViewModel, TViewModelPropertyType>> viewModelExpr)
    {
        // map here
        return this;
    }
}

You don't have to specify all previously set generic types because they are already remembered as generic parameters of returned type. Generic parameters for Properties method call can be skipped because they will be inferred by compiler. And you get better typing than using objects everywhere.

Of course that's the simplest version. You can pass much more information between these types, because you specify how next necessary type is created.

It also make calling MapViewModel without calling MapDomainModel first impossible (as soon as you make the constructors internal and close everything in separate dll), what should be a good thing.

Autres conseils

There are two common ways to create a fluent interface.

One way is to add to the current instance of the class being built and return this from each method.

Something like this:

public class NamesBuilder
{
    private List<string> _names = new List<string>();
    public NamesBuilder AddName(string name)
    {
        _names.Add(name);
        return this;
    }
}

The problem with this kind of builder is that you can write buggy code easily:

var namesBuilder = new NamesBuilder();

var namesBuilder1 = namesBuilder.AddName("John");
var namesBuilder2 = namesBuilder.AddName("Jack");

If I saw this code I would expect that namesBuilder1 and namesBuilder2 would each only have one name, and that namesBuilder wouldn't have any. However the implementation would have both names in all three variables as they are the same instance.

The better way to implement a fluent interface is to create a chain on builder classes that are lazily evaluated so that you create the final class once you're done building. Then if you branch in the middle of the building process you can make a mistake.

Here's the kind of code I would expect to write:

var bookMap =
    ModelStateMappings
        .Build<Book, BookViewModel>()
        .AddProperty(book => book.Author, vm => vm.AuthorsName)
        .AddProperty(book => book.Price, vm => vm.BookPrice)
        .Create();

var bookStore =
    ModelStateMappings
        .Build<Store, StoreViewModel>()
        .AddProperty(store => store.Owner, vm => vm.OwnersName)
        .AddProperty(store => store.Location, vm => vm.Location)
        .Create();

The code to make this work is a little more complicated than the "names" example.

public static class ModelStateMappings
{
    public static Builder<M, VM> Build<M, VM>()
    {
        return new Builder<M, VM>();
    }

    public class Builder<M, VM>
    {
        public Builder() { }

        public Builder<M, VM> AddProperty<T>(
            Expression<Func<M, T>> domainMap,
            Expression<Func<VM, T>> viewModelMap)
        {
            return new BuilderProperty<M, VM, T>(this, domainMap, viewModelMap);
        }

        public virtual Map Create()
        {
            return new Map();
        }
    }

    public class BuilderProperty<M, VM, T> : Builder<M, VM>
    {
        private Builder<M, VM> _previousBuilder;
        private Expression<Func<M, T>> _domainMap;
        private Expression<Func<VM, T>> _viewModelMap;

        public BuilderProperty(
            Builder<M, VM> previousBuilder,
            Expression<Func<M, T>> domainMap,
            Expression<Func<VM, T>> viewModelMap)
        {
            _previousBuilder = previousBuilder;
            _domainMap = domainMap;
            _viewModelMap = viewModelMap;
        }

        public override Map Create()
        {
            var map = _previousBuilder.Create();
            /* code to add current map to Map class */
            return map;
        }
    }
}

The other advantage to this type of builder is that you also maintain strongly-typed property fields.

Of course you would need to put in the correct code for your mapping in the Create method.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top