.NET Core — статические классы, запускаются отовсюду, но по-прежнему нуждаются в DI и Entity Framework.

Позвольте мне начать с нескольких примеров кода, которые очень распространены, очень полезны и МНОГО используются в (веб) приложении:

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).


person CularBytes    schedule 26.06.2019    source источник
comment
Однопользовательские приложения (из которых вы предположительно скопировали этот код) сильно отличаются от ASP.Net/ASP.Net-core… Поскольку вы знаете, что то, что вы делаете, в целом плохая идея, вам, вероятно, придется идти в одиночку в своем путешествии. Я бы порекомендовал получить четкое представление о том, какие объекты относятся к HTTP-запросу (следовательно, они не могут быть кэшированы где-либо вне запроса и доступны только через текущий запрос), а какие не зависят от запроса - это, по-видимому, основная проблема, с которой вы сталкиваетесь.   -  person Alexei Levenkov    schedule 26.06.2019
comment
Просто: не используйте статику. Это может потребовать некоторой доработки, но это перерывы в игре, когда вы меняете фреймворки, и не заблуждайтесь, ASP.NET Core сильно отличается от ASP.NET MVC, несмотря на то, что оба имеют ASP.NET в названии. . Короче говоря, если вы собираетесь использовать DI, вы должны использовать DI постоянно. ASP.NET Core — это инфраструктура с внедрением зависимостей, поэтому либо придерживайтесь MVC, либо делайте все правильно.   -  person Chris Pratt    schedule 26.06.2019
comment
Ваши статические методы являются реализацией антишаблона Ambient Context. Я частично согласен с @ChrisPratt, но я хотел бы провести различие между Изменчивые зависимости и стабильные зависимости. Когда зависимость стабильна (это означает, что нет необходимости когда-либо заменять, имитировать или перехватывать зависимость), использование статики в порядке. Однако ваша зависимость InfrastructureHelper очевидно является нестабильной зависимостью и, следовательно, ее необходимо внедрить.   -  person Steven    schedule 26.06.2019
comment
Спасибо за все ваши комментарии, я согласен с тем, что ASP.NET Core и MVC совершенно разные и поэтому нуждаются в корректировках (причина запроса), но, несмотря на то, что это изменчивая зависимость (которая также имеет свои преимущества для имитации вызовов сеанса/авторизации), Я надеялся найти решение, в котором можно было бы эффективно использовать контейнер для поддержания гибкости статического класса, но при этом делать это в стиле ASP.NET Core.   -  person CularBytes    schedule 27.06.2019