Accesso diretto ai dati senza Entity Framework
17 maggio 2024
IIS | ASP.net C# | net core 8 | Microsoft.Data.SqlClient 5.2.1

Quando si ha a che fare con tabelle popolate con un grandissimo numero di record sulle quali è necessario effettuare correlazioni complesse la scelta di EF non è la più idonea. In questo articolo verrà presentata una comoda soluzione costituita da una interfaccia e una classe wrapper.




using System.Data;
using Microsoft.Data.SqlClient;

namespace [myapp].Database
{
public interface DataConnectionInterface
{
public string DefaultConnectionStringKey { get; }
public void OpenConnection();
public void OpenConnection(string ConnectionStringKey);
public void CloseConnection();
public Boolean TransactionPending { get; }
public ConnectionState ConnectionState { get; }
public void BeginTransaction(IsolationLevel IsolationLevel = IsolationLevel.Unspecified);
public void CommitTransaction();
public void RollbackTransaction();
public DataTable GetDataTable(string sql);
public SqlDataReader GetSqlDataReader(string sql);
public SqlCommand GetsqlCommand(string sql);
public void Execute(string sql);
}
}

using Microsoft.Data.SqlClient;
using System.Data;

namespace [myapp].Database
{
    public class DataConnection : DataConnectionInterface
    {
        private SqlConnection _SqlConnection = new SqlConnection();
        private SqlTransaction? _sqlTransaction = null;
        public string DefaultConnectionStringKey { get { return "default"; } }
        public void OpenConnection()
        {
            OpenConnection(DefaultConnectionStringKey);
        }
        public void OpenConnection(string ConnectionStringKey)
        {
            try
            {
                var builder = WebApplication.CreateBuilder();
                string? connectionString = builder.Configuration.GetConnectionString(ConnectionStringKey);
                if (connectionString == null) throw new Exception("The connection string is empty");
                _SqlConnection.ConnectionString = connectionString;
                _SqlConnection.Open();
            }
            catch (Exception ex) { throw new Exception("DataConnection:OpenConnection - " + ex.Message, ex); }
        }
        public void CloseConnection()
        {
            if (ConnectionState != ConnectionState.Closed)  
            try
            {  
                _SqlConnection.Close();
            }
            catch (Exception ex) { throw new Exception("DataConnection:CloseConnection - " + ex.Message, ex); }
        }
        public Boolean TransactionPending { get { return _sqlTransaction != null; } }
        public ConnectionState ConnectionState { get { return _SqlConnection.State; } }
        public void BeginTransaction(IsolationLevel IsolationLevel = IsolationLevel.Unspecified)
        {
            if (ConnectionState == ConnectionState.Open)
            try
            {
                _sqlTransaction = _SqlConnection.BeginTransaction(IsolationLevel);
            }
            catch (Exception ex) { throw new Exception("DataConnection:BeginTransaction - " + ex.Message, ex); }
        }
        public void CommitTransaction()
        {
            if (_sqlTransaction != null)
            try
            {
                _sqlTransaction.Commit();
                _sqlTransaction = null;
            }
            catch (Exception ex) { throw new Exception("DataConnection:CommitTransaction - " + ex.Message, ex); }
        }
        public void RollbackTransaction()
        {
            if (_sqlTransaction != null)
            try
            {
                _sqlTransaction.Rollback();
                _sqlTransaction = null;
            }
            catch (Exception ex) { throw new Exception("DataConnection:RollbackTransaction - " + ex.Message, ex); }
        }
        public SqlCommand GetsqlCommand(string sql)
        {
            SqlCommand sqlCommand;
            try
            {
                if (_sqlTransaction == null) sqlCommand = new SqlCommand(sql, _SqlConnection);
                else
                sqlCommand = new SqlCommand(sql, _SqlConnection, _sqlTransaction);
                return sqlCommand;
            }
            catch (Exception ex) { throw new Exception("DataConnection:GetsqlCommand - " + ex.Message, ex); }
        }
        public DataTable GetDataTable(string sql)
        {
            DataTable dt = new DataTable();
            try
            {
                SqlDataAdapter da = new SqlDataAdapter(sql, _SqlConnection);
                da.Fill(dt);
                return dt;
            }
            catch (Exception ex) { throw new Exception("DataConnection:GetDataTable - " + ex.Message, ex); }
        }
        public SqlDataReader GetSqlDataReader(string sql){
            try
            {
                SqlCommand cmd = GetsqlCommand(sql);
                return cmd.ExecuteReader();
            }
            catch (Exception ex) { throw new Exception("DataConnection:GetSqlDataReader - " + ex.Message, ex); }
        }
        public void Execute(string sql)
        {
            try
            {
                GetsqlCommand(sql).ExecuteNonQuery();
            }
            catch (Exception ex) { throw new Exception("DataConnection:GetSqlDataReader - " + ex.Message, ex); }
        }
        public string TextFormatted(string text)
        {
            return "'" + text.Replace("'","''") + "'";
        }

    }
}


appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
      "ConnectionStrings": {
        "default": "User ID=*****;Password=*****;Data Source=*****\\******;initial catalog=mydatabase;Encrypt=false;",
    }
}




Torna all'inizio
Come garantire la sessione utente nelle architetture di tipo load-balancing
7 maggio 2023
IIS | ASP.net C# | net framework 4.7 | web form | mvc

Il bilanciamento del carico è molto comune nei data center poichè distribuendo il traffico in entrata su più server o risorse assicura un utilizzo efficiente dell'infrastruttura massimizzandone le prestazioni ed evitando il sovraccarico delle singole risorse. Il load balancing del carico svolge dunque un ruolo fondamentale nel garantire prestazioni ottimali e disponibilità delle applicazioni ma priva lo sviluppatore della persistenza delle sessioni e il motivo è semplice. IIS genera localmente sessioni univoche per ciascun client accessibili tramite l'oggetto HttpContext.Current.Session (name space System.Web) ma il Load Balancer può reindirizzare le successive richieste di un client ad un altro web server equipaggiato con una copia speculare dell'applicazione web richiesta con conseguente generazione di una differente sessione. La soluzione più naturale a questa problematica che non richieda l'introduzione di eccezioni all'interno della tabella d'instradamento del Load Balancer è quella di spostare la generazione e la gestione della sessione sul client mediante l'utilizzo di uno specifico cookie. Di seguito descriviamo una soluzione con due classi c# utilizzabile sia con web form che con mvc senza necessità di personalizzazioni.


Struttura del cookie

Il cookie è costituito da due attributi:
1) UiCulture { identificatore alfanumerico della lingua }
2) Payload { oggetto serializzato e firmato con crittografia AES 32 byte}
UiCulture fornisce il supporto per la gestione della scelta della lingua effetuata dall'utente quando implementata. Payload memorizza gli attributi per la generazione e la gestione della sessione, ovvero:
- int? UserID;
- string authip;
- DateTimeOffset? expdate;
- double? MinutesTimeout;
- string TokenID;
La scelta del nome "Payload" è presa a prestito dalla convenzione JWT ma le similitudini terminano qui.
"UserID" è la chiave univoca che identificherà l'utente;
"authip" (Authorized IP) è l'indirizzo IP dell'host cristallizzata nel cookie alla prima richiesta pervenuta al web server;
"expdate" è la data di scadenza del cookie calcolata incrementando la data al momento della richiesta del valore "MinutesTimeout";
"TokenID" è l'equivalente di sessionID di IIS.

public class SessionCookiePayload
{
 public int? UserID { get; set; }
 public string authip { get; set; }
 public DateTimeOffset? expdate { get; set; }
 public double? MinutesTimeout { get; set; }
 public string TokenID { get; set; }
 public string JsonString()
 {
 return JsonConvert.SerializeObject(this);
 }
 public bool expired { get { if (expdate != null && MinutesTimeout != null) return (DateTime.Now > expdate); else return (UserID == null); } }
 public bool authorized { get { return authip == HttpContext.Current.Request.UserHostAddress; } }
}

Payload viene sottoposto a serializzazione e cifrato con algoritmo di crittografia AES 32 byte che garantisce una sufficiente sicurezza dai tentativi di violazione della chiave e poche risorse di elaborazione.
La chiave di criptazione ha per caratteristiche proprie dell'algoritmo natura privata ed è incorporata all'interno della successiva classe:


using System;
using System.Security.Cryptography;
using System.IO;

    public class AesEncryption
    {
        //key size = 32 byte; IV size = 16 byte;
        private static byte[] m_key = { 12, 23, 34, 45, 56, 67, 78, 89, 90, 101, 112, 123, 134, 145, 156, 167, 178, 189, 190, 201, 212, 223, 234, 245, 125, 69, 77, 128, 99, 36, 29, 3 };
        private static byte[] m_iv = { 66, 59, 12, 70, 65, 44, 38, 89, 75, 99, 80, 21, 5, 7, 11, 9 };
        public static string Encrypted(string plainText)
        {
            using (Aes myAes = Aes.Create())
            {
                myAes.Key = m_key;
                myAes.IV = m_iv;
                ICryptoTransform encryptor = myAes.CreateEncryptor(myAes.Key, myAes.IV);
                using (MemoryStream msEncrypt = new MemoryStream())
                {
                    using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                    {
                        using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                        {
                            swEncrypt.Write(plainText);
                        }
                        return Convert.ToBase64String(msEncrypt.ToArray());
                    }
                }
            }
        }
        public static string Decrypted(string plainText)
        {
            using (Aes aesAlg = Aes.Create())
            {
                aesAlg.Key = m_key;
                aesAlg.IV = m_iv;
                ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
                using (MemoryStream msDecrypt = new MemoryStream(Convert.FromBase64String(plainText)))
                {
                    using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                    {
                        using (StreamReader srDecrypt = new StreamReader(csDecrypt))
                        {
                            return srDecrypt.ReadToEnd();
                        }
                    }
                }
            }
        }
    }

La successiva classe denominata "SessionCookies" implementa il processo di generazione e gestione del nostro cookie.
Prima di utilizzarla si rammenti d'impostare CookieName con il nome desiderato.
La generazione automatica del Cookie avviene nel Global.asax con l'aggiunta della seguente interfaccia:

protected void Application_BeginRequest(object sender, EventArgs e)
 {
    SessionCookies.RefreshCookie();
 }

La logica di funzionamento è semplice.
Alla prima richiesta in assoluto pervenuta dal client verrà generato il cookie registrando l'indirizzo ip dell'host e il valore corrente della lingua.
Come già accennato all'inizio dell'articolo, la sezione Payload del cookie è serializzata e criptata. Per tale motivo, nelle richieste successive, si procederà preliminarmente alla sua decriptazione e deserializzazione controllando la corrispondenza tra l'indirizzo ip dell'host richiedente con quello indicato all'interno del suo attributo "authip".
Una volta appurata la correttezza dell'origine si verificherà l'esistenza di una sessione attiva rappresentata da un valore non null dell'attributo UserID e in caso affermativo se ne valuterà la scadenza qualora questa fosse stata impostata.
Al fine di agevolare il recupero lato server degli attributi UserID e TokenID questi vengono salvati all'interno del flusso HttpContext risparmiando un inutile nuovo sbustamento del Payload:

UserID = SessionCookies.UserId;
TokenID = SessionCookies.TokenID;

La registrazione di una sessione utente risulta estremamente agevole:
int UserID = 1000;
double? minutesTimeout = 60;
SessionCookies.WriteSessionKeyId(UserID, MinutesTimeout);
Il valore di "minutesTimeout" viene cristallizzato all'interno del Payload per gestire il ciclo di vita della sessione. Un valore null di tale attributo indicherà all'infrastruttura che il cookie non è soggetto a scadenza. In entrambi i casi il cookie, che non è accessibile tramite javascript, verà rimosso dal browser alla chiusura della sua ultima istanza.
Altrettanto semplice è la rimozione della sessione utente: SessionCookies.RemoveSessionKeyId();
Sia la registrazione di una nuova sessione utente che la sua rimozione determineranno la generazione di un nuovo TokenID.
Come già accennato al termine della descrizione della classe SessionCookiePayload l'attributo TokenID è equivalente al SessionID di IIS:
string TokenID = ComputeSHA256(DateTime.Now.ToString() + HttpContext.Current.Request.UserHostAddress)
TokenID viene utilizzato nell'infrastruttura per implementare la funzionalità di anti forgery.

public static string AntiForgeryToken()
{
  return "<input id= "" + AntiforgeryHtmlName + " name= "" + AntiforgeryHtmlName + " " type= "hidden " value= "" + TokenIDUrlEncoded + " />";
}

public static bool AntiForgeryTokenValidated(HttpRequestBase Request) {
  if (Request[AntiforgeryHtmlName] != null)
  {
    return (Request[AntiforgeryHtmlName].ToString() == TokenIDUrlEncoded);
  }
    else return false;
}

Il metodo SessionCookies.AntiForgeryToken() deve essere invocato all'interno del tag form.
Per consentire l'utilizzo della soluzione sia nei progetti web form che mvc AntiForgeryToken() restituisce il tipo string anziché MvcHtmlString. Pertanto tramite razor scriveremo:
@using (Html.BeginForm("Blog", "Home", FormMethod.Post))
{
  @Html.Raw(SessionCookies.AntiForgeryToken())
.
.
}


public ActionResult Blog()
{
  if (SessionCookies.AntiForgeryTokenValidated(Request))
  {
  //OK

  }
  else
  {
  //invalid
  }
}

A seguire un esempio con un web service mvc:

[HttpPost]
IsValidToken public void (string TokenId)
  {
    bool result = (TokenId== sessioncookies.TokenID);
    Response.StatusCode = 200;
    Response.Write("{ "risultato ":" + risultato.tostring().tolower() + " }");
  }

Javascript:

function IsValidToken(TokenId) {
  const xhr = nuovo xmlhttprequest();
  xhr.open('post', '/home/IsValidToken?TokenId=' + TokenId);
  xhr.responseType = 'json';
  xhr.send ();
  xhr.onreadystatechange = funzione () {
   //readyState
    //0 (UNSET) = Client has been created. open() not called yet.
    //1 (OPENED) = open() has been called.
    //2 (HEADERS_RECEIVED) = send() has been called, and headers and status are available.
    //3 (LOADING) = Downloading; responseText holds partial data.
   //4 (DONE) = The operation is complete.
    if (xhr.readyState == 4) {
     switch (true) {
       case (xhr.status == 200):
       //SUCCESS
       alert(xhr.response.result);
       break;
     case (xhr.status >= 400 && xhr.status < 500):
        //client error
       break;
     case (xhr.status >= 500):
     //server error
     }
    }
  }
}

Chiamata nella view:

@using (Html.BeginForm("Blog", "Home", FormMethod.Post))
{
  @Html.Raw( sessioncookies.AntiForgeryToken())
  <input id="Button1" type="button" value="IsValidToken" onclick="IsValidToken('@sessioncookies.TokenIDUrlEncoded')" />


using System;
using System.Linq;
using System.Web;
using Newtonsoft.Json;
using System.Security.Cryptography;
using System.Text;

public class SessionCookies
{
    private const string CookieName = "...........";
    private const string PayloadAttribute = "Payload";
    private const string UiCultureKey = "UiCulture";
    private const string UserIDkey = "UserIDkey";
    private const string TokenIDkey = "TokenIDkey";
    //
    private const string AntiforgeryHtmlName = "TokenIDUrlEncoded";
    public static string AntiForgeryToken()
    {
        return "<input id=\"" + AntiforgeryHtmlName + "\" name=\"" + AntiforgeryHtmlName + "\" type=\"hidden\" value=\"" + TokenIDUrlEncoded + "\" />";
    }

    public static bool AntiForgeryTokenValidated(HttpRequestBase Request) {
        if (Request[AntiforgeryHtmlName] != null)
        {
            return (Request[AntiforgeryHtmlName].ToString() == TokenIDUrlEncoded);
        }
        else return false;
    }
    //
    private static string ComputeSHA256(string s)
    {
        using (SHA256 sha256 = SHA256.Create())
        {
            byte[] hashValue = sha256.ComputeHash(Encoding.UTF8.GetBytes(s));
            //44 caratteri
            return Convert.ToBase64String(hashValue);
        }
    }

    private static SessionCookiePayload NewPayload()
    {
        SessionCookiePayload Payload = new SessionCookiePayload()
        {
            UserID = null,
            authip = HttpContext.Current.Request.UserHostAddress,
            TokenID = ComputeSHA256(DateTime.Now.ToString() + HttpContext.Current.Request.UserHostAddress),
            MinutesTimeout = null,
            expdate = null
        };
        return Payload;
    }

    private static SessionCookiePayload CurrentPayload(HttpCookie cookie)
    {
        SessionCookiePayload Payload;
        try
        {
            if (cookie[PayloadAttribute] != null)
            {
                Payload = JsonConvert.DeserializeObject(AesEncryption.Decrypted(cookie[PayloadAttribute]));
            }
            else
            {
                Payload = NewPayload();
            }
        }
        catch { Payload = NewPayload(); }
        return Payload;
    }

    private static HttpCookie SessionCookie()
    {
        HttpCookie cookie = HttpContext.Current.Response.Cookies.AllKeys.Contains(CookieName)
        ? HttpContext.Current.Response.Cookies[CookieName]
        : HttpContext.Current.Request.Cookies[CookieName];
        if (cookie == null)
        {
            cookie = new HttpCookie(CookieName);
            cookie.HttpOnly = true;
            cookie[UiCultureKey] = System.Globalization.CultureInfo.CurrentUICulture.ToString();
            cookie[PayloadAttribute] = AesEncryption.Encrypted(NewPayload().JsonString());
            HttpContext.Current.Response.Cookies.Set(cookie);
            HttpContext.Current.Response.Cookies.Add(cookie);
        }
        return cookie;
    }

    public static void RefreshCookie()
    {
        HttpCookie cookie = SessionCookie();
        System.Globalization.CultureInfo.CurrentUICulture = new System.Globalization.CultureInfo(cookie[UiCultureKey]);
        SessionCookiePayload Payload = CurrentPayload(cookie);
        if (!Payload.authorized)
        {
            cookie[PayloadAttribute] = AesEncryption.Encrypted(NewPayload().JsonString());
            HttpContext.Current.Response.Cookies.Set(cookie);
        }
        else
        switch (Payload.expired)
        {
            case true:
                if (Payload.UserID != null)
                    {
                    Payload = NewPayload();
                    cookie[PayloadAttribute] = AesEncryption.Encrypted(Payload.JsonString());
                    HttpContext.Current.Response.Cookies.Set(cookie);
                    }
                break;
            case false:
                Payload.expdate = DateTime.Now.AddMinutes(Convert.ToDouble(Payload.MinutesTimeout));
                cookie[PayloadAttribute] = AesEncryption.Encrypted(Payload.JsonString());
                HttpContext.Current.Response.Cookies.Set(cookie);
                HttpContext.Current.Items[UserIDkey] = Payload.UserID;
                break;
        }
        HttpContext.Current.Items[TokenIDkey] = Payload.TokenID;
    }
    public static int? UserId { get { return (int?)HttpContext.Current.Items[UserIDkey]; } }

    public static string TokenID { get { return HttpContext.Current.Items[TokenIDkey].ToString(); } }

    public static string TokenIDUrlEncoded { get { return HttpUtility.UrlEncode( HttpContext.Current.Items[TokenIDkey].ToString());} }

    public static void WriteSessionKeyId(int UserId, double? minutesTimeout = null)
    {
        HttpCookie cookie = SessionCookie();
        SessionCookiePayload token = NewPayload();
        token.UserID = UserId;
        if (minutesTimeout != null)
        {
            token.MinutesTimeout = minutesTimeout;
            token.expdate = DateTime.Now.AddMinutes(Convert.ToDouble(token.MinutesTimeout));
        }
        cookie[PayloadAttribute] = AesEncryption.Encrypted(token.JsonString());
        HttpContext.Current.Response.Cookies.Set(cookie);
        HttpContext.Current.Items[UserIDkey] = token.UserID;
        HttpContext.Current.Items[TokenIDkey] = token.TokenID;
    }

    public static void RemoveSessionKeyId()
    {
        HttpCookie cookie = SessionCookie();
        SessionCookiePayload token = NewPayload();
        cookie[PayloadAttribute] = AesEncryption.Encrypted(token.JsonString());
        HttpContext.Current.Response.Cookies.Set(cookie);
        HttpContext.Current.Items.Remove(UserIDkey);
        HttpContext.Current.Items[TokenIDkey] = token.TokenID;
    }

    public static void SetCulture(string Culture)
    {
        HttpCookie cookie = SessionCookie();
        System.Globalization.CultureInfo.CurrentUICulture = new System.Globalization.CultureInfo(Culture);
        cookie[UiCultureKey] = Culture;
        HttpContext.Current.Response.Cookies.Set(cookie);
    }

    public static void GenerateNewTokenID()
    {
        HttpCookie cookie = SessionCookie();
        SessionCookiePayload Payload = CurrentPayload(cookie);
        Payload.TokenID = ComputeSHA256(DateTime.Now.ToString() + HttpContext.Current.Request.UserHostAddress);
        cookie[SessionCookies.Payload] = AesEncryption.Encrypted(Payload.JsonString());
        HttpContext.Current.Response.Cookies.Set(cookie);
        HttpContext.Current.Items[TokenIDkey] = Payload.TokenID;
    }
}

Torna all'inizio
Gestione della lingua
11 maggio 2023
IIS | ASP.net C# | net framework 4.7 | web form | mvc

I file di risorse (.resx) rappresentano la soluzione naturale all'implementazione del supporto multilingua. Si tratta, in effetti, di dizionari di tipo chiave univoca \ valore \ commento scelti automaticamente durante il processo di caricamento delle viste mediante il confronto dell'estensione dei file .resx con il valore della stringa rappresentante la uiculture corrente. A seguire un semplice esempio di applicazione che si propone di tradurre il testo relativo alla politica dei cookie presentato in calce alla solita vista condivisa "_Layout.cshtml".
Per prima cosa si crei una cartella all'interno della root della soluzione intitolandola, ad esempio, "resources":




Si aggiunga un file di risorse intitolandolo, ad esempio, "_Layout.resx" e se ne modifichi la proprietà "Spazio dei nomi dello strumento personalizzato" con il nome della cartella "resources".




Dal pannello delle proprietà ci si sposti a questo punto all'interno della pagina di configurazione del file _Layout.resx. All'interno del menu a discesa, in alto a destra, intitolato "Modificatore di accesso" si selezioni il valore "Public".
In fine, si aggiunga una nuova chiave intitolata "coockiepolicy" e valore "utilizziamo esclusivamente cookie di natura tecnica".




_Layout.resx rappresenta la radice del nostro dizionario linguistico. I successivi file di risorse, uno per ciascuna lingua di nostro interesse, dovranno essere aggiunti nella stessa cartella resources con la seguente convenzione:

["_Layout"] +["."] + ["uiculture"] + [.resx]

esempio:

_Layout.it-IT.resx (italiano)
_Layout.en-US.resx (inglese USA)

Si aggiungano ora i seguenti due file di risorse impostando per ciascuno di essi solo la proprietà "Spazio dei nomi dello strumento personalizzato" con il nome della cartella "resources":




  <footer >
    .
    .
    <p class="small bi-cookie"><em class="ms-2">@resources._Layout.coockiepolicy</em></p>
 </footer>



Torna all'inizio