Initial commit
This commit is contained in:
19
SIG.Web/AppCode/Services/Database/DbConnectionFactory.cs
Normal file
19
SIG.Web/AppCode/Services/Database/DbConnectionFactory.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using SIG.Web.AppCode.Contracts.Database;
|
||||
namespace SIG.Web.AppCode.Services.Database
|
||||
{
|
||||
public sealed class DbConnectionFactory : IDbConnectionFactory
|
||||
{
|
||||
public IDbConnection CreateConnection(string domainName)
|
||||
{
|
||||
string name = ConfigurationManager.AppSettings["DefaultConnectionStringName"];
|
||||
if (string.IsNullOrWhiteSpace(name)) name = "SIG_DefaultSql";
|
||||
ConnectionStringSettings settings = ConfigurationManager.ConnectionStrings[name];
|
||||
if (settings == null) throw new InvalidOperationException("Connection string not found: " + name);
|
||||
return new SqlConnection(settings.ConnectionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
162
SIG.Web/AppCode/Services/Database/DbExecutor.cs
Normal file
162
SIG.Web/AppCode/Services/Database/DbExecutor.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using SIG.Web.AppCode.Contracts.Database;
|
||||
using SIG.Web.AppCode.Models.Common;
|
||||
using SIG.Web.AppCode.Services.Logging;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
|
||||
namespace SIG.Web.AppCode.Services.Database
|
||||
{
|
||||
public sealed class DbExecutor : IDbExecutor
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public DbExecutor(IDbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public DbResult<T> Execute<T>(string domainName, Func<IDbConnection, T> action)
|
||||
{
|
||||
IDbConnection connection = null;
|
||||
SqlConnection sqlConnection = null;
|
||||
SqlInfoMessageEventHandler infoMessageHandler = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException("action");
|
||||
}
|
||||
|
||||
connection = _connectionFactory.CreateConnection(domainName);
|
||||
sqlConnection = connection as SqlConnection;
|
||||
|
||||
if (sqlConnection != null)
|
||||
{
|
||||
/*
|
||||
* No activar FireInfoMessageEventOnUserErrors: al hacerlo, SQL Server
|
||||
* degrada errores de severidad 11-16 a InfoMessage y evita el catch.
|
||||
* Los RAISERROR informativos de severidad 10 llegan al evento sin esa opción.
|
||||
*/
|
||||
infoMessageHandler = delegate(object sender, SqlInfoMessageEventArgs eventArgs)
|
||||
{
|
||||
foreach (SqlError error in eventArgs.Errors)
|
||||
{
|
||||
ExceptionLogger.Log(
|
||||
CreateDiagnosticException(new InvalidOperationException(error.Message)),
|
||||
"DbExecutor.SqlInfoMessage");
|
||||
}
|
||||
};
|
||||
sqlConnection.InfoMessage += infoMessageHandler;
|
||||
}
|
||||
|
||||
if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
connection.Open();
|
||||
}
|
||||
|
||||
T data = action(connection);
|
||||
return DbResult<T>.Ok(data);
|
||||
}
|
||||
catch (StoredProcedureException ex)
|
||||
{
|
||||
ExceptionLogger.Log(CreateDiagnosticException(ex), "DbExecutor.StoredProcedure", ex.UserMessage);
|
||||
return DbResult<T>.Fail(ex.UserMessage, BuildTechnicalMessage(ex), ex);
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
string userMessage;
|
||||
|
||||
switch (ex.Number)
|
||||
{
|
||||
case 208:
|
||||
userMessage = "Falta una tabla, vista o procedimiento requerido en la base de datos.";
|
||||
break;
|
||||
|
||||
case 207:
|
||||
userMessage = "Falta una columna requerida en la base de datos.";
|
||||
break;
|
||||
|
||||
case 2812:
|
||||
userMessage = "Falta un procedimiento requerido en la base de datos.";
|
||||
break;
|
||||
|
||||
case 4060:
|
||||
case 18456:
|
||||
userMessage = "No fue posible abrir la base de datos configurada.";
|
||||
break;
|
||||
|
||||
case -2:
|
||||
userMessage = "La base de datos tardó demasiado en responder.";
|
||||
break;
|
||||
|
||||
default:
|
||||
userMessage = "Ocurrió un error al consultar la base de datos.";
|
||||
break;
|
||||
}
|
||||
|
||||
ExceptionLogger.Log(CreateDiagnosticException(ex), "DbExecutor.SqlException", userMessage);
|
||||
return DbResult<T>.Fail(userMessage, BuildTechnicalMessage(ex), ex);
|
||||
}
|
||||
catch (TimeoutException ex)
|
||||
{
|
||||
const string userMessage = "La base de datos tardó demasiado en responder. Intente nuevamente.";
|
||||
ExceptionLogger.Log(CreateDiagnosticException(ex), "DbExecutor.TimeoutException", userMessage);
|
||||
return DbResult<T>.Fail(userMessage, BuildTechnicalMessage(ex), ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
const string userMessage = "Ocurrió un error al consultar la base de datos.";
|
||||
ExceptionLogger.Log(CreateDiagnosticException(ex), "DbExecutor.Exception", userMessage);
|
||||
return DbResult<T>.Fail(userMessage, BuildTechnicalMessage(ex), ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (sqlConnection != null && infoMessageHandler != null)
|
||||
{
|
||||
sqlConnection.InfoMessage -= infoMessageHandler;
|
||||
}
|
||||
|
||||
if (connection != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (connection.State != ConnectionState.Closed)
|
||||
{
|
||||
connection.Close();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
connection.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
StoredProcedureDiagnostics.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static Exception CreateDiagnosticException(Exception exception)
|
||||
{
|
||||
string diagnostic = StoredProcedureDiagnostics.BuildDiagnostic();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(diagnostic))
|
||||
{
|
||||
return exception;
|
||||
}
|
||||
|
||||
return new InvalidOperationException(
|
||||
exception.Message + Environment.NewLine + diagnostic,
|
||||
exception);
|
||||
}
|
||||
|
||||
private static string BuildTechnicalMessage(Exception exception)
|
||||
{
|
||||
string diagnostic = StoredProcedureDiagnostics.BuildDiagnostic();
|
||||
return string.IsNullOrWhiteSpace(diagnostic)
|
||||
? exception.Message
|
||||
: exception.Message + Environment.NewLine + diagnostic;
|
||||
}
|
||||
}
|
||||
}
|
||||
133
SIG.Web/AppCode/Services/Database/StoredProcedureCommand.cs
Normal file
133
SIG.Web/AppCode/Services/Database/StoredProcedureCommand.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using SIG.Web.AppCode.Models.Common;
|
||||
|
||||
namespace SIG.Web.AppCode.Services.Database
|
||||
{
|
||||
public static class StoredProcedureCommand
|
||||
{
|
||||
public static void Configure(IDbCommand command, string procedureName, int cveUsuarioEjecuta)
|
||||
{
|
||||
if (command == null)
|
||||
{
|
||||
throw new ArgumentNullException("command");
|
||||
}
|
||||
|
||||
StoredProcedureDiagnostics.Start(procedureName);
|
||||
command.CommandText = procedureName;
|
||||
command.CommandType = CommandType.StoredProcedure;
|
||||
|
||||
AddInput(command, "@CveUsuarioEjecuta", cveUsuarioEjecuta, DbType.Int32);
|
||||
AddOutput(command, "@ErrorMostrar", DbType.String, -1);
|
||||
AddOutput(command, "@ErrorTipo", DbType.Int32, 0);
|
||||
AddOutput(command, "@Return", DbType.Boolean, 0);
|
||||
}
|
||||
|
||||
public static IDbDataParameter AddInput(IDbCommand command, string name, object value)
|
||||
{
|
||||
return AddInput(command, name, value, null);
|
||||
}
|
||||
|
||||
public static IDbDataParameter AddInput(IDbCommand command, string name, object value, DbType? dbType)
|
||||
{
|
||||
IDbDataParameter parameter = command.CreateParameter();
|
||||
parameter.ParameterName = name;
|
||||
parameter.Direction = ParameterDirection.Input;
|
||||
parameter.Value = value ?? DBNull.Value;
|
||||
|
||||
if (dbType.HasValue)
|
||||
{
|
||||
parameter.DbType = dbType.Value;
|
||||
}
|
||||
|
||||
command.Parameters.Add(parameter);
|
||||
StoredProcedureDiagnostics.AddParameter(
|
||||
parameter.ParameterName,
|
||||
parameter.Value,
|
||||
parameter.DbType,
|
||||
parameter.Direction);
|
||||
|
||||
return parameter;
|
||||
}
|
||||
|
||||
public static StoredProcedureResult ReadResult(IDbCommand command)
|
||||
{
|
||||
StoredProcedureDiagnostics.CaptureOutputs(command);
|
||||
|
||||
return new StoredProcedureResult
|
||||
{
|
||||
Return = ReadBoolean(command, "@Return"),
|
||||
ErrorTipo = ReadInteger(command, "@ErrorTipo"),
|
||||
ErrorMostrar = ReadString(command, "@ErrorMostrar")
|
||||
};
|
||||
}
|
||||
|
||||
public static void EnsureSuccess(IDbCommand command)
|
||||
{
|
||||
StoredProcedureResult result = ReadResult(command);
|
||||
|
||||
if (result.Return)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
object returnValue = ((IDataParameter)command.Parameters["@Return"]).Value;
|
||||
bool outputsReceived = returnValue != null && returnValue != DBNull.Value;
|
||||
|
||||
string message;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(result.ErrorMostrar))
|
||||
{
|
||||
message = result.ErrorMostrar;
|
||||
}
|
||||
else if (!outputsReceived)
|
||||
{
|
||||
message = "La versión instalada del procedimiento no devolvió los parámetros requeridos; favor de contactar a Sistemas.";
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "La operación no pudo completarse.";
|
||||
}
|
||||
|
||||
throw new StoredProcedureException(message, result.ErrorTipo);
|
||||
}
|
||||
|
||||
private static void AddOutput(IDbCommand command, string name, DbType dbType, int size)
|
||||
{
|
||||
IDbDataParameter parameter = command.CreateParameter();
|
||||
parameter.ParameterName = name;
|
||||
parameter.Direction = ParameterDirection.Output;
|
||||
parameter.DbType = dbType;
|
||||
|
||||
if (size != 0)
|
||||
{
|
||||
parameter.Size = size;
|
||||
}
|
||||
|
||||
command.Parameters.Add(parameter);
|
||||
StoredProcedureDiagnostics.AddParameter(
|
||||
parameter.ParameterName,
|
||||
parameter.Value,
|
||||
parameter.DbType,
|
||||
parameter.Direction);
|
||||
}
|
||||
|
||||
private static bool ReadBoolean(IDbCommand command, string name)
|
||||
{
|
||||
object value = ((IDataParameter)command.Parameters[name]).Value;
|
||||
return value != null && value != DBNull.Value && Convert.ToBoolean(value);
|
||||
}
|
||||
|
||||
private static int ReadInteger(IDbCommand command, string name)
|
||||
{
|
||||
object value = ((IDataParameter)command.Parameters[name]).Value;
|
||||
return value == null || value == DBNull.Value ? 0 : Convert.ToInt32(value);
|
||||
}
|
||||
|
||||
private static string ReadString(IDbCommand command, string name)
|
||||
{
|
||||
object value = ((IDataParameter)command.Parameters[name]).Value;
|
||||
return value == null || value == DBNull.Value ? string.Empty : Convert.ToString(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
276
SIG.Web/AppCode/Services/Database/StoredProcedureDiagnostics.cs
Normal file
276
SIG.Web/AppCode/Services/Database/StoredProcedureDiagnostics.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace SIG.Web.AppCode.Services.Database
|
||||
{
|
||||
public static class StoredProcedureDiagnostics
|
||||
{
|
||||
private static readonly AsyncLocal<DiagnosticContext> CurrentContext =
|
||||
new AsyncLocal<DiagnosticContext>();
|
||||
|
||||
public static void Start(string procedureName)
|
||||
{
|
||||
CurrentContext.Value = new DiagnosticContext
|
||||
{
|
||||
ProcedureName = procedureName
|
||||
};
|
||||
}
|
||||
|
||||
public static void AddParameter(string name, object value, DbType dbType, ParameterDirection direction)
|
||||
{
|
||||
DiagnosticContext context = CurrentContext.Value;
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.Parameters.Add(new DiagnosticParameter
|
||||
{
|
||||
Name = name,
|
||||
Value = value,
|
||||
DbType = dbType,
|
||||
Direction = direction
|
||||
});
|
||||
}
|
||||
|
||||
public static void CaptureOutputs(IDbCommand command)
|
||||
{
|
||||
DiagnosticContext context = CurrentContext.Value;
|
||||
|
||||
if (context == null || command == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (IDataParameter commandParameter in command.Parameters)
|
||||
{
|
||||
if (commandParameter.Direction == ParameterDirection.Input)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DiagnosticParameter parameter = context.Parameters.FirstOrDefault(
|
||||
item => item.Name.Equals(commandParameter.ParameterName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (parameter != null)
|
||||
{
|
||||
parameter.Value = commandParameter.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string BuildDiagnostic()
|
||||
{
|
||||
DiagnosticContext context = CurrentContext.Value;
|
||||
|
||||
if (context == null || string.IsNullOrWhiteSpace(context.ProcedureName))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var result = new StringBuilder();
|
||||
result.AppendLine("StoredProcedure: " + context.ProcedureName);
|
||||
result.AppendLine("Parametros:");
|
||||
|
||||
foreach (DiagnosticParameter parameter in context.Parameters)
|
||||
{
|
||||
result.AppendLine(parameter.Name + "=" + FormatSqlValue(parameter));
|
||||
}
|
||||
|
||||
result.AppendLine("ReplaySql:");
|
||||
AppendReplaySql(result, context);
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
CurrentContext.Value = null;
|
||||
}
|
||||
|
||||
private static void AppendReplaySql(StringBuilder result, DiagnosticContext context)
|
||||
{
|
||||
IList<DiagnosticParameter> outputs = context.Parameters
|
||||
.Where(parameter => parameter.Direction != ParameterDirection.Input)
|
||||
.ToList();
|
||||
|
||||
foreach (DiagnosticParameter output in outputs)
|
||||
{
|
||||
result.Append("DECLARE ")
|
||||
.Append(output.Name)
|
||||
.Append(' ')
|
||||
.Append(GetSqlType(output.DbType))
|
||||
.AppendLine(";");
|
||||
}
|
||||
|
||||
result.Append("EXEC ").Append(QuoteProcedureName(context.ProcedureName));
|
||||
|
||||
for (int index = 0; index < context.Parameters.Count; index++)
|
||||
{
|
||||
DiagnosticParameter parameter = context.Parameters[index];
|
||||
result.Append(index == 0 ? Environment.NewLine + " " : "," + Environment.NewLine + " ");
|
||||
result.Append(parameter.Name).Append(" = ");
|
||||
|
||||
if (parameter.Direction == ParameterDirection.Input)
|
||||
{
|
||||
result.Append(FormatSqlValue(parameter));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append(parameter.Name).Append(" OUTPUT");
|
||||
}
|
||||
}
|
||||
|
||||
result.AppendLine(";");
|
||||
|
||||
if (outputs.Count > 0)
|
||||
{
|
||||
result.Append("SELECT ");
|
||||
|
||||
for (int index = 0; index < outputs.Count; index++)
|
||||
{
|
||||
if (index > 0)
|
||||
{
|
||||
result.Append(", ");
|
||||
}
|
||||
|
||||
result.Append(outputs[index].Name);
|
||||
}
|
||||
|
||||
result.AppendLine(";");
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatSqlValue(DiagnosticParameter parameter)
|
||||
{
|
||||
if (IsSensitive(parameter.Name))
|
||||
{
|
||||
return "N'[REDACTADO]'";
|
||||
}
|
||||
|
||||
object value = parameter.Value;
|
||||
|
||||
if (value == null || value == DBNull.Value)
|
||||
{
|
||||
return "NULL";
|
||||
}
|
||||
|
||||
if (value is bool)
|
||||
{
|
||||
return (bool)value ? "1" : "0";
|
||||
}
|
||||
|
||||
if (value is DateTime)
|
||||
{
|
||||
return "'" + ((DateTime)value).ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture) + "'";
|
||||
}
|
||||
|
||||
if (value is DateTimeOffset)
|
||||
{
|
||||
return "'" + ((DateTimeOffset)value).ToString("o", CultureInfo.InvariantCulture) + "'";
|
||||
}
|
||||
|
||||
if (value is Guid)
|
||||
{
|
||||
return "'" + value + "'";
|
||||
}
|
||||
|
||||
if (value is byte[])
|
||||
{
|
||||
byte[] bytes = (byte[])value;
|
||||
int length = Math.Min(bytes.Length, 64);
|
||||
return "0x" + BitConverter.ToString(bytes, 0, length).Replace("-", string.Empty)
|
||||
+ (bytes.Length > length ? "/*TRUNCADO*/" : string.Empty);
|
||||
}
|
||||
|
||||
if (value is string || value is char)
|
||||
{
|
||||
string text = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
|
||||
if (text.Length > 1000)
|
||||
{
|
||||
text = text.Substring(0, 1000) + " [TRUNCADO]";
|
||||
}
|
||||
|
||||
return "N'" + text.Replace("'", "''") + "'";
|
||||
}
|
||||
|
||||
if (value is float || value is double || value is decimal ||
|
||||
value is byte || value is sbyte || value is short || value is ushort ||
|
||||
value is int || value is uint || value is long || value is ulong)
|
||||
{
|
||||
return Convert.ToString(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
string fallback = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
return "N'" + fallback.Replace("'", "''") + "'";
|
||||
}
|
||||
|
||||
private static bool IsSensitive(string parameterName)
|
||||
{
|
||||
string name = (parameterName ?? string.Empty).ToLowerInvariant();
|
||||
return name.Contains("contrasena") ||
|
||||
name.Contains("password") ||
|
||||
name.Contains("hash") ||
|
||||
name.Contains("token") ||
|
||||
name.Contains("secreto") ||
|
||||
name.Contains("secret") ||
|
||||
name.Contains("apikey") ||
|
||||
name.Contains("api_key");
|
||||
}
|
||||
|
||||
private static string GetSqlType(DbType dbType)
|
||||
{
|
||||
switch (dbType)
|
||||
{
|
||||
case DbType.Boolean: return "bit";
|
||||
case DbType.Byte: return "tinyint";
|
||||
case DbType.Int16: return "smallint";
|
||||
case DbType.Int32: return "int";
|
||||
case DbType.Int64: return "bigint";
|
||||
case DbType.Decimal: return "decimal(38, 10)";
|
||||
case DbType.Double: return "float";
|
||||
case DbType.Single: return "real";
|
||||
case DbType.Date: return "date";
|
||||
case DbType.DateTime:
|
||||
case DbType.DateTime2: return "datetime2(3)";
|
||||
case DbType.DateTimeOffset: return "datetimeoffset(7)";
|
||||
case DbType.Guid: return "uniqueidentifier";
|
||||
case DbType.Binary: return "varbinary(max)";
|
||||
case DbType.AnsiString: return "varchar(max)";
|
||||
default: return "nvarchar(max)";
|
||||
}
|
||||
}
|
||||
|
||||
private static string QuoteProcedureName(string procedureName)
|
||||
{
|
||||
return string.Join(".", (procedureName ?? string.Empty)
|
||||
.Split('.')
|
||||
.Select(part => "[" + part.Replace("]", "]]" ) + "]"));
|
||||
}
|
||||
|
||||
private sealed class DiagnosticContext
|
||||
{
|
||||
public DiagnosticContext()
|
||||
{
|
||||
Parameters = new List<DiagnosticParameter>();
|
||||
}
|
||||
|
||||
public string ProcedureName { get; set; }
|
||||
public IList<DiagnosticParameter> Parameters { get; private set; }
|
||||
}
|
||||
|
||||
private sealed class DiagnosticParameter
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public object Value { get; set; }
|
||||
public DbType DbType { get; set; }
|
||||
public ParameterDirection Direction { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace SIG.Web.AppCode.Services.Database
|
||||
{
|
||||
public sealed class StoredProcedureException : Exception
|
||||
{
|
||||
public StoredProcedureException(string userMessage, int errorType)
|
||||
: base(userMessage)
|
||||
{
|
||||
UserMessage = userMessage;
|
||||
ErrorType = errorType;
|
||||
}
|
||||
|
||||
public string UserMessage { get; private set; }
|
||||
public int ErrorType { get; private set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user