Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#
Tip/Trick

A Third Incarnation of DiponRoy's Simple Model/Entity Mapper in C#

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
5 Feb 2022CPOL2 min read 4.7K   49   3   4
Some code tweaks including the ability to alias a mapped property name
This third rework of CPian DiponRoy's Tip & Trick article Simple Model/Entity Mapper in C# published Sept 2, 2014 makes a couple useful changes. Moreover, using something like AutoMapper is overkill.

Introduction

This is a third rework of CPian DiponRoy's Tip & Trick article Simple Model/Entity Mapper in C# published Sept 2, 2014. A revision was posted by Cpian ThiagoTane on March 12, 2015. And of course, if you want the whole kettle of fish including generated IL code for optimization, there is AutoMapper on GitHub.

The purpose of my revision to the two previous articles on Code Project is because I wanted to make a couple useful changes and using something like AutoMapper is overkill. While I appreciate the IL generation, I really don't want to have to register my maps with CreateMap in the MapperConfiguration object. I just want to map two objects something when needed, with a couple bells, no whistles. A theme in my life seems to be the KISS principle which is why I end up rolling my own so often!

Code Changes

There are three changes I made to the code:

First, the mapping method CreateMapped determines the source type from the this object, so instead of writing:

C#
Student source = new Student() { Id = 1, Name = "Smith" };
StudentLog newMapped = source.CreateMapped<Student, StudentLog>();

one can write:

C#
Student source = new Student() { Id = 1, Name = "Smith" };
StudentLog newMapped = source.CreateMapped<StudentLog>();

Notice that the removal of the generic parameter Student.

Second, I added an attribute MapperPropertyAttribute that is used to specify the source property when the target property is of a different name.

For example, I have a class User:

C#
public class User
{
  public int Id { get; set; }
  public string UserName { get; set; }
  public string Password { get; set; }
  public string Salt { get; set; }
  public string AccessToken { get; set; }
  public string RefreshToken { get; set; }
  public bool IsSysAdmin { get; set; }
  public DateTime? LastLogin { get; set; }
  public int? ExpiresIn { get; set; }
  public long? ExpiresOn { get; set; }
  public bool Deleted { get; set; }
}

but I want the login response to return a subset of properties with different names. The MapperProperty is used to specify the property name conversion in the target class:

C#
public class LoginResponse
{
  [MapperProperty(Name = "AccessToken")]
  public string access_token { get; set; }

  [MapperProperty(Name = "RefreshToken")]
  public string refresh_token { get; set; }

  [MapperProperty(Name = "ExpiresIn")]
  public int expires_in { get; set; }

  [MapperProperty(Name = "ExpiresOn")]
  public long expires_on { get; set; }

  public string token_type { get; set; } = "Bearer";
}

An example use case snippet is:

C#
var response = user.CreateMapped<LoginResponse>();

Third, I renamed the variable names in some places.

Implementation

The attribute is simple:

C#
public class MapperPropertyAttribute : Attribute
{
  public string Name { get; set; }

  public MapperPropertyAttribute() { }
}

The extension method has been modified to provide two public methods which share a common private implementation.

C#
public static class MapExtensionMethods
{
  public static TTarget MapTo<TSource, TTarget>(this TSource source, TTarget target)
  {
    var ret = MapTo(source.GetType(), source, target);

    return ret;
  }

  public static TTarget CreateMapped<TTarget>(this object source) where TTarget : new()
  {
    return MapTo(source.GetType(), source, new TTarget());
  }

  private static TTarget MapTo<TTarget>(Type tSource, object source, TTarget target)
  {
    const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | 
                               BindingFlags.NonPublic;

    var srcFields = (from PropertyInfo aProp in tSource.GetProperties(flags)
        where aProp.CanRead //check if prop is readable
        select new
        {
            Name = aProp.Name,
            Alias = (string)null,
            Type = Nullable.GetUnderlyingType(aProp.PropertyType) ?? aProp.PropertyType
        }).ToList();

    var trgFields = (from PropertyInfo aProp in target.GetType().GetProperties(flags)
        where aProp.CanWrite //check if prop is writeable
        select new
        {
            Name = aProp.Name,
            Alias = aProp.GetCustomAttribute<MapperPropertyAttribute>()?.Name,
            Type = Nullable.GetUnderlyingType(aProp.PropertyType) ?? aProp.PropertyType
        }).ToList();

    var commonFields = trgFields.In(srcFields, /* T1 */ t => t.Alias ?? 
                                    t.Name, /* T2 */ t => t.Name).ToList();

    foreach (var field in commonFields)
    {
      var value = tSource.GetProperty(field.Alias ?? field.Name).GetValue(source, null);
      PropertyInfo propertyInfos = target.GetType().GetProperty(field.Name);
      propertyInfos.SetValue(target, value, null);
    }

    return target;
  }
}

The "secret sauce" is the addition of the Alias property in the anonymous object being returned by the select statement and the null resolution operator ?? to determine whether to use the aliased name or the property name for the source property. One other interesting thing is that since these are anonymous properties, assigning Alias to null requires casting the null: Alias = (string)null, to a string. Not something you often see.

What's That "In" Extension Method?

Unfortunately, Linq's IntersectBy is only available in .NET 6, so I have my own extension method altered from a code example courtesy of a comment posted by CPian Mr.PoorInglish to another article that I wrote. 

C#
// See Mr.PoorInglish's rework of my article here:
// https://www.codeproject.com/Articles/5293576/A-Performant-Items-in-List-A-that-are-not-in-List?msg=5782421#xx5782421xx
public static IEnumerable<T1> In<T1, T2, TKey>(
  this IEnumerable<T1> items1,
  IEnumerable<T2> items2,
  Func<T1, TKey> keySelector1, Func<T2, TKey> keySelector2)
  {
    var dict1 = items1.ToDictionary(keySelector1);
    var k1s = dict1.Keys.Intersect(items2.Select(itm2 => keySelector2(itm2)));
    var isIn = k1s.Select(k1 => dict1[k1]);

  return isIn;
}

Furthermore, .NET 6 implementation of IntersectedBy is really not the signature I want, and I don't want to implement an iEqualityComparer so we'll go with the extension method above.

A Simple Test Program

The download for this article has a sample program you can run that demonstrates this version of the mapper:

C#
public static void Main()
{
  // We declare the epoch to be 1/1/1970.
  var ts = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
  var expiresSeconds = 24 * 60 * 60;

  var user = new User()
  {
    Id = 1,
    UserName = "fubar",
    Password = "fizbin",
    Salt = "pepper",
    AccessToken = Guid.NewGuid().ToString(),
    RefreshToken = Guid.NewGuid().ToString(),
    ExpiresIn = expiresSeconds,
    ExpiresOn = ts + expiresSeconds,
    LastLogin = DateTime.Now,
  };

  var response = user.CreateMapped<LoginResponse>();

  Console.WriteLine($"access_token: {response.access_token}");
  Console.WriteLine($"refresh_token: {response.refresh_token}");
  Console.WriteLine($"expires_in: {response.expires_in}");
  Console.WriteLine($"expires_on: {response.expires_on}");
  Console.WriteLine($"token_type: {response.token_type}");
}

Output:

access_token: 86384067-9193-449a-a6ff-8023be5fe203
refresh_token: 12e04d46-882e-4a25-a777-d1440f4783cd
expires_in: 86400
expires_on: 1644175047
token_type: Bearer

Conclusion

Not much to conclude here - all this is a third incarnation of a short and useful Tip & Trick written almost 8 years ago!

History

  • 5th February, 2022: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
QuestionThank you and a question Pin
LightTempler7-Feb-22 10:28
LightTempler7-Feb-22 10:28 
Cool stuff, as all of your articles!
Stucking on VB.NET, much too lazy to jump onto th C# train, I wonder: Is it possible to use this gem with VB.NET is the reflection magic behind 'C# only' ?

Anyway: Thank you for sharing with us!
LiTe

AnswerRe: Thank you and a question Pin
Marc Clifton8-Feb-22 4:52
mvaMarc Clifton8-Feb-22 4:52 
GeneralRe: Thank you and a question Pin
LightTempler8-Feb-22 10:06
LightTempler8-Feb-22 10:06 
QuestionKISS at work Pin
mph627-Feb-22 2:03
mph627-Feb-22 2:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.