Trying to reduse some code and follow the DRY principle with a generic approach. The code is kind of trivial, but I can not manage to compile it. I guess you see what I'm trying to do here (not repeat GetData(), but rather have several Logic() func's), what am I doing wrong?
Caller:
IEnumerable<IMyClass> result = Factory.GetData<IEnumerable<IMyClass>>("storedProsedureName", Factory.LogicFunction());
Logic:
public static IEnumerable<T> GetData<T>(string storedProsedureName, Func<string, OracleConnection, OracleCommand, IEnumerable<T>> functionCall) where T : class
{
using (OracleConnection conn = new OracleConnection(Helpers.GetConnectionString()))
{
using (OracleCommand cmd = conn.CreateCommand())
{
try
{
return functionCall(storedProsedureName, conn, cmd);
}
catch (Exception e)
{
throw e;
}
}
}
}
public static IEnumerable<IMyClass> LogicFunction(string storedProsedureName= "", OracleConnection conn = null, OracleCommand cmd = null)
{
conn.Open();
{...}
conn.Close();
return IEnumerable<IMyClass>;
}
Error:
Argument 2: cannot convert
from
'System.Collections.Generic.IEnumerable_IMyClass>'
to
'System.Func_string, OracleConnection, OracleCommand, IEnumerable _IEnumerable_IMyClass>>>'
Change your caller to below code.
Removed () and also changed generic closing type from IEnumerable<IMyClass> to IMyClass.
IEnumerable<IMyClass> result = Factory.GetData<IMyClass>("storedProsedureName", Factory.LogicFunction);
Related
I would like to know if it is possible to declare a generic variable inside a generic method. In the following code, I am trying to make "results" a generic variable but I am having a hard time doing so. Therefore, I would appreciate any help on this. Thanks in advance
public static T GetSQLResults<T>(string StoredProcName, Dictionary<string, string> StoredProcParameters)
{
var results;
using (SqlConnection conn = new SqlConnection(Settings.ConnStringDb))
{
using (SqlCommand cmd = new SqlCommand(StoredProcName, conn))
{
cmd.CommandType = CommandType.StoredProcedure;
foreach (var param in StoredProcParameters)
{
cmd.Parameters.AddWithValue(param.Key, param.Value);
}
conn.Open();
if (typeof(T) == typeof(Boolean))
{
results = cmd.ExecuteReader().HasRows;
}
else if (typeof(T) == typeof(DataTable))
{
results.Load(cmd.ExecuteReader());
}
//results.Load(cmd.ExecuteReader());
conn.Close();
}
return (T)Convert.ChangeType(results, typeof(T));
}
}
First of all: I got my code running without using oop. I declared all my variables inside the same class and opened/closed the connection right before and after passing the query to the db. That worked! Now with some new experiences I tried to split my code into different classes. Now it wont work anymore.
It tells me "Connection must be valid and open". Enough text, here's my current code:
Services.cs
public static MySqlConnection conn // Returns the connection itself
{
get
{
MySqlConnection conn = new MySqlConnection(Services.ServerConnection);
return conn;
}
}
public static string ServerConnection // Returns the connectin-string
{
get
{
return String.Format("Server={0};Port=XXXX;Database=xxx;Uid=xxx;password=xxXxxXxXxxXxxXX;", key);
}
}
public static void DB_Select(string s, params List<string>[] lists)
{
try
{
MySqlCommand cmd = conn.CreateCommand();
cmd.CommandType = CommandType.Text;
string command = s;
cmd.CommandText = command;
MySqlDataReader sqlreader = cmd.ExecuteReader();
while (sqlreader.Read())
{
if (sqlreader[0].ToString().Length > 0)
{
for (int i = 0; i < lists.Count(); i++)
{
lists[i].Add(sqlreader[i].ToString());
}
}
else
{
foreach (List<string> save in lists)
{
save.Add("/");
}
}
}
sqlreader.Close();
}
catch (Exception ex)
{
MessageBox.Show("Error while selecting data from database!\nDetails: " + ex);
}
}
LoginForm.cs
private void checkUser(string username, string password)
{
using (Services.conn)
{
Services.conn.Open();
Services.DB_Select("..a short select statement..");
Services.conn.Close();
}
I guess this is all we need. I have shortened my code to get a focus on the problem.
I created Services.cs to get a global way to access the db from all forms without copy&pasting the connection info. Now when I reach my LoginForm.cs it throws an error "Connection must be valid and open". I've already debugged my code. It's all time closed. Even when passing conn.Open() it stays closed. Why?
Another try: I've also tried placing conn.Open() and conn.Close() inside Services.DB_Select(..) at the beginning and end. Same error here.
I have to say: The code worked before and I've used the same connection-string. So the string itself is surely valid.
I appreciate any help given here!
The problem is that you don't store the connection that was returned from your factory property. But don't use a property like a method. Instead use it in this way:
using (var con = Services.conn)
{
Services.conn.Open();
Services.DB_Select("..a short select statement..", con ));
//Services.conn.Close(); unnecessary with using
}
So use the same connection in the using that was returned from the property(or better created in the using) and pass it to the method which uses it. By the way, using a property as factory method is not best practise.
But in my opinion it's much better to create the connection where you use it, best place is in the using statement. And throw the con property to the garbage can, it is pointless and a source for nasty errors.
public static void DB_Select(string s, params List<string>[] lists)
{
try
{
using(var conn = new MySqlConnection(Services.ServerConnection))
{
conn.Open();
MySqlCommand cmd = conn.CreateCommand();
cmd.CommandText = s;
using( var sqlreader = cmd.ExecuteReader())
while (sqlreader.Read())
{
if (sqlreader[0].ToString().Length > 0)
{
for (int i = 0; i < lists.Count(); i++)
{
lists[i].Add(sqlreader[i].ToString());
}
}
else
{
foreach (List<string> save in lists)
{
save.Add("/");
}
}
} // unnecessary to close the connection
} // or the reader with the using-stetement
}
catch (Exception ex)
{
MessageBox.Show("Error while selecting data from database!\nDetails: " + ex);
}
}
Try to restructure your Services class as follows
public static MySqlConnection conn // Returns the connection itself
{
get
{
MySqlConnection conn = new MySqlConnection(Services.ServerConnection);
return conn;
}
}
private static string ServerConnection // Returns the connectin-string - PRIVATE [Improved security]
{
get
{
return String.Format("Server={0};Port=XXXX;Database=xxx;Uid=xxx;password=xxXxxXxXxxXxxXX;", key);
}
}
// Rather than executing result here, return the result to LoginForm - Future improvement
public static void DB_Select(MySqlConnection conn ,string s, params List<string>[] lists)
{
try
{
MySqlCommand cmd = conn.CreateCommand();
cmd.CommandType = CommandType.Text;
string command = s;
cmd.CommandText = command;
MySqlDataReader sqlreader = cmd.ExecuteReader();
while (sqlreader.Read())
{
if (sqlreader[0].ToString().Length > 0)
{
for (int i = 0; i < lists.Count(); i++)
{
lists[i].Add(sqlreader[i].ToString());
}
}
else
{
foreach (List<string> save in lists)
{
save.Add("/");
}
}
}
sqlreader.Close();
}
catch (Exception ex)
{
MessageBox.Show("Error while selecting data from database!\nDetails: " + ex);
}
}
In LoginForm.cs use returning connection and store it there. When you need to execute query, use
MySqlConnection conn=Services.conn(); // Get a new connection
Services.DB_Select(conn,"..a short select statement.."); // Executing requirement
Services.conn.Close();
Additional - I suggest you need to return MySqlDataReader to LoginForm and handle results there
private MySqlConnection _conn;
public MySqlConnection conn // Returns the connection itself
{
get
{
if(_conn == null)
_conn = new MySqlConnection(Services.ServerConnection);
return _conn;
}
}
I am working on a project centered around an Oracle database (though honestly I don't expect that to matter) and I find myself have a fair amount of duplicate code, specifically exceptions. The best way I've seen so far is from this question https://stackoverflow.com/a/1554/865868, which suggests the use of delegates. This looked like the perfect solution until I tried to implement it in my project. I found that I had one case where it was not practical.
Let me describe my program a bit. I have two sections of code that deal with database operations. The idea is that I call a function that returns a DataTable, called LoadDataTable(). I then have a function that deals inserting items from a list into a table.
private void AddToList(List<string> itemList) {
try {
using (OracleConnection connection = new OracleConnection(connectionString)) {
connection.Open();
foreach (string item in itemList) {
using (OracleCommand command = new OracleCommand()) {
command.Connection = connection;
//Insert operation here
//......
command.ExecuteNonQuery();
}
}
} catch (OracleException ex) {
string messageboxtitle = "Database Exception";
switch (ex.Number) {
case 00001:
MessageBox.Show("Constraint Violation Error", messageboxtitle, MessageBoxButtons.OK);
break;
case 12154:
MessageBox.Show(string.Format("Connection Error: {0}", ex.Message), messageboxtitle);
break;
default:
MessageBox.Show(ex.ToString());
break;
}
}
}
private DataTable LoadDataTable() {
DataTable dataTable = new DataTable();
try {
using (OracleConnection connection = new OracleConnection(connectionString)) {
connection.Open();
using (OracleCommand command = new OracleCommand()) {
command.Connection = connection;
command.CommandText = sql;
command.CommandType = CommandType.Text;
using (OracleDataAdapter oda = new OracleDataAdapter(command)) {
oda.Fill(dataTable);
}
}
}
} catch (OracleException ex) {
string messageboxtitle = "Database Exception";
switch (ex.Number) {
case 12154:
MessageBox.Show(string.Format("Connection Error: {0}", ex.Message), messageboxtitle); //Duplicate Exception
break;
default:
MessageBox.Show(ex.ToString());
break;
}
}
return dataTable;
}
Keep in mind I had to rewrite and simplify that code so I could discuss it. Anyway, looking at the delegate example I quickly realized that paramaters are an issue. You can't use params for List<string> types, but there is no doubt in the usefulness of the delegates as I would be able to one one centralized section for exceptions that are not duplicated.
private delegate void DatabaseOperation(List<string> itemList);
private void PerformDatabaseOperation(DatabaseOperation operation, List<string> itemList){
try {
operation(itemList);
} catch (OracleException ex) {
string messageboxtitle = "Database Exception";
switch (ex.Number) {
case 00001:
MessageBox.Show("Constraint Violation Error", messageboxtitle, MessageBoxButtons.OK);
break;
case 12154:
MessageBox.Show(string.Format("Connection Error: {0}", ex.Message), messageboxtitle);
break;
default:
MessageBox.Show(ex.ToString());
break;
}
}
}
private void AddToList(List<string> itemList) {
using (OracleConnection connection = new OracleConnection(connectionString)) {
connection.Open();
foreach (string item in itemList) {
using (OracleCommand command = new OracleCommand()) {
command.Connection = connection;
//Insert operation here
//......
command.ExecuteNonQuery();
}
}
}
How to use:
List<string> itemList = new List<string>();
//code to fill list
PerformDatabaseOperation(AddToList, itemList);
Problem is now that I am unable to implement LoadDataTable() with this delegate as it does not have any parameters. The use of params on the delegate does not work since List is incompatible. I'm looking to improve my coding techniques for re usability and readability but I find myself spinning my wheels reading various threads on the subject, mostly since they do not go in depth enough beyond a simple example that doesn't really catch the problem I find myself having now. To ensure this gets answered let me pose a final question. How could I write code which would avoid duplicating exceptions?
UPDATE
For anyone looking to solve a similar problem, see below. Keep in my there is plenty more that can be done to improve the code. Also, anyone interested in the discussion around the var keyword discussed here, go here. I hope this helps:
private delegate void DatabaseOperation();
private void PerformDatabaseOperation(DatabaseOperation operation) {
try {
operation();
} catch (OracleException ex) {
string messageboxtitle = "Database Exception";
switch (ex.Number) {
case 00001:
MessageBox.Show("Constraint Violation Error", messageboxtitle, MessageBoxButtons.OK);
break;
case 12154:
MessageBox.Show(string.Format("Connection Error: {0}", ex.Message), messageboxtitle);
break;
default:
MessageBox.Show(ex.ToString());
break;
}
}
}
private void AddToList(List<string> itemList) {
using (OracleConnection connection = new OracleConnection(connectionString)) {
connection.Open();
foreach (string item in itemList) {
using (OracleCommand command = new OracleCommand()) {
command.Connection = connection;
//Insert operation here
//......
command.ExecuteNonQuery();
}
}
}
private DataTable LoadDataTable() {
DataTable dataTable = new DataTable();
using (OracleConnection connection = new OracleConnection(connectionString)) {
connection.Open();
using (OracleCommand command = new OracleCommand()) {
command.Connection = connection;
command.CommandText = sql;
command.CommandType = CommandType.Text;
using (OracleDataAdapter oda = new OracleDataAdapter(command)) {
oda.Fill(dataTable);
}
}
}
return dataTable;
}
Keep in mind that delegates capture variables from the parent method's body. This is also called a closure. Therefore it is often not necessary for the delegate to have a parameter list.
var itemList = new List<string>();
PerformDatabaseOperation(
() => {
...
itemList.Add(...);
...
}
);
UPDATE
You would call it like this:
List<string> itemList = new List<string>();
PerformDatabaseOperation(() => AddToList(itemList));
The trick is to pass a lambda expression to the PerformDatabaseOperation that has no parameters (the empty braces ()); also PerformDatabaseOperation has no itemList parameter. The body of the lambda expression uses itemList declared and initialized just before the call to PerformDatabaseOperation. C# compiler magic does the rest.
I would suggest that you use a micro ORM such as PetaPoco which has Oracle support. Why write all that code if it can be auto generated? If you want to write your own data access layer I suggest you create CRUD methods for your classes such as
public bool Insert(Person p)
{...}
You can also look into using generic methods to do this such as:
public bool Insert<T>(T item)
{...}
One very often used pattern of passing varying parameters is to pass them to a constructor. The advantage is that constructors are not part of interfaces. However, it requires you to work with objects instead of delegates.
public interface IDatabaseOperation
{
void Execute();
}
public class LoadListDatabaseOperation : IDatabaseOperation
{
private List<string> _itemList;
public LoadListDatabaseOperation(List<string> itemList)
{
_itemList = itemList;
}
public void Execute()
{
...
// Fill the list here
...
}
}
Passing objects is a bit more costly but has also advantages, like being able to create hierarchies of operations having different degrees of specialization. You might have generic base operations and derive operations specialized for specific types. You can also use properties for scalar returns values etc.
I originally posted the question to check for presence of either ADO.NET/OLEDB connection types. That being resolved, I'd like to know, how to change the code when it comes to inserting to the DB.
For example, when the connection type is ADO.NET we use the "Transaction" in the connection type.
SqlConnection connection = (SqlConnection)connections[_connectionName].AcquireConnection(transaction);
Now if I have the OLEDB connection (instead of ADO.NET), I'd like to handle that situation in this code. What do I need to do. Sorry if I dont sound technical enough, but I am not a C# person. Thanks again for your kind help.
EDIT
I am pasting the whole code here because I cant seem to use OLEDB type connections. I can only use ADO.NET...I know its something simple, but I dont know what it is.
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using Microsoft.SqlServer.Dts.Runtime;
using System.Data.OleDb;
using System.Data.Common;
namespace AOC.SqlServer.Dts.Tasks
{
[DtsTask(
DisplayName = "Custom Logging Task",
Description = "Writes logging info into a table")]
public class CustomLoggingTask : Task
{
private string _packageName;
private string _taskName;
private string _errorCode;
private string _errorDescription;
private string _machineName;
private double _packageDuration;
private string _connectionName;
private string _eventType;
private string _executionid;
private DateTime _handlerdatetime;
public string ConnectionName
{
set
{
_connectionName = value;
}
get
{
return _connectionName;
}
}
public string Event
{
set
{
_eventType = value;
}
get
{
return _eventType;
}
}
public override DTSExecResult Validate(Connections connections, VariableDispenser variableDispenser, IDTSComponentEvents componentEvents, IDTSLogging log)
{
const string METHOD_NAME = "CustomLoggingTask-Validate";
try
{
if (string.IsNullOrEmpty(_eventType))
{
componentEvents.FireError(0, METHOD_NAME, "The event property must be specified", "", -1);
return DTSExecResult.Failure;
}
if (string.IsNullOrEmpty(_connectionName))
{
componentEvents.FireError(0, METHOD_NAME, "No connection has been specified", "", -1);
return DTSExecResult.Failure;
}
DbConnection connection = connections[_connectionName].AcquireConnection(null) as DbConnection;
if (connection == null)
{
componentEvents.FireError(0, METHOD_NAME, "The connection is not a valid ADO.NET connection", "", -1);
return DTSExecResult.Failure;
}
if (!variableDispenser.Contains("System::SourceID"))
{
componentEvents.FireError(0, METHOD_NAME, "No System::SourceID variable available. This task can only be used in an Event Handler", "", -1);
return DTSExecResult.Failure;
}
return DTSExecResult.Success;
}
catch (Exception exc)
{
componentEvents.FireError(0, METHOD_NAME, "Validation Failed: " + exc.ToString(), "", -1);
return DTSExecResult.Failure;
}
}
public override DTSExecResult Execute(Connections connections, VariableDispenser variableDispenser, IDTSComponentEvents componentEvents, IDTSLogging log, object transaction)
{
try
{
string commandText =
#"INSERT INTO SSISLog (EventType, PackageName, TaskName, EventCode, EventDescription, PackageDuration, Host, ExecutionID, EventHandlerDateTime)
VALUES (#EventType, #PackageName, #TaskName, #EventCode, #EventDescription, #PackageDuration, #Host, #Executionid, #handlerdatetime)";
ReadVariables(variableDispenser);
DbConnection connection = connections[_connectionName].AcquireConnection(transaction) as DbConnection;
//SqlConnection connection = (SqlConnection)connections[_connectionName].AcquireConnection(transaction);
DbCommand command = null;
//using (SqlCommand command = new SqlCommand())
if (connection is SqlConnection)
command = new SqlCommand();
else if (connection is OleDbConnection)
command = new OleDbCommand();
{
command.CommandText = commandText;
command.CommandType = CommandType.Text;
command.Connection = connection;
command.Parameters.Add(new SqlParameter("#EventType", _eventType));
command.Parameters.Add(new SqlParameter("#PackageName", _packageName));
command.Parameters.Add(new SqlParameter("#TaskName", _taskName));
command.Parameters.Add(new SqlParameter("#EventCode", _errorCode ?? string.Empty));
command.Parameters.Add(new SqlParameter("#EventDescription", _errorDescription ?? string.Empty));
command.Parameters.Add(new SqlParameter("#PackageDuration", _packageDuration));
command.Parameters.Add(new SqlParameter("#Host", _machineName));
command.Parameters.Add(new SqlParameter("#ExecutionID", _executionid));
command.Parameters.Add(new SqlParameter("#handlerdatetime", _handlerdatetime));
command.ExecuteNonQuery();
}
return DTSExecResult.Success;
}
catch (Exception exc)
{
componentEvents.FireError(0, "CustomLoggingTask-Execute", "Task Errored: " + exc.ToString(), "", -1);
return DTSExecResult.Failure;
}
}
private void ReadVariables(VariableDispenser variableDispenser)
{
variableDispenser.LockForRead("System::StartTime");
variableDispenser.LockForRead("System::PackageName");
variableDispenser.LockForRead("System::SourceName");
variableDispenser.LockForRead("System::MachineName");
variableDispenser.LockForRead("System::ExecutionInstanceGUID");
variableDispenser.LockForRead("System::EventHandlerStartTime");
bool includesError = variableDispenser.Contains("System::ErrorCode");
if (includesError)
{
variableDispenser.LockForRead("System::ErrorCode");
variableDispenser.LockForRead("System::ErrorDescription");
}
Variables vars = null;
variableDispenser.GetVariables(ref vars);
DateTime startTime = (DateTime)vars["System::StartTime"].Value;
_packageDuration = DateTime.Now.Subtract(startTime).TotalSeconds;
_packageName = vars["System::PackageName"].Value.ToString();
_taskName = vars["System::SourceName"].Value.ToString();
_machineName = vars["System::MachineName"].Value.ToString();
_executionid = vars["System::ExecutionInstanceGUID"].Value.ToString();
_handlerdatetime = (DateTime)vars["System::EventHandlerStartTime"].Value;
if (includesError)
{
_errorCode = vars["System::ErrorCode"].Value.ToString();
_errorDescription = vars["System::ErrorDescription"].Value.ToString();
}
// release the variable locks.
vars.Unlock();
// reset the dispenser
variableDispenser.Reset();
}
}
}
In System.Data.Common you will find the base classes that the concrete database types (SqlConnection, OleDbConnection, SqlDataAdapter etc.) are all derived from.
If you use those, ie. DbConnection, it doesn't matter which of the concrete implementations you are working with, the code will be the same.
DbConnection connection = connections[_connectionName]
.AcquireConnection(transaction) as DbConnection;
Then, you can call DbConnection.BeginTransaction() instead of SqlConnection.BeginTransaction() and it doesn't matter whether the connection is OleDbConnection or SqlConnection.
This will work as long as all the methods you need to call are inherited from DbConnection.
The rest of your code could use the base types as well, so DbTransaction, DbDataAdapter, DbDataReader etc.
Because your AquireConnection() method does not return a concrete connection type, you are able to take advantage of Dependency Injection and write code that is not implementation-specific.
Specifically, for an insert you'll have something like this:
DbCommand command = null;
if (connection is SqlConnection)
command = new SqlCommand();
else if (connection is OleDbConnection)
command = new OleDbCommand();
command.CommandText = "INSERT STATEMENT HERE";
command.Connection = connection;
command.ExecuteNonQuery();
Don't forget that you will need connection.Close() somewhere. (Or ReleaseConnection if you are using ConnectionManager)
Since the connections[_connectionName].AcquireConnection(transaction); part of your code has nothing to do with a SqlConnection (you are just casting to SqlConnection afterwards), you should just be able to declare connection with var and then do anything with connection you need to.
var connection = connections[_connectionName].AcquireConnection(transaction);
Once you have your connection object, you can cast it to whatever connection type you need to and perform the same operation as if you had declared your connection variable as that type. E.g.
if(connection is DbConnection)
{
// ((DbConnection)connection).SomethingToDoWithDbConnection
}
if(connection is SqlConnection)
{
// ((SqlConnection)connection).SomethingToDoWithSqlConnection
}
if(connection is OleDbConnection)
{
// ((OleDbConnection)connection).SomethingToDoWithOleDbConnection
}
I'm looking to write a C# SQL Server wrapper to call some stored procedures. If I was writing a single function I'd do something like the following (which I think is correct/proper):
void RunStoredProc1(object arg1)
{
using(SqlConnection conn = new SqlConnection(connStr)){
try{
SqlCommand cmd = new SqlCommand("storedProc1", conn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("#input1", arg1);
conn.Open();
cmd.ExecuteNonQuery();
} catch (Exception ex){
//handle the exception appropriately.
}
}
}
The problem I'm having is that it seems like a lot of repeated code... every function will have the same using/try(open/execute)/catch code, and it'd be nice to have it all in only one place. Is there a clean way of doing this? How about for queries that I'd want to use a data reader on?
Something like this should do:
void RunStoredProc(string storedProcName, IDictionary<string, object> args)
{
using (SqlConnection conn = new SqlConnection(connStr))
using (SqlCommand cmd = conn.CreateCommand())
{
cmd.CommandText = storedProcName;
cmd.CommandType = CommandType.StoredProcedure;
foreach (KeyValuePair<string, object> kvp in args)
{
cmd.Parameters.AddWithValue(kvp.Key, kvp.Value);
}
conn.Open();
cmd.ExecuteNonQuery();
}
}
The connection object itself would probably also be better off as a parameter to this helper method, so you could make it static. It might be interesting to write it as an extension method on SqlConnection.
I would keep the exception handling in your RunStoredProc1 method or even more likely: in the methods that call RunStoredProc1, because exception handling will likely differ on a case by case basis.
void RunStoredProc1(object input1)
{
var args = new Dictionary<string, object>()
{
{ "input1", input1 }
};
try
{
RunStoredProc("storedProc1", args);
}
catch (Exception ex)
{
// Handle exception properly
}
}
Just a fun exercise for me, and not necessarily the way you'd want to implement it. I wrote a quick fluent interface for building and executing SqlCommands.
A couple of sample usages:
int i = Sql.UsingConnection("sample")
.GetTextCommandFor("Select Top 1 ActorID From Actor Where FirstName = #fname")
.AddParameters(new {fname = "Bob"})
.OnException(e => Console.WriteLine(e.Message))
.ExecuteScalar<int>();
var q = Sql.UsingConnection("sample")
.GetTextCommandFor("Select * From Actor Where FirstName=#fname and ActorID > #id")
.AddParameters(new {id = 1000, fname = "Bob"});
using(var reader = q.ExecuteReader())
{
while(reader.Read())
{
// do something
}
}
The actual class(es) and Interfaces are below:
public class Sql
{
public static ISqlCommandTypeSelector UsingConnection(string connection)
{
return new SqlBuilder(connection);
}
private class SqlBuilder : ISqlCommandTypeSelector, ISqlParameterManager, ISqlExecutor
{
private string _connection;
private string _sqltext;
private CommandType _commandtype;
private Action<Exception> _exceptionBehavior = DefaultExceptionBehavior;
private IList<SqlParameter> _inParams;
public SqlBuilder(string connection)
{
_connection = ConfigurationManager.ConnectionStrings[connection].ConnectionString;
_inParams = new List<SqlParameter>();
}
public ISqlParameterManager GetTextCommandFor(string text)
{
_sqltext = text;
_commandtype = CommandType.Text;
return this;
}
public ISqlParameterManager GetProcCommandFor(string proc)
{
_sqltext = proc;
_commandtype = CommandType.StoredProcedure;
return this;
}
public ISqlExecutor OnException(Action<Exception> action)
{
_exceptionBehavior = action;
return this;
}
public void ExecuteNonQuery()
{
try
{
using (var connection = new SqlConnection(_connection))
using (var cmd = connection.CreateCommand())
{
ConfigureCommand(cmd);
PopulateParameters(cmd);
connection.Open();
cmd.ExecuteNonQuery();
}
}
catch(Exception ex)
{
_exceptionBehavior(ex);
}
}
public T ExecuteScalar<T>()
{
T result = default(T);
try
{
using (var connection = new SqlConnection(_connection))
using (var cmd = connection.CreateCommand())
{
ConfigureCommand(cmd);
PopulateParameters(cmd);
connection.Open();
result = (T) cmd.ExecuteScalar();
return result;
}
}
catch(InvalidCastException ex)
{
// rethrow?
}
catch(Exception ex)
{
_exceptionBehavior(ex);
}
return result;
}
public IDataReader ExecuteReader()
{
try
{
var connection = new SqlConnection(_connection);
var cmd = connection.CreateCommand();
ConfigureCommand(cmd);
PopulateParameters(cmd);
connection.Open();
var reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
return reader;
}
catch(Exception ex)
{
_exceptionBehavior(ex);
}
return null;
}
public ISqlExecutor AddParameters(object #params)
{
var type = #params.GetType();
var props = type.GetProperties();
foreach (var propertyInfo in props)
{
var param = new SqlParameter("#" + propertyInfo.Name, propertyInfo.GetValue(#params, null));
param.Direction = ParameterDirection.Input;
_inParams.Add(param);
}
return this;
}
public ISqlExecutor WithoutParams()
{
return this;
}
private void ConfigureCommand(SqlCommand cmd)
{
cmd.CommandText = _sqltext;
cmd.CommandType = _commandtype;
}
private void PopulateParameters(SqlCommand cmd)
{
cmd.Parameters.AddRange(_inParams.ToArray());
}
private static void DefaultExceptionBehavior(Exception e)
{
// do something
}
}
}
public interface ISqlCommandTypeSelector
{
ISqlParameterManager GetTextCommandFor(string text);
ISqlParameterManager GetProcCommandFor(string proc);
}
public interface ISqlExecutor
{
ISqlExecutor OnException(Action<Exception> action);
void ExecuteNonQuery();
T ExecuteScalar<T>();
IDataReader ExecuteReader();
}
public interface ISqlParameterManager
{
ISqlExecutor AddParameters(object #params);
ISqlExecutor WithoutParams();
}
There is some repeated code that could probably be refactored some more if you really hate repeated code. This is just a fun exercise, and probably not how you want to do your data access however. This also doesn't support out parameters as it is written.
The Microsoft Enterprise Library Data Access Application Block can help to reduce redundant code like that, if you're sticking to pure ADO.NET for your data layer. See http://msdn.microsoft.com/en-us/library/ff664408(v=PandP.50).aspx. There are lots of code samples online and in the download as well, i.e. http://msdn.microsoft.com/en-us/library/ff664702(v=PandP.50).aspx.
I'm a big fan of letting computers do the rote, repetitive work. They're very good at it. I only have to teach them to do it once. So I wrote a code generator that uses a reference database to generate strongly typed access code. The advantage of this technique is that if you change the stored procedure's signatures, all you have to do it re-gen your data access layer. Any breaking changes will cause compile errors.
The code generate I wrote reads an XML file identifying the stored procedures of interest and retrieves their metadata from the specified reference database(s).
The XML file contains flags identifying whether each stored procedure returns
multiple result sets (dataset)
a single result set (datatable)
a single row (datarow)
a single row with a single column (a scalar value)
a DataReader
an XmlReader
or nothing (nonquery)
From that it generates appropriate code, 1 class per stored procedure. The generated code provides access to the stored procedure's return code as well as the returned value for any output parameters.
It also parses the declaration for the stored procedure in the stored procedure's source code to identify any optional arguments (those with default values): the generated code allows those to be omitted in the call to execute the stored procedure.
Invoking the generated code goes like this:
public DataTable GetRiskFactorsForPatient( int patientID )
{
dbo_GetRiskbyPatient sp = new dbo_GetRiskbyPatient( CONNECT_STRING_ID ) ;
int rc = sp.Exec( patientID ) ;
DataTable dt = sp.ResultSet ;
if ( dt == null ) throw new InvalidOperationException( "nothing returned from stored procedure" ) ;
return dt ;
}
Personally, i prefer
void RunStoredProc1(object arg1)
{
try
{
using(SqlConnection conn = new SqlConnection(connStr))
{
using SqlCommand cmd = new SqlCommand("storedProc1", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("#input1", arg1);
conn.Open();
cmd.ExecuteNonQuery();
}
}
}
catch (Exception ex)
{
//handle the exception appropriately.
}
}
Over the traditional try catch finally that you would need to do to manage your resources.
But overall, I like doing it with separate methods, so that you can custom tailor your catch blocks for the sproc.
Also, You might need more than one parameter down the road, and you would just be making a mess of a fairly straight-forward function