Press "Enter" to skip to content

2 – Diamoci un contesto

In questo secondo articolo che introduce il concetto di Business Context creiamo la classe Context per SQL Server e iniziamo a pensare a come lo utilizzeremo.

SqlDataContext

namespace Dnw.Base.Data.SqlServer.Entities
{
	public class SqlDataContext : IBusinessContext
	{

La dichiarazione della classe, e la sua implementazione dell’interfaccia IBusinessContext.

public SqlDataContext(string contextName, SqlConnectionInfo connectionInfo)
{
	mContextName = contextName;
	mContextType = "SQLDC";
	mConnectionInfo = connectionInfo;
}

Il costruttore, che pretende i dati che creano il nostro contesto. Come avete di certo notato, il Context Type viene inizializzato con una stringa arbitraria che abbiamo definito noi, poteva anche essere una enumerazione, ma considerato che i contesti potrebbero essere molti e trovarsi in librerie diverse che non interagiscono direttamente, avremmo dovuto mettere l’enumerazione in una Dll comune a tutti, un idea poteva essere metterla in Dnw.Base.v4.0.dll, decidete voi se preferite questo approccio.

public string ContextName
{
	get
	{
		return mContextName;
	}
}

public string ContextType
{
	get
	{
		return mContextType;
	}

}

public SqlConnectionInfo ConnectionInfo
{
	get
	{
		return mConnectionInfo;
	}
}

public bool IsValid
{
	get
	{
		return this.ConnectionInfo.IsValid;
	}
}

Le proprietà che definiscono il contesto, in questo caso non abbiamo implementato l’ evento property changed e tutte le property sono a sola lettura per un motivo specifico, il context è un oggetto dello strato Business di una applicazione, non ha interazione diretta con l’interfaccia utente come invece può averne l’oggetto SqlConnectionInfo che invece può essere usato per creare e salvare dati relativi alle connessioni.

Uso operativo, quanto generato ci basta?

Abbiamo generato un oggetto che può contenere le informazioni di connessione ad un database, SQL Server, pertanto possiamo utilizzarlo nell’ambito dei nostri programmi per interagire con le librerie ADO.Net del framework che ci permettono di lavorare con SQL Server. La nostra connection string fornita dal SqlConnectionInfo, potrà essere utilizzata per generare una SqlConnection che a sua volta potrà essere assegnata ad un SqlCommand. Pertanto abbiamo quanto necessario a lavorare, ma visto che alla domanda del titolo di questo paragrafo la risposta è ovviamente No, poniamoci un altra domanda:

In quanti modi funziona una connessione e come la utilizzano i command?

Risposta: ci sono due modalità per usare Sql Server da .Net:

  1. Una connessione non transazionale.Questo tipo di connessione è diretto, veloce ed è il più adatto alle operazioni di Query, quindi SELECT sui dati del nostro database oppure operazioni atomiche, la modifica di una riga in una tabella, l’inserimento o la cancellazione di una singola riga.
  2. Una connessione transazionale. Questo  tipo di connessione è invece adatto ad operazioni multiple su diversi dati e diverse tabelle che è necessario vengano eseguite completamente per mantenere il database consistente, in questo caso apriremo una transazione eseguiremo una serie di modifiche al database, quando tutte le modifiche al database saranno andate a buon fine, effettueremo un Commit della transazione e la concluderemo con successo, se al contrario una delle operazioni sul database non andasse a buon fine, potremo effettuare il Rollback della transazione riportando il database allo stato precedente evitando qualsiasi inconsistenza.

Il SqlDataContext che abbiamo disegnato noi non contiene ne una connessione ne una transazione, entrambi gli oggetti possono essere generati quando necessario da codice usando le informazioni del contesto, quindi potremo fermarci qui, se però pensiamo ad un contesto in produzione, con una applicazione complessa, potrebbe anche essere per noi necessario passare connessione e transazione a più oggetti data provider che concorrono ad una transazione, perciò, perché non creare da subito un oggetto predisposto allo scopo?

SqlOperationalContext

Anche questo è un oggetto di tipo Context, ma è costruito per essere un elemento Usa e Getta, quando devo fare qualcosa su SQL Server, ne genero uno, e quando ho terminato lo butto via. In questo contesto metteremo la connection e l’eventuale transaction da utilizzare per una o più operazioni su SQL Server. Vediamo come lo abbiamo implementato:

using System;
using System.Data;
using System.Data.SqlClient;
using Dnw.Base;

namespace Dnw.Base.Data.SqlServer.Entities
{
	public class SqlOperationalContext : IBusinessContext, IDisposable
	{

La parte dichiarativa, possiamo notare che abbiamo implementato l’interfaccia IBusinessContext anche qui ed abbiamo aggiunto l’ interfaccia IDisposable, perché lo abbiamo fatto? Gli oggetti che rispondono all’interfaccia IDisposable possono essere istanziati con una using, inoltre, implementando il metodo Dispose, possiamo assicurarci che la connessione e la transazione vengano sempre rilasciate ed eliminate.

public SqlOperationalContext(string contextName, bool isTransactional, string connectionString)
{
	mContextName = contextName;
	mContextType = "SQLOC";
	mIsTransactional = isTransactional;
	mConnection = new SqlConnection(connectionString);
}

Il costruttore del nostro Contesto operativo per SQL Server, effettua due cose importanti, decide se vogliamo usare le transazioni e crea la Connessione a SQL Server.

public bool TransactionIsOpen
{
	get
	{
		return (Transaction != null);
	}
}

public bool ConnectionIsOpen
{
	get
	{
		return (Connection.State == ConnectionState.Open);
	}
}

public SqlConnection Connection
{
	get
	{
		return (mConnection);
	}
}

public SqlTransaction Transaction
{
	get
	{
		return (mTransaction);
	}
}

public string ContextType
{
	get
	{
		return (mContextType);
	}
}

public string ContextName
{
	get
	{
		return (mContextName);
	}
}

public bool IsTransactional
{
	get
	{
		return mIsTransactional;
	}

}

Le proprietà che espongono gli oggetti del Contesto ed il suo stato che saranno utilizzabili  per fornire quanto necessario alle operazioni effettuate tramite ADO.Net.

public SqlContextResult OpenConnection()
{
	SqlContextResult isok = SqlContextResult.Ok;
	try
	{
		if (!ConnectionIsOpen)
		{
			mConnection.Open();
		}
		else
		{
			isok = SqlContextResult.ConnectionAlreadyOpened;
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		isok = SqlContextResult.ErrorOpeningConnection;
	}
	return (isok);
}

public SqlContextResult CloseConnection()
{
	SqlContextResult isok = SqlContextResult.Ok;
	try
	{
		mConnection.Close();
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		isok = SqlContextResult.ConnectionCloseError;
	}
	return (isok);
}

I due metodi per usare in modo non transazionale la connessione del SQL Operational Context, Nulla vieta di utilizzare i metodi diretti della SqlConnection, ma in questo caso abbiamo aggiunto un informazione per gestire eventuali errori in modo semplice evitando le Exception, anche se come potete osservare in ogni caso Aggiorniamo il Log dell’applicazione quando si verifica un exception.

public SqlContextResult OpenTransaction(IsolationLevel isolationLevel = IsolationLevel.RepeatableRead)
{
	SqlContextResult isok = SqlContextResult.Ok;

	if (mIsTransactional)
	{
		try
		{
			mConnection.Open();
			try
			{
				mTransaction = mConnection.BeginTransaction(isolationLevel, Guid.NewGuid().ToString().XDwLeft(32).ToString());
			}
			catch (Exception ex)
			{
				EventLogger.SendMsg(ex);
				isok = SqlContextResult.ErrorOpeningTransaction;
			}
		}
		catch (Exception ex)
		{
			EventLogger.SendMsg(ex);
			isok = SqlContextResult.ErrorOpeningConnection;
		}

	}
	else
	{
		isok = SqlContextResult.ContextNotTransactional;
	}
	return (isok);
}

public SqlContextResult CommitTransaction()
{
	SqlContextResult isok = SqlContextResult.Ok;
	try
	{
		if (mTransaction != null)
		{
			this.mTransaction.Commit();
		}
		else
		{
			isok = SqlContextResult.NoTransactionToCommit;
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		isok = SqlContextResult.CommitTransactionError;
	}
	finally
	{
		mTransaction.Dispose();
		mTransaction = null;
		mConnection.Close();
	}
	return (isok);
}

public SqlContextResult RollbackTransaction()
{
	SqlContextResult isok = SqlContextResult.Ok;
	try
	{
		if (mTransaction != null)
		{
			mTransaction.Rollback();
		}
		else
		{
			isok = SqlContextResult.NoTransactionToRollback;
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		isok = SqlContextResult.TransactionRollbackError;
	}
	finally
	{
		mTransaction.Dispose();
		mTransaction = null;
		mConnection.Close();

	}
	return (isok);
}

I tre metodi che invece utilizzano la transazione, controllandone il funzionamento e fornendo gli eventuali errori. Questi tre metodi è opportuno siano utilizzati ove necessario un contesto transazionale per avere il completo controllo sui risultati delle operazioni su Database.

public void Dispose()
{
	if (TransactionIsOpen)
	{
		mTransaction.Rollback();
	}
	if (mTransaction != null)
	{
		mTransaction.Dispose();
		mTransaction = null;
	}
	if (ConnectionIsOpen)
	{
		mConnection.Close();
	}
	if (mConnection != null)
	{
		mConnection.Dispose();
		mConnection = null;
	}
}

Il metodo Dispose del nostro operational context, che controlla e rilascia gli oggetti collegati al Database.

L’enumerazione per i codici di errore generati da questa classe, la riportiamo qui sotto, si trova nel file SqlContextResult.cs

public enum SqlContextResult
{
	Ok,
	ConnectionAlreadyOpened,
	TransactionAlreadyOpened,
	ContextNotTransactional,
	ErrorOpeningConnection,
	ErrorOpeningTransaction,
	NoTransactionToRollback,
	TransactionRollbackError,
	CommitTransactionError,
	NoTransactionToCommit,
	ConnectionCloseError
}

Adesso che abbiamo creato il nostro contesto operativo, possiamo modificare il SqlDataContext per fare in modo che sia in grado di generare gli operational context su richiesta, aggiungiamo quindi un GetOperationalContext alla classe.

public SqlOperationalContext GetOperationalContext(bool transactional)
{
	string name = string.Format( "{0}_OP_{1}", this.ContextName,(++mOpContextProgres).ToString("00#") );
	return new SqlOperationalContext(name, false, ConnectionInfo.ConnectionString);
}

Per creare un nome univoco ai contesti operativi abbiamo introdotto un contatore che si auto incrementa ad ogni richiesta, è una delle possibili soluzioni, volendo, il nome potete richiederlo come parametro al chiamante.

Il progetto di test

Per fare un test, per quanto minimale del funzionamento del nostro contesto SQL, abbiamo creato una mini applicazione WPF simile a quelle dei precedenti due progetti di test. Tale applicazione ha una finestra che ci permette di testare la connessione ad un database e  provare a lanciare un comando a SQL Server in modo transazionale o in  modo non transazionale.

test_Sql_Context

Il codice significativo per il test è quello che si trova sull’evento click dei buttons della window:

private void btnConnection_Click(object sender, RoutedEventArgs e)
{
	try
	{

		SqlConnectionInfo ci = new SqlConnectionInfo();
		ci.Server = this.txtServer.Text;
		ci.Database = this.txtDatabase.Text;
		ci.Trusted = this.chkTrusted.IsChecked.Value;
		ci.Username = this.txtUsername.Text;
		ci.Password = this.txtPassword.Password;
		SqlDataContext dc = new SqlDataContext("SQLTEST", ci);

		using (SqlOperationalContext oc = dc.GetOperationalContext(false))
		{
			this.txtResult.Text = Environment.NewLine + string.Format("Result {0}", oc.OpenConnection().ToString()) + this.txtResult.Text;
			SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS", oc.Connection, oc.Transaction);
			string resCols = cmd.ExecuteScalar().ToString();
			SqlCommand cmd2 = new SqlCommand("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES", oc.Connection, oc.Transaction);
			string resTb = cmd2.ExecuteScalar().ToString();
			this.txtResult.Text = Environment.NewLine + string.Format("Ci sono {0} colonne e {1} tabelle nel database", resCols, resTb) + this.txtResult.Text;
			this.txtResult.Text = Environment.NewLine + string.Format("Result {0}", oc.CloseConnection().ToString()) + this.txtResult.Text;
			this.txtStatus.Text = "Test connessione non transazionale completato";
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		MessageBox.Show(ex.Message, "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
	}

}

private void btnTransaction_Click(object sender, RoutedEventArgs e)
{
	try
	{

		SqlConnectionInfo ci = new SqlConnectionInfo();
		ci.Server = this.txtServer.Text;
		ci.Database = this.txtDatabase.Text;
		ci.Trusted = this.chkTrusted.IsChecked.Value;
		ci.Username = this.txtUsername.Text;
		ci.Password = this.txtPassword.Password;
		SqlDataContext dc = new SqlDataContext("SQL", ci);

		using (SqlOperationalContext oc = dc.GetOperationalContext(true))
		{
			this.txtResult.Text = Environment.NewLine + string.Format("Result {0}", oc.OpenTransaction().ToString()) + this.txtResult.Text;
			SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS", oc.Connection, oc.Transaction);
			string resCols = cmd.ExecuteScalar().ToString();
			SqlCommand cmd2 = new SqlCommand("SELECT COUNT(*) FROM INFORMATION_SCHEMA.Tables", oc.Connection, oc.Transaction);
			string resTb = cmd2.ExecuteScalar().ToString();
			this.txtResult.Text = Environment.NewLine + string.Format("Ci sono {0} colonne e {1} tabelle nel database", resCols, resTb) + this.txtResult.Text;
			this.txtResult.Text = Environment.NewLine + string.Format("Result {0}", oc.CommitTransaction().ToString()) + this.txtResult.Text;
			this.txtStatus.Text = "Test connessione transazionale completato";
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		MessageBox.Show(ex.Message, "Orrore", MessageBoxButton.OK, MessageBoxImage.Error);
	}

}

Si tratta di un test minimale, testeremo le due classi in modo più completo nelle applicazioni del loro uso perciò seguite i prossimi post.

Conclusioni

In questi articoli abbiamo introdotto il concetto di Business Context, abbiamo creato i primi due oggetti Context per le nostre applicazioni, vedremo come utilizzarli in una piccola applicazione per la manutenzione dei database.

Il progetto esempio che contiene il codice per questo articolo ed il precedente è disponibile al link seguente:

La parte di uso comune, e le classi riusabili in più progetti sono disponibili al link seguente:

Per qualsiasi commento, domanda, approfondimento, usate pure il link al modulo di contatto in cima alla pagina.