I'm trying to make a thread safe Data Access Layer (kind of like a SQL Data Client wrapper). What are some steps I should be making to make this thread safe, while maximizing performance.
For example, if i add a lock on the sqlConn before it closes the connection (since it implements IDisposable); what if the connection is in the middle of a transaction or query?
In summary, I'm trying to accomplish a thread-safe solution; but at the same time, I do not want to risk any critical exceptions, or any delays. Is there any way I can prioritize the closing thread?
public class SQLWrapper : IDisposable
{
private SqlConnection _sqlConn;
public SQLWrapper(string serverName_, string dbName_)
{
SqlConnectionStringBuilder sqlConnSB = new SqlConnectionStringBuilder()
{
DataSource = serverName_,
InitialCatalog = dbName_,
ConnectTimeout = 30,
IntegratedSecurity = true,
};
sqlConnSB["trusted_connection"] = "yes";
this.start(sqlConnSB.ConnectionString);
}
public SQLWrapper(string connString_)
{
this.start(connString_);
}
private void start(string connString_)
{
if (string.IsNullOrEmpty(connString_) == true)
throw new ArgumentException("Invalid connection string");
**lock (this._sqlConn)**
{
this._sqlConn = new SqlConnection(connString_);
this._sqlConn.Open();
}
}
private void CloseConnection()
{
**lock (this._sqlConn)**
{
this._sqlConn.Close();
this._sqlConn.Dispose();
this._sqlConn = null;
}
}
}
The step you should do is:
NOT making it thread safe.
Simple.
Every thread should have it's own copy, with locking / synchronization happening on the database.
THen it will also scale across computers.
This is the standard approach for the last 20 years or so.
So, every thread creates a new SqlWrapper and everything is fine.
The database performs connection pooling for you; lean on it as much as you can. You really shouldn't require locking.
Option 1.
SqlConnection is not encapsulated by the DAO class; appropriate using structures and storage of the connection string is required at the method level.
public class SomeDAO
{
private readonly string _connectionString;
public SomeDAO(string dsn)
{
_connectionString = dsn;
}
public IEnumerable<AssetVO> DoWork()
{
const string cmdText = "SELECT [AssetId] FROM [dbo].[Asset]";
using (var conn = new SqlConnection(_connectionString))
{
conn.Open();
using (var cmd = new SqlCommand(cmdText, conn))
using (var dr = cmd.ExecuteReader())
{
while (dr.Read())
{
yield return new AssetVO
{
AssetId = Guid.Parse(dr["AssetId"].ToString()),
};
}
}
}
}
}
Option 2.
The SqlConnection is a class member; it's state is carefully maintained by helper methods. Using syntax is used for SqlDataReader and SqlCommand.
public class SomeDAO : IDisposable
{
#region backing store
private readonly SqlConnection _connection;
#endregion
public SomeDAO(string dsn)
{
_connection = new SqlConnection(dsn);
}
public SqlConnection OpenConnection()
{
if (_connection.State != ConnectionState.Closed)
_connection.Open();
return _connection;
}
public void CloseConnection()
{
if (_connection.State != ConnectionState.Closed)
_connection.Close();
}
public IEnumerable<AssetVO> DoWork()
{
const string cmdText = "SELECT [AssetId] FROM [dbo].[Asset]";
try
{
using (var cmd = new SqlCommand(cmdText, OpenConnection()))
using (var dr = cmd.ExecuteReader())
{
while (dr.Read())
{
yield return new AssetVO
{
AssetId = Guid.Parse(dr["AssetId"].ToString()),
};
}
}
}
finally
{
CloseConnection();
}
}
#region Implementation of IDisposable
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <filterpriority>2</filterpriority>
public void Dispose()
{
_connection.Dispose();
}
#endregion
}
Both solutions survive a threaded test without the need for explicit locking.
private static volatile bool _done;
private static void Main()
{
#region keyboard interrupt
ThreadPool.QueueUserWorkItem(delegate
{
while (!_done)
{
if (!Console.KeyAvailable) continue;
switch (Console.ReadKey(true).Key)
{
case ConsoleKey.Escape:
_done = true;
break;
}
}
});
#endregion
#region start 3 threads in the pool
ThreadPool.QueueUserWorkItem(DatabaseWorkerCallback);
ThreadPool.QueueUserWorkItem(DatabaseWorkerCallback);
ThreadPool.QueueUserWorkItem(DatabaseWorkerCallback);
#endregion
Thread.Sleep(Timeout.Infinite);
}
private static void DatabaseWorkerCallback(object state)
{
Console.WriteLine("[{0}] Starting", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
while (!_done)
{
using (var dao = new SomeDAO(Properties.Settings.Default.DSN))
{
foreach (var assetVo in dao.DoWork())
Console.WriteLine(assetVo);
}
}
Console.WriteLine("[{0}] Stopping", Thread.CurrentThread.ManagedThreadId);
}
Related
I have SignalR ASP.NET Core project and trying to send message tp specific user which I manage to do so. The problem is same message send n time n=number of tabs or connections when ever there is change from database it send notification to that certain User but duplicate the message.
public class MessageHub: Hub
{
public MessageHub(IConfiguration cc, IHubContext<MessageHub> _mess, UserManager<ApplicationUser> rep)
{
mess = _mess;
_conf = cc;
_usermanager = rep;
RegisterNotification();
}
public void RegisterNotification()
{
using (SqlConnection con = new SqlConnection(conStr))
{
SqlCommand cmd = new SqlCommand(sqlCommand, con);
if (con.State != System.Data.ConnectionState.Open)
{
con.Open();
}
cmd.Notification = null;
SqlDependency sqlDep = new SqlDependency(cmd);
sqlDep.OnChange += sqlDep_OnChange;
using (SqlDataReader reader = cmd.ExecuteReader())
{
// nothing need to add here now
}
}
}
static int count = 1;
private void sqlDep_OnChange(object sender, SqlNotificationEventArgs e)
{
if (e.Type == SqlNotificationType.Change )
{
SqlDependency sqlDep = sender as SqlDependency;
sqlDep.OnChange -= sqlDep_OnChange;
mess.Clients.User("64ed09d7-255f-4aae-a25f-7a50e59943b4").SendAsync("abc", "hello"+count);
count += 1;
RegisterNotification();
}
}
}
as you have mentioned, the problem is RegisterNotification is being called everytime an instance of MessageHub is created. Instead of this, you can create background service and register to SqlDependency there and send the events using IHubContext. And after every notification, you call RegisterNotification again, which might cause the duplication as well.
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services
https://learn.microsoft.com/en-us/aspnet/core/signalr/hubcontext
I haven't tested this code, but here is an example:
public class EventBroadcaster : IHostedService
{
private readonly IHubContext<MessageHub> _hubContext;
private readonly string _connectionString;
public EventBroadcaster(
IConfiguration configuration,
IHubContext<MessageHub> hubContext)
{
_hubContext = hubContext;
_connectionString = // Get Connection String from configuration
}
public Task StartAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Started event broadcasting service");
RegisterNotification();
return Task.CompletedTask;
}
public void RegisterNotification()
{
using (SqlConnection con = new SqlConnection(_connectionString))
{
SqlCommand cmd = new SqlCommand(sqlCommand, con);
if (con.State != System.Data.ConnectionState.Open)
{
con.Open();
}
cmd.Notification = null;
SqlDependency sqlDep = new SqlDependency(cmd);
sqlDep.OnChange += sqlDep_OnChange;
using (SqlDataReader reader = cmd.ExecuteReader())
{
// nothing need to add here now
}
}
}
static int count = 1;
private async void sqlDep_OnChange(object sender, SqlNotificationEventArgs e)
{
if (e.Type == SqlNotificationType.Change )
{
SqlDependency sqlDep = sender as SqlDependency;
sqlDep.OnChange -= sqlDep_OnChange;
await _hubContext.Clients.User("64ed09d7-255f-4aae-a25f-7a50e59943b4").SendAsync("abc", "hello"+count);
count += 1;
}
}
public Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Event Broadcasting Service is stopping.");
// TODO: Stop listening to SqlDependency notifications
return Task.CompletedTask;
}
I am currently working on a problem that involves abstract classes and stopwatches. I have two classes, SQL and Oracle. They both take a string for a connection code (this stuff doesn't actually do anything, but I am trying to make this a bit realistic). I want to start a stopwatch, then stop it in a different method - but the Timespan always says 00:00...
Am I accessing my parent class' properties correctly?
I have tried initializing my stopwatches and timespan in different places.
public class Program
{
public static void Main(string[] args)
{
// ConnectionManagement management = new ConnectionManagement();
// management.SetUpOptions();
}
}
public class ConnectionManagement
{
public void SetUpOptions()
{
while (true)
{
SqlConnection sqlGatherer = new SqlConnection("placeholder");
OracleConnection oracleGatherer = new OracleConnection("placeholder");
Console.WriteLine("1. Open an SQL connection.");
Console.WriteLine("2. Close an SQL connection.");
Console.WriteLine("3. Open an Oracle connection.");
Console.WriteLine("4. Close an SQL connection.");
string choice = Console.ReadLine();
if (choice == "1")
{
Console.WriteLine("Enter your connection string.");
string enteredConnectionString = Console.ReadLine();
sqlGatherer.ConnectionString = enteredConnectionString;
sqlGatherer.OpenConnection();
}
else if (choice == "2")
{
sqlGatherer.CloseConnection();
}
else if (choice == "3")
{
Console.WriteLine("Enter your connection string.");
string enteredConnectionString = Console.ReadLine();
oracleGatherer.ConnectionString = enteredConnectionString;
oracleGatherer.OpenConnection();
}
else if (choice == "4")
{
oracleGatherer.CloseConnection();
}
else
{
Console.WriteLine("That was not a valid option.");
}
}
}
}
public abstract class DataBaseConnection
{
public string ConnectionString { get; set; }
public TimeSpan Timeout { get; set; }
public Stopwatch OracleStoppy { get; set; }
public Stopwatch SqlStoppy { get; set; }
public abstract void OpenConnection();
public abstract void CloseConnection();
}
public class SqlConnection : DataBaseConnection
{
private bool CurrentConnection = false;
public SqlConnection()
{
Timeout = new TimeSpan();
SqlStoppy = new Stopwatch();
}
public SqlConnection(string connectionString)
{
Timeout = new TimeSpan();
SqlStoppy = new Stopwatch();
if (connectionString == null || String.IsNullOrWhiteSpace(connectionString))
{
throw new ArgumentException("Program has an invalid SQL connection string.");
}
else
{
this.ConnectionString = connectionString;
}
}
public override void OpenConnection()
{
if (CurrentConnection == true)
{
throw new Exception("A connection has already been established.");
}
else
{
Console.WriteLine("SQL connection established.");
SqlStoppy.Start();
CurrentConnection = true;
}
}
public override void CloseConnection()
{
if (CurrentConnection == false)
{
SqlStoppy.Stop();
TimeSpan reportedTimeout = Timeout;
Console.WriteLine("Connection closed. \nThe connection was active for {0}", reportedTimeout);
SqlStoppy.Reset();
CurrentConnection = false;
}
else
{
throw new Exception("There is no SQL connection to close.");
}
}
}
public class OracleConnection : DataBaseConnection
{
private bool CurrentConnection = false;
public OracleConnection()
{
Timeout = new TimeSpan();
OracleStoppy = new Stopwatch();
}
public OracleConnection(string connectionString)
{
Timeout = new TimeSpan();
OracleStoppy = new Stopwatch();
if (connectionString == null || String.IsNullOrWhiteSpace(connectionString))
{
throw new Exception("Program has an invalid Oracle connection string.");
}
else
{
this.ConnectionString = connectionString;
}
}
public override void OpenConnection()
{
if (CurrentConnection == true)
{
throw new Exception("A connection has already been established.");
}
else
{
Console.WriteLine("Oracle connection established.");
OracleStoppy.Start();
CurrentConnection = true;
}
}
public override void CloseConnection()
{
if (CurrentConnection == false)
{
throw new Exception("There is no Oracle connection to close.");
}
else
{
OracleStoppy.Stop();
this.Timeout = OracleStoppy.Elapsed;
Console.WriteLine("Connection closed. \nThe connection was active for {0}", Timeout);
OracleStoppy.Reset();
CurrentConnection = false;
}
}
}
After I close an opened connection, the method should print how long it was opened for, or the stopwatch duration. The same problem happened with my CurrentConnection variable, so I put it as a private variable in each class. But isn't the point of having a parent class like this to have common properties that its children classes can interact with?
Problem
I see that in oracle you have:
this.Timeout = OracleStoppy.Elapsed;
Console.WriteLine("Connection closed. \nThe connection was active for {0}", Timeout);
But in sql:
TimeSpan reportedTimeout = Timeout;
Console.WriteLine("Connection closed. \nThe connection was active for {0}", reportedTimeout);
This should be
this.Timeout = SqlStoppy.Elapsed;
Console.WriteLine("Connection closed. \nThe connection was active for {0}", Timeout);
Isn't it?
Advices
You use a parent class, so you don't need two stopwatches, only one is needed else inheritence is useless and you repeat code.
You should consider remove some setters too.
And move CurrentConnection that is repeated, as protected.
This should be:
public abstract class DataBaseConnection
{
protected bool CurrentConnection;
public string ConnectionString { get; }
public TimeSpan Timeout { get; }
public Stopwatch Stoppy { get; }
public abstract void OpenConnection();
public abstract void CloseConnection();
public DataBaseConnection()
{
Timeout = new TimeSpan();
Stoppy = new StopWatch();
}
}
Also use call pattern between constructors:
public OracleConnection()
{
...
}
public OracleConnection(string connectionString)
: this()
{
...
}
You repeat too many same code and you really should refactor and better abstract things.
When code is repeated you can:
create one method and call it instead of repeated code.
sometimes you can move that in a parent class.
When variables is repeated in child classes you can remove them to have only one in a parent class.
You do that playing with modifiers like public, private, protected, internal, abstract, virtual, override...
You should also rename some vars to be more consistent and coherent.
The rule is that a name must be simple and designate exactly what it is.
For example Chrono may be better that Stoppy as well as index or indexRowis better that i.
So here Duration or Elapsed may be better than Timeout that indicates the delay to stop a connection attempt.
Also because classes are named Connection you don't need to specify OpenConnection and CloseConnection: Open and Close is sufficient.
Perhaps you will find this usefull:
How to choose between private and protected access modifier
What is polymorphism
I'm getting some troubles in the last days to use the SQL Dependency.
With the sample in this link I have ALMOST reach my goal. "Almost" because the event get fired correctly, but also at the startup, when I call the Start method of the MyService class. Here there is my code, but as you can see it's the same (or almost) of the link.
I think I can simply insert a bool value, only to avoid the initialitation event, but I think also that another solution would be better.
SQLWatcher:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
public enum SqlWatcherNotificationType
{
Blocking,
Threaded // Launch in another thread so SqlWatcher can immediately start monitoring again.
}
public class SqlWatcher : IDisposable
{
private string ConnectionString;
private SqlConnection Connection;
private SqlCommand Command;
private SqlDataAdapter Adapter;
private DataSet Result;
private SqlWatcherNotificationType NotificationType;
public SqlWatcher(string ConnectionString, SqlCommand Command, SqlWatcherNotificationType NotificationType)
{
this.NotificationType = NotificationType;
this.ConnectionString = ConnectionString;
SqlDependency.Start(this.ConnectionString);
this.Connection = new SqlConnection(this.ConnectionString);
this.Connection.Open();
this.Command = Command;
this.Command.Connection = this.Connection;
Adapter = new SqlDataAdapter(this.Command);
}
public void Start()
{
RegisterForChanges();
}
public void Stop()
{
SqlDependency.Stop(this.ConnectionString);
}
public delegate void SqlWatcherEventHandler(DataSet Result);
public event SqlWatcherEventHandler OnChange;
public DataSet DataSet
{
get { return Result; }
}
private void RegisterForChanges()
{
//Remove old dependency object
this.Command.Notification = null;
//Create new dependency object
SqlDependency dep = new SqlDependency(this.Command);
dep.OnChange += new OnChangeEventHandler(Handle_OnChange);
//Save data
Result = new DataSet();
Adapter.Fill(Result);
//Notify client of change to DataSet
switch (NotificationType)
{
case SqlWatcherNotificationType.Blocking:
OnChange(Result);
break;
case SqlWatcherNotificationType.Threaded:
ThreadPool.QueueUserWorkItem(ChangeEventWrapper, Result);
break;
}
}
public void ChangeEventWrapper(object state)
{
DataSet Result = (DataSet)state;
OnChange(Result);
}
private void Handle_OnChange(object sender, SqlNotificationEventArgs e)
{
if (e.Type != SqlNotificationType.Change)
throw new ApplicationException("Failed to create queue notification subscription!");
//Clean up the old notification
SqlDependency dep = (SqlDependency)sender;
dep.OnChange -= Handle_OnChange;
//Register for the new notification
RegisterForChanges();
}
public void Dispose()
{
Stop();
}
}
and this is MyService class:
public class MyService
{
private static SqlWatcher SqlQueueWatcher;
public static void Start()
{
string connS = MainWindow.dbContext.Database.Connection.ConnectionString + "Password=111;";
//Build the command object we want to monitor (don't include a SqlConnection)
SqlCommand cmd = new SqlCommand();
cmd = new SqlCommand("SELECT CODVEI FROM dbo.ArchivioErogazioni");
cmd.CommandType = CommandType.Text;
//Setup the SQLWatcher
SqlQueueWatcher = new SqlWatcher(connS, cmd, SqlWatcherNotificationType.Blocking);
SqlQueueWatcher.OnChange += new SqlWatcher.SqlWatcherEventHandler(QueueSQLWatcher_OnChange);
SqlQueueWatcher.Start();
}
private static void QueueSQLWatcher_OnChange(DataSet Result)
{
//Do something with the updated DataSet object
Debug.WriteLine("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); ---> ENTERS HERE IN THE INIT PHASE
}
public static void Stop()
{
SqlQueueWatcher.Dispose();
}
}
and this is how I symply call it:
MyService.Start();
Is the bool flag a bad idea? Why does it enter in QueueSQLWatcher_OnChange at startup time?
To fix this issue, you need only to set the notification for that SqlCommand to null.
cmd.Notification = null;
Just try this and i think your problem will be resolved. Enter this line before starting the dependency using the SqlWatcher class
I'm trying to build a class that will be responsible for all operations on database.
Using singleton pattern from here: http://codebender.denniland.com/a-singleton-base-class-to-implement-the-singleton-pattern-in-c/
I've build a class like so:
class DB : SingletonBase<DB>
{
public static readonly string SqlConnectionString = #"Data Source=MYDB;Initial Catalog=PRODUCTS;Integrated Security=True;Asynchronous Processing=true;";
private DB()
{
}
public void loadData()
{
SqlConnection conn = new SqlConnection(SqlConnectionString);
SqlCommand cmd = conn.CreateCommand();
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "STATISTICS_1";
cmd.Connection = conn;
conn.Open();
IAsyncResult result = cmd.BeginExecuteReader(new AsyncCallback(HandleCallback), cmd, CommandBehavior.CloseConnection);
}
private void HandleCallback(IAsyncResult result)
{
SqlDataReader dr;
SqlCommand _this = (SqlCommand)result.AsyncState;
if (result.IsCompleted)
{
dr = _this.EndExecuteReader(result);
}
else
dr = null;
DataTable dt = new DataTable();
dt.Load(dr);
dr.Close();
MessageBox.Show("loaded");
}
}
In my main class I'm using this like so:
private void loadStatistics(object sender, EventArgs e)
{
showStatus("loading data");
DB.Instance.loadData();
}
But this will give me only my message box.
What I would like to do is to declare function in my main class that will be called after SQL query return something.
I think that the best way would by using events, but I don't know how to do that proper way.
I would like to do something like this in my main class:
private void loadCompleted(string msg)
{
MessageBox.Show(msg);
}
private void loadStatistics(object sender, EventArgs e)
{
showStatus("loading data");
DB.Instance.loadData(loadCompleted);
}
So that I can specify function that will be called after SQL call is finished.
I don't know if this is the best way of doing this, so any comments, suggestions and solutions are welcome.
What I would like to achieve is to have one class responsible for calling SQL asynchronously and passing data to other functions that will process it.
public delegate void NotifyCallback(string message);
public class ClassWithCommandAndCallback
{
public SqlCommand Sql;
public NotifyCallback Callback;
}
public void loadData(NotifyCallback callback)
{
ClassWithCommandAndCallback ar = new ClassWithCommandAndCallback();
ar.Sql = cmd;
ar.Callback = callback;
IAsyncResult result = cmd.BeginExecuteReader(new AsyncCallback(HandleCallback), ar, CommandBehavior.CloseConnection);
}
private void HandleCallback(IAsyncResult result)
{
ClassWithCommandAndCallback ar = (ClassWithCommandAndCallback)result.AsyncState;
ar.Callback("loaded (SQL was: "+ar.Sql+")");
}
I have a class that is responsible for all my Database actions, in general it calls stored procedures.
I've created 2 delegates in my class, one responsible for positive response (server returned OK for example) and second for all error handling.
public delegate void Part1_Callback(string message);
public delegate void Part2_Callback(DataTable dt);
public delegate void Part3_Callback(DataTable dt, int x, int y);
public delegate void ErrorHandler(string message);
I call all methods asynch like shown in my previous question: C# asynch SQL inside singleton and delegates
I have a problem when I need my delegate to return a different type of data.
For example my first method returns a String, second a DataTable, and third a DataTable and 2 ints.
Right now for every method I must create class that holds parameters:
public class Part1_CommandAndCallback
{
public SqlCommand Sql;
public Part1_Callback Callback;
public ErrorHandler Error;
}
public class Part2_CommandAndCallback
{
public SqlCommand Sql;
public Part2_Callback Callback;
public ErrorHandler Error;
}
public class Part3_CommandAndCallback
{
public SqlCommand Sql;
public Part3_Callback Callback;
public ErrorHandler Error;
}
Is it possible to create one generic delegate so that I will be able to have one delegate for responses and one class for parameters?
This way I'll be able to more easily control my code.
I found article on codeproject: http://www.codeproject.com/Articles/192027/Delegates-101-Part-III-Generic-Delegates but I don't know how to use this in my case :/
Should I declare my delegate like this:
delegate void MyDelegate (params object[] params);
or:
public delegate void MyDelegate2<T>(T param1);
but this way I'll be able to pass only one parameter, I won't be able to use same delegate for 3 parameters.
Which solution is better?
I would like to have one generic delegate that will be able to take one-to-three parameters with different types.
Can this be done?
EDIT:
I'll try to show my scenario:
In my main form class I'm calling my DB class like so:
private void form1_Enter(object sender, EventArgs e)
{
showStatus("Loading statistics...");
DB.Instance.Part1(part1_ok,ErrorHandler);
DB.Instance.Part2(part2_ok, ErrorHandler);
}
private void ErrorHandler(string msg)
{
hideStatus();
//viewStack1.InvokeIfRequired(c => { c.moveToFirst(); });
MessageBox.Show(msg, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
private void part1_ok(string msg)
{
MessageBox.Show(msg);
}
private void part2_ok(DataTable dt)
{
dataGridView1.InvokeIfRequired(c =>
{
c.DataSource = dt;
});
}
My DB class looks like so:
public delegate void Part1_Callback(string message);
public delegate void Part2_Callback(DataTable dt);
public delegate void Part3_Callback(DataTable dt, int x, int y);
public delegate void ErrorHandler(string message);
public class Part1_CommandAndCallback
{
public SqlCommand Sql;
public Part1_Callback Callback;
public ErrorHandler Error;
}
public class Part2_CommandAndCallback
{
public SqlCommand Sql;
public Part2_Callback Callback;
public ErrorHandler Error;
}
public class Part3_CommandAndCallback
{
public SqlCommand Sql;
public Part3_Callback Callback;
public ErrorHandler Error;
}
class DB : SingletonBase<DB>
{
public static readonly string SqlConnectionString = #"Data Source=MyDB;Initial Catalog=Stats;Integrated Security=True;Asynchronous Processing=true;";
private DB()
{
}
public void Part1(Part1_Callback callback, ErrorHandler error)
{
SqlConnection conn = new SqlConnection(SqlConnectionString);
SqlCommand cmd = conn.CreateCommand();
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "Part1";
try
{
conn.Open();
}
catch (Exception ex)
{
error(ex.ToString());
return;
}
Part1_CommandAndCallback ar = new Part1_CommandAndCallback() { Callback = callback, Error = error, Sql = cmd };
IAsyncResult result = cmd.BeginExecuteReader(new AsyncCallback(Part1_Handler), ar, CommandBehavior.CloseConnection);
}
private void Part1_Handler(IAsyncResult result)
{
string stats = string.Empty;
Part1_CommandAndCallback ar = (Part1_CommandAndCallback)result.AsyncState;
SqlDataReader dr;
if (result.IsCompleted)
{
dr = ar.Sql.EndExecuteReader(result);
}
else
dr = null;
while (dr.Read())
{
stats += dr[0].ToString() + Environment.NewLine;
}
dr.NextResult();
while (dr.Read())//problem is here
{
stats += dr[0].ToString() + " - " + dr[1].ToString() +Environment.NewLine;
}
dr.Close();
ar.Callback(stats);
}
public void Part2(Part2_Callback callback, ErrorHandler error)
{
SqlConnection conn = new SqlConnection(SqlConnectionString);
SqlCommand cmd = conn.CreateCommand();
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "Part2";
try
{
conn.Open();
}
catch (Exception ex)
{
error(ex.ToString());
return;
}
Part2_CommandAndCallback ar = new Part2_CommandAndCallback() { Callback = callback, Error = error, Sql = cmd };
IAsyncResult result = cmd.BeginExecuteReader(new AsyncCallback(Part2_Handler), ar, CommandBehavior.CloseConnection);
}
private void Part2_Handler(IAsyncResult result)
{
DataTable dt = new DataTable();
Part2_CommandAndCallback ar = (Part2_CommandAndCallback)result.AsyncState;
SqlDataReader dr;
if (result.IsCompleted)
{
dr = ar.Sql.EndExecuteReader(result);
}
else
dr = null;
dt.Load(dr);
dr.Close();
ar.Callback(dt);
}
}
My idea was to use my singleton DB controller in many forms at the same time. So in first form I'll see some stats that will auto refresh, and if I'll like then is second form I'll be able to see some different stats that I'll be able to refresh on button click.
I would use the built in delegates I think,
Action<T>
Action<T, T>
etc
See here:
http://msdn.microsoft.com/en-us/library/018hxwa8.aspx
It looks like what you need is to create a generic CommandAndCallback class:
public class CommandAndCallback<TCallback>
{
public SqlCommand Sql;
public TCallback Callback;
public ErrorHandler Error;
}
And, for example, where you previously used Part3_CommandAndCallback, you would now use CommandAndCallback<Part3_Callback> or even CommandAndCallback<Action<DataTable, int, int>>, if you didn't want to create a delegate type for each part either.
Also, public fields are generally discouraged, so you might consider changing them to autoproperties:
public class CommandAndCallback<TCallback>
{
public SqlCommand Sql { get; set; }
public TCallback Callback { get; set; }
public ErrorHandler Error { get; set; }
}