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.