Archivo de la etiqueta: Visual Studio

SQL Server insert using JSON


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:

OPENJSON(@Details) WITH([OrderID] [INT], [ProductID] [INT],
[UnitPrice] [MONEY], [Quantity] [SMALLINT], [Discount]
[REAL]);

Prepare the Stored Procedure.

Parameters.

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.

   @OrderID        INT OUTPUT
, @CustomerID     NCHAR(5)
, @EmployeeID     INT
, @OrderDate      DATETIME
, @RequiredDate   DATETIME      = NULL
, @ShippedDate    DATETIME      = NULL
, @ShipVia        INT
, @Freight        MONEY
, @ShipName       NVARCHAR(40)
, @ShipAddress    NVARCHAR(60)
, @ShipCity       NVARCHAR(15)
, @ShipRegion     NVARCHAR(15)  = NULL
, @ShipPostalCode NVARCHAR(10)
, @ShipCountry    NVARCHAR(15)
, @Details        NVARCHAR(MAX)

Insert the Orders new row values.

It is a simple insert – values sentence, as follows:

INSERT INTO [Orders]
      (
   [CustomerID]
 , [EmployeeID]
 , [OrderDate]
 , [RequiredDate]
 , [ShippedDate]
 , [ShipVia]
 , [Freight]
 , [ShipName]
 , [ShipAddress]
 , [ShipCity]
 , [ShipRegion]
 , [ShipPostalCode]
 , [ShipCountry]
  )
VALUES
    (
  @CustomerID
, @EmployeeID
, @OrderDate
, @RequiredDate
, @ShippedDate
, @ShipVia
, @Freight
, @ShipName
, @ShipAddress
, @ShipCity
, @ShipRegion
, @ShipPostalCode
, @ShipCountry
);

Get the new inserted OrderId.

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.

INSERT INTO [Order Details]
      (
   [OrderID]
 , [ProductID]
 , [UnitPrice]
 , [Quantity]
 , [Discount]
  )
       SELECT 
          @OrderID /* Using the new Order ID*/
 
        , [Productid]
        , [UnitPrice]
        , [Quantity]
        , [Discount]
       FROM 
          OPENJSON(@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,

Order order = 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 sample
var details = 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.

SqlConnection con = new SqlConnection(InsertUsingJSONSample.Settings1.Default.ConString);
SqlCommand com = new SqlCommand("InsertWithJSONSP", con)
{
   CommandType = System.Data.CommandType.StoredProcedure
};

Add the parameters

Then, all the Order properties are added as parameters, plus one more, Details, containing the Order_Detail array expressed as JSON.

To do so, the code use Reflection to get all the properties in the Order instance and their values.

Note the parameter @OrderID is defined as InputOutput, so the code could retrieve the new Order Id once the procedure ends execution.

foreach (PropertyInfo item in order.GetType().GetProperties())
{
   com.Parameters.AddWithValue("@" + item.Name, item.GetValue(order));
 
}
com.Parameters["@OrderId"].Direction = System.Data.ParameterDirection.InputOutput;

Finally, the command is executed to insert the new Order with the Details and retrieve the new Id.

using (con)
{
   con.Open();
   int retValue = await com.ExecuteNonQueryAsync();
   int NewOrderID = (int)com.Parameters["@OrderId"].Value;
}

As usual, you will find the code sample here.

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.

HTH

GENERATING A REST API (JSON) [3]


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#.

CREATE PROCEDURE [dbo].[Owid Covid Data Get By Country] 
   @fromDate   SMALLDATETIME NULL
 , @toDate     SMALLDATETIME NULL
 , @CountryId  INT
 , @Page       INT           = 1
AS
  BEGIN
    SELECT 
        @froMDate = ISNULL(@fromDate,
    (
        SELECT 
            MIN([o].[date])
        FROM 
           [Owid Covid Data] AS [o]
    ));
    SELECT 
        @toDate = ISNULL(@toDate,
    (
        SELECT 
            MAX([o].[date])
        FROM 
           [Owid Covid Data] AS [o]
    ));
    DECLARE 
           @Skip  INT = ((@Page - 1) * 100);
    BEGIN
      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;
    END;
  END;

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(
   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(100select 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.

public async Task<string> GetCountryData(
   int CountryId, 
   DateTime? fromDate = null, 
   DateTime? toDate = null, 
   int Page = 1)
{
 
   var result =await _dal.GetDataAsync("[Owid Covid Data Get By Country]",new { fromDate, toDate, CountryId, Page });
   string json = JsonSerializer.Serialize(result, new JsonSerializerOptions()
   {
      WriteIndented = true,
      ReferenceHandler = ReferenceHandler.Preserve
   });
   return json;
}

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.

public async Task<string> GetCountryData(
   int CountryId, 
   DateTime? fromDate = null, 
   DateTime? toDate = null, 
   int Page = 1)
{
   var command = _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;
   var json =await _dal.GetJSONDataAsync(command);
   return json;
}

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;

GENERANDO UNA API REST (JSON) [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#.

CREATE PROCEDURE [dbo].[Owid Covid Data Get By Country] 
   @fromDate   SMALLDATETIME NULL
 , @toDate     SMALLDATETIME NULL
 , @CountryId  INT
 , @Page       INT           = 1
AS
  BEGIN
    SELECT 
        @froMDate = ISNULL(@fromDate,
    (
        SELECT 
            MIN([o].[date])
        FROM 
           [Owid Covid Data] AS [o]
    ));
    SELECT 
        @toDate = ISNULL(@toDate,
    (
        SELECT 
            MAX([o].[date])
        FROM 
           [Owid Covid Data] AS [o]
    ));
    DECLARE 
           @Skip  INT = ((@Page - 1) * 100);
    BEGIN
      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;
    END;
  END;

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(
   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(100select 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.

public async Task<string> GetCountryData(
   int CountryId, 
   DateTime? fromDate = null, 
   DateTime? toDate = null, 
   int Page = 1)
{
 
   var result =await _dal.GetDataAsync("[Owid Covid Data Get By Country]",new { fromDate, toDate, CountryId, Page });
   string json = JsonSerializer.Serialize(result, new JsonSerializerOptions()
   {
      WriteIndented = true,
      ReferenceHandler = ReferenceHandler.Preserve
   });
   return json;
}

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.

public async Task<string> GetCountryData(
   int CountryId, 
   DateTime? fromDate = null, 
   DateTime? toDate = null, 
   int Page = 1)
{
   var command = _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;
   var json =await _dal.GetJSONDataAsync(command);
   return json;
}

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;

GENERATING A REST API (JSON) [2]


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(100select 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<stringGetCountryData(
   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(010)}'))
        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<stringGetCountryData(
      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.

GENERANDO UNA API REST (JSON) [2]


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.

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(100select 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.

[HttpGet]
public async Task<stringGetCountryData(
   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(010)}'))
        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;
}

Código

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.

         [HttpGet]
   public async Task<stringGetCountryData(
      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);
   }
}

Las sentencias SQL

Para facilitar la comparativa, aquí están, juntas, las tres sentencias SQL utilizadas.

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;

Código

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;

Es llamativo que en el caso de código, utiliza sp_executesql, al igual que EF, lo cual demora un poco la ejecución.

Parece que esto habrá que mejorarlo en la siguiente publicación.

Rendimiento

Se utilizó el mismo procedimiento que en la publicación anterior, para evaluar los resultados.

Como se ve, la cosa ha mejorado bastante para EF, aunque en este caso, Dapper parece ser quien da mejor resultado.

La diferencia está, precisamente, el sp_executesql.

PAGING DATA, NOT CLIENT SIDE (2) (Blazor client)


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.

public interface IPagesInfo
{
   #region Properties
 
   /// <summary>
   /// Gets or sets the current page.....
   /// </summary>
   System.Int32 CurrentPage { getset; }
 
   /// <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 { getset; }
 
   /// <summary>
   /// Gets or sets the size of the page.....
   /// </summary>
   System.Int32 PageSize { getset; }
 
   /// <summary>
   /// Gets or sets the Qty of rows skipped from the top of the select.....
   /// </summary>
   System.Int32 Skip { getset; }
 
   /// <summary>
   /// Gets or sets the Qty of rows taken.....
   /// </summary>
   System.Int32 Take { getset; }
 
   /// <summary>
   /// Gets or sets the amount of items to display.
   /// </summary>
   System.Int32 TotalItems { getset; }
 
   /// <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.

public class PagerInfo : IPagesInfo
{
   #region Fields
 
   /// <summary>
   /// Defines the lastPageNumber.
   /// </summary>
   internal Int32 lastPageNumber = 0;
 
   #endregion
 
   #region Properties
   public Int32 TotalPages
   {
      get => (Int32)Math.Ceiling(TotalItems / (Double)PageSize);
   }
   /// <summary>
   /// Gets the page number for the previous group start.
   /// </summary>
   /// <value>
   /// The page number.
   /// </value>
   public Int32 PreviousGroupStart => LastPageNumber - NumberOfLinks;
   /// <summary>
   /// Gets the  page number for the next group start.
   /// </summary>
   /// <value>
   /// The page number.
   /// </value>
   public Int32 NextGroupStart => LastPageNumber + 1;
 
   /// <summary>
   /// Gets or sets the current page.
   /// </summary>
   public System.Int32 CurrentPage { getset; }
 
   /// <summary>
   /// Gets the current first page number..
   /// </summary>
   public Int32 FirstPageNumber
   {
      get => LastPageNumber - (NumberOfLinks - 1);
   }
 
   /// <summary>
   /// Gets a value indicating whether this instance has next page.
   /// </summary>
   public Boolean HasNextGroup
   {
      get => CurrentPage + NumberOfLinks < TotalPages;
   }
 
   /// <summary>
   /// Gets a value indicating whether this instance has previous page.
   /// </summary>
   public Boolean HasPreviousGroup
   {
      get => CurrentPage > NumberOfLinks;
   }
 
   /// <summary>
   /// Gets the last page number..
   /// </summary>
   public Int32 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>
   public Int32 NumberOfLinks { getset; } = 10;
 
   /// <summary>
   /// Gets or sets the size of the page.....
   /// </summary>
   public System.Int32 PageSize { getset; }
 
   /// <summary>
   /// Gets or sets the Qty of rows skipped from the top of the select.....
   /// </summary>
   public System.Int32 Skip { getset; }
 
   /// <summary>
   /// Gets or sets the Qty of rows taken.....
   /// </summary>
   public System.Int32 Take { getset; }
 
   /// <summary>
   /// Gets or sets the amount of items to display.
   /// </summary>
   public System.Int32 TotalItems { getset; }
 
   /// <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.

public class PersonsPager
{
 
   public List[] List { getset; }
   public PagerInfo Pager { getset; }
}
public class List
{
   public int BusinessEntityID { getset; }
   public string FirstName { getset; }
   public string MiddleName { getset; }
   public string LastName { getset; }
}

The Pager Component.

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.

<nav class="text-center">
   <ul class="pagination">
      @{//Previous page group button.
 
         string className = "page-item " + (!PagesInfo.HasPreviousGroup ? " disabled" : ""); //If there is in previous group, the button will be disabled
         <li class="@className">
            <button class="page-link "
                    @onclick="@(()=>
                                   {
                                      if (PagesInfo.HasPreviousGroup)
                                        ChangePage(PagesInfo.PreviousGroupStart);
                                   }
               )">
               ⏪
            </button>
         </li>
      }
      @*Buttons for page numbers*@
      @for (Int32 i = PagesInfo.FirstPageNumber; i <= PagesInfo.LastPageNumber; i++)
      {
         int pageSelector = i;
 
         className = "page-item " + (i == PagesInfo.CurrentPage ? " active" : "");
         <li class="@className">
            <button class="page-link" @onclick="@(()=>ChangePage(pageSelector))">@string.Format("{0:00}"pageSelector)</button>
         </li>
      }
      @{//Next page group button.
         className = "page-item  " + (!PagesInfo.HasNextGroup ? " disabled" : "");
         <li class="@className">
            <button class="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.

@code {
   [Parameter]
   public IPagesInfo PagesInfo { getset; }
   [Parameter]
   public int SelectedPage { get => PagesInfo.CurrentPage; set => PagesInfo.CurrentPage = value; }
   [Parameter]
   public EventCallback OnPageChange { getset; }
   void ChangePage(int newPage)
   {
      PagesInfo.CurrentPage = newPage;
      SelectedPage = newPage;
      OnPageChange.InvokeAsync(SelectedPage);
   }
}

The controller.

To get the information from the database, you will need an API REST controller which returns the JSON string from the stored procedure.

The method must be decorated with the HttpGet attribute to react when the client page code calls it.

[Route("/[controller]")]
[ApiController]
public class PersonDataController : ControllerBase
{
   public PersonDataController(IConfiguration configuration)
   {
      Configuration = configuration;
   }
 
   public IConfiguration Configuration { get; }
 
   [HttpGet]
   public async Task<stringGetPersons(int selectedPageint pageSize = 10)
   {
      using SqlConnection con = new SqlConnection(Configuration.GetConnectionString("aw"));
      try
      {
         SqlCommand com = new SqlCommand("[Person].[Person_GetforPager]"con);
         com.CommandType = System.Data.CommandType.StoredProcedure;
         com.Parameters.AddWithValue("@skip", (selectedPage == 0 ? 0 : selectedPage - 1* pageSize);
         com.Parameters.AddWithValue("@take"pageSize);
         con.Open();
         string values = (await com.ExecuteScalarAsync()).ToString();
         return values;
      }
      catch (System.Exception ex)
      {
 
         throw;
      }
   }
}

The client page.

A new component will be defined to display the data.

It will contain any type of display for your data (in the sample, it is just a UL list), and an instance of the Pager component.

As usual in a Web assembly app, you must check if you have data to display before performing the UI generation.

Moreover, you can decide if you need display the pager component in case of no more than one page is needed to display the information.

@inject HttpClient httpClient
<h3>Persons</h3>
@if (result != null)
{
   <div>@result.StatusCode</div>
   <div>@result.Content.ReadAsStringAsync().Result</div>
   <div>@httpClient.BaseAddress</div>
}
@if (personsPager != null && personsPager.List.Count() > 0)
{
   <ul>
      @foreach (var item in personsPager.List)
      {
         <li>@item.LastName</li>
 
      }
   </ul>
   @if (personsPager.Pager != null && personsPager.Pager.TotalPages > 1)
   {
      <Pager PagesInfo="PagesInfo" OnPageChange="ChangePage" />
      @**@
   }
}
else SelectedPage = 1;

The code of this page will call the controller get method to retrieve the information.

@code {
   PersonsPager personsPager;
   int SelectedPage;
   public IPagesInfo PagesInfo { get => personsPager.Pager; }
   HttpResponseMessage result;
   protected async override Task OnInitializedAsync()
   {
      await GetDataAsync();
   }
   async void ChangePage()
   {
      SelectedPage = personsPager.Pager.CurrentPage;
      //SelectedPage = PagesInfo.CurrentPage;
      await GetDataAsync();
      this.StateHasChanged();
 
   }
   async Task GetDataAsync()
   {
      //result = await httpClient.GetAsync($"PersonData?Selectedpage={SelectedPage}");
      personsPager = await httpClient.GetFromJsonAsync<PersonsPager>($"PersonData?Selectedpage={SelectedPage}");
 
   }
 
}

And this is the result:

Persons page with Pager sample
Persons page with Pager sample

Note: The “aw” connection string points to the AdventureWorks2017 database where the stored procedure from the previous post has been created.

BTW here is the sample

Hablemos de Herencia


Una de las características más importantes de la programación orientada a objetos, es la posibilidad de estructurar mostró código en distintos niveles o capas, de modo tal de poder ir encapsulando funcionalidades desde las más básicas hasta las más complejas, incluyendo modificaciones en la funcionalidad de cualquiera en otros objetos, dependiendo de con quién están tratando o qué tipo de datos se van manipular o acciones a realizar.

Sin embargo, esto tiene una contrapartida; si no se piensan adecuadamente las cosas a implementar, se puede caer en una Maraña de códigos que van llamando funcionalidades hay into niveles y por tanto a ser que dicho código resulte un poco inmanejable, o inclusive que tienda a a incrementar las posibilidades de error.

Por tanto, lo más importante de todo, más allá de conocer las distintas te indica y sintaxis respecto escritura de código, es la comprensión clara de que se trata precisamente esto de la herencia.

Desde la realidad.

La mejor manera de comprender esto entonces, es observar que es lo que sucede dentro de la naturaleza. Así por ejemplo, podemos consideran desde el punto de vista biológico, que absolutamente cualquier ente vivo compre cuatro funciones básicas a saber:

  • Nacer
  • Crecer
  • Reproducirse
  • Morir

Cada reino y cada especie a su vez, podrán realizar cualquiera de estas actividades de distinta forma e inclusive podrán incrementar sus características, dependiendo del grado el colectivo.

Tomemos por caso la producción de energía: en todos los seres vivos este hecho en necesario, pero la forma de hacerlo es totalmente diferente cuando hablamos del reino vegetal y el reino animal. Entretanto agreguemos sencillamente esta funcionalidad,  imprescindible en cualquier ente vivo.

  • Generar energía.

Así, el reino vegetal utiliza en General la fotosíntesis, mientras que en el reino animal la generación de energía se produce por mecanismos de digestión de los alimentos. Para decirlo muy genéricamente, los vegetales toman sol, los animales combing.

 

Sin embargo, cualquiera estados acciones están pensadas para obtener el mismo resultado: energía.

 

Si fuésemos must identificó extraterrestre, lo que trataremos de investigar es precisamente, más en estas cosas que están en el planeta tierra para generar energía. Nos encontraríamos entonces que investigando la generación, veríamos dos mecanismos totalmente diferentes entre sí (en realidad no son tan diferentes desde un punto de vista bioquímico dado que termina en procesos bastante similares y que en alguna parte son los mismos).

Podríamos entonces considerar que, cualquiera sea vegetal o animal hereby características que son comunes: las cinco que hemos indicado con viñetas.

Cierto es también, que alguna de esas actividades por ejemplo generar energía, si bien desde fuera (científico extraterrestre) lo veríamos cómo una sola acción, internamente cada uno de los reinos lo estaría realizando de maneras diferentes.

Esto nos lleva a entender algunas definiciones respecto the la exposición de las funcionalidades desde nuestros objetos hacía observadores externos, o para darles un hombre más adecuado a nuestro entorno, llamadores or instanciadores.

Interfaz

Una interfaz es en definitiva, un contrato que los objetos confirman respecto de cómo se exponen a sí mismos y como brindan sus funcionalidades de modo tal que cualquier otro objeto que pretenda utilizar los tenga claro cuáles son los mecanismos y propiedades que podrá encontrar en dichos objetos.

La interfaz en definitiva, es una tipificación de que miembros, entendiendo como miembros el conjunto de propiedades, métodos y eventos, que expone un objeto y cuáles son las posibles formas de utilizarlos en cada caso. Se dice que cada miembro expone una firma o sea el nombre por el que se lo llamará, los argumentos que puede recibir, y el tipo de valor que retornará.

A veces el propio objeto será responsable de exponer su interfaz, mientras que en otros casos la interfaz será declarada en forma independiente y luego implementada por el objeto en cuestión.

 

Además, en términos de herencia, una clase que sirve como base para definir otras puede ser responsable de definir la interfaz que va a ser común a todas ellas, aunque a su vez las descendientes podrán tener miembros agregados que modifiquen o expandan la interfaz de cada uno de ellos.

Ejemplo

En conclusión entonces, podríamos crear una clase para el ejemplo que esté utilizando llamada SerVivo que tendría cinco métodos, como se muestra en el siguiente esquema.

image

Una segunda clase que hereda de esta llamada Vegetal, y que dentro del miembro genera energía utilizará los métodos de fotosíntesis.

image

Y otra clase que hereda también de SerVivo, llamada Animal que en el miembro generar energía utilizará procedimientos de digestión.

image

Algunas cuestiones de imagen.

Tenemos bastante para hablar respecto de cómo se representa las cosas, sin embargo para esta primera entrega quiero dejarle claro los elementos que estamos viendo en estos tres gráficos.

En principio un rectángulo de bordes redondeados representa una clase: lo sabemos además porque debajo del nombre precisamente aparece la palabra class.

Cada método está definido por debajo del grupo métodos, como un cubo con el nombre del método su lado. En el caso de la clase SerVivo, uno de ellos aparece en cursiva porque si bien la clase y lo define, no lo tiene implementado; delega la implementación en cada una de las clases que heredan de esta; decimos entonces que ese método debe heredarse, y normalmente una clase  que tiene métodos con estas características pasa a ser también una clase que obligadamente debe heredarse.

Finalmente, los métodos Fotosíntesis de la clase Vegetal y Digestión de la clase Animal están representadas como métodos con un cubo pero con el icono de un candado abajo la derecha: esto significa que ese método es privado a la clase y por tanto no es accesible desde fuera por parte de ningún otro objeto. Los miembros GenerarEnergía de Animal y de Vegetal internamente deberán llamar cada uno al que le corresponde.

image

En la representación gráfica del conjunto completo, la clase SerVivo tiene un indicador de que debe heredarse y  a su vez las clases Vegetal y Animal indican con una flecha hacia la derecha que heredan de SerVivo. Finalmente el diagrama General no representa esto gráficamente con las flechas que apunta desde las clases derivadas hacia la clase base

En la próxima entrega estaremos analizando cómo quedaría dicho código.

Mejorando la toma de decisiones


Ya vimos como se puede “traducir” de un proceso lógico, a sentencias en los lenguajes más comunes.

Sin embargo, esto, muy lineal, puede mejorarse utilizando las ventajas de los lenguajes. De eso va la publicación de hoy. Distintas formas de establecer lógica.

Los operadores lógicos.

La primera opción de mejora que tenemos, es la de combinar decisiones lógicas entre si: Esto es si tengo que preguntarme, por ejemplo, el tema de los meses:

image

En la publicación anterior, utilizamos 4 sentencias “if”, una detrás de otra. ¡Y sus respectivas acciones!  1530413_690331531007178_1565415261_n

Sin embargo, podríamos encadenar las comparaciones, como diciendo: “si el mes es 4, o es 6, o es 9, o es 11…” y para eso sirven los operadores lógicos.

Esta tabla contiene los más comunes.

Operador VB C# Detalles
Y And & Si ambas condiciones se cumplen
Y también AndAlso && Si la primera se cumple se evalúa la segunda
O Or | Si una u otra
O sino OrElse || Si la primera se cumple, no se llega a evaluar la segunda
O Excluyente Xor ^ Solo una de las condiciones ha de ser verdadera
NO Not ! Negación de la condición

Entonces, la codificación del ejemplo de la publicación, quedaría:

VB

        If mes < 1 
            OrElse mes > 12 
            OrElse día < 1 
            OrElse día < 1 
            OrElse día > 31 
            Then
            EsVálida = False
            Return EsVálida
        End If

CS

            if (mes < 1 || 
                mes > 12 || 
                día < 1 || 
                día < 1 || 
                día > 31)
            {
                EsVálida = false;
                return EsVálida;
            }

Encadenado de condiciones

A veces, no es cuestión de juntar condiciones, sino de establecer acciones dependiendo de distintas condiciones.

Si pasa esto, entonces hago a, pero si no pero pasa esto otro, entonces hago esto otro. A eso, lo podríamos denominar, “decisiones en cascada”.

Si la condición es if, y la acción por el opuesto es else, pero a su vez debo preguntar otra cosa, debería poner uno dentro del otro (anidado). Por ejemplo:

        If condición1 Then
        Else
            If condición2 Then

            End If
        End If

Sin embargo, existen sentencias más precisas para esto:

VB

        If condición1 Then

        ElseIf condición2 Then

        End If

CS

            if (condición1)
            {

            }
            else if (condición2)
            {
            }

Selectores de valor

A veces, las decisiones pasan por coincidencias de una variable con ciertos valores y de acuerdo a ello, se toman distintas acciones (como con los meses de 30 días, por ejemplo).  Para ello existen los “selectores de valor”. En ellos, se permite agrupar sentencias por ciertos valores específicos (uno o más), y además, se da la opción de realizar acciones cuando ninguno coincide.

VB

        Select Case variable
            Case valor1
                acción1()
            Case valor2
                acción2()
            Case valor3
                accion3()
            Case Else
                accionExcluyente()
        End Select

CS

            switch (variable)
            {
                case valor1:
                    acción1();
                    break;
                case valor2:
                    acción2();
                    break;
                case valor3:
                    accion3();
                    break;
                default:
                    accionExcluyente();
                    break;
            }

Los selectores posibilitan además, que una misma acción se aplique a varios valores:

VB

        Select Case variable
            Case valor1, valor2
                acción2()
            Case valor3
                accion3()
            Case Else
                accionExcluyente()
        End Select

CS

            switch (variable)
            {
                case valor1:
                case valor2:
                    acción2();
                    break;
                case valor3:
                    accion3();
                    break;
                default:
                    accionExcluyente();
                    break;
            }

A veces, se puede hacer “trampa” con los selectores.  EL ejemplo de la validación de fecha es un buen caso: hay varios motivos distintos por los cuales una fecha puede ser inválida. Entonces, se puede codificar, dentro del Select, todas ellas. Pero … estamos comparando días y meses… son cosas distintas (“cerdos con velocidad al cuadrado”, decía mi padre Sonrisa ).  Pero funciona.

VB

        Dim EsVálida As Boolean = True
        ' Selecciona cualquier caso que resulte verdadero
        Select Case True
            Case día < 1, día > 31, mes < 1, mes > 12
                EsVálida = False
            Case mes = 4, EsVálida = 6, mes = 9, mes = 11
                EsVálida = día < 31
            Case mes = 2
                If año Mod 4 = 0 Then
                    EsVálida = día < 30
                Else
                    EsVálida = día < 29
                End If
        End Select

CS

¡No Funciona!  En C# no se admite…

Una Nota:  Cuando una condición hace que la salida sea un valor verdadero o falso, no es necesario “encerrarlo” en un if. Como en la sentencia EsVálida = día < 30.

También, si debes preguntar si una variable contiene verdadero o falso, no cometas el error de preguntar si la variable es igual a verdadero. Más pasos del procesador para obtener el mismo resultado.

O sea, las dos sentencias que siguen, son idénticas… pero la de abajo es un poquito más rápida (se notará con muchos usos, no con uno solo).

VB

        If condición = True Then

        End If
        If condición Then

        End If

CS

            if (condición == true)
            {
            }
            if (condición)
            {
            }

En la próxima,  hablaremos de bucles y repetidores.

Pero… ¿Que es un programa?


Acostumbrados estamos a usar programas de todo tipo.

Pero la pregunta del título no es tan fácil de responder.

Los programas, como es obvio, evolucionaron, pero si podemos decir que hay algo básico que sigue siendo cierto:

Un programa es una sucesión de acciones.

Antaño, cuando programar no era tan sencillo (ni barato Sonrisa ), era necesario tener bien claro cada paso. Un programa podían ser de cientos de líneas… o sea cientos de tarjetas perforadas… que si había un error, habría que re codificar. Tiempo, costo, etc.

Por eso, se diseñó una metodología para esquematizar el programa y hacer pruebas de su funcionalidad previas a cualquier codificación.

La diagramación lógica.

Se basa en esquematizar un proceso paso a paso. Hay diseños estándar para representar los distintos tipos de actividades.

image 

Los “terminadores” (Inicio y fin), establecen precisamente eso, los extremos del proceso.

Las acciones, como se ve en el diagrama, se representa por rectángulos.

Por supuesto, a estas alturas, nadie usa diagramación lógica para programar. 

Sin embargo, en este blog haré uso de ellos; de cuando en vez,  echaré mano a ellos, para contarte mejor de que va algo.

Ejemplo simple (y, de paso, sentencias de control de flujo).

Si bien ahora el .Net Framework es capaz de hacer matemática de fechas (en realidad, desde bastante antes, lenguajes como Visual Basic, o los dBase, ya lo tenían), hubo épocas en que las fechas se debían ingresar en campos separados (día, mes y año), y luego validarla adecuadamente. Insisto, no es una problemática actual pero sirve para equiparar diagramas y códigos.

Aunque parezca simple, se lleva varios pasos.

  • El mes no puede ser menor de uno.
  • El mes no puede ser mayor que doce.
  • El día no puede ser menor de uno
  • El día no puede ser mayor que 31
  • Si el mes es abril, junio, setiembre o noviembre, el día no debiera ser mayor que 30.
  • Si el mes es febrero, no debiera ser mayor que 28, excepto si el año es bisiesto, que no podrá ser mayor que 29.

image

Bueno, como se ve, los rombos simbolizan preguntas (en realidad, toma de decisión lógica). No existe un “estándar” para indicar las salidas, aunque suelen marcarse con “si y no”. En el diagrama, usé colores, que también sirve.

La sentencia de decisión lógica.

En definitiva, es una pregunta:

SI sucede esto ENTONCES hacemos esto.

VB

        If condición Then
            acción()
        End If

CS

            if (condición)
            {
                acción;
            }

En ocasiones, es cuestión de tomar acciones alternativas, dependiendo del valor lógico, como la decisión del año bisiesto:

image

VB

        If Bisiesto Then
            If Día > 29 Then
                acción()
            End If
        Else
            If Día > 28 Then
                acción()
            End If
        End If

CS

            if (Bisiesto)
            {
                if (Día>29)
                {
                 acción;           
                }
            }
            else
            {
                if (Día > 28)
                {
                    acción;
                }
            }

En conclusión, el diagrama completo (exceptuando que en realidad las variables no existen), sería

VB

        EsVálida = True
        If Mes < 1 Then
            EsVálida = False
            Return EsVálida
        End If
        If Mes > 12 Then
            EsVálida = False
            Return EsVálida
        End If
        If Día < 1 Then
            EsVálida = False
            Return EsVálida
        End If
        If Día < 1 Then
            EsVálida = False
            Return EsVálida
        End If
        If Día > 31 Then
            EsVálida = False
            Return EsVálida
        End If
        If Mes = 4 Then
            If Día > 30 Then
                EsVálida = False
                Return EsVálida
            End If
        End If
        If Mes = 6 Then
            If Día > 30 Then
                EsVálida = False
                Return EsVálida
            End If
        End If
        If Mes = 9 Then
            If Día > 30 Then
                EsVálida = False
                Return EsVálida
            End If
        End If
        If Mes = 11 Then
            If Día > 30 Then
                EsVálida = False
                Return EsVálida
            End If
        End If
        If Mes = 2 Then
            Bisiesto = (Año Mod 4 = 0)
            If Bisiesto Then
                If Día > 29 Then
                    EsVálida = False
                End If
            Else
                If Día > 28 Then
                    EsVálida = False
                End If
            End If
        End If
        Return EsVálida

CS

            EsVálida = true;
            if (Mes < 1)
            {
                EsVálida = false;
                return EsVálida;
            }
            if (Mes > 12)
            {
                EsVálida = false;
                return EsVálida;
            }
            if (Día < 1)
            {
                EsVálida = false;
                return EsVálida;
            }
            if (Día < 1)
            {
                EsVálida = false;
                return EsVálida;
            }
            if (Día > 31)
            {
                EsVálida = false;
                return EsVálida;
            }
            if (Mes == 4)
            {
                if (Día > 30)
                {
                    EsVálida = false;
                    return EsVálida;
                }
            }
            if (Mes == 6)
            {
                if (Día > 30)
                {
                    EsVálida = false;
                    return EsVálida;
                }
            }
            if (Mes == 9)
            {
                if (Día > 30)
                {
                    EsVálida = false;
                    return EsVálida;
                }
            }
            if (Mes == 11)
            {
                if (Día > 30)
                {
                    EsVálida = false;
                    return EsVálida;
                }
            }
            if (Mes == 2)
            {
                Bisiesto = (Año % 4 == 0);
                if (Bisiesto)
                {
                    if (Día > 29)
                    {
                        EsVálida = false;
                    }
                }
                else
                {
                    if (Día > 28)
                    {
                        EsVálida = false;
                    }
                }
            }
            return EsVálida;

NOTA: Este código dista de ser óptimo y NO debe usarse como ejemplo…. solo es útil para entender la lógica. Ya veremos otras versiones.

Hasta la próxima.

Las variables


Cuando empezamos a trabajar en programación, obviamente nos encontramos en la situación que querer guardar valores para usarlos más tarde.

O sea, reservar un espacio en la memoria de la computadora (ordenador), por cada valor que queremos utilizar.

Obviamente, poder identificar cada una de las que quieras usar, implica darles un nombre. Ese es entonces el concepto de variable.

Por otra parte, en muchos (no en todos), los lenguajes de programación se puede decir además, que tipo de dato va a almacenarse allí.

Cada lenguaje de programación utiliza su sintaxis para esto, como los dos siguientes ejemplos, donde vemos declaraciones de una variable de tipo entero y otra para almacenar caracteres.

VB
    Dim Entero As Integer
    Dim Cadena As String

CS

 

        int Entero;
        string Cadena;

También es posible darle a cada variable un valor predeterminado.

(No solo se puede, Es lo recomendado, para que no haya errores) Sonrisa

    Dim Entero As Integer = 0
    Dim Cadena As String = ""
        int Entero = 0;
        string Cadena = "";

Obviamente esto vale para cualquier tipo de dato, inclusive objetos complejos. Y así, se reservan espacios para cualquier uso. Sin embargo, existen algunas cuestiones más para tener en cuenta.

Alcance. (Ámbito)

Se define como alcance o ámbito, la característica que determina en que parte del código existe realmente cada variable.

Para determinar esto, existen dos mecanismos distintos; uno posicional y otro declarativo.

Declarativo.

En la siguiente tabla vemos los modos posibles:

Tipo
Definición
CS
VB
Público La variable es visible en el mismo módulo de código y puede accederse desde fuera
public
Public
Privado Existe en el mismos segmento de código donde se declara
private
Private
Protegido Es visible para el mismo módulo y cualquiera que herede de él (este concepto es útil entre clases que veremos más adelante)
protected
Protected
Conocido (amigo) Es visible para cualquier conjunto de código dentro del mismo proyecto, pero no desde otros
internal
Friend
Protegido o conocido La combinatoria de ambas anteriores
(Considerando que una clase puede heredar de otra que se haya definido en otro proyecto o biblioteca de carga dinámica)
protected internal
Protected Friend

Posicional.

Por otra parte, una variable tiene el ámbito del conjunto de código dentro del cual se declara.

Cuando la declaración se realiza en el conjunto de código (clase, por ejemplo), estará disponible en todos y cada uno de sus miembros

Si en cambio, se declara dentro de un miembro, solo será hallable dentro del mismo.

Finalmente, si se declara dentro de un bloque de ejecución, por ejemplo, en el interior de un bucle de repetición, o una sentencia condicional, sólo será alcanzable dentro de éste, ni siquiera en el resto del miembro.

VB
Public Class Variables
    Private DeLaClase As Integer = 0
    Private Function EstoEsUnaFunción() As String
        Dim DeLaFunción As Integer = 0
        If condición Then
            Dim DelBloque As Integer = 0
        End If
        'Aqui no se ve la variable DelBloque
    End Function
    ' Aquí no se ven ni DelBloque ni DeLaFunción
End Class
    public class Variables
    {
        int DeLaClase = 0;
        string EstoEsUnaFunción()
        {
            int DeLaFunción = 0;
            if (condición)
            {
                int DelBloque = 0;
            }
            //Aqui no se ve la variable DelBloque
        }
        // Aquí no se ven ni DelBloque ni DeLaFunción
    }

Las variables genéricas

Antiguamente, los lenguajes de programación se dividían entre:

los que exigían tipificación de variables como COBOL, C, C++.

Los que no exigían declaración aunque permitían al programador establecer la obligatoriedad, la familia de los Basic: BASIC, BASICA, GWBASIC, Quick Basic, Visual Basic.

Los que no tipificaban las variables como la familia de los xBase, como dBase, Fox, Clipper, Visual Fox.

Cuando surge .Net, se establece la obligatoriedad de tipificar las variables en todos los casos.

Sin embargo, bajo ciertas situaciones, la tipificación estricta puede conllevar imposibilidades programáticas. Desde la aparición de la versión 3 del .net Framework, es factible declara variables no tipificadas, pero que el compilador haga lo necesario para que el código siga siendo estricto.

En C#, por ejemplo, aparece la palabra reservada var, para indicar la declaración de una variable sin tipificar.

            var a = 1;

En Visual Basic, la ya existente sentencia Dim hace las veces de “comodín” declarativo.

Dim a = 1

 

Sin embargo, ten en cuenta que cuando se compila, estas declaraciones «genéricas» se traducen al tipo que el compilador estima debe corresponder.

En General funciona… pero no siempre.