Let’s consider another requirement, to evaluate how you can best take advantage of the features of the Entity Framework and emulate that functionality in cases where it cannot be used, or it is more convenient to do something else.
In this example, we are using the same database explained in Data-for-demos
The requirement
You need to get the statistical information of cases, vaccinations, etc. By country, between certain dates, with the following conditions:
- If no start date is entered, the first available date is used.
- If the end date is not entered, the last available date is used.
- The information must be returned in batches of 100 entries, so the requested page number must be received.
In this case, it will be implemented in the “Country» controller
Entity Framework.
The code leverages EF’s Fluent capabilities to nest conditions. Similarly, below, the Entity Framework generates a statement according to the data engine in use, in this case, SQL Server.
public async Task<ActionResult<IEnumerable<OwidCovidDatum>>> GetCountryData( int CountryId, DateTime? fromDate=null, DateTime? toDate=null, int Page=1) { fromDate=fromDate?? (from el in _context.OwidCovidData orderby el.Date select el.Date).FirstOrDefault(); toDate=toDate?? (from el in _context.OwidCovidData orderby el.Date descending select el.Date).FirstOrDefault(); return (from OwidCovidDatum el in _context.OwidCovidData .Where(x=> x.Date>=fromDate && x.Date<=toDate && x.CountriesId==CountryId) .Skip((Page-1)*100) .Take(100) select el).ToList(); }
Dapper
Using the returned DapperRows, we implement the call with an SQL statement that is almost the same as the one automatically generated by EF.
[HttpGet] public async Task<string> GetCountryData( int CountryId, DateTime? fromDate = null, DateTime? toDate = null, int Page = 1) { int skip = ((Page - 1) * 100); fromDate = fromDate ?? await _dal.GetValueAsync<DateTime>("SELECT MIN([o].[date]) FROM [Owid Covid Data] AS [o];"); toDate = toDate ?? await _dal.GetValueAsync<DateTime>("SELECT MAX([o].[date]) FROM [Owid Covid Data] AS [o];"); string sql = $@"SELECT [o].[ContinentId] , [o].[CountriesId] , [o].[date] , [o].[hosp_patients] , [o].[hosp_patients_per_million] , [o].[icu_patients] , [o].[icu_patients_per_million] , [o].[new_cases] , [o].[new_cases_per_million] , [o].[new_cases_smoothed] , [o].[new_cases_smoothed_per_million] , [o].[new_deaths] , [o].[new_deaths_per_million] , [o].[new_deaths_smoothed] , [o].[new_deaths_smoothed_per_million] , [o].[new_tests] , [o].[new_tests_per_thousand] , [o].[new_tests_smoothed] , [o].[new_tests_smoothed_per_thousand] , [o].[new_vaccinations] , [o].[new_vaccinations_smoothed] , [o].[new_vaccinations_smoothed_per_million] , [o].[people_fully_vaccinated] , [o].[people_fully_vaccinated_per_hundred] , [o].[people_vaccinated] , [o].[people_vaccinated_per_hundred] , [o].[positive_rate] , [o].[tests_per_case] , [o].[tests_units] , [o].[total_cases] , [o].[total_cases_per_million] , [o].[total_deaths] , [o].[total_deaths_per_million] , [o].[total_tests] , [o].[total_tests_per_thousand] , [o].[total_vaccinations] , [o].[total_vaccinations_per_hundred] , [o].[weekly_hosp_admissions] , [o].[weekly_hosp_admissions_per_million] , [o].[weekly_icu_admissions] , [o].[weekly_icu_admissions_per_million] FROM [Owid Covid Data] AS[o] WHERE(([o].[date] >= '{fromDate.Value.ToString("u").Substring(0,10)}') AND([o].[date] <= '{ toDate.Value.ToString("u").Substring(0, 10)}')) AND([o].[CountriesId] = {CountryId}) ORDER BY date OFFSET {skip} ROWS FETCH NEXT 100 ROWS ONLY; "; var result =await _dal.GetDataAsync(sql); string json = JsonSerializer.Serialize(result, new JsonSerializerOptions() { WriteIndented = true, ReferenceHandler = ReferenceHandler.Preserve }); return json; }
Code
As in the previous example, we create a parameterized Command object that returns a string of characters with the resulting JSON, implemented in the SQL statement.
[HttpGet] public async Task<string> GetCountryData( int CountryId, DateTime? fromDate = null, DateTime? toDate = null, int Page = 1) { int skip = ((Page - 1) * 100); fromDate = fromDate ?? await _dal.GetValueAsync<DateTime>("SELECT MIN([o].[date]) FROM [Owid Covid Data] AS [o];"); toDate = toDate ?? await _dal.GetValueAsync<DateTime>("SELECT MAX([o].[date]) FROM [Owid Covid Data] AS [o];"); string sql = $@"SELECT [o].[ContinentId] , [o].[CountriesId] , [o].[date] , [o].[hosp_patients] , [o].[hosp_patients_per_million] , [o].[icu_patients] , [o].[icu_patients_per_million] , [o].[new_cases] , [o].[new_cases_per_million] , [o].[new_cases_smoothed] , [o].[new_cases_smoothed_per_million] , [o].[new_deaths] , [o].[new_deaths_per_million] , [o].[new_deaths_smoothed] , [o].[new_deaths_smoothed_per_million] , [o].[new_tests] , [o].[new_tests_per_thousand] , [o].[new_tests_smoothed] , [o].[new_tests_smoothed_per_thousand] , [o].[new_vaccinations] , [o].[new_vaccinations_smoothed] , [o].[new_vaccinations_smoothed_per_million] , [o].[people_fully_vaccinated] , [o].[people_fully_vaccinated_per_hundred] , [o].[people_vaccinated] , [o].[people_vaccinated_per_hundred] , [o].[positive_rate] , [o].[tests_per_case] , [o].[tests_units] , [o].[total_cases] , [o].[total_cases_per_million] , [o].[total_deaths] , [o].[total_deaths_per_million] , [o].[total_tests] , [o].[total_tests_per_thousand] , [o].[total_vaccinations] , [o].[total_vaccinations_per_hundred] , [o].[weekly_hosp_admissions] , [o].[weekly_hosp_admissions_per_million] , [o].[weekly_icu_admissions] , [o].[weekly_icu_admissions_per_million] FROM [Owid Covid Data] AS[o] WHERE(([o].[date] >= @fromDate) AND([o].[date] <= @toDate)) AND([o].[CountriesId] = @CountryId) ORDER BY date OFFSET @skip ROWS FETCH NEXT 100 ROWS ONLY FOR JSON PATH; "; SqlCommand com = _dal.CreateCommand(sql); com.Parameters.AddWithValue("@CountryId", CountryId); com.Parameters.AddWithValue("@fromDate", fromDate); com.Parameters.AddWithValue("@toDate", toDate); com.Parameters.AddWithValue("@skip", skip); return await _dal.GetJSONDataAsync(com); } }
SQL sentences
For ease of comparison, here are, together, the three SQL statements used.
Entity Framework
EXEC [sp_executesql] N'SELECT [o].[ContinentId] , [o].[CountriesId] , [o].[date] , [o].[hosp_patients] , [o].[hosp_patients_per_million] , [o].[icu_patients] , [o].[icu_patients_per_million] , [o].[new_cases] , [o].[new_cases_per_million] , [o].[new_cases_smoothed] , [o].[new_cases_smoothed_per_million] , [o].[new_deaths] , [o].[new_deaths_per_million] , [o].[new_deaths_smoothed] , [o].[new_deaths_smoothed_per_million] , [o].[new_tests] , [o].[new_tests_per_thousand] , [o].[new_tests_smoothed] , [o].[new_tests_smoothed_per_thousand] , [o].[new_vaccinations] , [o].[new_vaccinations_smoothed] , [o].[new_vaccinations_smoothed_per_million] , [o].[people_fully_vaccinated] , [o].[people_fully_vaccinated_per_hundred] , [o].[people_vaccinated] , [o].[people_vaccinated_per_hundred] , [o].[positive_rate] , [o].[tests_per_case] , [o].[tests_units] , [o].[total_cases] , [o].[total_cases_per_million] , [o].[total_deaths] , [o].[total_deaths_per_million] , [o].[total_tests] , [o].[total_tests_per_thousand] , [o].[total_vaccinations] , [o].[total_vaccinations_per_hundred] , [o].[weekly_hosp_admissions] , [o].[weekly_hosp_admissions_per_million] , [o].[weekly_icu_admissions] , [o].[weekly_icu_admissions_per_million] FROM [Owid Covid Data] AS [o] WHERE(([o].[date] >= @__fromDate_0) AND ([o].[date] <= @__toDate_1)) AND ([o].[CountriesId] = @__CountryId_2) ORDER BY ( SELECT 1 ) OFFSET @__p_3 ROWS FETCH NEXT @__p_4 ROWS ONLY;' , N'@__fromDate_0 datetime,@__toDate_1 datetime,@__CountryId_2 int,@__p_3 int,@__p_4 int' , @__fromDate_0 = '2020-01-01 00:00:00' , @__toDate_1 = '2021-06-11 00:00:00' , @__CountryId_2 = 4 , @__p_3 = 300 , @__p_4 = 100;
Dapper
SELECT [o].[ContinentId] , [o].[CountriesId] , [o].[date] , [o].[hosp_patients] , [o].[hosp_patients_per_million] , [o].[icu_patients] , [o].[icu_patients_per_million] , [o].[new_cases] , [o].[new_cases_per_million] , [o].[new_cases_smoothed] , [o].[new_cases_smoothed_per_million] , [o].[new_deaths] , [o].[new_deaths_per_million] , [o].[new_deaths_smoothed] , [o].[new_deaths_smoothed_per_million] , [o].[new_tests] , [o].[new_tests_per_thousand] , [o].[new_tests_smoothed] , [o].[new_tests_smoothed_per_thousand] , [o].[new_vaccinations] , [o].[new_vaccinations_smoothed] , [o].[new_vaccinations_smoothed_per_million] , [o].[people_fully_vaccinated] , [o].[people_fully_vaccinated_per_hundred] , [o].[people_vaccinated] , [o].[people_vaccinated_per_hundred] , [o].[positive_rate] , [o].[tests_per_case] , [o].[tests_units] , [o].[total_cases] , [o].[total_cases_per_million] , [o].[total_deaths] , [o].[total_deaths_per_million] , [o].[total_tests] , [o].[total_tests_per_thousand] , [o].[total_vaccinations] , [o].[total_vaccinations_per_hundred] , [o].[weekly_hosp_admissions] , [o].[weekly_hosp_admissions_per_million] , [o].[weekly_icu_admissions] , [o].[weekly_icu_admissions_per_million] FROM [Owid Covid Data] AS [o] WHERE(([o].[date] >= '01/01/2020 00:00:00') AND ([o].[date] <= '06/11/2021 00:00:00')) AND ([o].[CountriesId] = 4) ORDER BY date OFFSET 300 ROWS FETCH NEXT 100 ROWS ONLY;
Code
EXEC [sp_executesql] N'SELECT [o].[ContinentId] , [o].[CountriesId] , [o].[date] , [o].[hosp_patients] , [o].[hosp_patients_per_million] , [o].[icu_patients] , [o].[icu_patients_per_million] , [o].[new_cases] , [o].[new_cases_per_million] , [o].[new_cases_smoothed] , [o].[new_cases_smoothed_per_million] , [o].[new_deaths] , [o].[new_deaths_per_million] , [o].[new_deaths_smoothed] , [o].[new_deaths_smoothed_per_million] , [o].[new_tests] , [o].[new_tests_per_thousand] , [o].[new_tests_smoothed] , [o].[new_tests_smoothed_per_thousand] , [o].[new_vaccinations] , [o].[new_vaccinations_smoothed] , [o].[new_vaccinations_smoothed_per_million] , [o].[people_fully_vaccinated] , [o].[people_fully_vaccinated_per_hundred] , [o].[people_vaccinated] , [o].[people_vaccinated_per_hundred] , [o].[positive_rate] , [o].[tests_per_case] , [o].[tests_units] , [o].[total_cases] , [o].[total_cases_per_million] , [o].[total_deaths] , [o].[total_deaths_per_million] , [o].[total_tests] , [o].[total_tests_per_thousand] , [o].[total_vaccinations] , [o].[total_vaccinations_per_hundred] , [o].[weekly_hosp_admissions] , [o].[weekly_hosp_admissions_per_million] , [o].[weekly_icu_admissions] , [o].[weekly_icu_admissions_per_million] FROM [Owid Covid Data] AS[o] WHERE(([o].[date] >= @fromDate) AND([o].[date] <= @toDate)) AND([o].[CountriesId] = @CountryId) ORDER BY date OFFSET @skip ROWS FETCH NEXT 100 ROWS ONLY FOR JSON PATH; ' , N'@CountryId int,@fromDate datetime,@toDate datetime,@skip int' , @CountryId = 4 , @fromDate = '2020-01-01 00:00:00' , @toDate = '2021-06-11 00:00:00' , @skip = 200;
It’s striking that in the case of code, it uses sp_executesql,just like EF, which takes a bit longer to execute.
It seems that this will need to be improved in the next publication.
Performance
The same procedure was used as in the previous publication to evaluate the results.
As you can see, things have improved quite a bit for EF, although in this case, Dapper seems to be the one who works best.
The difference is precisely the sp_executesql.
