During this post, I explained how to get information from a database in JSON format. In this one, I will show you how to store information sent to the database in JSON format.
I will use a very common scenario, storing a master-detail combination. For this, I use the Order-Order Details tables in Northwind database, which you can get here.
The goal is to store a new Order, with several Order Details in the same procedure, by using T-SQL OPENJSON.
Like a cooking recipe, I will explain this step by step.
Define the JSON schema.
We want to store the information received in the Order Details table, so its schema will be the schema received in the JSON information.
Simply get the Order Details schema and remove the NOT NULL modifiers. This will be used in the WITH modifier of the OPENJSON statement this way:
It must have parameters to receive all the data for the Orders Table’s columns, and one more containing the entire JSON information for the Order Details table. Notice the OrderID parameter is declared as OUTPUT, so the calling code could retrieve the new Order ID for the inserted row.
For this, the procedure must use the IDENT_CURRENT function.
SET@OrderID=IDENT_CURRENT('[Orders]');
Insert the Order Details using OPENJSON.
In this case, using Insert – select statement, and OPENJSON from the Details parameter as source, declaring it with the previously obtained schema. Notice the utilization of the @OrderID parameter for the Order Id value in each row.
INSERTINTO[Order Details]([OrderID],[ProductID],[UnitPrice],[Quantity],[Discount])SELECT@OrderID/* Using the new Order ID*/,[Productid],[UnitPrice],[Quantity],[Discount]FROMOPENJSON(@Details)WITH([OrderID][INT],[ProductID][INT],[UnitPrice][MONEY],[Quantity][SMALLINT],[Discount][REAL]);
The C# code.
Define the Order and Order Details entities in your Application.
I created a simple C# Console Application Sample. In it, the Order and Order_Details has been defined by using the Paste Special Paste JSON as Classes feature in Visual Studio. You can see the step by step here.
The Insert routine in the main program.
The code creates a new instance of the Order class, with values,
Orderorder=new(){CustomerID="ALFKI",EmployeeID=1,OrderDate=DateTime.UtcNow,RequiredDate=DateTime.UtcNow.AddDays(5),ShipAddress="Obere Str. 57",ShipCity="Berlin",Freight=12.05F,ShipCountry="Germany",ShipName="Alfreds Futterkiste",ShipPostalCode="12209",ShipRegion=null,ShipVia=1};
Then get information from a previously defined set, (as JSON), to have an array of Order Details.
// Create the details. To Avoid a long code, just get it from a JSON samplevardetails=System.Text.Json.JsonSerializer.Deserialize<OrderDetail[]>(InsertUsingJSONSample.Properties.Resources.Details);
Create the Connection and Command objects.
A connection object to the database is assigned to a Command object, with the name of the stored procedure as text, which is defined as a Stored Procedure command type.
By using this method, you reduce the calls between your app and the database server, optimizing the response, and including the entire store process in a unique implicit transaction.
In our current globalized world, at this time, any web site must be multilingual, enabling the user to select the language to use.
Moreover, a global company needs to know from which country the user connected.
A lot of sites, more of the streaming services as examples, identifies the country based on the IP address. Which could be a mistake, since a lot of people are in a different country of which belongs to.
Anyway, your database must need a language list and a countries list.
This post shows you a sample application which, using the standardized languages and countries from .Net Framework, and adding some information from external sources, set up some tables to manage this in your databases.
System.Globalization Namespace
The tool uses information from the System.Globalization namespace, which appears with .Net Framework from it very beginning.
Indicates if it is just a language or a language and country specification
Finally, I get information from two external sources. I got the GPS coordinates of each country from here meanwhile the flag’s pictures are from here. You can found the urls inside the code as well.
Storage’s schema.
Languages/Countries database schema
The tool create 3 tables as you can see in the Diagram.
It is necessary this way, because some countries use more than one language, and the relationship must be preserved.
The tables have a InUse column, to enable/disable each row for your application. So, you can query the Languages table for all the rows with the InUse value in 1, to display only those languages you desire use, or have enabled.
Note: It is important using nvarchar/nchar data types, since several Native names are in UTF-8 chars.
Using the tool.
The tool expects at least the connection string to your database. It accepts a second parameter for the schema name under the tables will be created. If this value is not provided, the tool assumes “Masters” as schema name.
In any case, the DDL scripts manage the creation of the schema if it does not exist.
The source code of the DataGen solution is in my GitHub.
If you prefer just use a T-SQL script to add the tables, here is the script.
In future posts, I will show some faqncy methods for site AND CONTENT localization.
In this example, we will see how we can test the different versions from a web application.
It is a simple page that calls consecutively the different versions, repeatedly (100), and returns the number of milliseconds involved in the whole process.
To increase the impedance of the evaluation, the option is given in a check box, to perform the same query, 100 times, but for all countries.
In any case, the responses will be in approximately the same proportion.
Performance
As you can see, from the point of view of the client application, the final result is faster with EF than with simple code, exactly the opposite of the performance in the first test.
It should be noted, however, that in that first test, the process requested was not the same at all, since data from different tables were required, while, in this case, the data came from only one.
In any case, the comparison could be established with the second publication of this series, which obtains the same data as the latter.
In any case, this last test has some advantage on the part of EF, compared to the rest.
In other words, let us point out conclusions:
The result is not always as expected. You must try. ALWAYS.
There is no single way to do things. We must investigate to improve the quality of the applications we generate.
An unit test of the final set of the application can lead to false conclusions, since other factors, such as communication, data transformation, etc., also influence performance. That is, unit tests are highly valid for testing functionality, but they are not always valid for evaluating performance.
In fact, most likely, in a real-world application environment, performance results can also change.
Therefore, it is important to monitor the application deployed in production, include counters, logs etc. to have permanent information and be able to periodically evaluate it, to anticipate possible problems.
En este ejemplo, veremos como podemos probar las distintas versiones desde un aplicativo web.
Se trata de una página sencilla que, llama consecutivamente a las distintas versiones, repetidas veces (100), y nos retorna la cantidad de milisegundos implicados en todo el proceso.
Para incrementar la impedancia de la evaluación, se da la opción en una casilla, de realizar la misma consulta, 100 veces, pero para todos los países.
En cualquier caso, las respuestas serán aproximadamente en la misma proporción.
Rendimiento
Como se puede ver, desde el punto de vista de la aplicación cliente, el resultado final es más rápido con EF que con código plano, exactamente lo contrario a la respuesta en la primera prueba.
Es de notar, sin embargo, que en aquella primera prueba, el proceso solicitado no era el mismo en lo absoluto, dado que se requerían datos de distintas tablas, mientras que, en este caso, los datos provienen de una sola.
En todo caso, la comparativa la podríamos establecer con la segunda publicación de esta serie, que obtiene los mismos datos que ésta última.
En cualquier caso, esta última prueba presenta alguna ventaja de parte de EF, respecto del resto.
O sea, puntualicemos conclusiones:
No siempre el resultado es el esperado. Hay que probar. SIEMPRE.
No hay una sola forma de hacer las cosas. Hay que investigar para mejorar la calidad de las aplicaciones que generamos.
Una prueba aislada del conjunto final de la aplicación, puede llevarnos a falsas conclusiones, ya que otros factores, como comunicación, transformación de datos, etc., también influyen en el rendimiento. O sea, las pruebas unitarias son muy validad para comprobar funcionalidad, pero no siempre son válidas para evaluar rendimiento.
De hecho, muy probablemente, en un entorno de una aplicación real, también los resultados de rendimiento pueden cambiar.
Por ello, es importante hacer seguimiento de la aplicación desplegada en producción, incluir contadores, bitácoras etc. para tener información permanente y poder evaluar periódicamente la misma, para adelantarnos a posibles problemas.
Finally, I’ll add projects to use stored procedures instead of statements constructed in code.
Moreover, and based on the recommendation of a greater entity framework expert than me, I added «AsNoTracking()» to the Entity Framework LINQ query set to Generating a REST API (JSON)[2].
The Stored Procedure.
This is the stored procedure which receives the country ID, the date from and date to, and the page to display.
Is the stored procedure which is responsible for setting values that are appropriate to the date parameters, rather than setting them from the component in C#.
Exactly the same, but with «FOR JSON PATH» at the end, is used in the project that uses pure code.
El Cambio en Entity Framework
Based on the proposal and comment, the code is as follows:
public IEnumerable<OwidCovidDatum> GetCountryData(
intCountryId,
DateTime?fromDate=null,
DateTime?toDate=null,
intPage=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 descendingselect 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).AsNoTracking().ToList();
}
Dapper Using Stored Procedure
We use Dapper’s stored procedure execution capability, which is capable of assign values to parameters by name matching.
publicasyncTask<string>GetCountryData(intCountryId,DateTime?fromDate=null,DateTime?toDate=null,intPage=1){varresult=await_dal.GetDataAsync("[Owid Covid Data Get By Country]",new{fromDate,toDate,CountryId,Page});stringjson=JsonSerializer.Serialize(result,newJsonSerializerOptions(){WriteIndented=true,ReferenceHandler=ReferenceHandler.Preserve});returnjson;}
Code using Stored Procedure
In the case of direct code, we assign the parameters one by one, also specifying the data type, which allows greater specificity.
publicasyncTask<string>GetCountryData(intCountryId,DateTime?fromDate=null,DateTime?toDate=null,intPage=1){varcommand=_dal.CreateCommand("[Owid Covid Data Get By Country JSON]");command.Parameters.Add("@fromDate",System.Data.SqlDbType.SmallDateTime).Value=fromDate;command.Parameters.Add("@toDate",System.Data.SqlDbType.SmallDateTime).Value=toDate;command.Parameters.Add("@CountryId",System.Data.SqlDbType.Int).Value=CountryId;command.Parameters.Add("@skip",System.Data.SqlDbType.Int).Value=Page;varjson=await_dal.GetJSONDataAsync(command);returnjson;}
Performance
The graph shows that even when you use features that improve effectiveness, the simplicity of the code improves performance.
That is, for better response to the user, more time should be invested by developers in improving their development.
As a detail, stored procedure calls directly make calls to the SP, instead of using, as we saw in the previous post, sp_executesql.
EXEC[Owid Covid Data Get By Country]@fromDate=NULL,@toDate=NULL,@CountryId=4,@Page=3;
Aquí, finalmente, agregaré proyectos para utilizar procedimientos almacenados en lugar de sentencias construidas en el código.
De paso, y por recomendación de un mayor experto que yo en Entity Framework, agregué «AsNoTracking()» a la consulta LINQ de Entity Framework establecida en Generando una API REST (JSON) [2].
El procedimiento Almacenado.
Este es el procedimiento almacenado que recibe, el Id de país, la fecha desde y la fecha hasta, y la página a mostrar.
Es el procedimiento almacenado el responsable de establecer valores adecuados a los parámetros de fecha, en lugar de establecerlos desde el componente en C#.
Exactamente igual, pero con «FOR JSON PATH» al final, se usa en el proyecto que utiliza código puro.
El Cambio en Entity Framework
Basado en la propuesta y comentario, el código queda como sigue:
public IEnumerable<OwidCovidDatum> GetCountryData(
intCountryId,
DateTime?fromDate=null,
DateTime?toDate=null,
intPage=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 descendingselect 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).AsNoTracking().ToList();
}
Dapper Usando Procedimientos Almacenados
Utilizamos la capacidad de ejecución de procedimientos almacenados de Dapper, que es capaz de asignar valores a los parámetros por coincidencia de nombres.
publicasyncTask<string>GetCountryData(intCountryId,DateTime?fromDate=null,DateTime?toDate=null,intPage=1){varresult=await_dal.GetDataAsync("[Owid Covid Data Get By Country]",new{fromDate,toDate,CountryId,Page});stringjson=JsonSerializer.Serialize(result,newJsonSerializerOptions(){WriteIndented=true,ReferenceHandler=ReferenceHandler.Preserve});returnjson;}
Código usando Procedimientos Almacenados
En el caso del código directo, asignamos los parámetros uno a uno, especificando además el tipo de dato, que permite una mayor especificidad.
publicasyncTask<string>GetCountryData(intCountryId,DateTime?fromDate=null,DateTime?toDate=null,intPage=1){varcommand=_dal.CreateCommand("[Owid Covid Data Get By Country JSON]");command.Parameters.Add("@fromDate",System.Data.SqlDbType.SmallDateTime).Value=fromDate;command.Parameters.Add("@toDate",System.Data.SqlDbType.SmallDateTime).Value=toDate;command.Parameters.Add("@CountryId",System.Data.SqlDbType.Int).Value=CountryId;command.Parameters.Add("@skip",System.Data.SqlDbType.Int).Value=Page;varjson=await_dal.GetJSONDataAsync(command);returnjson;}
Rendimiento
El gráfico muestra que, aún cuando se utilizan características que mejoran la efectividad, la simpleza del código mejora el rendimiento.
O sea, para mejor respuesta al usuario, se deberá invertir más tiempo de los desarrolladores en mejorar su desarrollo.
Como detalle, las llamadas de procedimiento almacenado, realizan directamente llamadas al mismo, en lugar de utilizar, como vimos en la publicación anterior, sp_executesql.
EXEC[Owid Covid Data Get By Country]@fromDate=NULL,@toDate=NULL,@CountryId=4,@Page=3;
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.
publicasync Task<ActionResult<IEnumerable<OwidCovidDatum>>> GetCountryData(
intCountryId,
DateTime?fromDate=null,
DateTime?toDate=null,
intPage=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 descendingselect 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.
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.
Consideremos otro requisito, para evaluar como puede aprovecharse mejor las características de Entity Framework y emular esa funcionalidad en los casos en que no se pueda utilizar, o sea más conveniente otra forma de realizar la tarea.
En este ejemplo, estamos utilizando la misma base de datos explicada en Datos-para-demos
El requisito
Se necesita obtener la información estadística de casos, vacunaciones, etc. Por país, entre determinadas fechas, con las siguientes condiciones:
Si no se consigna fecha de inicio, se usa la primera disponible.
Si no se consigna la fecha de fin, se usa la última disponible.
Se debe retornar la información en lotes de a 100 entradas, con lo cual, se deberá recibir el número de página solicitado.
En este caso, se implementará en el controlador «Country«
Entity Framework.
El código aprovecha las funcionalidades Fluent de EF para anidar las condiciones. Igualmente, por debajo, Entity Framework genera una sentencia acorde al motor de datos en uso, en este caso, SQL Server.
publicasync Task<ActionResult<IEnumerable<OwidCovidDatum>>> GetCountryData(
intCountryId,
DateTime?fromDate=null,
DateTime?toDate=null,
intPage=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 descendingselect 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
Utilizando los DapperRow de retorno, implementamos la llamada con una sentencia SQL que es casi igual a la generada automáticamente por EF.
Al igual que en el ejemplo previo, creamos un objeto Command con parámetros que retorne una cadena de caracteres con el JSON resultante, implementado en la sentencia SQL.
In the previous post,Paging data, NOT client side. | Universidad Net, I described how to use OFFSET-FETCH pair to paginate data in the server side. In this one, I will describe an example of how to use it in a Web Assembly App.
Note: To test this code, you must create a new Blazor app in Visual Studio.
The Model.
The data model will be a class with two properties, one for the list of items and other with the page’s information.
This classes will be added in the Shared project of the Application.
The first class vary depending on the data you want to display, but the second one will be always the same, enhanced by some code.
Since the pager class will be a standardized and enhanced version of the data retrieved by using the stored procedure, and could be used in different projects, let’s define an interface for it.
IPagesInfo interface.
This is the code for the Interface.
publicinterfaceIPagesInfo
{
#region Properties
///<summary>/// Gets or sets the current page.....///</summary>
System.Int32 CurrentPage { get; set; }
///<summary>/// Gets the current first page number..///</summary>Int32 FirstPageNumber { get; }
///<summary>/// Gets the HasNextGroup/// Gets a value indicating whether this instance has next page..///</summary>Boolean HasNextGroup { get; }
///<summary>/// Gets the HasPreviousGroup/// Gets a value indicating whether this instance has previous page..///</summary>Boolean HasPreviousGroup { get; }
///<summary>/// Gets the last page number..///</summary>Int32 LastPageNumber { get; }
///<summary>/// Gets or sets the number of links to show..///</summary>Int32 NumberOfLinks { get; set; }
///<summary>/// Gets or sets the size of the page.....///</summary>
System.Int32 PageSize { get; set; }
///<summary>/// Gets or sets the Qty of rows skipped from the top of the select.....///</summary>
System.Int32 Skip { get; set; }
///<summary>/// Gets or sets the Qty of rows taken.....///</summary>
System.Int32 Take { get; set; }
///<summary>/// Gets or sets the amount of items to display.///</summary>
System.Int32 TotalItems { get; set; }
///<summary>/// Gets the total pages available to display..///</summary>Int32 TotalPages { get; }
///<summary>/// Gets the page number for the previous group start.///</summary>///<value>/// The page number.///</value>Int32 PreviousGroupStart { get; }
///<summary>/// Gets the page number for the next group start.///</summary>///<value>/// The page number.///</value>Int32 NextGroupStart { get; }
#endregion
}
PagerInfo class.
Here, you have the code for the class implementing the IPagesInfo interface.
Notice the class is responsible of the calculations about page numbers displayed, if there are next or previous groups of pages, etc.
publicclassPagerInfo : IPagesInfo
{
#region Fields
///<summary>/// Defines the lastPageNumber.///</summary>internalInt32 lastPageNumber =0;
#endregion#region Properties
publicInt32 TotalPages
{
get=> (Int32)Math.Ceiling(TotalItems / (Double)PageSize);
}
///<summary>/// Gets the page number for the previous group start.///</summary>///<value>/// The page number.///</value>publicInt32 PreviousGroupStart => LastPageNumber - NumberOfLinks;
///<summary>/// Gets the page number for the next group start.///</summary>///<value>/// The page number.///</value>publicInt32 NextGroupStart => LastPageNumber +1;
///<summary>/// Gets or sets the current page.///</summary>public System.Int32 CurrentPage { get; set; }
///<summary>/// Gets the current first page number..///</summary>publicInt32 FirstPageNumber
{
get=> LastPageNumber - (NumberOfLinks -1);
}
///<summary>/// Gets a value indicating whether this instance has next page.///</summary>publicBoolean HasNextGroup
{
get=> CurrentPage + NumberOfLinks < TotalPages;
}
///<summary>/// Gets a value indicating whether this instance has previous page.///</summary>publicBoolean HasPreviousGroup
{
get=> CurrentPage > NumberOfLinks;
}
///<summary>/// Gets the last page number..///</summary>publicInt32 LastPageNumber
{
get
{
lastPageNumber = (Int32)Math.Ceiling((Double)CurrentPage / NumberOfLinks) * NumberOfLinks;
if (lastPageNumber > TotalPages)
{
lastPageNumber = TotalPages;
}
return lastPageNumber;
}
}
///<summary>/// Gets or sets the number of links to show..///</summary>publicInt32 NumberOfLinks { get; set; } =10;
///<summary>/// Gets or sets the size of the page.....///</summary>public System.Int32 PageSize { get; set; }
///<summary>/// Gets or sets the Qty of rows skipped from the top of the select.....///</summary>public System.Int32 Skip { get; set; }
///<summary>/// Gets or sets the Qty of rows taken.....///</summary>public System.Int32 Take { get; set; }
///<summary>/// Gets or sets the amount of items to display.///</summary>public System.Int32 TotalItems { get; set; }
///<summary>/// Gets the total pages available to display..///</summary>#endregion
}
The Data Class.
This class will contain your data and the IPagesInfo properties. In this sample, it will be called PersonsPager.
Note: You can easily create it by executing the stored procedure, copying the result and in a new code window (for example, an empty one created for PersonsPager), paste it by the Edit – Paste Special-Past JSON as Classes.
Then replace the names autogenerated by your own and change the type for the second one for the PagesInfo class.
The pasted code names the main class as Rootobject, which has been renamed to PersonsPager.
The Pager class will be changed by PagesInfo, and the Pager class defined could be removed.
This is the final code for the PersonsPager class.
You must add a Razor Component. In this sample, it is called Pager.razor.
In the UI code, I use an UL tag, adding a button for previous group of pages (when the user moves beyond the first set of page numbers), buttons for the different page numbers, and a button for the next group as well.
For the buttons, the pagination, page-item, and page-link classes are used.
<navclass="text-center"><ulclass="pagination">@{//Previous page group button.stringclassName="page-item "+ (!PagesInfo.HasPreviousGroup ?" disabled":""); //If there is in previous group, the button will be disabled<liclass="@className"><buttonclass="page-link "@onclick="@(()=>
{
if (PagesInfo.HasPreviousGroup)
ChangePage(PagesInfo.PreviousGroupStart);
}
)">
⏪
</button></li>}@*Buttons for page numbers*@@for (Int32i= PagesInfo.FirstPageNumber; i<= PagesInfo.LastPageNumber; i++)
{
intpageSelector=i;
className="page-item "+ (i== PagesInfo.CurrentPage ?" active":"");
<liclass="@className"><buttonclass="page-link"@onclick="@(()=>ChangePage(pageSelector))">@string.Format("{0:00}", pageSelector)</button></li>
}
@{//Next page group button.className="page-item "+ (!PagesInfo.HasNextGroup ?" disabled":"");
<liclass="@className"><buttonclass="page-link "@onclick="@(()=>
{ if (PagesInfo.HasNextGroup)
ChangePage(PagesInfo.NextGroupStart);
}
)">
⏩
</button></li>}</ul></nav>
In the code section, parameters are defined for:
an instance of a class implementing the IPagerInfo
a value to persist the selected page
an EventCallback to notify the client page about the changes in the selection by the user.
Finally, the ChangePage function called every time the user clicks ant of the buttons is defined to change the selected page and notify the client page.
Segunda época del rockblog "Atascado en los 70". VIEJAS canciones y artistas PASADOS DE MODA. Tratamos al lector de usted y escribimos "rocanrol" y "roquero" con ortografía castellana.