is this shared DbCommand object thread safe? - c#

I don't see why I have to create a DbCommand object every time I need to call a stored procedure. So I'm trying to come up with a way to do that. I have tested my code (see below). But I would like to check with the community in case there is something I have missed. I would be using it with in an ASP.NET app. Is this code thread safe?
SharedDbCommand - wraps up the creation and storage of the DbCommand object
Db - the wrapper for the database, uses the SharedDbCommand via a static field and the ThreadStatic attribute
Program - the console app that starts threads and uses the Db object which
// SharedDbCommand.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Data.Common;
using System.Data.SqlClient;
using System.Data;
namespace TestCmdPrepare {
public class SharedDbCommand {
[ThreadStatic]
static DbCommand cmd;
public SharedDbCommand(string procedureName, ConnectionStringSettings dbConfig) {
var factory = DbProviderFactories.GetFactory(dbConfig.ProviderName);
cmd = factory.CreateCommand();
cmd.Connection = factory.CreateConnection();
cmd.Connection.ConnectionString = dbConfig.ConnectionString;
cmd.CommandText = procedureName;
cmd.CommandType = System.Data.CommandType.StoredProcedure;
if (cmd is SqlCommand) {
try {
cmd.Connection.Open();
SqlCommandBuilder.DeriveParameters(cmd as SqlCommand);
} finally {
if (cmd != null && cmd.Connection != null)
cmd.Connection.Close();
}
}
}
public DbParameter this[string name] {
get {
return cmd.Parameters[name];
}
}
public IDataReader ExecuteReader() {
try {
cmd.Connection.Open();
return cmd.ExecuteReader(CommandBehavior.CloseConnection);
} finally {
cmd.Connection.Close();
}
}
public void ExecuteNonQuery() {
try {
cmd.Connection.Open();
cmd.ExecuteNonQuery();
} finally {
cmd.Connection.Close();
}
}
}
}
// Db.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Data.Common;
using System.Data;
using System.Data.SqlClient;
using System.Threading;
using System.Diagnostics;
namespace TestCmdPrepare {
public class Db {
ConnectionStringSettings dbSettings;
DbProviderFactory factory;
public Db() {
dbSettings = ConfigurationManager.ConnectionStrings["db"];
factory = DbProviderFactories.GetFactory(dbSettings.ProviderName);
}
IDataReader ExecuteReader(DbCommand cmd) {
cmd.Connection.Open();
return cmd.ExecuteReader(CommandBehavior.CloseConnection);
}
private DbConnection CreateConnection() {
var c = factory.CreateConnection();
c.ConnectionString = dbSettings.ConnectionString;
return c;
}
DbCommand CreateCommand(string procedureName) {
var cmd = factory.CreateCommand();
cmd.Connection = CreateConnection();
cmd.CommandText = "get_stuff";
cmd.CommandType = CommandType.StoredProcedure;
if (cmd is SqlCommand) {
try {
cmd.Connection.Open();
SqlCommandBuilder.DeriveParameters(cmd as SqlCommand);
} finally {
cmd.Connection.Close();
}
}
return cmd;
}
[ThreadStatic]
static DbCommand get_stuff;
DbCommand GetStuffCmd {
get {
if (get_stuff == null)
get_stuff = CreateCommand("get_stuff");
return get_stuff;
}
}
public string GetStuff(int id) {
GetStuffCmd.Parameters["#id"].Value = id;
using (var reader = ExecuteReader(GetStuffCmd)) {
if (reader.Read()) {
return reader.GetString(reader.GetOrdinal("bar"));
}
}
return null;
}
[ThreadStatic]
static SharedDbCommand get_stuff2;
public string GetStuff2(int id) {
if (get_stuff2 == null)
get_stuff2 = new SharedDbCommand("get_stuff", dbSettings);
get_stuff2["#id"].Value = id;
using (var reader = get_stuff2.ExecuteReader()) {
if (reader.Read()) {
Thread.Sleep(1000);
return reader.GetString(reader.GetOrdinal("bar"));
}
}
return null;
}
}
}
// Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;
using System.Configuration;
using System.Data.SqlClient;
using System.Threading;
namespace TestCmdPrepare {
class Program {
static void Main(string[] args) {
var db = new Db();
var threads = new List<Thread>();
for (int i = 0; i < 4; i++) {
var one = new Thread(Run2);
var two = new Thread(Run1);
threads.Add(one);
threads.Add(two);
one.Start();
two.Start();
Write(db, 1);
Write(db, 2);
}
Console.WriteLine("Joining");
foreach (var thread in threads) {
thread.Join();
}
Console.WriteLine();
Console.WriteLine("DONE");
Console.ReadLine();
return;
}
static void Write(Db db, int id) {
Console.WriteLine("2:{0}:{1}", Thread.CurrentThread.ManagedThreadId, db.GetStuff2(id));
Console.WriteLine("1:{0}:{1}", Thread.CurrentThread.ManagedThreadId, db.GetStuff(id));
}
static void Run1() {
var db = new Db();
Write(db, 1);
}
static void Run2() {
var db = new Db();
Write(db, 2);
}
}
}

Bad idea for lots of reasons. Others have mentioned some of them, but I'll give you one specific to your implementation: using ThreadStatic in ASP.NET will bite you eventually (see here). You don't control the pipeline, so it's possible for multiple threads to end up servicing one request (think between event handlers on a page, etc- how much code is running that isn't yours?). It's also easy to orphan data on a request thread you don't own due to unhandled exceptions. Might not be a showstopping problem, but best case you're looking at a memory leak and increased server resource usage while your connection just sits there...
I'd recommend you just let the ConnectionPool do its job- it has some warts, but performance isn't one of them compared to what else is going on in the ASP.NET pipeline. If you really want to do this, at least consider storing the connection object in HttpContext.Current.Items. You can probably make this work in limited circumstances, but there will always be gotchas, especially if you ever start writing parallel code.
Just $0.02 from a guy who's been there. :)

This is not a good practice to keep DbCommand created. Also, this makes your application very complex because of thread handling logic.
As Microsoft suggests you should create and dispose connection and command objects right after you have executed your query. They are very lightweight to create. No need to keep memory used with them - dispose them when your query execution is complete and you have fetched all of the results.

Related

Writing to a csv after extracting data from a db, cannot modify variable with data

using Dapper;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.IO;
namespace writingCSV
{
class Program
{
static async Task Main(string[] args)
{
var PatientEmails = await GetPatients();
Type myType = PatientEmails.GetType();
Console.WriteLine(myType);
}
public class PatientSurvey: IDisposable
{
public string Location { get; set; }
public string Email { get; set; }
public void Dispose()
{
throw new NotImplementedException();
}
}
static public async Task<IEnumerable<PatientSurvey>> GetPatients()
{
var connectionString = "SERVER INFORMATION";
using (var cnn = new SqlConnection(connectionString))
{
cnn.Open();
var patientSurveys = await cnn.QueryAsync<PatientSurvey>("STORED PROC NAME",
null,
commandTimeout: 120,
commandType: CommandType.StoredProcedure);
return patientSurveys;
}
}
}
}
I'm attempting to write data to a CSV file from a database. I've successfully connected to the db and extracted the data into a C# object, but I cannot figure out how to modify my data so I can actually write it into the file. The PatientEmails variable has the data within it, but it seems like it's just an instance of the PatientSurvey class.
If I run my variable through a foreach loop, it prints out writingCSV.Program.PatientSurvey for each time it loops.
I don't see any problem. GetPatients returns an IEnumerable of PatientSurvey so indeed looping on the list just print out writingCSV.Program.PatientSurvey.
What would you expect?
If your goal is to print patients email then you should write something like
foreach (var p in patientEmails) {
Console.WriteLine(p.Email);
}

Cannot figure out 'The name does not exist in the current context; Error

I want to assign my program a Guid and use it to pull config data from the SQL server. However, I keep getting the error:
The name 'ApplicationID' does not exist in the current context
I've always struggled with variable declaration. I can usually figure it out, but I'm stuck on this one....
Program.cs creates the ApplicationID, then I call LoadConfig to grab and set the rest of the variables based on that ApplicationID. However when I go to use the ApplicationID in LoadConfig it gives me that error.
I confirmed that if I replaced the ApplicationID with the Guid string it pulls everything as expected.
I also tried adding using ABBAS.ProgC to Config, but that didn't help either.
using System;
using System.Collections.Generic;
using ABBAS.sFTP;
using Renci.SshNet;
using Renci.SshNet.Sftp;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics;
using System.Reflection;
namespace ABBAS.ProgC {
class Program {
static private Guid processid;
static private SftpClient sftpc;
static private sFTPFiles sFTPFiles;
static public Guid ApplicationID = new Guid("d71b536f-1428-4797-95e6-3948e736fcca");
static async Task Main() {
ManualResetEvent exitEvent = new(false);
TaskCompletionSource tcs = new();
Console.WriteLine($"Program Starting at {DateTime.Now}");
Console.WriteLine($"Process id: {Environment.ProcessId}");
programversion = FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).FileVersion;
Console.WriteLine($"Version: {programversion}");
Config.LoadConfig();
...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Thinktecture.IdentityModel.Client;
using System.Data.SqlClient;
using Newtonsoft.Json.Linq;
using ABBASLibrary.Configuration;
using System.Text.Json;
using System.Data;
namespace ABBAS {
static public partial class Config {
// Environmental Variables
static public int ConfigurationID;
//Global Variables
static public string sFTPServer;
static public string sFTPServerUID;
static public string sFTPServerPWD;
static public int sFTPServerPort;
static public int sFTPRetryMax;
static public int sFTPConnectDelayMin;
static public int sFTPConnectDelayMax;
static public void LoadConfig() {
using (SqlConnection _sqlconn = new SqlConnection())
{
_sqlconn.ConnectionString = sqlconnstring;
_sqlconn.Open();
SqlDataAdapter da;
DataSet ds = new DataSet();
string varName;
string varValue;
int newNum;
bool isParsable;
try
{
String query = "SELECT * FROM [ConfigurationDetails] WHERE [ApplicationID] = " + ApplicationID;
da = new SqlDataAdapter(query, _sqlconn);
da.Fill(ds);
for (int cnt = 0; cnt < ds.Tables[0].Rows.Count; cnt++)
{
varName = "";
varValue = "";
varName = ds.Tables[0].Rows[cnt][4].ToString(); // VariableName
varValue = ds.Tables[0].Rows[cnt][5].ToString(); // VariableValue
if (varName.ToString() == "sFTPServer")
sFTPServer = varValue;
if (varName.ToString() == "sFTPServerUID")
sFTPServerUID = varValue;
if (varName.ToString() == "sFTPServerPWD")
sFTPServerPWD = varValue;
if (varName.ToString() == "P21CompanyID")
P21CompanyID = varValue;
if (varName.ToString() == "sFTPServerPort")
{
isParsable = Int32.TryParse(varValue, out newNum);
if (isParsable)
sFTPServerPort = newNum;
}
if (varName.ToString() == "sFTPRetryMax")
{
isParsable = Int32.TryParse(varValue, out newNum);
if (isParsable)
sFTPRetryMax = newNum;
}
if (varName.ToString() == "sFTPConnectDelayMin")
{
isParsable = Int32.TryParse(varValue, out newNum);
if (isParsable)
sFTPConnectDelayMin = newNum;
}
if (varName.ToString() == "sFTPConnectDelayMax")
{
isParsable = Int32.TryParse(varValue, out newNum);
if (isParsable)
sFTPConnectDelayMax = newNum;
}
}
}
catch (Exception ex)
{
_sqlconn.Close();
}
finally
{
if (_sqlconn != null)
_sqlconn.Dispose();
if (ds != null)
ds.Dispose();
}
}
}
}
}
The original idea (not mine!) was to create the variable in Config, and they set it in Program.cs, but since we are calling LoadConfig and need the variable set to get the rest of our variables that clearly won't work.
Because it doesn't exist in that context. It's declared as a static member of the Program class:
class Program {
//...
static public Guid ApplicationID = new Guid("d71b536f-1428-4797-95e6-3948e736fcca");
To access it you'd need to reference it statically from that class:
String query = "SELECT * FROM [ConfigurationDetails] WHERE [ApplicationID] = " + Program.ApplicationID;
Note that there are a few anti-patterns in your code. Including, but not limited to:
Making everything static is going to drastically increase complexity and the potential for bugs. (You've already encountered a problem with it here in this question.) This is generally the approach when developers want "global variables" for everything. It sounds nice at first, because that way you always have access to everything and can use what you want when you want. But it quickly becomes problematic because you always have access to everything and can't effectively isolate any of the code into meaningful components.
Silently ignoring exceptions is going to make it very difficult to identify, diagnose, and correct problems. At the very least there should be some logging or output of exception information.

System.TypeInitializationException when referencing dll

I'm trying to create a Data Framework in the form of a .dll so that I can reference it when creating new projects, as opposed to reinventing the wheel with each project I create.
I have an app.config in which I store my SQL connections, a class that uses the app.config to build my SQL ConnectionString (ConnectionStrings.cs) and a Logic class (Logic.cs) that'll build whatever objects I require from the SQL Server.
Here's the classes in the .dll:
ConnectionStrings.cs:
using System.Configuration;
using System.Data.SqlClient;
namespace DataFramework
{
public static class ConnectionStrings
{
static string _liveConnectionString = ConfigurationManager.ConnectionStrings["LiveDataSource"].ConnectionString;
static string _liveMISConnectionString = ConfigurationManager.ConnectionStrings["LiveDataSource_MIS"].ConnectionString;
static string _devConnectionString = ConfigurationManager.ConnectionStrings["DevDataSource"].ConnectionString;
static string _devMISConnectionString = ConfigurationManager.ConnectionStrings["DevDataSource_MIS"].ConnectionString;
public static SqlConnection CreateLiveConnection
{
get { return new SqlConnection(_liveConnectionString); }
}
public static SqlConnection CreateLiveMISConnection
{
get { return new SqlConnection(_liveMISConnectionString); }
}
public static SqlConnection CreateDevConnection
{
get { return new SqlConnection(_devConnectionString); }
}
public static SqlConnection CreateDevMISConnection
{
get { return new SqlConnection(_devMISConnectionString); }
}
}
}
Logic.cs:
using System;
using System.Threading.Tasks;
using System.Data;
using System.Data.SqlClient;
namespace DataFramework
{
public class Logic
{
SqlConnection liveConnection = ConnectionStrings.CreateLiveMISConnection;
SqlConnection devMISConnection = ConnectionStrings.CreateDevMISConnection;
public bool IsConnecting { get; set; }
public string ConnectionMessage { get; set; }
public async Task<DataTable> ResultDataTable(bool connectToLive, string commandText, CommandType commandType)
{
DataTable dt = new DataTable();
using (SqlCommand command = new SqlCommand())
{
try
{
command.CommandType = commandType;
command.CommandTimeout = 360000000;
switch (connectToLive)
{
case true:
command.CommandText = commandText;
command.Connection = liveConnection;
if (liveConnection.State == ConnectionState.Connecting)
{
IsConnecting = true;
ConnectionMessage = "Connecting to Data Source...";
}
if (liveConnection.State != ConnectionState.Closed)
liveConnection.Close();
if (liveConnection.State != ConnectionState.Open)
{
liveConnection.Open();
IsConnecting = false;
ConnectionMessage = "";
}
break;
case false:
command.CommandType = commandType;
command.CommandText = "";
command.Connection = devMISConnection;
if (devMISConnection.State == ConnectionState.Connecting)
{
IsConnecting = true;
ConnectionMessage = commandText;
}
if (devMISConnection.State != ConnectionState.Closed)
devMISConnection.Close();
if (devMISConnection.State != ConnectionState.Open)
{
devMISConnection.Open();
IsConnecting = false;
ConnectionMessage = "";
}
break;
}
using (SqlDataReader reader = await command.ExecuteReaderAsync())
{
dt.Load(reader);
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show(ex.Message, "An Error Has Occured", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
}
finally
{
if (devMISConnection.State != ConnectionState.Closed)
devMISConnection.Close();
if (liveConnection.State != ConnectionState.Closed)
liveConnection.Close();
}
}
return dt;
}
}
}
I include this dll as a reference in the app that I'm writing:
using System.Data;
using System.Threading.Tasks;
using System.Windows.Forms;
using DataFramework;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
DataTable dt = new DataTable();
DataFramework.Logic logic = new Logic();
public Form1()
{
InitializeComponent();
}
private async void Form1_Load(object sender, EventArgs e)
{
dt = await Task.Run(() => logic.ResultDataTable(true, "SELECT * FROM MIS.dbo.ETL_Table", CommandType.StoredProcedure));
}
}
}
The code throws the exception here:
SqlConnection liveConnection = ConnectionStrings.CreateLiveMISConnection;
So why, when I'm initializing the class, do I get this issue?
When you reference a DLL (or project) from another project, the app.config from the top most project is used. So, if you have your DataFramework being called from your WinformsApp, then your WinformsApp needs to have the right config settings in it. By default, it will ignore any app.config in the DataFramework. A bit frustrating at times! Copy your settings from your DataFramework app.config in to the WinformsApp app.config and it will work.
Another unrelated observation is that you have the following:
"SELECT * FROM MIS.dbo.ETL_Table", CommandType.StoredProcedure
The command type should be text and not a stored procedure.

Can't get updating of dataset working

I have got a couple of questions. I am making gym management system to submit in my university. I implemented a service based database by reading the tutorials at www.homeandlearn.co.uk. The problem is I can not update it
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Gym_Management_System
{
class GMSDConnectionClass
{
public string sql_string;
public string strCon;
public string Sql
{
set { sql_string = value; }
}
public string connection_string
{
set { strCon = value; }
}
public System.Data.DataSet GetConnection
{
get { return MyDataSet(); }
}
public System.Data.DataSet MyDataSet()
{
System.Data.SqlClient.SqlConnection con = new
System.Data.SqlClient.SqlConnection(strCon);
con.Open();
System.Data.SqlClient.SqlDataAdapter da_1 = new
System.Data.SqlClient.SqlDataAdapter(sql_string, con);
System.Data.DataSet dat_set = new System.Data.DataSet();
da_1.Fill(dat_set, "Table_data_1");
con.Close();
return dat_set;
}
public void UpdateDatabase(System.Data.DataSet ds)
{
System.Data.SqlClient.SqlCommandBuilder cb = new
System.Data.SqlClient.SqlCommandBuilder(da_1);
cb.DataAdapter.Update(ds.Tables[0]);
}
}
}
The problem is that I can not access da_1 in UpdateDatabase() method.
What's wrong there? I have tried couple of things but I couldn't get it. It may be a simple problem but I am totally new to database and this stuff.
Note:
I have to submit this project on 23rd January 2014.
Variables cannot be accessed outside of their declaring scope. There are various scoping rules.. the one you're most concerned with here is method-scope. This might help explain:
public System.Data.DataSet MyDataSet()
{
// da_1 is delcared in this method.. it is only available here
...
System.Data.SqlClient.SqlDataAdapter da_1 = new
System.Data.SqlClient.SqlDataAdapter(sql_string, con);
...
}
public void UpdateDatabase(System.Data.DataSet ds)
{
// not available here
}
In order to access it across methods within a class.. you must declare it at class level:
class GMSDConnectionClass
{
System.Data.SqlClient.SqlDataAdapter da_1;
Then you can assign it like this in your other method:
da_1 = new System.Data.SqlClient.SqlDataAdapter(sql_string, con);

N-tier architecture transacions handling

I'd like to implement N-tier architecture in my WinForms applications to separate (just logically - in one project) business logic from data access, however I have some doubts about using transacion in BLL. All tutorials I've found in the Internet either are very simple implementations of that architecture (without transactions), or are too complex for my needs. Trying to find my own way, I've come to the point, where I don't know the best way to handle transactions of in BLL layer.
I'll try to use some simple example to illustrate the problem (all classes are in separate files):
//DTO - Data Transfer Objects
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
}
public class SomeOtherItem
{
public int Id { get; set; }
public string Name { get; set; }
}
//DAL - Data Access layer
public class ItemDAL
{
public ItemDAL()
{
}
public void Add(Item item)
{
using (NpgsqlConnection conn = new NpgsqlConnection(connString))
{
conn.Open();
using (NpgsqlCommand cmd = new NpgsqlCommand())
{
cmd.Connection = conn;
cmd.CommandText = #"INSERT INTO tbl_items (name)
VALUES (#name)";
cmd.Parameters.AddWithValue("#name", item.Name);
cmd.ExecuteNonQuery();
}
}
}
}
public class SomeOtherItemDAL
{
public SomeOtherItemDAL()
{
}
public void Add(SomeOtherItem someOtherItem)
{
using (NpgsqlConnection conn = new NpgsqlConnection(connString))
{
conn.Open();
using (NpgsqlCommand cmd = new NpgsqlCommand())
{
cmd.Connection = conn;
cmd.CommandText = #"INSERT INTO tbl_some_other_items (name)
VALUES (#name)";
cmd.Parameters.AddWithValue("#name", someOtherItem.Name);
cmd.ExecuteNonQuery();
}
}
}
}
//BLL - Business Logic Layer
public class SomeBLL
{
public SomeBLL()
{
}
public void Add(Item item, SomeOtherItem someOtherItem)
{
ItemDAL itemDAL = new ItemDAL();
SomeOtherItemDAL someOtherItemDAL = new SomeOtherItemDAL();
// *** this must be done in one transaction ***
itemDAL.Add(item);
someOtherItemDAL.Add(someOtherItem);
}
}
Now, the problem is that if I want to use Transacion, I cannot use:
using (NpgsqlConnection conn = new NpgsqlConnection(connString))
in DAL. To use NpgsqlTransacion object I must somehow keep connection opened and visible in both DAL classes.
I've tried use TransacionScope object for that, but from from some reasons it's not working with PostgreSQL and the driver I'm using (INSERTS are done just after executed and there is no transaction rollback when exception within TransacionScope occures).
What I've come into is to make additional Singleton class to keep connection alive and manage transactions:
public class DB
{
private static DB instance;
private const string connString = #"Server=localhost;Port=5432;Database=db_test;User Id=usr_test;Password=pass";
private NpgsqlConnection conn;
private DB()
{
conn = new NpgsqlConnection(connString);
}
public static DB Instance
{
get
{
if (instance == null)
{
instance = new DB();
}
return instance;
}
}
#region --- connection ---
public NpgsqlConnection GetOpenConnection()
{
OpenConnection();
return conn;
}
private void OpenConnection()
{
if (conn.State == ConnectionState.Closed || conn.State == ConnectionState.Broken)
conn.Open();
}
public void CloseConnection()
{
if (conn != null && !inTransaction)
{
conn.Close();
}
}
#endregion
#region --- transaction ---
private NpgsqlTransaction trans;
private bool inTransaction;
public bool InTransaction { get { return inTransaction; } }
public void TransactionStart()
{
OpenConnection();
trans = conn.BeginTransaction();
inTransaction = true;
}
public void TransactionCommit()
{
if (inTransaction)
{
try
{
trans.Commit();
trans.Dispose();
}
finally
{
inTransaction = false;
CloseConnection();
}
}
}
public void TransactionRollback()
{
if (inTransaction)
{
try
{
trans.Rollback();
trans.Dispose();
}
finally
{
inTransaction = false;
CloseConnection();
}
}
}
#endregion
}
and rebuild both DAL Add methods to access connection like that:
//DAL - Data Access layer
public class ItemDAL
{
public ItemDAL()
{
}
public void Add(Item item)
{
using (NpgsqlCommand cmd = new NpgsqlCommand())
{
cmd.Connection = DB.Instance.GetOpenConnection();
cmd.CommandText = #"INSERT INTO tbl_items (name)
VALUES (#name)";
cmd.Parameters.AddWithValue("#name", item.Name);
cmd.ExecuteNonQuery();
}
if (!DB.Instance.InTransaction)
DB.Instance.CloseConnection();
}
}
public class SomeOtherItemDAL
{
public SomeOtherItemDAL()
{
}
public void Add(SomeOtherItem someOtherItem)
{
using (NpgsqlCommand cmd = new NpgsqlCommand())
{
cmd.Connection = DB.Instance.GetOpenConnection();
cmd.CommandText = #"INSERT INTO tbl_some_other_items (name)
VALUES (#name)";
cmd.Parameters.AddWithValue("#name", someOtherItem.Name);
cmd.ExecuteNonQuery();
}
if (!DB.Instance.InTransaction)
DB.Instance.CloseConnection();
}
}
Note that I'd like to follow the rule to "close database connection as soon as possible", so when Add method is called without transaction scope, I'd like it to close the connection.
So, the final questions are:
1. What do you think about it, is there a better way to handle that issue, any suggestions?
2. Should I dispose a connection in DB.CloseConnecion()? I surely do when using using (NpgsqlConnection conn = ...) { ... } pattern, but as Singleton is alive as long as application, does it make sense? Connection is returned to ConnectionPool after Close(), isn't it? Or maybe I should also dispose a Singleton object (together with connection), after each using?
3. It's not directly connected question, but if I use DTO objects (just properties, no methods) and have also some BusinessObjects (BO) with the same properties, but also with additional methods (validations, calculations, operations etc.), can it be inherited from DTO? Or maybe I can use full BusinessObject to transfer it between layers, and get rid off DTO?
EDIT: TransacionScope
As requested, I add some code from my tries with TransactionScope. Simply WinForm application, no Exceptions handling. As a result, there is an Exception window when I throw it, but in database I see records with values test1 ans test2. Both when debbuging in VS and executing application from .exe
using Npgsql;
using System.Transactions;
//...
private void button1_Click(object sender, EventArgs e)
{
using (System.Transactions.TransactionScope scope = new TransactionScope())
{
AddValue("test1");
AddValue("test2");
throw new Exception("bam!");
AddValue("test3");
scope.Complete();
}
}
private void AddValue(string value)
{
string connString = "Server=localhost;Port=5432;Database=db_test;User Id=usr_test;Password=pass";
using (NpgsqlConnection conn = new NpgsqlConnection(connString))
{
conn.Open();
using (NpgsqlCommand cmd = new NpgsqlCommand())
{
cmd.Connection = conn;
cmd.CommandText = #"INSERT INTO tbl_test (name)
VALUES (#name)";
cmd.Parameters.AddWithValue("#name", value);
cmd.ExecuteNonQuery();
}
}
}
I've never used NpgSql, but reading the documentation of NpgSql it appears they have some support of TransactionScope() if you add "enlist=true" in your connection string.
I'm looking at the "System.Transactions Support" section of the below NpgSql documentation:
http://npgsql.projects.postgresql.org/docs/manual/UserManual.html
Assuming TransactionScope() did work, then you can do simething like this...
using (var scope = new System.Transactions.TransactionScope())
{
ItemDAL itemDAL = new ItemDAL();
SomeOtherItemDAL someOtherItemDAL = new SomeOtherItemDAL();
// *** this must be done in one transaction ***
itemDAL.Add(item);
someOtherItemDAL.Add(someOtherItem);
scope.Complete()
}
What you did is brave, but not scalable. I'm not familiar with PGSQL, but this problem is the exact reason why the TransactionScope API was designed.
Can you show your code using the TransactionScope api? Make sure that you are not calling scope.Complete(); if an error occurs in one of the methods. Be careful to not "eat" the exceptions inside the methods because in that case the flow will continue just like if nothing happened.
More reading about TransactionScope here:
http://msdn.microsoft.com/en-us/library/ms172152.aspx
Update 1
Thanks for sharing your code that uses the TransactionScope class. The code looks perfectly correct to me. According to this (http://npgsql.projects.postgresql.org/docs/manual/UserManual.html) document (the same quoted by ChrisNeil52), Enlist=true should be included in the connection string for transactions to work.
You might be dealing with a buggy API. Good luck with this.
I know this sounds peculiar, but something I would try would be to use a different NpgsqlCommand constructor. new NpgsqlCommand("sql query", connection), instead of creating the command and assigning it the connection. They should be equivalent. but who know...

Categories