The Dynamic Mapping Library is a lightweight, high-performance, and extensible library designed to simplify data transformations between different structures.
- Dynamic Mapping Library
- I tried to adhere to the given signature
_mapHandler.Map(object data, string sourceType, string targetType)
. - I am uncertain whether the third-party data models will always have corresponding concrete types (such as classes or records) in our project. If third-party data models have corresponding concrete types in our project, using generic is a better option. Using generic instead of
object
will provide strong typing. - Since passed argument data is of type
object
in_mapHandler.Map(object data, string sourceType, string targetType)
, so I am returning the typeobject
as well which is not strongly typed. - Mapping logic between source and target will be provided by the developer for now.
- Pure mapping library to evaluate the thought process.
- High Performance: Designed for optimal performance avoiding reflection and assembly scanning.
- Supports Nested Structures: Works with complex, multi-level data models.
- Extensible Mapping Template: Provides an extensible template to take care of mapping logics.
- Customizable: Multple ways to provide custom mapping logic with ease.
- External Dependency: No third party packages are used.
- Test Coverage: Thoroughly validated through comprehensive unit and integration tests.
- MaxRecursionDepth : Enforced a maximum depth to limit the depth when mapping nested objects and circular references. Caution should be exercised when setting it to a large value.
Default value is 3
Add class library DynamicMap.Core
to your application.
First, specify the configuration between source and target. Second map between them.
// configuration
var mapConfiguration = new MapConfiguration()
mapConfiguration.AddMap("GoogleUser", "Dirs21User", (source, handlerContext) =>
{
var src = TypeCastUtil.CastTypeBeforeMap<GoogleUser>(source);
return new Dirs21User
{
UserId = Guid.Parse(src.UserId),
FullName = src.FirstName + " " + src.LastName
};
})
// map
var mapHandler = new MapHandler(mapConfiguration)
var dirs21User = (Dirs21User)mapHandler.Map(externalUser, "GoogleUser", "Dirs21User");
Map Strategy: Define the strategies to map between Dirs21Room
and GoogleRoom
.
public class Dirs21RoomToGoogleRoomMap : MapStrategy<Dirs21Room, GoogleRoom>
{
public override GoogleRoom Map(Dirs21Room src, IMapHandlerContext handlerContext)
{
return new GoogleRoom
{
RoomId = src.RoomId.ToString(),
RoomType = src.RoomType.ToString()
};
}
public override Dirs21Room ReverseMap(GoogleRoom target, IMapHandlerContext handlerContext)
{
return new Dirs21Room
{
RoomId = Guid.Parse(target.RoomId),
RoomType = Enum.Parse<RoomType>(target.RoomType),
};
}
}
Add Configuration: add mappers to configuration.
public class ExampleMapConfiguration : MapConfiguration
{
public ExampleMapConfiguration()
{
AddMap("Dirs21.Room",
"Google.Room",
new Dirs21RoomToGoogleRoomMap())
.AddReverseMap();
}
}
Register dynamic map: register the dynamic map dependencies using Dependency Injection
.
serviceCollection.AddDynamicMap(typeof(ExampleMapConfiguration))
Map: finally, map googleRoom
to Dirs21Room
or vise versa.
var mapHandler = serviceProvider.GetRequiredService<IMapHandler>();
var dirs21Room = (Dirs21Room)mapHandler.Map(googleRoom, "Google.Room", "Dirs21.Room");
Incase we don't want to implement third part provider's type in our project. Here our project doesn't have the data model for GoogleUser
. We want to map incoming google user data model directly to our type Dirs21User
and vise-versa.
public class Dirs21UserToGoogleUserJsonMap : MapStrategy<Dirs21User, string>
{
public override string Map(Dirs21User source, IMapHandlerContext handlerContext)
{
return new JObject
{
{ "FirstName", source.FullName.Split(" ").First() },
{ "LastName", source.FullName.Split(" ").Last() }
}.ToString();
}
public override Dirs21User ReverseMap(string target, IMapHandlerContext handlerContext)
{
var jObject = JObject.Parse(target);
return new Dirs21User
{
FullName = $"{jObject["FirstName"]} {jObject["LastName"]}"
};
}
}
Add the mapper to configuration:
AddMap("Dirs21User", "GoogleUserJsonString",new Dirs21UserToGoogleUserJsonMap())
.AddReverseMap();
Here is an example of mapping nested objects. Don't forget to set the MaxRecursionDepth
by default = 3
.
public class Dirs21ToGoogleReservationMap : MapStrategy<Dirs21Reservation, GoogleReservation>
{
public override GoogleReservation Map(Dirs21Reservation src, IMapHandlerContext handlerContext)
{
return new GoogleReservation
{
...
Room = (GoogleRoom?)handlerContext.Map(src.Dirs21Room,
"Dirs21.Room",
"Google.Room")
};
}
public override Dirs21Reservation ReverseMap(GoogleReservation dest, IMapHandlerContext handlerContext)
{
return new Dirs21Reservation
{
...
Dirs21Room = (Dirs21Room?)handlerContext.Map(dest.Room,
"Google.Room",
"Dirs21.Room")
};
}
}
Set maxRecursionDepth
in this way:
var mapConfiguration = new MapConfiguration(maxRecursionDepth: 5);
Another way, you can set it:
public class ExampleMapConfiguration : MapConfiguration
{
public ExampleMapConfiguration()
{
MaxRecursionDepth = 5;
}
}
This approach provides strong type validation and casting. Here, Dirs21RoomToGoogleRoomMap
is a class based mapper which should inherit MapStrategy
or implement IMapStrategy
.
public class Dirs21RoomToGoogleRoomMap : MapStrategy<Dirs21Room, GoogleRoom>
{
public override GoogleRoom Map(Dirs21Room src, IMapHandlerContext handlerContext)
{
return new GoogleRoom
{
RoomId = src.RoomId.ToString(),
};
}
public override Dirs21Room ReverseMap(GoogleRoom target, IMapHandlerContext handlerContext)
{
return new Dirs21Room
{
RoomId = Guid.Parse(target.RoomId),
};
}
}
You can add this mapper:
AddMap("Model.Reservation",
"Google.Reservation",
new Dirs21ToGoogleReservationMap())
With this approach, you can also configure ReverseMap
right here:
AddMap("Model.Reservation",
"Google.Reservation",
new Dirs21ToGoogleReservationMap())
.AddReverseMap();
With this approach you can quickly register a function MapToGoogleUser
of type Func<object, IMapHandlerContext ,object?>
:
public GoogleUser MapToGoogleUser(object source, IMapHandlerContext mapHandlerContext)
{
var src = TypeCastUtil.CastTypeBeforeMap<Dirs21User>(source);
return new GoogleUser
{
UserId = src.UserId.ToString(),
Email = src.Email,
FirstName = src.FullName.Split(" ").First(),
LastName = src.FullName.Split(" ").Last()
};
}
Now you can add to configuration:
....
AddMap("Model.User",
"Google.User", MapToGoogleUser);
Currently, we are not supporting ReverseMap
out of the box with this approach since you can just do AddMap("Google.User", "Model.User", MapToModelUser);
- MaxRecursionDepth
ArgumentException
: 1 <= value <= 100
- AddMap
MappingRulesAlreadyExistsException
- Map
MappingRulesNotFoundException
,NullMappingResultException
,InvalidCastException
- DynamicMap.Core: Holds the core skeleton and logics of mapping library.
- DynamicMap.Examples: Depends on the
DynamicMap.Core
layer to use it's mapping capablities. - DynamicMap.Tests: Depends on both
DynamicMap.Core
andDynamicMap.Examples
to tests the functionalities.
- MapConfiguration: A configuration store that provides interface to add and retrieve map configuration.
- MapHandler: Provides a method to map an object from a specified source type to a target type.
- MapHandlerContext: Manage map execution context and recursion while mapping. It also provides a method to handle nested map.
- MapStrategy<TSource, TTarget>: A template to provide mapping logics between source and target. Strategy pattern is followd here.
Designing a map library takes a lot thoughts and time. Due to time time constraint, everything is not taken care of. We can focus on following in the future.
- Implement an approach to map properties automatically.
- Have separate config meta data for each map rule. For example
MaxRecursionDepth
is same for all mapping rules now.