I have this method that opens the connection to sqlserver, I need it to be asynchronous, so far everything is correct, but I see that it asks to wait "await"
public async static Task<DataTable> ToList(string nombreProcedimiento, List<Parameter>? parametros = null)
{
SqlConnection conexion = new(conn);
try
{
conexion.Open();
SqlCommand cmd = new(nombreProcedimiento, conexion)
{
CommandType = CommandType.StoredProcedure
};
if (parametros != null)
{
foreach (var parametro in parametros)
{
cmd.Parameters.AddWithValue(parametro.Name, parametro.Value);
}
}
DataTable tabla = new();
SqlDataAdapter da = new(cmd);
da.Fill(tabla);
return tabla;
}
catch (Exception)
{
return null!;
}
finally
{
conexion.Close();
}
}
I see that I am missing something like that
return await sqlCommand.ExecuteNonQueryAsync();
but i can't put it because i'm not executing a query but a stored procedure
CommandType = CommandType.StoredProcedure
How can I create this async method correctly?
I see in your code that you've created a SqlCommand object but never executed it.
Your query Type isn't important, to execute any type of query and make your method asynchronous you have to call one of these methods provided in the SqlCommand object and await it.
ExecuteScalarAsync
ExecuteReaderAsync
ExecuteNonQueryAsync
Related
I have a code to check if the stored procedure with some specified name has already been deployed or not. The code is
protected virtual async Task<bool?> IsProcedureDeployed(string storedProcedureName)
{
try
{
SqlCommand sqlCommand = new SqlCommand
{
CommandText = "select count(*) from sysobjects where type = 'P' and name = #storedProcedureName",
CommandType = CommandType.Text,
CommandTimeout = this.CommandTimeout
};
await this.EnsureConnectionOpened();
int count = (int)(await sqlCommand.ExecuteScalarAsync());
return count > 0;
}
catch (Exception exception)
{
this.SqlConnection.Close();
ExceptionDispatchInfo.Capture(exception).Throw();
return null;
}
}
this.EnsureConnectionOpened looks like this:
protected async Task EnsureConnectionOpened()
{
if (SqlConnection.State == ConnectionState.Closed && SqlTransaction == null)
{
await SqlConnection.OpenAsync();
}
}
and when it comes to the execution of int count = (int)(await sqlCommand.ExecuteScalarAsync()); it always throws an exception which says... "Invalid operation. The connection is closed.". I have checked the state of SqlConnection, and it is Open! What the hell am I doing wrong?
EDIT. Every SqlCommand must have a SqlConnection and properly setted parameters (if needed). The final version is
protected virtual async Task<bool?> IsProcedureDeployed(string storedProcedureName)
{
try
{
SqlCommand sqlCommand = new SqlCommand("select count(*) from sysobjects where type = 'P' and name = #storedProcedureName", this.SqlConnection)
{
CommandType = CommandType.Text,
CommandTimeout = this.CommandTimeout
};
SqlParameter sqlParameter = new SqlParameter
{
ParameterName = "#storedProcedureName",
Value = storedProcedureName
};
sqlCommand.Parameters.Add(sqlParameter);
await this.EnsureConnectionOpened();
int count = (int)(await sqlCommand.ExecuteScalarAsync());
return count > 0;
}
catch (Exception exception)
{
this.SqlConnection.Close();
ExceptionDispatchInfo.Capture(exception).Throw();
return null;
}
}
I would rework this method a bit. Get all your connection stuff in the same place instead of spread all over the place. Also, since your posted code was only using the try/catch to close the connection (and rethrow the exception) I removed it entirely. You don't need any error handling here. Let the exception happen and bubble up to the calling method. I am guessing this logic is in your data layer?
Something like this is contained nice and tidy.
protected virtual async Task<bool?> IsProcedureDeployed(string storedProcedureName)
{
bool IsDeployed = false;
using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["YourConnectionString"].ConnectionString))
{
conn.Open();
using (var cmd = new SqlCommand("select count(*) from sysobjects where type = 'P' and name = #storedProcedureName", conn))
{
cmd.Parameters.Add("#storedProcedureName", SqlDbType.NVarChar, 128).Value = storedProcedureName; //using nvarchar(128) because object names use sysname which is a synonym for nvarchar(128)
var result = await cmd.ExecuteScalarAsync();
bool.TryParse(result.ToString(), out IsDeployed);
}
}
return IsDeployed;
}
I have been working on an ASP.Net application for a long time and there are above 10 clients using the application. But now I found a problem in the application, that is, I have a stored procedure call which takes about 30 seconds to execute. It is not a problem, because the SQL code highly complicated and looping many times. The problem is :
Whenever that stored procedure call is executing, I am not able to using any other functions or stored procedure call.
When I tried debugging, the problem is that 'DataAdapter.Fill()' function is waiting for the first stored procedure call to finish.
My code that executes stored procedure call and returning data is :
public static DataSet ExecuteQuery_SP(string ProcedureName, object[,] ParamArray)
{
SqlDataAdapter DataAdapter = new SqlDataAdapter();
DataSet DS = new DataSet();
try
{
if (CON.State != ConnectionState.Open)
OpenConnection();
SqlCommand cmd = new SqlCommand();
cmd.CommandTimeout = 0;
cmd.CommandText = ProcedureName;
cmd.CommandType = CommandType.StoredProcedure;
cmd.Connection = CON;
cmd.Transaction = SqlTrans;
string ParamName;
object ParamValue;
for (int i = 0; i < ParamArray.Length / 2; i++)
{
ParamName = ParamArray[i, 0].ToString();
ParamValue = ParamArray[i, 1];
cmd.Parameters.AddWithValue(ParamName, ParamValue);
}
DataAdapter = new SqlDataAdapter(cmd);
DataAdapter.Fill(DS);
cmd.CommandText = "";
}
catch (Exception ea)
{
}
return DS;
}
All stored procedure calls are working through this function. Hence when my first stored procedure call 'A' is running, stored procedure call 'B' will not execute until 'A' is finished.
This reduces overall performance of the application and causes problem in data retrieval.
I surfed google and found that 'Threading' can be helpful but I am not able to execute threading properly. I am not so familiar with these kind of things. It will helpful if you can rectify the problem.
My first stored procedure call is:
ds = DB.ExecuteQuery_SP("SelectOutstandingReportDetailed", parArray);
Where ds is the DataSet object.
Second stored procedure call is :
ds = DB.ExecuteQuery_SP("[SelectAccLedgersDetailsByID]", ParamArray);
My current DB connection open function is :
public static bool OpenConnection()
{
try
{
Server = (String)HttpContext.GetGlobalResourceObject("Resource", "Server");
DBName = (String)HttpContext.GetGlobalResourceObject("Resource", "DBName");
UserName = (String)HttpContext.GetGlobalResourceObject("Resource", "UserName");
PassWord = (String)HttpContext.GetGlobalResourceObject("Resource", "PassWord");
string ConnectionString;
ConnectionString = "server=" + Server + "; database=" + DBName + "; uid=" + UserName + "; pwd=" + PassWord + "; Pooling='true';Max Pool Size=100;MultipleActiveResultSets=true;Asynchronous Processing=true";
CON.ConnectionString = ConnectionString;
if (CON.State != ConnectionState.Open)
{
CON.Close();
CON.Open();
}
}
catch (Exception ea)
{
}
return false;
}
Where 'CON' is a public SqlConnection variable
static SqlConnection CON = new SqlConnection();
I found the problem, that is, all stored procedure calls are performed through this 'CON' object. If there is seperate SqlConnection object for each stored procedure call, then no problem is there.
So is it possible to make separate SqlConnection for every ExecuteQuery_SP calls.
If any doubt in question, kindly comment.
Thank you
SQL Server will allow thousands of connections simultaneously, by default. This is NOT the source of your problem. You have forced every call to a stored procedure to be funneled through a single method. Factor out your calls to the stored procedures - in other words, lose the ExecuteQuery_SP method which is a bottleneck. Then test again.
Here's is an introduction to data layers.
Heres the simplest version I can create for you.
Important: to understand you should read about async-await.
You can start at the Microsoft C# Async-Await Docs
// TODO set up your connection string
private string connectionString = "<your connection string>";
// Gets data assyncronously
public static async Task<DataTable> GetDataAsync(string procedureName, object[,] ParamArray)
{
try
{
var asyncConnectionString = new SqlConnectionStringBuilder(connectionString)
{
AsynchronousProcessing = true
}.ToString();
using (var conn = new SqlConnection(asyncConnectionString))
{
using (var SqlCommand = new SqlCommand())
{
SqlCommand.Connection = conn;
SqlCommand.CommandText = procedureName;
SqlCommand.CommandType = CommandType.StoredProcedure;
string ParamName;
object ParamValue;
for (int i = 0; i < ParamArray.Length / 2; i++)
{
ParamName = ParamArray[i, 0].ToString();
ParamValue = ParamArray[i, 1];
SqlCommand.Parameters.AddWithValue(ParamName, ParamValue);
}
conn.Open();
var data = new DataTable();
data.BeginLoadData();
using (var reader = await SqlCommand.ExecuteReaderAsync().ConfigureAwait(true))
{
if (reader.HasRows)
data.Load(reader);
}
data.EndLoadData();
return data;
}
}
}
catch (Exception Ex)
{
// Log error or something else
throw;
}
}
public static async Task<DataTable> GetData(object General, object Type, string FromDate, string ToDate)
{
object[,] parArray = new object[,]{
{"#BranchID",General.BranchID},
{"#FinancialYearID",General.FinancialYearID},
{"#Type",Type},
{"#FromDate",DateTime.ParseExact(FromDate, "dd/MM/yyyy", System.Globalization.CultureInfo.InvariantCulture)},
{"#ToDate",DateTime.ParseExact(ToDate, "dd/MM/yyyy", System.Globalization.CultureInfo.InvariantCulture)}
};
return await DataBaseHelper.GetDataAsync("SelectOutstandingReportDetailed", parArray);
}
// Calls database assyncronously
private async Task ConsumeData()
{
DataTable dt = null;
try
{
// TODO configure your parameters here
object general = null;
object type = null;
string fromDate = "";
string toDate = "";
dt = await GetData(general, type, fromDate, toDate);
}
catch (Exception Ex)
{
// do something if an error occurs
System.Diagnostics.Debug.WriteLine("Error occurred: " + Ex.ToString());
return;
}
foreach (DataRow dr in dt.Rows)
{
System.Diagnostics.Debug.WriteLine(dr.ToString());
}
}
// Fired when some button is clicked. Get and use the data assyncronously, i.e. without blocking the UI.
private async void button1_Click(object sender, EventArgs e)
{
await ConsumeData();
}
I have this API, which calls a stored procedure to insert a value and it has few PRINT statements in the stored procedure
public class ConfirmJobController : ApiController
{
[HttpPost]
public IHttpActionResult Post(JobConfirmation confirmation)
{
using (SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString))
{
string outputMessage;
con.Open();
SqlCommand cmd = new SqlCommand("usp_PhoneApp_ConfirmBooking", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("#p_jobId", confirmation.JobID);
cmd.Parameters.AddWithValue("#p_jobStatus", confirmation.JobStatus);
cmd.Parameters.AddWithValue("#p_createdDate", confirmation.CreatedDate);
cmd.ExecuteNonQuery();
con.InfoMessage += delegate (object sender1, SqlInfoMessageEventArgs e1)
{
outputMessage = e1.Message;
};
return outputMessage;
}
}
}
I would like get custom message from the stored procedure to the return value of the API. How can I do this?
currently the line return outputMessage gives an error with "use of unassigned variable and cannot implicitly convert string to IHttpActionResult"
Any help is greatly appreciated.
To answer only the question in the title, you could add an output parameter to the stored procedure, that's what I usually use.
https://technet.microsoft.com/en-us/library/ms187004.aspx
The other issues are simply a matter of returning the correct type that the method declares. Return a ContentResult or some other object that implements the declared interface.
You can make use of ExecuteNonQuery to return int value, method looks like inserting data via stored procedure. Alternately you can also make use out parameter in stored procedure for any specific success message.
[HttpPost]
public IHttpActionResult Post(JobConfirmation confirmation)
{
using (SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString))
{
string outputMessage;
con.Open();
SqlCommand cmd = new SqlCommand("usp_PhoneApp_ConfirmBooking", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("#p_jobId", confirmation.JobID);
cmd.Parameters.AddWithValue("#p_jobStatus", confirmation.JobStatus);
cmd.Parameters.AddWithValue("#p_createdDate", confirmation.CreatedDate);
if(cmd.ExecuteNonQuery() > 0)
outputMessage = "Success";
else
outputMessage = "Failed";
return Ok(outputMessage);
}
}
I am running a SQLQuery that takes roughly 45 seconds to run and display results. I am using Task<DataSet> to populate two drop downs on my page. Well the 1st drop down populates fine (the query is completed in about 2 seconds), the second it seems that the adapter.Fill(dataSet) is not waiting on the query to complete before it begins to fill the drop down with a null dataset. What should I alter so that the code execution halts until the query executes completely?
Task.Factory.ContinueWhenAll(new[]
{
One("Data Source=server;Initial Catalog=db;Integrated Security=True;MultipleActiveResultSets=True"),
Two("Data Source=server;Initial Catalog=db;Integrated Security=True;MultipleActiveResultSets=True"),
}, tasks =>
{
try
{
this.ddl1.DataSource = tasks[0].Result.Tables[0];
this.ddl1.DataTextField = "One";
this.ddl1.DataValueField = "ID";
this.ddl1.DataBind();
int indexOfLastItem = this.ddl1.Items.Count - 1;
this.ddl1.SelectedIndex = indexOfLastItem;
ddl2.DataSource = tasks[1].Result.Tables[0];
this.ddl2.DataTextField = "Two";
this.ddl2.DataValueField = "ID";
this.ddl2.DataBind();
this.ddl2.Items.Insert(0, new ListItem(Constants.All, Constants.All));
}
catch (Exception exception) { throw exception; }
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
public System.Threading.Tasks.Task<DataSet> One(string databaseConnection)
{
return FillDS("Select * from activeemployees", databaseConnection);
}
public System.Threading.Tasks.Task<DataSet> Two(string databaseConnection)
{
return FillDS("Select * from mastersalesdatabase", databaseConnection);
}
public System.Threading.Tasks.Task<DataSet> FillDS(string sqlQuery, string connectionString)
{
try
{
var dataSet = new DataSet();
using (var adapter = new SqlDataAdapter(sqlQuery, connectionString))
{
adapter.Fill(dataSet);
return dataSet;
}
}
catch (Exception exception) { throw exception; }
}
My query Select * from activeemployees completes in about 2 seconds and populates fine, my query Select * from mastersalesdatabase takes roughly 45 seconds and it seems the code just moves on w/o a delay to let the query execute to completion.
If you're going to do async to retrieve data into a datatable, it should look more like this:
public static async Task<DataTable> GetDataTableAsync(string connectionString, SqlCommand command)
{
using (var connection = new SqlConnection(connectionString))
{
command.Connection = connection;
await connection.OpenAsync();
using (var dataReader = await command.ExecuteReaderAsync())
{
var dataTable = new DataTable();
dataTable.Load(dataReader);
return dataTable;
}
}
}
Notice there's no need for a dataset.
Then in WebForms, we have to handle async code differently.
protected void Page_Load(object sender, EventArgs e)
{
RegisterAsyncTask(new PageAsyncTask(DoWorkAsync));
}
private async Task DoWorkAsync()
{
ActiveEmployeesDropDownList.DataSource = GetDataTableAsync(databaseConnection, new SqlCommand("select * from activeemployees"));
ActiveEmployeesDropDownList.DataBind();
}
Notice I renamed the control from ddl1 to ActiveEmployeesDropDownList because ddl1 is a horrible name. Your names should have semantic meaning.
You'll need to add the async=true attribute to your page according to MSDN.
And you should also fix your query to not take 45 seconds, but that's a separate question entirely.
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