Localizando desde la base de datos


En .Net Core, la interfaz IStringLocalizer, permite estandarizar la forma en que se presentan los textos según el idioma del usuario.

En general, todos los ejemplos se basan en la utilización de archivos de recursos.

En este caso, veremos cómo podemos, implementando la interfaz, obtener los textos desde una tabla de base de datos, de modo tal de poder ir incrementando dinámicamente los idiomas a los que damos soporte.

Se utiliza una tabla para contener las cadenas, con, al menos, el siguiente esquema.

CREATE TABLE [Masters].[AppResources](
	[Culture] [nvarchar](20) NOT NULL,
	[Key] [nvarchar](50) NOT NULL,
	[StringText] [nvarchar](max) NULL,
 CONSTRAINT [PK_AppResources] PRIMARY KEY CLUSTERED 
(
	[Culture] ASC,
	[Key] ASC
)

Definimos entonces una clase que implemente la interfaz Microsoft.Extensions.Localization.IStringLocalizer.

La interfaz expone directamente las propiedades de su contenido como indexador, con el método this[Clave]

/// <summary>
/// Represents a service that provides localized strings.
/// </summary>
public interface IStringLocalizer
{
    /// <summary>
    /// Gets the string resource with the given name.
    /// </summary>
    /// <param name="name">The name of the string resource.</param>
    /// <returns>The string resource as a <see cref="LocalizedString"/>.</returns>
    LocalizedString this[string name] { get; }

    /// <summary>
    /// Gets the string resource with the given name and formatted with the supplied arguments.
    /// </summary>
    /// <param name="name">The name of the string resource.</param>
    /// <param name="arguments">The values to format the string with.</param>
    /// <returns>The formatted string resource as a <see cref="LocalizedString"/>.</returns>
    LocalizedString this[string nameparams object[] arguments] { get; }

    /// <summary>
    /// Gets all string resources.
    /// </summary>
    /// <param name="includeParentCultures">
    /// A <see cref="System.Boolean"/> indicating whether to include strings from parent cultures.
    /// </param>
    /// <returns>The strings.</returns>
    IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);

Existe, además, una extensión a la interfaz, que expone el método GetString, que facilita aún más la obtención de valores

public static class StringLocalizerExtensions
{
    /// <summary>
    /// Gets the string resource with the given name.
    /// </summary>
    /// <param name="stringLocalizer">The <see cref="IStringLocalizer"/>.</param>
    /// <param name="name">The name of the string resource.</param>
    /// <returns>The string resource as a <see cref="LocalizedString"/>.</returns>
    public static LocalizedString GetString(
        this IStringLocalizer stringLocalizer,
        string name)
    {
        ArgumentNullThrowHelper.ThrowIfNull(stringLocalizer);
        ArgumentNullThrowHelper.ThrowIfNull(name);

        return stringLocalizer[name];
    }

Hagamos que nuestra clase mantenga en memoria aquellos textos que ya ha obtenido, preservándolos en un diccionario

private System.Collections.Concurrent.ConcurrentDictionary<stringstring> cachedElements
   = new();

Necesitaremos también preservar la cadena de conexión a la base de datos (que pasaremos al constructor) así como 2 objetos SqlCommand, uno para leer los valores y otro, que inserte aquellas claves que no encuentre para que resulte fácil saber que textos e idiomas nos están faltando

private readonly string ConnectionString;
SqlCommand command;
SqlCommand insertCommand;

En el constructor de la clase, aprovechemos para definir las sentencias T-SQL y los parámetros que corresponden a ambos objetos Command.

public Localizer(string connectionString)
{
   ConnectionString = connectionString;
   command = new SqlCommand("""
            SELECT [StringText]   
            FROM [Masters].[AppResources]
            WHERE  [Culture]=@Culture
            and [Key]=@Key
            """);
   command.Parameters.Add("@Culture", SqlDbType.NVarChar, 20);
   command.Parameters.Add("@Key", SqlDbType.NVarChar, 50);
   insertCommand = new SqlCommand("""
            INSERT INTO [Masters].[AppResources]
            ([Culture]
            ,[Key]
            ,[StringText]
            )
            VALUES
            (@Culture
            ,@Key
            ,'['+@Key+']')
 
            """);
   insertCommand.Parameters.Add("@Culture", SqlDbType.NVarChar, 20);
   insertCommand.Parameters.Add("@Key", SqlDbType.NVarChar, 50);
}

Definamos ahora una función que obtenga los textos, a la cual llamarán todos los métodos de implementación de la interfaz para centralizar la búsqueda en el diccionario en memoria y/o en base de datos.

private LocalizedString GetResultString(string key)
{
   if(string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key));
   string value;
   string lookFor = $"{System.Threading.Thread.CurrentThread.CurrentCulture.Name}_{key}";
   if(cachedElements.ContainsKey(lookFor))
      value = cachedElements[lookFor];
   else
   {
      value = GetFromDb(key) ?? string.Empty;
      cachedElements.TryAdd(lookFor, value);
   }
   return new LocalizedString(key, value);
}

Las acciones realizadas con la base de datos, las implementamos en otra función, que realizará la lectura, así como la inserción de claves no encontradas.

private string? GetFromDb(string key)
{
   using SqlConnection con = new SqlConnection(ConnectionString);
   con.Open();
   command.Connection = con;
   CultureInfo culture = System.Threading.Thread.CurrentThread.CurrentCulture;
   command.Parameters["@Culture"].Value = culture.Name;
   command.Parameters["@Key"].Value = key;
   var result = command.ExecuteScalar();
   // If do not found the key for the current culture,
   // try to search the same for the parent culture,
   // which is more generic as in es-AR and es
   while(result is null)
   {
      culture = culture.Parent;
      command.Parameters["@Culture"].Value = culture.LCID == 127 ? " " : culture.Name;
      result = command.ExecuteScalar();
      if(culture.LCID == 127 && result is null//This is invariant culture. No text found
      {
         result = $"[{key}]";
         culture = System.Threading.Thread.CurrentThread.CurrentCulture;
         // if we match the Invariant culture with no results, means the key is not present in the table.
         // Must be inserted
         while(!culture.IsNeutralCulture)
            culture = culture.Parent;
         insertCommand.Connection = con;
         insertCommand.Parameters["@Culture"].Value = culture.Name;
         insertCommand.Parameters["@Key"].Value = key;
         insertCommand.ExecuteNonQuery();
         insertCommand.Parameters["@Culture"].Value = " ";
         insertCommand.ExecuteNonQuery();
      }
   }
   return $"{result}";
}

Finalmente, solo resta registrar como instancia única nuestra clase para la interfaz. Por ejemplo, en el archivo program.cs de una Aplicación ASP. Net Core

builder.Services.AddSingleton<IStringLocalizer>(
   provider => new Localizer(builder.Configuration.GetConnectionString("DefaultDatabase")));

Entonces, obtendremos la instancia de la clase por inyección de dependencias, tanto en código C#, como en páginas Blazor, etc.

Para que resulte más fácil de seguir, a continuación, la clase completa.

/// <summary>
/// Implements the <see cref="IStringLocalizer"/> interface,getting the values from a database table
/// </summary>
/// <seealso cref="Microsoft.Extensions.Localization.IStringLocalizer" />
/// <remarks>
/// Requires a table with at least the following schema
/// CREATE TABLE [Masters].[AppResources](
/// 	   [Culture] [nvarchar](20) NOT NULL,
///      [Key] [nvarchar] (50) NOT NULL,
///      [StringText] [nvarchar](max) NULL,
///  CONSTRAINT[PK_AppResources] PRIMARY KEY CLUSTERED
///   (
///      [Culture] ASC,
///      [Key] ASC
///   )
/// </remarks>
public class Localizer : IStringLocalizer
 
{
   private System.Collections.Concurrent.ConcurrentDictionary<stringstring> cachedElements
      = new();
   private readonly string ConnectionString;
   SqlCommand command;
   SqlCommand insertCommand;
   public Localizer(string connectionString)
   {
      ConnectionString = connectionString;
      command = new SqlCommand("""
               SELECT [StringText]   
               FROM [Masters].[AppResources]
               WHERE  [Culture]=@Culture
               and [Key]=@Key
               """);
      command.Parameters.Add("@Culture", SqlDbType.NVarChar, 20);
      command.Parameters.Add("@Key", SqlDbType.NVarChar, 50);
      insertCommand = new SqlCommand("""
               INSERT INTO [Masters].[AppResources]
               ([Culture]
               ,[Key]
               ,[StringText]
               )
               VALUES
               (@Culture
               ,@Key
               ,'['+@Key+']')
 
               """);
      insertCommand.Parameters.Add("@Culture", SqlDbType.NVarChar, 20);
      insertCommand.Parameters.Add("@Key", SqlDbType.NVarChar, 50);
   }
   public LocalizedString this[string name] { get => GetResultString(name); }
   public LocalizedString this[string nameparams object[] arguments] { get => GetResultString(name, arguments); }
 
   public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures=> throw new NotImplementedException();
   /// <summary>
   /// Gets the localized string for the <paramref name="key"/>
   /// </summary>
   /// <param name="key">The key to search.</param>
   /// <returns>The string for the current culture</returns>
   /// <exception cref="ArgumentNullException">nameof(key)</exception>
   private LocalizedString GetResultString(string key)
   {
      if(string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key));
      string value;
      string lookFor = $"{System.Threading.Thread.CurrentThread.CurrentCulture.Name}_{key}";
      if(cachedElements.ContainsKey(lookFor))
         value = cachedElements[lookFor];
      else
      {
         value = GetFromDb(key) ?? string.Empty;
         cachedElements.TryAdd(lookFor, value);
      }
      return new LocalizedString(key, value);
   }
   /// <summary>
   ///  Gets the localized string for the <paramref name="key"/>
   /// </summary>
   /// <param name="key">The key.</param>
   /// <param name="arguments">The arguments.</param>
   /// <returns>The string for the current culture, with the <paramref name="arguments"/> included as values</returns>
   private LocalizedString GetResultString(string keyparams object[] arguments)
   {
      var resp = GetResultString(key);
      return new LocalizedString(resp.Name, string.Format(resp.Value, arguments));
   }
   private string? GetFromDb(string key)
   {
      using SqlConnection con = new SqlConnection(ConnectionString);
      con.Open();
      command.Connection = con;
      CultureInfo culture = System.Threading.Thread.CurrentThread.CurrentCulture;
      command.Parameters["@Culture"].Value = culture.Name;
      command.Parameters["@Key"].Value = key;
      var result = command.ExecuteScalar();
      // If do not found the key for the current culture,
      // try to search the same for the parent culture,
      // which is more generic as in es-AR and es
      while(result is null)
      {
         culture = culture.Parent;
         command.Parameters["@Culture"].Value = culture.LCID == 127 ? " " : culture.Name;
         result = command.ExecuteScalar();
         if(culture.LCID == 127 && result is null//This is invariant culture. No text found
         {
            result = $"[{key}]";
            culture = System.Threading.Thread.CurrentThread.CurrentCulture;
            // if we match the Invariant culture with no results, means the key is not present in the table.
            // Must be inserted
            while(!culture.IsNeutralCulture)
               culture = culture.Parent;
            insertCommand.Connection = con;
            insertCommand.Parameters["@Culture"].Value = culture.Name;
            insertCommand.Parameters["@Key"].Value = key;
            insertCommand.ExecuteNonQuery();
            insertCommand.Parameters["@Culture"].Value = " ";
            insertCommand.ExecuteNonQuery();
         }
      }
      return $"{result}";
   }
}
/// <summary>
/// /
/// </summary>
/// <typeparam name="T"></typeparam>
public class Localizer<T> : IStringLocalizer<T>
{
   IStringLocalizer _localizer;
 
   public Localizer(string connectionString)
   {
      _localizer = new Localizer(connectionString);
   }
   public LocalizedString this[string name] { get => _localizer[name]; }
   public LocalizedString this[string nameparams object[] arguments] { get => _localizer[name, arguments]; }
 
   public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures=> _localizer.GetAllStrings();
}

CREANDO CADENA DE CONEXIÓN PARA SQL SERVER: WPF IU


Al igual que hicimos en publicación anterior para formularios Windows, armemos ahora un diálogo, pero para WPF, que utilice nuestro componente de creación de cadenas de conexión.

Para la interactividad, será necesario definir algunas propiedades, en el modelo de la página WPF, pero como dependientes (DependencyProperty)

public Boolean ShowDBs
{
   get { return (Boolean)GetValue(ShowDBsProperty); }
   set { SetValue(ShowDBsProperty, value); }
}
 
// Using a DependencyProperty as the backing store for ShowDBs.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty ShowDBsProperty =
    DependencyProperty.Register("ShowDBs"typeof(Boolean), typeof(dlgConnectionModel), new PropertyMetadata(false));

De igual forma, para poder habilitar el botón Aceptar cuando la conexión sea correcta, etc.

A su vez, el componente lanza un evento cuando, por cambio en el tipo de autenticación, requiere (o no), credenciales.

Este evento, atrapado en el modelo, permitirá a su vez, habilitar o deshabilitar los controles de usuario y contraseña.

public dlgConnectionModel()
{
   CreateCommands();
   currentConnection = new();
   Current.RequireCredentials += Connection_RequireCredentials;
}
private void Connection_RequireCredentials(
   object? sender, 
   RequireCredentialsEventArgs e)
   => NeedCredentials = e.NeedCredentials;

Finalmente, al igual que en el diálogo para WinForms, el constructor de la ventana se establece como internal, para que sólo la clase que retorna la cadena de conexión pueda crear una nueva instancia.

Desde una aplicación WPF entonces, se podrá solicitar una cadena de conexión, a través de

ConnectionBuilder.PromptForConnection

El código fuente de este diálogo está aquí.

También utiliza una biblioteca de clases de apoyo para WPF que se encuentra aquí.

Creando cadena de conexión para SQL Server: WinForms IU


Teniendo ya el componente que nos permite generar una cadena de conexión, procedamos a crear un componente que cree un diálogo para utilizar dicha clase, desde Windows Forms.

La interfaz de usuario será la siguiente:

Para ello, generaremos un proyecto de tipo biblioteca de clases, al cual agregaremos un formulario de Windows (aunque sea biblioteca de clases), con la interfaz requerida.

Nótese que la primera imagen presenta los literales en español, mientras que, en diseño, los textos están en inglés. Esto es así, porque estamos operando con localización del componente.

El proyecto tiene una clase, con un único método para obtener la cadena de conexión:

public static class ConnectionBuilder
{
   public static string? PromptForConnection(string connectionString = null)
   {
      var dlg = new NDSoft.SqlTools.WinForms.dlgConnection();
      if(!string.IsNullOrEmpty(connectionString))
         dlg.ConnectionString = connectionString;
      if(dlg.ShowDialog() == DialogResult.OK)
         return dlg.ConnectionString;
      else
         return null;
   }
}

Un detalle importante es que el constructor del formulario NO es público, sino internal. De esa forma, no se puede crear una instancia del formulario desde otro proyecto, sino que, necesariamente, debe utilizarse el método PromptForConnection de la clase estática.

El formulario maneja la interactividad, validación, etc. que corresponde a los distintos controles de captura de datos.

Además, reaccionará ante el evento del componente para habilitar o deshabilitar los controles de captura de usuario y contraseña, cuando corresponda.

private void EnableCredentials(Boolean enable)
{
   txtPassword.Enabled = enable;
   txtUsername.Enabled = enable;
}

Finalmente, en las propiedades del proyecto, habilitaremos el empaquetado, lo que permitirá que dispongamos de un paquete Nuget con nuestro componente, para agregar a los proyectos en los que lo necesitemos.

El paquete se generará dentro de la carpeta bin, en la carpeta que corresponda al perfil de compilación (Debug, Release).

Puedes encontrar el proyecto fuente aquí

Creando cadenas de conexión para SQL Server


La cadena de conexión de SQL Server, varía según la ubicación (local, remota o en la nube) del servidor, así como, según el método de autenticación del usuario. Éste último, ha tenido varios agregados al integrarse con el Directorio Activo y su extensión en la nube, incluyendo Microsoft Entra.

Es interesante que la biblioteca Microsoft.Data.SqlClient expone una clase, SqlConnectionStringBuilder, que nos permite generar la cadena de conexión pasándole los parámetros requeridos.

En este ejemplo, les muestro una clase, definida en una biblioteca de clases, que permite generar cadenas de conexión, encapsulando la funcionalidad

SqlConnStringDefinition

Como se ve, expone como propiedades, los distintos elementos necesarios u opcionales, requeridos para definir una cadena de conexión.

A su vez, la propiedad ConnectionString genera la misma, al retornarla, según este código:

public string ConnectionString
{
   get
   {
      SqlAuthenticationMethod method = (SqlAuthenticationMethod)Enum.Parse(typeof(SqlAuthenticationMethod), AuthenticationMethod);
      SqlConnectionStringBuilder sqlConnectionStringBuilder = new SqlConnectionStringBuilder();
      sqlConnectionStringBuilder.DataSource = Server;
      sqlConnectionStringBuilder.InitialCatalog = Database ?? "master";
      sqlConnectionStringBuilder.Authentication = method;
      sqlConnectionStringBuilder.MultipleActiveResultSets = MultipleActiveResultSets;
      sqlConnectionStringBuilder.Encrypt = Encrypt;
      sqlConnectionStringBuilder.TrustServerCertificate = TrustServerCertificate;
      sqlConnectionStringBuilder.ApplicationName = ApplicationName ?? "No name";
      switch(method)
      {
         case SqlAuthenticationMethod.ActiveDirectoryIntegrated:
            sqlConnectionStringBuilder.IntegratedSecurity = true;
            break;
 
         case SqlAuthenticationMethod.ActiveDirectoryInteractive:
            break;
 
         case SqlAuthenticationMethod.ActiveDirectoryPassword:
         case SqlAuthenticationMethod.SqlPassword:
            sqlConnectionStringBuilder.UserID = Username;
            sqlConnectionStringBuilder.Password = Password;
            break;
 
         case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal:
         case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow:
         case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity:
         case SqlAuthenticationMethod.ActiveDirectoryMSI:
         case SqlAuthenticationMethod.ActiveDirectoryDefault:
         case SqlAuthenticationMethod.NotSpecified:
            break;
 
         default:
            break;
      }
      return sqlConnectionStringBuilder.ToString();
   }
}

El proceso inverso, esto es obtener un objeto SqlConnectionStringBuilder a partir de una cadena de conexión, lo realiza el método BuildFromString

public static SQLConStringDefinition BuildFromString(String connectionString)
{
   SqlConnectionStringBuilder sqlConnectionStringBuilder = new SqlConnectionStringBuilder(connectionString);
   SQLConStringDefinition conStringDefinition = new SQLConStringDefinition()
   {
      Server = sqlConnectionStringBuilder.DataSource,
      Database = sqlConnectionStringBuilder.InitialCatalog,
      AuthenticationMethod = sqlConnectionStringBuilder.Authentication.ToString(),
      ApplicationName = sqlConnectionStringBuilder.ApplicationName,
      Username = sqlConnectionStringBuilder.UserID,
      Password = sqlConnectionStringBuilder.Password,
      TrustServerCertificate = sqlConnectionStringBuilder.TrustServerCertificate,
      Encrypt = sqlConnectionStringBuilder.Encrypt,
      MultipleActiveResultSets = sqlConnectionStringBuilder.MultipleActiveResultSets
   };
   return conStringDefinition;
}

Finalmente, la clase expone un evento, RequireCredentials, que se lanza cuando cambia la condición de requerir (o no), usuario y contraseña.

Ese evento se lanza al cambiar el valor de la propiedad AuthenticationMethod

public string AuthenticationMethod
{
   get => authenticationMethod; set
   {
      authenticationMethod = value;
      SqlAuthenticationMethod method = (SqlAuthenticationMethod)Enum.Parse(typeof(SqlAuthenticationMethod), AuthenticationMethod);
      switch(method)
      {
         case SqlAuthenticationMethod.ActiveDirectoryPassword:
         case SqlAuthenticationMethod.SqlPassword:
            CallRequireCredentials(true);
            break;
 
         case SqlAuthenticationMethod.ActiveDirectoryIntegrated:
         case SqlAuthenticationMethod.ActiveDirectoryInteractive:
         case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal:
         case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow:
         case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity:
         case SqlAuthenticationMethod.ActiveDirectoryMSI:
         case SqlAuthenticationMethod.ActiveDirectoryDefault:
         case SqlAuthenticationMethod.NotSpecified:
            CallRequireCredentials(false);
            break;
 
         default:
            CallRequireCredentials(false);
            break;
      }
   }
}

Nota: La idea de tenerlo como una clase en una biblioteca de clases, es para poder utilizarlo desde distintos clientes (Windows Forms, WPF, UWP, etc.)

Cosa que haremos en próximas entregas 🙂

De paso, mis deseos de un próspero y venturoso 2024 para todos.

Windows Forms IU

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

El blog de Dani Seara

Microsoft Azure Blog

El blog de Dani Seara

VIVIENDO RODANDO

de rodar con cámara a rodar en una silla

Matías Iacono

Cynically bored

Leandro Tuttini Blog

El blog de Dani Seara

WindowServer

El blog de los paso a paso

campusMVP.es

El blog de Dani Seara

Angel \"Java\" Lopez on Blog

Software Development, in the Third Millenium

Angel "Java" Lopez

El blog de Dani Seara

Atascado en los 70 II (El regreso)

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.

Geeks.ms

El blog de Dani Seara

El Bruno

Dev Advocating 🥑 @Microsoft

Cajon desastre

Just another WordPress.com site

Pasión por la tecnología...

Todo sobre tecnología Microsoft en general, y Office 365 y SharePoint en partícular...

return(GiS);

Mi sitio geek