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("'","''") + "'";
}
}
}
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.
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:
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')" />
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>