Post

ASP.NET Core Playground - 3. Adapting from a Model to a DTO

Previously we learned how awesome streaming the data from Entity Framework Core really is. But now we need to make the logic a little more production friendly. It’s not a good practice to pass the model directly from the endpoint. Let’s convert the data to a DTO ,and skip one column in the meantime. To keep code simple we’ll use the Mapster library, because that’s what it’s for.

As in my previous post, please don’t trust the logged times. They are here just to show you how long I had to wait for each code section to execute, and are not really comparable between different scenarios.

Adapting to Array

As usual we start with the worst scenario. We can use Mapster to return a realized array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[HttpGet("AdaptedArray")]
public IEnumerable<WeatherForecastDto> GetAdaptedArray(CancellationToken cancellationToken)
{
    var stopwatch = new Stopwatch();

    Console.WriteLine($"Start SQL"); stopwatch.Restart();
    var data = _dbContext.WeatherForecasts.AsEnumerable();
    Console.WriteLine($"End SQL - {stopwatch.ElapsedMilliseconds} ms");

    Console.WriteLine($"Start Adapting Data"); stopwatch.Restart();
    var adapted = data.Adapt<WeatherForecastDto[]>();
    Console.WriteLine($"Data Adapted - {stopwatch.ElapsedMilliseconds} ms");

    return adapted;
}
1
2
3
4
5
6
7
8
Start SQL
End SQL - 0 ms
Start Adapting Data
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (14ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [w].[Id], [w].[Date], [w].[Summary], [w].[TemperatureC]
      FROM [WeatherForecasts] AS [w]
Data Adapted - 17185 ms

Everything works fine, but since we return a real array all benefits of streaming are thrown out of the window. Whenever I call that endpoint with curl no response is being sent for around 20 seconds.

Adapting to IEnumerable

A first well working scenario is adapting the data to IEnumerable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[HttpGet("AdaptedEnumerable")]
public IEnumerable<WeatherForecastDto> GetAdaptedEnumerable(CancellationToken cancellationToken)
{
    var stopwatch = new Stopwatch();

    Console.WriteLine($"Start SQL"); stopwatch.Restart();
    var data = _dbContext.WeatherForecasts.AsEnumerable();
    Console.WriteLine($"End SQL - {stopwatch.ElapsedMilliseconds} ms");

    Console.WriteLine($"Start Adapting Data"); stopwatch.Restart();
    var adapted = data.Adapt<IEnumerable<WeatherForecastDto>>();
    Console.WriteLine($"Data Adapted - {stopwatch.ElapsedMilliseconds} ms");

    return adapted;
}
1
2
3
4
5
6
7
8
Start SQL
End SQL - 0 ms
Start Adapting Data
Data Adapted - 0 ms
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [w].[Id], [w].[Date], [w].[Summary], [w].[TemperatureC]
      FROM [WeatherForecasts] AS [w]

Result is given out super fast and curl receives first bytes of response after just few milliseconds, so i’m certain that streaming works well with this result, and data is adapted row by row while it’s being received from the database.

Adapting with LINQ

We can use LINQ to adapt the result one row at a time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[HttpGet("AdaptedWithLINQ")]
public IEnumerable<WeatherForecastDto> GetAdaptedWithLINQ(CancellationToken cancellationToken)
{
    var stopwatch = new Stopwatch();

    Console.WriteLine($"Start SQL"); stopwatch.Restart();
    var data = _dbContext.WeatherForecasts.AsEnumerable();
    Console.WriteLine($"End SQL - {stopwatch.ElapsedMilliseconds} ms");

    Console.WriteLine($"Start Adapting Data"); stopwatch.Restart();
    var adapted = data.Select(d => d.Adapt<WeatherForecastDto>());
    Console.WriteLine($"Data Adapted - {stopwatch.ElapsedMilliseconds} ms");

    return adapted;
}
1
2
3
4
5
6
7
8
Start SQL
End SQL - 0 ms
Start Adapting Data
Data Adapted - 0 ms
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [w].[Id], [w].[Date], [w].[Summary], [w].[TemperatureC]
      FROM [WeatherForecasts] AS [w]

This approach also allows us to stream, and it gives us more flexibility so we could even manipulate data of each row before or after adapting it.

Projecting to a type

Now here we have something new. Mapster adds a method called ProjectToType to IQueryable interface. It’s magical, and Entity Framework Core loves it!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[HttpGet("ProjectedQueryable")]
public IEnumerable<WeatherForecastDto> GetProjectedQueryable(CancellationToken cancellationToken)
{
    var stopwatch = new Stopwatch();

    Console.WriteLine($"Start SQL"); stopwatch.Restart();
    var data = _dbContext.WeatherForecasts.AsQueryable();
    Console.WriteLine($"End SQL - {stopwatch.ElapsedMilliseconds} ms");

    Console.WriteLine($"Start Adapting Data"); stopwatch.Restart();
    var adapted = data.ProjectToType<WeatherForecastDto>().AsEnumerable();
    Console.WriteLine($"Data Adapted - {stopwatch.ElapsedMilliseconds} ms");

    return adapted;
}
1
2
3
4
5
6
7
8
Start SQL
End SQL - 0 ms
Start Adapting Data
Data Adapted - 0 ms
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [w].[Date], [w].[TemperatureC]
      FROM [WeatherForecasts] AS [w]

Not only it’s super fast and data is streamed, it also works together with Entity Framework Core! This time the query sent to the SqlServer doesn’t download Id and Summary columns. That’s very desired behavior because the DTO has only Date and TemperatureC properties.

Summary

We have one and clear winner, and it’s ProjectToType! Instead of downloading Model from the database and then adapting it to the DTO it prepared the query in a way that’s necessary just for the DTO! By skipping the Summary column, that’s not mapped to the DTO the endpoint saved lot’s of time. All 100.000 records were returned as JSON in under 5 seconds while others endpoints returned exactly the same data in just under 30 seconds.

This post is licensed under CC BY 4.0 by the author.