Мне необходимо выполнить олицетворение SQL Server в существующем проекте EF Core. Я получил это работает (вроде). В настоящее время всякий раз, когда осуществляется доступ к любому свойству в DbContext
, происходит явный вызов функции, которая проверяет текущее состояние соединения контекста и пытается подключиться при необходимости.
Моя идея состояла в том, чтобы всякий раз, когда соединение открывалось с помощью этого метода, я возвращал любое олицетворение на месте (я полагаю, поскольку соединение является общим, это, вероятно, будет необходимо?). Для этого я просто отправляю DbCommand
с REVERT
в качестве текста команды. Это само по себе работает нормально.
После REVERT
я проверяю, требуется ли олицетворение для текущего запроса. Если это так, я запускаю EXECUTE AS USER = @disguise
. И это тоже работает, вроде как.
В том же запросе нет проблем, и кажется, что дальнейшие запросы действительно используют олицетворенного пользователя. Однако в большинстве случаев (не всегда?) При следующем запросе самого первого запроса (кажется, не имеет значения, что это за запрос) я получаю следующую ошибку:
Невозможно продолжить выполнение, поскольку сеанс находится в состоянии уничтожения.
Произошла серьезная ошибка в текущей команде. Результаты, если таковые имеются, должны быть отброшены.
Когда я изменяю порядок, хотя сначала выполняю оператор EXECUTE AS...
перед REVERT
, ошибки вообще не возникает (хотя, конечно, ни одно утверждение фактически не использует олицетворенный контекст, который мне нужен). Так что я не думаю, что просто подражание - это проблема сама по себе. Единственное различие, о котором я могу подумать, состоит в том, что в случае, когда это ломается, я позволяю EF делать все свои собственные фоновые вещи в олицетворенном контексте.
Кто-нибудь понимает, почему возникает эта ошибка? Мое лучшее предположение, что делает EF, когда освобождает соединение обратно в пул соединений или восстанавливает соединение из пула соединений, не очень хорошо с олицетворенным контекстом?
Более конкретно, я ищу способ, чтобы EF хорошо играл с подражанием или еще какие-то шаги по устранению неполадок, которые я мог бы предпринять, чтобы продолжить исследование.
РЕДАКТИРОВАТЬ: добавив мой DbContext
класс здесь для справки
namespace CM.App.Models
{
public class AppDataContext : DbContext
{
private DataContextUser dataContextUser;
private bool impersonationSet = false;
public bool HasAdminAccess() => Execute("SELECT IS_ROLEMEMBER('CM_Admin')", (row) => row.GetInt32(0) == 1).Single();
private DbSet<Report> reports;
public DbSet<Report> Reports
{
get
{
OpenConnection();
return reports;
}
set
{
this.reports = value;
}
}
private DbSet<Action> actions;
public DbSet<Action> Actions
{
get
{
OpenConnection();
return actions;
}
set
{
this.actions = value;
}
}
private DbSet<UserSettings> userSettings;
public DbSet<UserSettings> UserSettings
{
get
{
OpenConnection();
return userSettings;
}
set
{
this.userSettings = value;
}
}
private DbSet<UserStaticReportConfiguration> userStaticReportConfigurations;
public DbSet<UserStaticReportConfiguration> UserStaticReportConfigurations
{
get
{
OpenConnection();
return userStaticReportConfigurations;
}
set
{
this.userStaticReportConfigurations = value;
}
}
public AppDataContext() { }
public AppDataContext(DbContextOptions<AppDataContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserStaticReportConfiguration>()
.HasIndex(usrc => new { usrc.DbUserName, usrc.Key }).IsUnique();
}
public void SetDataContextUser(DataContextUser dataContextUser)
{
this.dataContextUser = dataContextUser;
impersonationSet = false;
OpenConnection();
}
private DataContextException getDataContextException(SqlException sqlException)
{
string message = string.Empty;
foreach (object objErr in sqlException.Errors)
{
SqlError err = objErr as SqlError;
if (message.Length > 0)
message += "\n";
message += err.Message;
}
if (string.IsNullOrEmpty(message))
message = sqlException.Message;
return DataContextException.GetDataContextException(message);
}
public async Task OpenConnectionAsync()
{
switch (Database.GetDbConnection().State)
{
case ConnectionState.Closed:
case ConnectionState.Broken:
case ConnectionState.Connecting:
impersonationSet = false;
try
{
await Database.OpenConnectionAsync();
}
catch (SqlException sqlException)
{
throw getDataContextException(sqlException);
}
break;
default:
return;
}
if (!impersonationSet && dataContextUser != null && dataContextUser.IsFacade)
{
impersonationSet = true;
await ExecuteAsync("EXECUTE AS USER = @disguise", new Dictionary<string, object> { { "@disguise", dataContextUser.DbUsername } });
}
return;
}
public void OpenConnection() =>
OpenConnectionAsync().Wait();
public void CloseConnection() =>
Database.CloseConnection();
public async Task<int> ExecuteAsync(string commandText, Dictionary<string, object> parameters = null, Action<DbCommand> prep = null)
{
if (commandText == null)
throw new ArgumentNullException(nameof(commandText));
using (DbCommand cmd = Database.GetDbConnection().CreateCommand())
{
cmd.CommandText = commandText;
if (parameters != null)
{
foreach (KeyValuePair<string, object> kvp in parameters)
{
DbParameter param = cmd.CreateParameter();
param.ParameterName = kvp.Key;
param.Value = kvp.Value ?? DBNull.Value;
cmd.Parameters.Add(param);
}
}
if (prep != null)
prep(cmd);
await OpenConnectionAsync();
try
{
return await cmd.ExecuteNonQueryAsync();
}
catch (SqlException sqlException)
{
throw getDataContextException(sqlException);
}
}
}
public async Task<List<T>> ExecuteAsync<T>(string commandText, Func<DbDataReader, T> rowReader, Dictionary<string, object> parameters = null, Action<DbCommand> prep = null)
{
if (commandText == null)
throw new ArgumentNullException(nameof(commandText));
if (rowReader == null)
throw new ArgumentNullException(nameof(rowReader));
List<T> ret = null;
using (DbCommand cmd = Database.GetDbConnection().CreateCommand())
{
cmd.CommandText = commandText;
if (parameters != null)
{
foreach (KeyValuePair<string, object> kvp in parameters)
{
DbParameter param = cmd.CreateParameter();
param.ParameterName = kvp.Key;
param.Value = kvp.Value ?? DBNull.Value;
cmd.Parameters.Add(param);
}
}
if (prep != null)
prep(cmd);
await OpenConnectionAsync();
try
{
using (DbDataReader reader = await cmd.ExecuteReaderAsync())
{
ret = new List<T>();
while (reader.Read())
{
ret.Add(rowReader(reader));
}
}
}
catch (SqlException sqlException)
{
throw getDataContextException(sqlException);
}
}
return ret;
}
public int Execute(string commandText, Dictionary<string, object> parameters = null, Action<DbCommand> prep = null)
{
try
{
return ExecuteAsync(commandText, parameters, prep: prep).Result;
}
catch (AggregateException ex)
{
if (ex.InnerExceptions.FirstOrDefault() is DataContextException && !ex.InnerExceptions.Skip(1).Any())
throw ex.InnerExceptions.Single();
throw ex;
}
}
public List<T> Execute<T>(string commandText, Func<DbDataReader, T> rowReader, Dictionary<string, object> parameters = null, Action<DbCommand> prep = null)
{
try
{
return ExecuteAsync(commandText, rowReader, parameters, prep).Result;
}
catch (AggregateException ex)
{
if (ex.InnerExceptions.FirstOrDefault() is DataContextException && !ex.InnerExceptions.Skip(1).Any())
throw ex.InnerExceptions.Single();
throw ex;
}
}
}
}
РЕДАКТИРОВАТЬ 2: дополнительный контекст
У наших пользователей есть имя пользователя / пароль для входа в веб-службу, которая затем использует имя пользователя / пароль в качестве фактических учетных данных подключения к базе данных сервера. Технические пользователи могут напрямую подключаться к базе данных, если хотят, но сервер обеспечивает более нетехническое дружественное представление данных. Олицетворение используется для администраторов, которые должны помочь пользователям понять, почему они видят данные, которые они видят. Имеется RLS, который ограничивает то, какие данные видны каждому пользователю, поэтому мы чувствовали, что олицетворение на уровне SQL Server будет самым верным представлением того, что будет видеть другой пользователь.