No voy a entrar aquí en detalles de descripción de lo que es REST (Representational State Transfer) una API (Application Programming Interface) o JSON (JavaScript Object Notation). Parto de la base que ya se conoce a que nos referimos.
En este caso, utilizando la base de datos publicada en la entrada anterior Datos para demos, presentaré como exponer la información de esa base, en una API de sólo lectura.
Al mismo tiempo, intentaré mostrar ventajas y desventajas de distintos métodos para lograr el mismo objetivo.
Entity Framework
Aunque en líneas generales, me disgusta bastante su utilización, intentaré incluirlo en cada demostración, por ver que ventajas aporta, y los inconvenientes que surjan.
El proyecto dentro de la solución es ApiRestEF
Dapper
Como paquete/biblioteca de acceso a datos, permite fácilmente obtener información de bases de datos.
El proyecto, dentro de la solución es ApiRestDapper
Código directo
En este caso, estaré mostrando como hacer, lo mismo, pero paso a paso, sin bibliotecas.
El proyecto, dentro de la solución es ApiRestCode
Primer requisito.
Se necesita un mecanismo por el cual obtener la información que se muestra, para un continente indicado como parámetro.
Continent | Country | Total Cases | Total Deaths | Total Cases per Million | Total Deaths per Million | Population | Population Density | GDP per Capita |
Africa | Seychelles | 12466 | 46 | 126764 | 468 | 98340 | 208 | 26382 |
Africa | Cape Verde | 31433 | 271 | 56535 | 487 | 555988 | 136 | 6223 |
Africa | Tunisia | 362658 | 13305 | 30685 | 1126 | 11818618 | 74 | 10849 |
Africa | South Africa | 1722086 | 57410 | 29036 | 968 | 59308690 | 47 | 12295 |
Africa | Libya | 188386 | 3155 | 27416 | 459 | 6871287 | 4 | 17882 |
Africa | Botswana | 59480 | 896 | 25293 | 381 | 2351625 | 4 | 15807 |
Africa | Namibia | 61374 | 968 | 24154 | 381 | 2540916 | 3 | 9542 |
Africa | Eswatini | 18705 | 676 | 16123 | 583 | 1160164 | 79 | 7739 |
Africa | Morocco | 522765 | 9192 | 14163 | 249 | 36910558 | 80 | 7485 |
Africa | Djibouti | 11570 | 154 | 11711 | 156 | 988002 | 41 | 2705 |
Africa | Gabon | 24696 | 156 | 11096 | 70 | 2225728 | 8 | 16562 |
Entity Framework
El código utilizado obtiene el continente y sus países constitutivos en una sola consulta pero requirió recorrer los países, para obtener de cada uno, los datos demográficos.
Como ya dije, no me gusta EF y quizás por ello, mi investigación de métodos para hacerlo, puede no haber encontrado otra forma.
Por supuesto, si alguien ofrece otra propuesta en los comentarios, la agregaré aquí, y también procederé a evaluar la métrica correspondiente.
public async Task<ActionResult<string>> GetContinent(int id) { var continent = await _context.Continents.FindAsync(id); continent.Countries = await _context.Countries .Where(x => x.ContinentId == id) .Select(x => new Country { Country1 = x.Country1, Id = x.Id, ContinentId = x.ContinentId } ) .ToListAsync(); foreach (var item in continent.Countries) { item.OwidCountriesData = await _context.OwidCountriesData .Where(x => x.CountriesId == item.Id) .Select(x => new OwidCountriesDatum { CountriesId = item.Id, TotalCases = x.TotalCases, TotalDeaths = x.TotalDeaths, TotalCasesPerMillion = x.TotalCasesPerMillion, TotalDeathsPerMillion = x.TotalDeathsPerMillion, Population = x.Population, PopulationDensity = x.PopulationDensity, GdpPerCapita = x.GdpPerCapita }).ToListAsync(); } if (continent == null) { return NotFound(); } string json = JsonSerializer.Serialize(continent, new JsonSerializerOptions() { WriteIndented = true, ReferenceHandler = ReferenceHandler.Preserve }); return json; }
Dapper
para el caso de la implementación con Dapper, utilizamos directamente los tipos «DapperRow» como retorno de la consulta, disminuyendo así el mapeo entre columnas y propiedades. De usarse clases definidas, seguramente el tiempo de respuesta sería mayor.
public async Task<ActionResult<string>> GetContinent(int id) { string sql = @" SELECT [C].[Continent] , [CO].[Country] , [D].[total_cases] , [D].[total_deaths] , [D].[total_cases_per_million] , [D].[total_deaths_per_million] , [D].[population] , [D].[population_density] , [D].[gdp_per_capita] FROM [OwidCountriesData] AS[D] INNER JOIN [Continents] AS[C] ON[D].[ContinentId] = [C].[Id] INNER JOIN [Countries] AS[CO] ON[D].[CountriesId] = [CO].[Id] WHERE([C].[Id] = @continent) ORDER BY [D].[total_cases_per_million] DESC".Replace("@continent", id.ToString()); ; var continent = await _dal.GetDataAsync(sql); if (continent == null) { return NotFound(); } string json = JsonSerializer.Serialize(continent, new JsonSerializerOptions() { WriteIndented = true, ReferenceHandler = ReferenceHandler.Preserve }); return json; }
Código
Finalmente, para la consulta directa por código, optimizamos utilizando el modificador FOR JOSN, y obteniendo entonces directamente la cadena JSON resultante.
public async Task<ActionResult<string>> GetContinent(int id) { string sql = @" SELECT [C].[Continent] , [CO].[Country] , [D].[total_cases] , [D].[total_deaths] , [D].[total_cases_per_million] , [D].[total_deaths_per_million] , [D].[population] , [D].[population_density] , [D].[gdp_per_capita] FROM [OwidCountriesData] AS [D] INNER JOIN [Continents] AS [C] ON [D].[ContinentId] = [C].[Id] INNER JOIN [Countries] AS [CO] ON [D].[CountriesId] = [CO].[Id] WHERE([C].[Id] = @continent) ORDER BY [D].[total_cases_per_million] DESC FOR JSON AUTO, INCLUDE_NULL_VALUES, ROOT('CountriesInfo');"; SqlCommand com = _dal.CreateCommand(sql); com.Parameters.AddWithValue("@continent", id); return await _dal.GetJSONDataAsync(com); }
Aclaración
Tanto en el proyecto Dapper como en el de código, se construyó una capa de acceso a datos mínima, para emular similar funcionalidad a la brindada por el código generado por EF.
Rendimiento
El gráfico inferior muestra la comparativa en utilización de CPU, tiempo de duración, y lecturas a disco, en cada uno de los casos.
Una imagen, vale más que mil palabras. 🙂
En el gráfico, solo se evalúan valores desde el punto de vista de la base de datos, no del código .Net, ni su tiempo de ejecución. Agregaré esto en la próxima entrega.

La comparación es bastante pobre.
En el ejemplo con Dapper haces un solo query mientras que en el ejemplo con EF haces 2 + N queries
Bastaba con hacer un simple Select a _context.OwidCountriesData con el where correspondiente y listo. Además si es solo de lectura conviene también utilizar el método .AsNoTracking() para optimizar la consulta.
¡Correcto!. De hecho, era la idea, mostrar que fácilmente se puede caer en errores de diseño.
Ahora bien, tu propuesta, no soluciona el tema, ya que «el where correspondiente» debe incluir los países del continente solicitado, que serán más de uno. ¿Cómo harías el where en EF en ese caso?