Позвольте мне начать с некоторых примеров кода, которые являются очень распространенными, очень полезными и используются во многих (веб) приложениях:
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
является временной (в классе запуска)
services.AddTransient<ICurrentSession, CurrentPortalSession>();
services.AddTransient<ICurrentUser, CurrentPortalUser>();
Я получаю сообщение об исключении на InfrastructureHelper.User.GetContactId()
, поскольку во время или после входа в систему оно используетсямного одновременно.Я мог бы попытаться избежать одновременного выполнения некоторых запросов или создать блокировку вокруг этого, но это не кажется правильным.
Ждем ваших ответов!Извините, это очень много анти-паттернов, это кусок старого кода, который был легко скопирован в новую базовую среду asp.net без перепроверки, потому что он прост в использовании:).
Обновление
Так что, согласно комментариям, я искал способ успешно внедрить InfrastructureHelper
везде.Но когда я пытаюсь внедрить это в каждый репозиторий (сделать его частью BaseRepository
), тогда, конечно, будет обнаружена циклическая зависимость: ICurrentUser
зависит от некоторых репозиториев, и каждый репозиторий зависит от InfrastructureHelper
(и, таким образом, ICurrentUser
).