My task is to log the raw SQL that dapper is going to be querying the DB with.
So what I have done is made use of the decorator pattern and created my own class that implements IDbConnection
More info can be found Why does the C# compiler choose Dapper's extension method for IDbConnection instead of the instance method on my own type?
public async Task<IEnumerable<T>> QueryAsync<T>(string sql, object param = null,
IDbTransaction transaction = null,
int? commandTimeout = null,
CommandType? commandType = null)
{
LogSqlIfEnabled(sql, param, commandType ?? CommandType.Text);
return await SqlMapper.QueryAsync<T>(_inner, sql, param, transaction, commandTimeout, commandType);
}
private void LogSqlIfEnabled(string sql, object param, CommandType commandType)
{
try
{
if (HttpContext.Current?.Session?["IsSQLLogsEnabled"] != null)
{
// Convert the object parameters to SqlParameters
SqlParameter[] sqlParameters = ConvertObjectToSqlParameters(param);
// Use the commandType name as a string for CollectSqlData
string commandTypeName = commandType.ToString() ?? "Text";
string query = Utility.CollectSqlData(sql, commandTypeName, sqlParameters);
if (!string.IsNullOrEmpty(query))
_sqlLogger.Info(query);
}
}
catch (Exception ex)
{
_sqlLogger.Error(ex, "Error logging Dapper SQL");
}
private SqlParameter[] ConvertObjectToSqlParameters(object param)
{
if (param == null)
return new SqlParameter[0];
// Handle DynamicParameters directly
if (param is DynamicParameters dynamicParams)
{
return ConvertToSqlParameters(dynamicParams);
}
// Handle anonymous objects by converting properties to parameters
var sqlParameters = new List<SqlParameter>();
foreach (var prop in param.GetType().GetProperties())
{
var value = prop.GetValue(param);
var paramName = $"@{prop.Name}";
var sqlParam = new SqlParameter(paramName, value ?? DBNull.Value);
// Try to set appropriate SqlDbType based on value
if (value != null)
{
sqlParam.SqlDbType = GetSqlDbType(value.GetType());
}
sqlParameters.Add(sqlParam);
}
return sqlParameters.ToArray();
}
private static SqlParameter[] ConvertToSqlParameters(DynamicParameters parameters)
{
if (parameters == null)
return new SqlParameter[0];
var sqlParameters = new List<SqlParameter>();
var x = string.Join(", ", from pn in parameters.ParameterNames select string.Format("@{0}={1}", pn, (parameters as SqlMapper.IParameterLookup)[pn]));
//x is "" here
foreach (var name in parameters.ParameterNames)
{
var value = parameters.Get<object>(name);
var parameter = new SqlParameter(name, value ?? DBNull.Value);
// Try to set appropriate SqlDbType based on value
if (value != null)
{
parameter.SqlDbType = GetSqlDbType(value.GetType());
}
sqlParameters.Add(parameter);
}
return sqlParameters.ToArray();
}
}
referenced code: https://stackoverflow.com/a/10510471/5759460
However, when debugging, I can see that the loop is never entered, even though I know there are parameters in the DynamicParameters object. Looking at the DynamicParameters object in the debugger:

ParameterNames: Empty or not showing expected parameters parameters: Count = 0 templates: Count = 1 (contains my anonymous object)
I'm creating the DynamicParameters object like this:
var parameters = new
{
@p_UserId = userId,
// more parameters...
};
DynamicParameters dynamicParameters = new DynamicParameters(parameters);
And then passing it to my database methods:
var results = await _dbConnection.QueryAsync<T>(spName, dynamicParameters, commandType: CommandType.StoredProcedure);
The SQL executes fine, but I can't log the parameter values because my ConvertToSqlParameters method isn't working as expected.
So Now I am thinking of using reflection to iterate through the template variable. But I just want to make sure what I am doing is alright? Or there could be a better way I am missing.
can't log the parameter valuesyes, because they're never part of the SQL text itself itself. These aren't placeholders that get replaced. When the database is called these will be passed outside the query itself as parameters of the RPC call. Even when the query engine compilessqlinto an execution plan, the plan will include those parameters. The database will cache that execution plan and reuse it every time it sees the same SQL text, using the new parameter valuesSqlDataSource.Create(connectionString)can have logging enabled