AutoMapper: mapping between IDataReader and DTO object

I have a DataReader that contains the result of a stored procedure. The naming convention for columns uses underscores for spaces.

I managed to successfully match between IDataReader and IEnumerable, but only if the fields match exactly. I do not want the naming convention used in stored procedures to determine the way I name fields in my objects. The same can be said about the database side. I do not think that I will be able to apply Pascal Case to database administrators.

I would like to avoid using the forMember () foreach field that I need to display. This will violate the purpose of using AutoMapper.

I found a previous post on a topic that I used as a reference in my tests. I could not get the correct configuration / mapping for the test to pass successfully. I hope someone can help.

public class DataReaderTests { private DTOObject _result; private IDataReader _dataReader; protected override void Establish_context() { Mapper.Initialize(cfg => { cfg.SourceMemberNamingConvention = new LowerUnderscoreNamingConvention(); cfg.DestinationMemberNamingConvention = new PascalCaseNamingConvention(); cfg.CreateMap<IDataReader, IEnumerable<DTOObject>>(); }); _dataReader = new DataBuilder().BuildDataReader(); _result = Mapper.Map<IDataReader, IEnumerable<DTOObject>>(_dataReader).FirstOrDefault(); } [Test] public void Then_a_column_containing_phone_number_should_be_read() { Assert.That(_result.PhoneNumber, Is.EqualTo(_dataReader[FieldName.PhoneNumber])); } } public class DataBuilder { public IDataReader BuildDataReader() { var resultData = new DataTable(); resultData.Columns.Add(FieldName.PhoneNumber, typeof(string)); var resultDataRow = resultData.NewRow(); resultDataRow[FieldName.PhoneNumber] = "111-222-3333"; resultData.Rows.Add(resultDataRow); return resultData.CreateDataReader(); } } internal class FieldName { public const String Id = "id"; public const String Name = "name"; public const String PhoneNumber = "phone_number"; public const String CreateDate = "create_date"; } public class DTOObject { public Guid Id { get; set; } public string Name { get; set; } public string PhoneNumber { get; set; } public DateTime CreatedDate { get; set; } } 
+4
c # automapper
source share
2 answers

To do this, we created custom attributes. We do the mapping-assignment using reflection, and here is an example code.

An attribute applied to the properties of a Business object for coloumn mapping.

 /// <summary> /// Holds mapping information between business objects properties and database table fields. /// </summary> [global::System.AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] public sealed class DataFieldMappingAttribute : Attribute { /// <summary> /// Initializes a new instance of the DataFieldMappingAttribute class. /// </summary> /// <param name="fieldName">Name of the Field in Database Table that the business object properties maps to.</param> public DataFieldMappingAttribute(string fieldName) { this.MappedField = fieldName; } /// <summary> /// Gets or Sets the mapped Database Table Field. /// </summary> public string MappedField { get; private set; } } 

A sample business object will look like this in my application.

User.cs

 [TableMapping("Users")] public class User : EntityBase { #region Constructor(s) public AppUser() { BookCollection = new BookCollection(); } #endregion #region Properties #region Default Properties - Direct Field Mapping using DataFieldMappingAttribute private System.Int32 _UserId; private System.String _FirstName; private System.String _LastName; private System.String _UserName; private System.Boolean _IsActive; [DataFieldMapping("UserID")] [DataObjectFieldAttribute(true, true, false)] [NotNullOrEmpty(Message = "UserID From Users Table Is Required.")] public override int Id { get { return _UserId; } set { _UserId = value; } } [DataFieldMapping("UserName")] [Searchable] [NotNullOrEmpty(Message = "Username Is Required.")] public string UserName { get { return _UserName; } set { _UserName = value; } } [DataFieldMapping("FirstName")] [Searchable] public string FirstName { get { return _FirstName; } set { _FirstName = value; } } [DataFieldMapping("LastName")] [Searchable] public string LastName { get { return _LastName; } set { _LastName = value; } } [DataFieldMapping("IsActive")] public bool IsActive { get { return _IsActive; } set { _IsActive = value; } } #region One-To-Many Mappings public BookCollection Books { get; set; } #endregion #region Derived Properties public string FullName { get { return this.FirstName + " " + this.LastName; } } #endregion #endregion public override bool Validate() { bool baseValid = base.Validate(); bool localValid = Books.Validate(); return baseValid && localValid; } } 

BookCollection.cs

 /// <summary> /// The BookCollection class is designed to work with lists of instances of Book. /// </summary> public class BookCollection : EntityCollectionBase<Book> { /// <summary> /// Initializes a new instance of the BookCollection class. /// </summary> public BookCollection() { } /// <summary> /// Initializes a new instance of the BookCollection class. /// </summary> public BookCollection (IList<Book> initialList) : base(initialList) { } } 

Here's the transaction method DataRow to BusinessObject, which is wrapped around a call to the extension method.

  /// <summary> /// Transforms DataRow into business object. /// </summary> /// <typeparam name="TEntity">A type that inherits EntityBase.</typeparam> /// <typeparam name="TDataRow">A type that inherits DataRow.</typeparam> /// <param name="dataRow">DataRow object which is transformed from business object.</param> /// <param name="entity">business object which is transformed into DataRow object.</param> public static void TransformDataRowToEntity<TEntity, TDataRow>(ref TDataRow dataRow, ref TEntity entity) where TDataRow : DataRow where TEntity : EntityBase { IQueryable<DataField> entityFields = entity.GetDataFields(); foreach (var entityField in entityFields) { if (dataRow[entityField.DataFieldMapping.MappedField] is System.DBNull) { entityField.Property.SetValue(entity, null, null); } else { if (entityField.Property.GetType().IsEnum) { Type enumType = entityField.Property.GetType(); EnumConverter enumConverter = new EnumConverter(enumType); object enumValue = enumConverter.ConvertFrom(dataRow[entityField.DataFieldMapping.MappedField]); entityField.Property.SetValue(entity, enumValue, null); } else { entityField.Property.SetValue(entity, dataRow[entityField.DataFieldMapping.MappedField], null); } } } } 
+3
source share

I downloaded the AutoMapper source and was able to do some debugging. I had to change the CreateBuilder method in DataReaderMapper.cs to pass the test.

  private static Build CreateBuilder(Type destinationType, IDataRecord dataRecord) { var method = new DynamicMethod("DynamicCreate", destinationType, new[] { typeof(IDataRecord) }, destinationType, true); var generator = method.GetILGenerator(); var result = generator.DeclareLocal(destinationType); generator.Emit(OpCodes.Newobj, destinationType.GetConstructor(Type.EmptyTypes)); generator.Emit(OpCodes.Stloc, result); for (var i = 0; i < dataRecord.FieldCount; i++) { var propertyInfo = destinationType.GetProperty(ConvertLowerUnderscoreNamingToPascalNaming(dataRecord.GetName(i))); var endIfLabel = generator.DefineLabel(); if (propertyInfo != null && propertyInfo.GetSetMethod(true) != null) { generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldc_I4, i); generator.Emit(OpCodes.Callvirt, isDBNullMethod); generator.Emit(OpCodes.Brtrue, endIfLabel); generator.Emit(OpCodes.Ldloc, result); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldc_I4, i); generator.Emit(OpCodes.Callvirt, getValueMethod); generator.Emit(OpCodes.Unbox_Any, dataRecord.GetFieldType(i)); generator.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod(true)); generator.MarkLabel(endIfLabel); } } generator.Emit(OpCodes.Ldloc, result); generator.Emit(OpCodes.Ret); return (Build)method.CreateDelegate(typeof(Build)); } //TODO: refactor to use INamingConvetion and resolve with RegEx pattern private static string ConvertLowerUnderscoreNamingToPascalNaming(string original) { var LowerOriginal = original.ToLower(); string[] tokens = LowerOriginal.Split('_'); string converted = ""; foreach (var token in tokens) converted += token.Substring(0, 1).ToUpper() + token.Substring(1); return converted; } 
0
source share

All Articles