Позвольте мне начать с нескольких примеров кода, которые очень распространены, очень полезны и МНОГО используются в (веб) приложении:
int? contactId = InfrastructureHelper.User.GetContactId()
int[] departmentIds = InfrastructureHelper.GetAuhthorizedDepartmentIds()
bool allowed = InfrastructureHelper.User.IsAllowedInPortal()
InfrastructureHelper.User.GetUserLanguage();
Вот лишь некоторые из наиболее часто используемых методов, которые я вызываю в течение жизненного цикла приложения.
Хотя все это кажется очень крутым и полезным, это создает проблемы при использовании с Entity Framework и в сильно асинхронном веб-приложении. Раньше в .NET Framework и когда все было синхронно (о боже, в те времена) это не было проблемой. Но теперь я начинаю видеть проблемы, и мне нужно решение.
Желание решения:
- Статический, если это возможно, чтобы избежать внедрения класса повсюду
- Он кэширует данные для выполнения (дополнительных) проверок авторизации (отделы, авторизация на основе контроллера поверх авторизации на основе ролей по умолчанию).
- Потокобезопасный, потому что это, конечно, проблема сейчас, здесь я получаю исключение ниже:
System.InvalidOperationException: 'Вторая операция началась в этом контексте до завершения предыдущей операции. Обычно это вызвано тем, что разные потоки используют один и тот же экземпляр DbContext, однако безопасность членов экземпляра не гарантируется. Это также может быть вызвано оцениванием вложенного запроса на клиенте, в этом случае перепишите запрос, избегая вложенных вызовов.
Итак, я понимаю как использовать DI и различные возможности, у меня есть видел другие вопросы, которые задают аналогичный вопрос, но решение работает с тем фактом, что мне нужен внедренный экземпляр Entity Framework.
InfrastructureHelper
содержит кучу статических элементов и 2 важных интерфейса, ICurrentSession
и ICurrentUser
. Эти интерфейсы реализуются по-разному для каждого веб-приложения в рамках решения. Реализации не являются статическими, но содержат статические элементы для кэширования дополнительных данных. ICurrentSession
здесь не проблема, эта реализация просто вводит IHttpContextAccessor
. Но CurrentPortalUser
, который реализует ICurrentUser
, внедряет репозитории, давайте дадим код для чтения:
public class CurrentPortalUser : ICurrentUser
{
private readonly IUserRepository _userRepository;
private readonly IControllerActionRepository _controllerActionRepository;
private readonly IDepartmentRepository _departmentRepository;
private static List<DepartmentModel> _allDepartments = new List<DepartmentModel>();
private static List<RoleModel> _allRoles = new List<RoleModel>();
private static List<ControllerActionModel> _allControllerActionRoles = new List<ControllerActionModel>();
/// <summary>
/// Simpler version of _allControllerActionRoles, Key: actionNamespace, Value: roleIds;
/// </summary>
private static Dictionary<string, string[]> _allControllerActionRoleIds = new Dictionary<string, string[]>();
/// <summary>
/// Key: CountryId, Value: DepartmentId
/// </summary>
private static Dictionary<int, int> _departmentCountryList = new Dictionary<int, int>();
private readonly IHttpContextAccessor _httpContextAccessor;
public CurrentPortalUser(IHttpContextAccessor httpContextAccessor, IUserRepository userRepository, IControllerActionRepository controllerActionRepository, IDepartmentRepository departmentRepository)
{
_httpContextAccessor = httpContextAccessor;
_userRepository = userRepository;
_controllerActionRepository = controllerActionRepository;
_departmentRepository = departmentRepository;
}
public void ReloadDepartmentAndRoleData()
{
_allDepartments = _departmentRepository.GetAll().ToList();
int[] departmentIds = _allDepartments.Select(x => x.DepartmentId).ToArray();
List<DepartmentCountryModel> allDepartmentCountries = _departmentRepository.GetDepartmentOperatingCountries(departmentIds);
_departmentCountryList = allWarehouseCountries.ToDictionary(x => x.CountryId, y => y.DepartmentId);
//load role and controller configuration, basically tells me what role is allowed to execute what controller action within the web app.
_allRoles = _userRepository.GetRoles().ToList();
_allControllerActionRoles = _controllerActionRepository.GetAllWithRoles().ToList();
_allControllerActionRoleIds = _allControllerActionRoles.ToDictionary(x => x.NameSpace.ToLower(), y => y.Roles.Select(z => z.RoleId).ToArray());
}
public List<DepartmentModel> GetAllowedDepartments()
{
if (!_allDepartments.Any())//store in memory, just like we are going to store all roles in memory
{
ReloadDepartmentAndRoleData();
}
if (!_allRoles.Any())
{
//load role and controller configuration
ReloadDepartmentAndRoleData();
}
if (IsInRole(RoleEnum.SuperAdmin))
{
return _allDepartments;
}
List<DepartmentModel> allowedDepartments = new List<DepartmentModel>();
foreach (DepartmentModel department in _allDepartments)
{
if (IsInRole(department.Name))
{
allowedDepartments.Add(department);
}
}
return allowedDepartments;
}
public string GetUserId()
{
if (_httpContextAccessor.HttpContext.User.Identity.IsAuthenticated)
{
return _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.Name)?.Value;
}
return null;
}
public int? GetContactId()
{
int? contactId = null;
if (_httpContextAccessor.HttpContext.User.Identity.IsAuthenticated)
{
contactId = _userRepository.GetContactId(GetUserId());
}
return contactId;
}
}
Это еще не все, в основном это сводится к получению некоторых данных, если они кэшированы, то получить их из кеша, иначе получить их из базы данных и кэшировать их или поместить в сеанс.
Итак, настала часть, где я покажу вам ужасный код, за который я немного смущен :).
public stattic class InfrastructureHelper
{
//..more static members that are heavily used here, but these one are a bit-less depended on a database, as long as data can be obtained from IcurrentUser
public static ICurrentUser User
{
get
{
return _container.GetInstance<ICurrentUser>();
}
}
//yup..that's static...
private static ISimpleContainer _container;
//this method is used via the Startup class, so only once
public static void Initialize(ISimpleContainer container)
{
_container = container;
}
}
//Since I don't have Asp.Net references in the project of InfrastructureHelper, I use a simple interface that of course does the same as the normal container
public interface ISimpleContainer
{
TService GetInstance<TService>() where TService : class;
}
И последнее, но не менее важное: регистрация ICurrentUser
является временной (в рамках класса Startup).
services.AddTransient<ICurrentSession, CurrentPortalSession>();
services.AddTransient<ICurrentUser, CurrentPortalUser>();
Я получаю сообщение об исключении в InfrastructureHelper.User.GetContactId()
, потому что во время или после входа в систему оно используется много раз одновременно. Я мог бы попытаться избежать одновременного выполнения некоторых запросов или построить вокруг этого блокировку, но это просто неправильно.
С нетерпением жду ваших ответов! Извините за это, это очень анти-шаблон, это часть старого кода, который был легко скопирован в новую основную среду asp.net без перепроверки, потому что его удобство использования настолько велико :).
Обновить
Итак, согласно комментариям, я искал способ успешно внедрить InfrastructureHelper
везде. Но когда я попытаюсь внедрить это в каждый репозиторий (сделать его частью BaseRepository
), тогда, конечно, будет обнаружена круговая зависимость: ICurrentUser
зависит от некоторых репозиториев, а каждый репозиторий зависит от InfrastructureHelper
(и, следовательно, ICurrentUser
).
InfrastructureHelper
очевидно является нестабильной зависимостью и, следовательно, ее необходимо внедрить. - person Steven   schedule 26.06.2019