Search code examples
c#asp.net-coreautomapperdapper

ASP.NET Core - DTO not working as expected with auto-mapper and Dapper implementation


In my ASP.NET Core 6 Web API application, I am using Dapper in connection with DTO to return results of queried database.

Response class:

public class Response<T>
{
    public T Data { get; set; }
    public bool Successful { get; set; }
    public string Message { get; set; }
    public int StatusCode { get; set; }

    public Response(int statusCode, bool success, string msg, T data, List<string> errors)
    {
        Data = data;
        Successful = success;
        StatusCode = statusCode;
        Message = msg;
    }

    public Response()
    {
    }

    public static Response<T> Success(string successMessage, T data, int statusCode = 200)
    {
        return new Response<T> { Successful = true, Message = successMessage, Data = data, StatusCode = statusCode };
    }
    public override string ToString() => JsonConvert.SerializeObject(this);
}

I have this model class:

[Table("employees")]
public class Employee
{
    public string EMPLOYEE_CODE { get; set; }
    public string EMPLOYEE_NAME { get; set; }
    public string EMPLOYEE_ADDR1 { get; set; }
    public string EMPLOYEE_ADDR2 { get; set; }
}

Then I created this DTO:

public class EmployeeDto
{
    public string EmployeeCode { get; set; }
    public string EmployeeName { get; set; }
    public string EmployeeAddr1 { get; set; }
    public string EmployeeAddr2 { get; set; }
}

Here's the mapping:

public class EmployeeMapperProfile : Profile
{
    public EmployeeMapperProfile() 
    {
        CreateMap<Employee, EmployeeDto>()
            .ForMember(
                dest => dest.EmployeeCode,
                opt => opt.MapFrom(src => src.EMPLOYEE_CODE)
            )
            .ForMember(
                dest => dest.EmployeeName,
                opt => opt.MapFrom(src => srcEMPLOYEE_NAME)
            )
            .ForMember(
                dest => dest.EmployeeAddr1,
                opt => opt.MapFrom(src => src.EMPLOYEE_ADDR1)
            )
            .ForMember(
                dest => dest.EmployeeAddr2,
                opt => opt.MapFrom(src => src.EMPLOYEE_ADDR2)
            )
            .ReverseMap();
    }
}

I did the mapping using AutoMapper as shown here:

public static class AutoMapperServiceExtension
{
    public static void ConfigureAutoMappers(this IServiceCollection services)
    {
        services.AddAutoMapper(typeof(EmployeeProfile));
    }
}

Injected it in Program.cs as shown below:

builder.Services.ConfigureAutoMappers();

MemoryCache:

public class CacheService : ICacheService
{
    private readonly IMemoryCache _memoryCache;

    public CacheService(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public T GetData<T>(string key)
    {
        try
        {
            T item = (T)_memoryCache.Get(key);
            return item;
        }
        catch (Exception e)
        {
            throw;
        }
    }
}

Then I have the repository code

public async Task<Response<IEnumerable<EmployeeDto>>> GetAllEmployeesAsync()
{
    var response = new Response<IEnumerable<EmployeeDto>>();

    using IDbConnection dbConnection = Connection;

    try
    {
        string sQuery = @"SELECT * FROM employees";
        dbConnection.Open();
        var cacheData = _cacheService.GetData<IEnumerable<EmployeeDto>>(sQuery);

        if (cacheData != null)
        {
            response.Data = cacheData;
            return response;
        }

        var expirationTime = DateTimeOffset.Now.AddHours(24.0);
        cacheData = await dbConnection.QueryAsync<EmployeeDto>(sQuery);

        response.Data = cacheData;
        response.StatusCode = 00;
        response.Successful = true;

        return response;
    }
    catch (Exception ex)
    {
        response.Message = $"Error occured: " + ex.Message;
        response.Successful = false;
        response.StatusCode = (int)HttpStatusCode.BadRequest;

        return response;
    }
    finally
    {
        dbConnection.Close();
    }
}

And finally the controller:

[Route("api/[controller]")]
[ApiController]
public class EmployeesController : ControllerBase
{
    private readonly IEmployeeRepository _employeeRepository;

    public EmployeesController(IEmployeesRepository EmployeeRepository)
    {
        _employeeRepository = employeeRepository;
    }

    [HttpGet("GetAllEmployees")]
    public async Task<ActionResult<Response<EmployeeDto>>> GetQueryEmployeeAsync()
    {
        var result = await _employeeRepository.GetAllEmployeesAsync();
        return Ok(result);
    }
}

However when I launched it in Swagger, instead of getting each field populated with data, they are null as shown below.

Swagger response:

{
  "data": [
    {
      "employeeCode": null,
      "employeeName": null,
      "employeeAddr1": null,
      "employeeAddr2": null
    },
    {
      "employeeCode": null,
      "employeeName": null,
      "employeeAddr1": null,
      "employeeAddr2": null
    }
  ],
  "successful": true,
  "statusCode": 0
}

But if I remove the DTO, and use the normal model, it populates the data just fine.

How do I correct this?


Solution

  • Assuming this is your repository class

    public class DataRepo 
    {
        private readonly IMapper _mapper;
    
        // Inject Automapper through your constructor
        public DataRepo(IMapper mapper)
        {
             _mapper = mapper;
        }
    
        public async Task<Response<IEnumerable<EmployeeDto>>> GetAllEmployeesAsync()
        {
            // removed for brevity
    
            try
            {
               // removed for brevity
    
               // changed IEnumerable<EmployeeDto> to IEnumerable<Employee>
               var cacheData = _cacheService.GetData<IEnumerable<Employee>>(sQuery);
    
               if (cacheData != null)
               {
                    // this is how you call the automapper
                    response.Data = _mapper.Map<IEnumerable<EmployeeDto>>(cacheData);
                    return response;
               }
    
               //UPDATED LINES
               var cacheData2 = await dbConnection.QueryAsync<EmployeeDTO>(sQuery);
               response.Data = cacheData2;
    
               // removed for brevity
    
               return response;
             }
             catch (Exception ex)
             {
                 //your code
             }
            finally
            {
                //your code
            }
        }
    }
    

    This is just an example. You can also inject the IMapper into your CacheService.

    What matters is, you have to call _mapper.Map