Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal - Mapster 3.0 #38

Closed
chaowlert opened this issue Jan 30, 2016 · 9 comments
Closed

Proposal - Mapster 3.0 #38

chaowlert opened this issue Jan 30, 2016 · 9 comments

Comments

@chaowlert
Copy link
Collaborator

I put this to dump my idea here and for discussion to implement new features.

1. Setting per hierarchy

Currently, Mapster can setup only per type pair. Suppose we have Student and School and those types are related each other, we need to create 2 configs for each type. The idea is we should be able to create only one config.

Suppose we create this setting:

TypeAdapterConfig<Student, StudentDTO>.NewConfig().PreserveReference(true);

The setting should apply to both Student mapping and School mapping. Anyway, if we call TypeAdapter.Adapt<School, SchoolDTO>() above setting should not be applies.

To make V3.0 compatible with V2.0, we could use global setting:

TypeAdapterConfig.GlobalSettings.SettingPerType = true;

Above setting will apply setting per type rather than setting per hierarchy.

2. IgnoreAll & IgnoreNavigationProperties & Include

Now we have Ignore to opt-out properties we wouldn't like to map. The idea is to have opt-in mapping properties. IgnoreAll will ignore all properties. IgnoreNavigationProperties will ignore all properties except primitives. And Include will include properties in mapping even that properties are ignored. This should be useful for projection where normally EF will exclude navigation properties by default and we need to call include to add properties in. We can do following setting.

TypeAdapterConfig.GlobalSettings.Default
                                .When((srcType, destType, mapType) => mapType == MapType.Projection)
                                .IgnoreNavigationProperties(true);

TypeAdapterConfig<Student, StudentDTO>.NewConfig().Include(s => s.Schools);

This IgnoreNavigationProperties & Include setting will also solve #34.

3. Navigation path

The idea is from #35. Map, Ignore, and Include method should accept navigation path, both string and expression. For example:

TypeAdapterConfig<Student, StudentDTO>.NewConfig()
                                      .Include(dest => dest.School.Instructors)
                                      .Map(dest => dest.School.Email, src => src.School.ContactInfo.Email)
                                      .Ignore(dest => dest.School.Name);

4. Inline setting

Currently, we need to separate between config and Adapt method. In some scenario, we might would like to maintain config and Adapt method together. For example:

var result = student.Adapt(config => config.ToType<StudentDTO>().Ignore("Name"));

Inline setting should apply global setting but it should not alter global setting. We can cache setting and compile result using caller information.

For projection, usage might be slightly different.

var result = context.Students.ProjectToType<StudentDTO>(setting => setting.Ignore("Name"));

5. Rename projection method (Done #40)

This should has high impact to everyone who use projection. But in VB, current To method is not fluent.

Dim result = context.Students.Project().To(Of StudentDTO)()

I think To method should be changed to ToType method.

Dim result = context.Students.Project().ToType(Of StudentDTO)()
var result = context.Students.Project().ToType<StudentDTO>();

I haven't prefer VB but I think To method naming is not correct per .NET convention. Another option is to have an alias, but I proposed to rename as it will be less confused.

6. Name mapping strategy (Done #57)

This is for resolving property name. In some cases, we might would like to match proper case to camel case (src.HelloWorld to dest.helloWorld) without manual mapping.

7. Passing run-time value (Done #55)

This is for Mapster to allow passing run-time value ie. this.User.Identity.Name to mapping context.

8. Object to Dictionary & Dictionary to Object (Done #56)

This is to allow conversion between Object and Dictionary.

9. Support dynamic

To convert from/to dynamic.

10. Preserve destination object

Currently when Mapster copy properties from source to destination object, Mapster will create new object for properties. In EF, if we create new navigation objects, EF will create new records. Mapster should preserve object, so when users copy entire object graph, EF will not create new records.

11. Match item when copy list

Continue from 10, when we copy list, Mapster will add new objects to list. EF will create new records for each item. Mapster should be able to identify objects from key and copy to that object.

@gabrielsimplicio
Copy link

+1

@centur
Copy link
Contributor

centur commented Aug 4, 2016

What do you think about this feature:

  1. Add many-to-one mapping with different overwriting strategies. Sometimes it's necessary to map multiple objects into a single DTO, e.g. we have Foo and Bar and we want to create SoBuz as a mix of properties from Foo and Bar - e.g. I want to do something like this chain
// values from barInstance will overwrite values mapped from fooInstance
SoBuz result = TypeAdapter
  .InjectFrom<Foo, SoBuz>(fooInstance)
  .InjectFrom<Bar, SoBuz>(barInstance)
  .WithBehaviourOverwrite();

// values from fooInstance will stay in result even if there is a different value from barInstance
SoBuz result = TypeAdapter.InjectFrom(fooInstance)  .InjectFrom(barInstance) .WithBehaviourFirstWriteWins();

it's possible now with double adapt like this:

SoBuz result = fooInstance.Adapt<Foo, SoBuz>();
result = barInstance.Adapt<Bar, SoBuz>(result);

but chain syntax guarantees that these two lines wont be split from eachother during refactorings and looks neat and I'm not sure how to add "strategy" with an existin

Or maybe you can advice how can I implement this as extension methods with strategy as a dynamic parameter ?

@chaowlert
Copy link
Collaborator Author

how about create extension methods AdaptFrom?

public static TDest AdaptFrom<TSrc, TDest>(this TDest dest, TSrc src)
{
    return src.Adapt(dest);
}

now you could do something like

var result = new SoBuz().AdaptFrom(barInstance)
                        .AdaptFrom(fooInstance);

NOTE: with this method last one will win. If you need first one win, you might need to create a builder class.

@chaowlert
Copy link
Collaborator Author

chaowlert commented Aug 7, 2016

@centur, sorry, I don't understand your comment.

@centur
Copy link
Contributor

centur commented Aug 7, 2016

Well, me neither now, Friday was a hard day :(... I deleted it.

I think the idea I had in mind was something like this:

Trying to understand if it'd be good (and it may be a bad idea, and it's ok ) or possible to have something like composable set of rules where user can define mapping behaviour in runtime. Gettting back to our example:

var result = new SoBuz()
                        .AdaptFrom(barInstance)
                        .AdaptFrom(fooInstance)
                        .WithExtraRuntimeRules(new []{
                             RedOnlyField<SoBuz>.ETag, 
                             IgnoreField<Foo>.Id})

As a result of such rules application - in this particular mapping SoBuz.ETag will not be changed at all, as it's marked as ReadOnly. Also Mapster will ignore Foo.Id and skip it's mapping (so we will have last value from Bar.Id if there is such field , or original one from SoBuz)

I suspect that this may be impossible because Mapster is creating and caching compiled versions of end-result functions that are used for actual mapping (is this right ?)

@chaowlert
Copy link
Collaborator Author

chaowlert commented Aug 8, 2016

This is the idea of inline mapping. It is # 4 in list above. I have idea of implementation, We can cache the compilation even it is specified inline. But I have no time to implement it.

For now, my suggestion is to clone config and cache it somewhere. For example:

var config = Cache.GetOrAdd(nameof(SoBuz), name => {
    var soBuzConfig = TypeAdapterConfig.GlobalSettings.Clone();
    soBuzConfig.ForDestinationType<SoBuz>().Ignore(dest => dest.ETag);
    soBuzConfig.ForType<Foo, SoBuz>().Ignore(dest => dest.Id);
    return soBuzConfig;
});
var result = new SoBuz().AdaptFrom(barInstance, config).AdaptFrom(fooInstance, config);

With above configuration, Cache could be global field of ConcurrentDictionary. Now you can have separate mapping for each service, and compilation is cached.

@satano
Copy link
Member

satano commented Aug 8, 2016

Hi.

About task no. 10. Is anybody thinking how would it be from user perspective? Or maybe even implementing it?

This feature is interesting for us as well. I cannot promise anything, but I could at least look at in my free time and try to do something. But it would be fine to have some "specification" - requirements what we expect and also how could it be configured etc.

Maybe we can create separate issue for that and discuss things there?

@chaowlert
Copy link
Collaborator Author

Hi @satano,

It would be great if you can help implement this feature. I'm bad in writing specification, but I will try.

If we have domain class

class Poco {
    public Foo Foo { get; set; }
}

And we map with dto.Adapt(poco), poco.Foo object reference should be the same as original object before mapping. Unless poco.Foo is null, then new object should be created. Or dto.Foo is null, then poco.Foo reference should be set to null.


To configure, here (link), ClassAdapter will always create new object and set to property. We should change its behavior based on above spec, if mapping type is MapToTarget.

To create MapToTarget expression, we can copy & adapt from here (link). Method name in linq should be GetMapToTargetFunction.

Hope this help :)

@soenneker
Copy link

@chaowlert is the include functionality you describe above in the current version of Mapster? I'm using ProjectToType, losing properties, and then am unable to add properties back to the IQueryable when using ProjectToType again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants