11

I'm learning C#.NET Core and trying to create DTO mapping without using AutoMapper as I'm working on a small project alone and want to understand fundamental before using extra packages, surpringly I could not easily find answer at stackoverflow.com or I may use wrong keyword searching.

BTW, below is my code which I successfully map to EmployeeForShortDto under GetEmployee method. Unfortunately, I don't how to map it under GetAllEmployee just because the return data is a collection, not a single record. Please advice.

EmployeeController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NetCoreWebApplication1.Dto;
using NetCoreWebApplication1.Repository;
using NetCoreWebApplication1.Other;

namespace NetCoreWebApplication1.Controller
{
    [Route("api/[controller]")]
    [ApiController]
    public class EmployeeController : ControllerBase
    {
        private readonly IMasterRepository _repo;

        public EmployeeController(IMasterRepository repo)
        {
            _repo = repo;
        }

        [HttpGet("{id}")]
        public async Task<IActionResult> GetEmployee(int id)
        {
            var data = await _repo.GetEmployee(id);
            if (data == null) return NotFound();
            var dataDto = new EmployeeForShortDto()
            {
                Id = data.Id,
                EmpCode = data.EmpCode,
                Fname = data.Fname,
                Lname = data.Lname,
                Age = NetCoreWebApplication1.Other.Extension.CalcAge(data.DateBirth)
            };

            return Ok(dataDto);
        }

        [HttpGet]
        public async Task<IActionResult> GetAllEmployee()
        {
            var data = await _repo.GetAllEmployee();
            return Ok(data);
        }

    }
}

MasterRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NetCoreWebApplication1.Models;

namespace NetCoreWebApplication1.Repository
{
    public class MasterRepository : IMasterRepository
    {
        private readonly PrDbContext _context;

        public MasterRepository(PrDbContext context)
        {
            _context = context;
        }


        // Employee
        public async Task<List<Employee>> GetAllEmployee()
        {
            var data = await _context.Employee.ToListAsync();
            return data;
        }

        public async Task<Employee> GetEmployee(int id)
        {
            var data = await _context.Employee.FirstOrDefaultAsync(x => x.Id == id);
            return data;
        }

        // Generic methods
        public void Add<T>(T entity) where T : class
        {
            _context.Add(entity);
        }

        public void Delete<T>(T entity) where T : class
        {
            _context.Remove(entity);
        }

        public async Task<bool> SaveAll()
        {
            return await _context.SaveChangesAsync() > 0;
        }
    }
}
Kirk Larkin
  • 84,915
  • 16
  • 214
  • 203
user10268539
  • 149
  • 1
  • 1
  • 11
  • FYI you should make your GetAllEmployee and GetEmployee generic like the rest. I think your only option is to write a for loop which maps it into a collection of the type needed. Manual and painful. – Seabizkit Aug 24 '18 at 09:15

4 Answers4

17

You can use an extension method to map from your entity type to your DTO type.

public static EmployeeForShortDto ToDto(this Employee employee)
{
    if (employee != null)
    {
        return new EmployeeForShortDto
        {
            Id = employee.Id,
            EmpCode = employee.EmpCode,
            Fname = employee.Fname,
            Lname = employee.Lname,
            Age = NetCoreWebApplication1.Other.Extension.CalcAge(employee.DateBirth)
        };
    }

    return null;
}

And then use where needed.

[HttpGet("{id}")]
public async Task<IActionResult> GetEmployee(int id)
{
    var data = await _repo.GetEmployee(id);

    if (data == null) 
    {
        return NotFound();
    }

    return Ok(data.ToDto());
}

[HttpGet]
public async Task<IActionResult> GetAllEmployee()
{
    var data = await _repo.GetAllEmployee();

    return Ok(data.Select(x => x.ToDto()));
}
Brad
  • 4,493
  • 2
  • 17
  • 24
3

For your problem, extract your implementation in a new method.

EmployeeForShortDto ConvertToDto(Employee data)
{
 var dataDto = new EmployeeForShortDto()
        {
            Id = data.Id,
            EmpCode = data.EmpCode,
            Fname = data.Fname,
            Lname = data.Lname,
            Age = NetCoreWebApplication1.Other.Extension.CalcAge(data.DateBirth)
        };
}

Then finally call it in loop,

 foreach(Employee e in EmployeeList)
    { 
       dtoList.Add(ConvertToDto(e));
    }

For generic implementation, Generate a list of properties of Model and Dto via reflection. and then match their types.

class AdapterHelper<T1, T2>
{
    public T1 Adapt(T2 source)
    {
        T1 targetItem = Activator.CreateInstance<T1>();
        var props = typeof(T1).GetProperties();
        var targetProps = typeof(T2).GetProperties();
        foreach (var prop in props)
        {
            foreach (var targetProp in targetProps)
            {
                if (prop.Name == targetProp.Name)
                {
                    targetProp.SetValue(targetItem, prop.GetValue(source));
                    //assign

                }
            }
        }
        return targetItem;
    }
}

This is the link to my original answer.

Code Name Jack
  • 2,856
  • 24
  • 40
3

thank you for all responses, all are very useful to me. Finally, I end up with solution from @Brad. I also learned how to make a reverse mapping from DTO to a class before adding a record to the database.

I put my code below in case someone want to see. Any comments/suggestions are more than welcome. Thank you.

Extension.cs

using NetCoreWebApplication1.Dto;
using NetCoreWebApplication1.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace NetCoreWebApplication1.Other
{
    public static class Extension
    {
        public static EmployeeForShortDto MapToEmployeeForShortDto(this Employee emp)
        {
            if (emp != null)
            {
                return new EmployeeForShortDto
                {
                    Id = emp.Id,
                    EmpCode = emp.EmpCode,
                    Fname = emp.Fname,
                    Lname = emp.Lname,
                    Age = emp.DateBirth.CalcAge()
                };
            }

            return null;
        }

        public static EmployeeForListDto MapToEmployeeForListDto(this Employee emp)
        {
            if (emp != null)
            {
                return new EmployeeForListDto
                {
                    Id = emp.Id,
                    EmpCode = emp.EmpCode,
                    Fname = emp.Fname,
                    Lname = emp.Lname,
                    Age = emp.DateBirth.CalcAge(),
                    EntityCode = emp.EntityCode,
                    IsActive = emp.IsActive
                };
            }

            return null;
        }

        public static Employee MapFromEmployeeForAddDto(this EmployeeForAddDto emp)
        {
            if (emp != null)
            {
                return new Employee
                {
                    EmpCode = emp.EmpCode,
                    Fname = emp.Fname,
                    Lname = emp.Lname,
                    IdCard = emp.IdCard,
                    IsActive = 1
                };
            }

            return null;
        }

        public static int CalcAge(this DateTime? dateBirth)
        {
            if (dateBirth.HasValue)
            {
                var age = DateTime.Today.Year - dateBirth.Value.Year;
                if (dateBirth.Value.AddYears(age) > DateTime.Today) age--;
                return age;
            }
            else
            {
                return 0;
            }
        }
    }
}

MasterRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NetCoreWebApplication1.Dto;
using NetCoreWebApplication1.Models;

namespace NetCoreWebApplication1.Repository
{
    public class MasterRepository : IMasterRepository
    {
        private readonly PrDbContext _context;

        public MasterRepository(PrDbContext context)
        {
            _context = context;
        }


        // Employee
        public async Task<List<Employee>> GetAllEmployee()
        {
            var data = await _context.Employee.ToListAsync();
            return data;
        }

        public async Task<Employee> GetEmployee(int id)
        {
            var data = await _context.Employee.FirstOrDefaultAsync(x => x.Id == id);
            return data;
        }

        public async Task<Employee> AddEmployee(Employee data)
        {
            await _context.Employee.AddAsync(data);
            await _context.SaveChangesAsync();
            return data;
        }

        public async Task<bool> EmployeeExists(string entityCode, string empCode)
        {
            if (await _context.Employee.AnyAsync(x =>
                x.EntityCode == entityCode &&
                x.EmpCode == empCode))
                return true;

            return false;
        }

        // Generic methods
        public void Add<T>(T entity) where T : class
        {
            _context.Add(entity);
        }

        public void Delete<T>(T entity) where T : class
        {
            _context.Remove(entity);
        }

        public async Task<bool> SaveAll()
        {
            return await _context.SaveChangesAsync() > 0;
        }
    }
}

EmployeeController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NetCoreWebApplication1.Dto;
using NetCoreWebApplication1.Repository;
using NetCoreWebApplication1.Other;
using NetCoreWebApplication1.Models;

namespace NetCoreWebApplication1.Controller
{
    [Route("api/[controller]")]
    [ApiController]
    public class EmployeeController : ControllerBase
    {
        private readonly IMasterRepository _repo;

        public EmployeeController(IMasterRepository repo)
        {
            _repo = repo;
        }

        [HttpPost("add")]
        public async Task<IActionResult> AddEmployee(EmployeeForAddDto emp)
        {
            if (await _repo.EmployeeExists(emp.EntityCode, emp.EmpCode))
                ModelState.AddModelError("Employee", "Employee is duplicate (EntityCode + EmpCode)");

            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            Employee employeeToAdd = emp.MapFromEmployeeForAddDto();

            await _repo.AddEmployee(employeeToAdd);

            return StatusCode(201);
        }


        [HttpGet("{id}")]
        public async Task<IActionResult> GetEmployee(int id)
        {
            var data = await _repo.GetEmployee(id);

            if (data == null) return NotFound();

            return Ok(data.MapToEmployeeForShortDto());
        }

        [HttpGet]
        public async Task<IActionResult> GetAllEmployee()
        {
            var data = await _repo.GetAllEmployee();

            //var dataDto = data.Select(x => x.MapToEmployeeForShortDto());
            var dataDto = data.Select(x => x.MapToEmployeeForListDto());

            return Ok(dataDto);
        }

    }
}
user10268539
  • 149
  • 1
  • 1
  • 11
2

Okay, the direct answer to your question is "do it pr returned values";

List<EmployeeForShortDto> result = new List<EmployeeForShortDto>();
foreach(Employee dbEmployee in data )
{
 result.add(new EmployeeForShortDto()
            {
                Id = dbEmployee .Id,
                EmpCode = dbEmployee .EmpCode,
                Fname = dbEmployee .Fname,
                Lname = dbEmployee .Lname,
                Age = NetCoreWebApplication1.Other.Extension.CalcAge(dbEmployee .DateBirth)
            });
}

This however, is type specific for your item. Why not make a generic method that uses reflection to map the object either by attributes attached, or by property name directly? If you get it done, you will be able to transfer any object to a DTO, as long as you adhere to the internal rules of property names or setup up the mappings via the attributes.

Morten Bork
  • 1,413
  • 11
  • 23
  • 1
    The problem with creating your own generic mapper is handling complex models. "Don't reinvent the wheel unless you plan on learning more about wheels". :/ – jegtugado Aug 24 '18 at 09:32
  • @JohnEphraimTugado Which he explicitly tells us he is trying to do. He does not want to use a third party mapper, he wants to learn the process before he uses a component. Thus doing what the component is doing is the aim of the entire ordeal he is going through. – Morten Bork Aug 24 '18 at 09:46
  • Thanks for your response, it's very useful for me. And yes, I want to learn a manual process before realizing the benefits of using 3rd party. Thanks for sharing, guys. – user10268539 Aug 24 '18 at 12:44