How to create a reusable mapping profile with Mapster?
Asked Answered
H

5

5

I have a .Net 5 Web Api project and want to use

Mapster v7.2.0

to avoid mapping objects manually. The following code shows a sample scenario

  • setup a mapping configuration
  • map from multiple sources
  • map to fields with different names

.

[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
    [HttpGet]
    public ActionResult<UsernameWithTodoTitle> Get()
    {
        TypeAdapterConfig<(User, Todo), UsernameWithTodoTitle>
            .NewConfig()
            .Map(dest => dest, src => src.Item1) // map everything from user
            .Map(dest => dest, src => src.Item2) // map everything from todo
            .Map(dest => dest.TodoTitle, src => src.Item2.Title); // map the special fields from todo
        
        var user = new User { Username = "foo", FieldFromUser = "x" };
        var todo = new Todo { Title = "bar", FieldFromTodo = "y" };
        
        var usernameWithTodoTitle = (user, todo).Adapt<(User, Todo), UsernameWithTodoTitle>();
        
        return Ok(usernameWithTodoTitle);
    }
}

public class User
{
    public string Username { get; set; }
    public string FieldFromUser { get; set; }
}

public class Todo
{
    public string Title { get; set; } // !! map this one to the TodoTitle field !!
    public string FieldFromTodo { get; set; }
}

public class UsernameWithTodoTitle
{
    public string Username { get; set; }
    public string TodoTitle { get; set; } // !! this one is special, is has a different name !!
    public string FieldFromUser { get; set; }
    public string FieldFromTodo { get; set; }
}

When running the app the mapping seems to work fine this way

enter image description here

I had to setup the configuration this way, other ways didn't work for me. But there are 3 things left to be solved

  • The configuration looks wrong to me. It maps everything from the todo and maps the special field again ... so it might loop through multiple times? This might get expensive, if there are multiple fields with different names
  • I created the configuration inside the controller. How can I create a reusable mapping profile class registered once globally?
  • When having a mapping profile this line var usernameWithTodoTitle = (user, todo).Adapt<(User, Todo), UsernameWithTodoTitle>(); looks quite messy to me. Better would be var usernameWithTodoTitle = UsernameWithTodoTitle.Adapt((user, todo)) /* pass in as a tuple */ because based on the parameter type it chooses the correct mapping profile

Do you guys have any ideas how to create such a mapping profile?

Husain answered 19/8, 2021 at 19:58 Comment(3)
can you add your automapper sample which you want to be like?Probable
@AliZeinali sorry, just to clarify => I'm using Mapster instead of Automapper. Automapper is not able to map from multiple sources #21413773Husain
I added a pseudo implementation ... hope it helpsHusain
H
3

Updated: Couldn't find way to do what you are trying to do with Mapster, but here is an example of it working with Automapper.

using AutoMapper;
using System;

namespace ConsoleApp5
{
    class A { public string FirstName { get; set; } }

    public class B { public string Address1 { get; set; } }

    public class C
    {
        public string FirstName { get; set; }
        public string Address1 { get; set; }
    }

    public class DemoProfile : Profile
    {
        public DemoProfile()
        {
            CreateMap<(A, B), C>()
                .ForMember(dest=> dest.FirstName, opts => opts.MapFrom(src => src.Item1.FirstName))
                .ForMember(dest => dest.Address1, opts => opts.MapFrom(src => src.Item2.Address1));
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var config = new MapperConfiguration(cfg => {
                cfg.AddProfile<DemoProfile>();
            });

            var mapper = config.CreateMapper();
            var destination = mapper.Map<C>((new A {  FirstName = "Test" }, new B { Address1 = "Addr" }));

            Console.ReadKey();
        }
    }
}

Hey I haven't used Mapster before till now but here is what I gather. It is very specific about the type of tuple you use Tuple<T1,T2> over (T1,T2) but aside from that minor thing I was able to get it running and mapping without issues. Here is a small console example as example.

using Mapster;
using System;

namespace ConsoleApp5
{
    class A { public string FirstName { get; set; } }

    public class B { public string Address1 { get; set; } }

    public class C
    {
        public string FirstName { get; set; }
        public string Address1 { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Option 1
            TypeAdapterConfig<Tuple<A, B>, C>.NewConfig()
                .Map(dest => dest.FirstName, src => src.Item1.FirstName)
                .Map(dest => dest.Address1, src => src.Item2.Address1);

            var destObject = new Tuple<A, B>(new A { FirstName = "Test" }, new B { Address1 = "Address 1" })
                .Adapt<Tuple<A, B>, C>();

            // Option 2
            TypeAdapterConfig<(A, B), C>.NewConfig()
                .Map(dest => dest.FirstName, src => src.Item1.FirstName)
                .Map(dest => dest.Address1, src => src.Item2.Address1);

            var destObject2 = (new A { FirstName = "Test" }, new B { Address1 = "Address 1" })
                .Adapt<(A, B), C>();

            Console.ReadKey();
        }
    }
}
Hogg answered 28/8, 2021 at 5:58 Comment(4)
thanks for your help. I played around and updated my question but unfortunately there are some problems left to be solved...Husain
I didn't find any reference to a profile like in Automapper in their documentation, but you can create a base class or interface and write an extension to load them on startup.Hogg
Added a reference for profile and tuple for Automapper hope that helps. Think you might be able to do it with Mapster, but it will probably require more work from your end.Hogg
not exactly what I was looking for but it's fine :) I posted a solution for the given problem based on your answer https://mcmap.net/q/1905839/-how-to-create-a-reusable-mapping-profile-with-mapsterHusain
V
8

I managed to do it with Mapster. What I did was

in Startup.cs

public void ConfigureServices(IServiceCollection services)
{
   // Some other magical code

   // Tell Mapster to scan this assambly searching for the Mapster.IRegister
   // classes and execute them
   TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly());
}

Create another class like this

using Mapster;

namespace Your.Cool.Namespace
{
    public class MappingConfig : IRegister
    {
        public void Register(TypeAdapterConfig config)
        {
            // Put your mapping logic here
            config
                .NewConfig<MySourceType, MyDestinyType>()
                .Map(dest => dest.PropA, src => src.PropB);
        }
    }
}

The key part is using TypeAdapterConfig.GlobalSettings, which is a static public singleton used by Mapster to hold the mappig config. If you do what Jack suggests, it will be a complety new TypeAdapterConfig and not the actual one being used by Mapster and won't work (at least it didn't for me).

On your unit tests remember to load the mapping profile too

[AssemblyInitialize] // Magic part 1 ~(˘▾˘~)
public static void AssemblyInitialization(TestContext testContext)
{
    // Magic part 2 (~˘▾˘)~
    TypeAdapterConfig.GlobalSettings.Scan(AppDomain.CurrentDomain.GetAssemblies());
}
Voyles answered 6/10, 2022 at 18:50 Comment(0)
H
3

Updated: Couldn't find way to do what you are trying to do with Mapster, but here is an example of it working with Automapper.

using AutoMapper;
using System;

namespace ConsoleApp5
{
    class A { public string FirstName { get; set; } }

    public class B { public string Address1 { get; set; } }

    public class C
    {
        public string FirstName { get; set; }
        public string Address1 { get; set; }
    }

    public class DemoProfile : Profile
    {
        public DemoProfile()
        {
            CreateMap<(A, B), C>()
                .ForMember(dest=> dest.FirstName, opts => opts.MapFrom(src => src.Item1.FirstName))
                .ForMember(dest => dest.Address1, opts => opts.MapFrom(src => src.Item2.Address1));
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var config = new MapperConfiguration(cfg => {
                cfg.AddProfile<DemoProfile>();
            });

            var mapper = config.CreateMapper();
            var destination = mapper.Map<C>((new A {  FirstName = "Test" }, new B { Address1 = "Addr" }));

            Console.ReadKey();
        }
    }
}

Hey I haven't used Mapster before till now but here is what I gather. It is very specific about the type of tuple you use Tuple<T1,T2> over (T1,T2) but aside from that minor thing I was able to get it running and mapping without issues. Here is a small console example as example.

using Mapster;
using System;

namespace ConsoleApp5
{
    class A { public string FirstName { get; set; } }

    public class B { public string Address1 { get; set; } }

    public class C
    {
        public string FirstName { get; set; }
        public string Address1 { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Option 1
            TypeAdapterConfig<Tuple<A, B>, C>.NewConfig()
                .Map(dest => dest.FirstName, src => src.Item1.FirstName)
                .Map(dest => dest.Address1, src => src.Item2.Address1);

            var destObject = new Tuple<A, B>(new A { FirstName = "Test" }, new B { Address1 = "Address 1" })
                .Adapt<Tuple<A, B>, C>();

            // Option 2
            TypeAdapterConfig<(A, B), C>.NewConfig()
                .Map(dest => dest.FirstName, src => src.Item1.FirstName)
                .Map(dest => dest.Address1, src => src.Item2.Address1);

            var destObject2 = (new A { FirstName = "Test" }, new B { Address1 = "Address 1" })
                .Adapt<(A, B), C>();

            Console.ReadKey();
        }
    }
}
Hogg answered 28/8, 2021 at 5:58 Comment(4)
thanks for your help. I played around and updated my question but unfortunately there are some problems left to be solved...Husain
I didn't find any reference to a profile like in Automapper in their documentation, but you can create a base class or interface and write an extension to load them on startup.Hogg
Added a reference for profile and tuple for Automapper hope that helps. Think you might be able to do it with Mapster, but it will probably require more work from your end.Hogg
not exactly what I was looking for but it's fine :) I posted a solution for the given problem based on your answer https://mcmap.net/q/1905839/-how-to-create-a-reusable-mapping-profile-with-mapsterHusain
R
1

Adding this because most answers provide solutions for AutoMapper. I used to use AutoMapper, but have recently started using Mapster and never looked back.

I configure my Mapster setup in the following way:

  1. I add all my custom mappings in MapsterSettings.cs:
public class MapsterSettings
{
    public static void Configure()
    {
        TypeAdapterConfig<Src, Dest>.NewConfig()
                                    .Map(dest => dest.Prop1, src => src.Prop2);
    }
}
  1. I Call the MapsterSettings.Configure() method when configuring services in Startup.cs (or Program.cs if you are not using a Startup.cs file):
       public void ConfigureServices(IServiceCollection services)
       {
           MapsterSettings.Configure();

           // below code removed for brevity
  1. I use Mapster in the following manner:
var destList = srcList.Adapt<List<Dest>>();

I prefer Mapster because simple mappings, like from Src to Dest, are automatically handled. Only custom property mappings need to be added to MapsterSettings.Configure().

Roaster answered 18/1 at 18:15 Comment(0)
H
0

Based on @Felipe Ramos answer I wasn't able to solve it with Mapster but with Automapper. This is my solution just for the sake of completeness. Please let me know if there is a solution for Mapster!

I installed the packages

AutoMapper v10.1.1

AutoMapper.Extensions.Microsoft.DependencyInjection v8.1.1

Inside the method Startup.ConfigureServices I added the line services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

The whole code then looks like

[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
    private readonly IMapper _mapper;
    
    public MyController(IMapper mapper)
    {
        _mapper = mapper;
    }
    
    [HttpGet]
    public ActionResult<UsernameWithTodoTitle> Get()
    {
        var user = new User { Username = "foo", FieldFromUser = "x" };
        var todo = new Todo { Title = "bar", FieldFromTodo = "y" };
        var usernameWithTodoTitle = _mapper.Map<UsernameWithTodoTitle>((user, todo));
    
        return Ok(usernameWithTodoTitle);
    }
}

public class User
{
    public string Username { get; set; }
    public string FieldFromUser { get; set; }
}

public class Todo
{
    public string Title { get; set; } // !! map this one to the TodoTitle field !!
    public string FieldFromTodo { get; set; }
}

public class UsernameWithTodoTitle
{
    public string Username { get; set; }
    public string TodoTitle { get; set; } // !! this one is special, is has a different name !!
    public string FieldFromUser { get; set; }
    public string FieldFromTodo { get; set; }
}

public class UsernameWithTodoTitleMappingProfile : Profile
{
    public UsernameWithTodoTitleMappingProfile()
    {
        CreateMap<(User, Todo), UsernameWithTodoTitle>()
            .ForMember(
                destination => destination.Username,
                memberOptions => memberOptions.MapFrom(source => source.Item1.Username))
            .ForMember(
                destination => destination.TodoTitle,
                memberOptions => memberOptions.MapFrom(source => source.Item2.Title))
            .ForMember(
                destination => destination.FieldFromUser,
                memberOptions => memberOptions.MapFrom(source => source.Item1.FieldFromUser))
            .ForMember(
                destination => destination.FieldFromTodo,
                memberOptions => memberOptions.MapFrom(source => source.Item2.FieldFromTodo));
    }
}
Husain answered 29/8, 2021 at 13:55 Comment(0)
O
0

You can use next:

    var config = new TypeAdapterConfig()
    {
        RequireExplicitMapping = true,
        RequireDestinationMemberSource = true,
        Compiler = exp => exp.CompileFast()
    };

    config.Scan("Your assembly");

    services.AddSingleton(config);
    services.AddTransient<IMapper, ServiceMapper>();

    public class RegisterConfig : IRegister
    {
        public void Register(TypeAdapterConfig config)
        {
            config.NewConfig<TSource, TDestination>();
        }
    }

Where services is IServiceCollection

Orthopedic answered 24/12, 2021 at 15:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.