Нулевой пользователь в HttpContext, полученный из StructureMap

Хорошо, в моем предыдущем вопросе/настройке было слишком много переменных, поэтому я разбираю его до базовых компонентов.

Учитывая приведенный ниже код с использованием StructureMap3...

//IoC setup
For<HttpContextBase>().UseSpecial(x => x.ConstructedBy(y => HttpContext.Current != null ? new HttpContextWrapper(HttpContext.Current) : null ));
For<ICurrentUser>().Use<CurrentUser>();

//Classes used
public class CurrentUser : ICurrentUser
{
    public CurrentUser(HttpContextBase httpContext)
    {
        if (httpContext == null) return;
        if (httpContext.User == null) return;
        var user = httpContext.User;
        if (!user.Identity.IsAuthenticated) return;
        UserId = httpContext.User.GetIdentityId().GetValueOrDefault();
        UserName = httpContext.User.Identity.Name;
    }

    public Guid UserId { get; set; }
    public string UserName { get; set; }
}

public static class ClaimsExtensionMethods
    public static Guid? GetIdentityId(this IPrincipal principal)
    {
        //Account for possible nulls
        var claimsPrincipal = principal as ClaimsPrincipal;
        if (claimsPrincipal == null)
            return null;
        var claimsIdentity = claimsPrincipal.Identity as ClaimsIdentity;
        if (claimsIdentity == null)
            return null;
        var claim = claimsIdentity.FindFirst(x => x.Type == ClaimTypes.NameIdentifier);
        if (claim == null)
            return null;

        //Account for possible invalid value since claim values are strings
        Guid? id = null;
        try
        {
            id = Guid.Parse(claim.Value);
        }
        catch (ArgumentNullException) { }
        catch (FormatException) { }
        return id;
    }
}

Как это возможно в окне Watch?

введите здесь описание изображения


У меня есть веб-приложение, которое я обновляю до использования StructureMap 3.x из 2.x, но я получаю странное поведение при определенных зависимостях.

У меня есть ISecurityService, который я использую для проверки некоторых вещей, когда пользователь запрашивает страницу. Эта служба зависит от небольшого интерфейса, который я назвал ICurrentUser. Реализация класса довольно проста, на самом деле это может быть структура.

public interface ICurrentUser
{
    Guid UserId { get; }
    string UserName { get; }
}

Это достигается путем внедрения зависимостей с использованием приведенного ниже кода.

For<ICurrentUser>().Use(ctx => getCurrentUser(ctx.GetInstance<HttpContextBase>()));
For<HttpContextBase>().Use(() => getHttpContext());

private HttpContextBase getHttpContext()
{
    return new HttpContextWrapper(HttpContext.Current);
}

private ICurrentUser getCurrentUser(HttpContextBase httpContext)
{
    if (httpContext == null) return null;
    if (httpContext.User == null) return null; // <---
    var user = httpContext.User;
    if (!user.Identity.IsAuthenticated) return null;
    var personId = user.GetIdentityId().GetValueOrDefault();
    return new CurrentUser(personId, ClaimsPrincipal.Current.Identity.Name);
}

Когда приходит запрос, сначала происходит проверка подлинности на всем сайте, что зависит от ISecurityService. Это происходит внутри OWIN и, по-видимому, происходит до того, как HttpContext.User будет заполнено, поэтому оно равно нулю, пусть будет так.

Позже у меня есть ActionFilter, который проверяет с помощью ISecurityService, согласен ли текущий пользователь с текущей версией Условия использования для сайта, если нет, он перенаправляется на страницу, чтобы сначала согласиться с ними.

Все это отлично работало в структуре карты 2.x. Для перехода на StructureMap3 я установил пакет Nuget StructureMap.MVC5, чтобы ускорить процесс.

Когда мой код попадает в строку моего ActionFilter для проверки условий использования, у меня есть это.

var securityService = DependencyResolver.Current.GetService<ISecurityService>();
agreed = securityService.CheckLoginAgreedToTermsOfUse();

Внутри CheckLoginAgreedToTermsOfUse() мой экземпляр CurrentUser равен нулю. Даже если бы это удалось, и моя точка останова внутри getCurrentUser() никогда не срабатывала. Это почти так, как если бы это было предрешено, поскольку в прошлый раз оно было нулевым, хотя на этот раз оно было бы решено.

Я немного озадачен тем, почему getCurrentUser() никогда не вызывается при запросе ISecurityService. Я даже пытался явно прикрепить .LifecycleIs<UniquePerRequestLifecycle>() к моему подключению для обработки ICurrentUser безрезультатно.

ОБНОВЛЕНИЕ: Итак, просто на заметку, я начал использовать метод, принятый ниже, и, хотя он до сих пор отлично работал, он не решил мою основную проблему. Оказывается, новый StructureMap.MVC5, основанный на StructureMap3, использует NestedContainers. Которые ограничивают свои запросы временем существования NestedContainer, независимо от значения по умолчанию Transient. Поэтому, когда я запросил HttpContextBase в первый раз, он вернет тот же самый экземпляр для остальной части запроса (хотя позже в течение срока действия запроса контекст изменился. Вам нужно либо не использовать NestedContainer (что, как я понимать, что это усложнит работу ASP.NET vNext), или вы явно задали жизненный цикл сопоставления For<>().Use<>(), чтобы предоставить вам новый экземпляр для каждого запроса. Обратите внимание, что эта область видимости для NestedContainer также вызывает проблемы с контроллерами в MVC. В то время как пакет StructureMap.MVC5 обрабатывает это с ControllerConvention, он не обрабатывает представления, а рекурсивные представления или представления, используемые несколько раз, вероятно, также вызовут у вас проблемы. Я все еще ищу постоянное решение проблемы с представлениями, на данный момент я вернулся к DefaultContainer.


person Nick Albrecht    schedule 21.11.2014    source источник
comment
Не могли бы вы опубликовать код для реализации ISecurityService?   -  person NightOwl888    schedule 21.11.2014
comment
У меня было слишком много переменных, которые уводили возможную помощь по ложному следу. Я упростил вопрос до проблемы с голыми костями.   -  person Nick Albrecht    schedule 22.11.2014


Ответы (1)


Я не работал с OWIN, но при размещении в интегрированном режиме IIS HttpContext не заполняется до тех пор, пока не завершится событие HttpApplication.Start. С точки зрения DI это означает, что вы не можете полагаться на использование свойств HttpContext в любом конструкторе.

Это имеет смысл, если подумать, потому что приложение должно быть инициализировано вне любого контекста отдельного пользователя.

Чтобы обойти это, вы можете внедрить абстрактную фабрику в свою реализацию ICurrentUser и использовать шаблон Singleton для доступа к ней, что гарантирует, что HttpContext не будет доступен, пока он не будет заполнен.

public interface IHttpContextFactory
{
    HttpContextBase Create();
}

public class HttpContextFactory
    : IHttpContextFactory
{
    public virtual HttpContextBase Create()
    {
        return new HttpContextWrapper(HttpContext.Current);
    }
}

public class CurrentUser // : ICurrentUser
{
    public CurrentUser(IHttpContextFactory httpContextFactory)
    {
        // Using a guard clause ensures that if the DI container fails
        // to provide the dependency you will get an exception
        if (httpContextFactory == null) throw new ArgumentNullException("httpContextFactory");

        this.httpContextFactory = httpContextFactory;
    }

    // Using a readonly variable ensures the value can only be set in the constructor
    private readonly IHttpContextFactory httpContextFactory;
    private HttpContextBase httpContext = null;
    private Guid userId = Guid.Empty;
    private string userName = null;

    // Singleton pattern to access HTTP context at the right time
    private HttpContextBase HttpContext
    {
        get
        {
            if (this.httpContext == null)
            {
                this.httpContext = this.httpContextFactory.Create();
            }
            return this.httpContext;
        }
    }

    public Guid UserId
    {
        get
        {
            var user = this.HttpContext.User;
            if (this.userId == Guid.Empty && user != null && user.Identity.IsAuthenticated)
            {
                this.userId = user.GetIdentityId().GetValueOrDefault();
            }
            return this.userId;
        }
        set { this.userId = value; }
    }

    public string UserName
    {
        get
        {
            var user = this.HttpContext.User;
            if (this.userName == null && user != null && user.Identity.IsAuthenticated)
            {
                this.userName = user.Identity.Name;
            }
            return this.userName;
        }
        set { this.userName = value; }
    }
}

Лично я бы сделал свойства UserId и UserName доступными только для чтения, что упростило бы дизайн и гарантировало, что они не будут перехвачены где-либо еще в приложении. Я бы также создал службу IClaimsIdentityRetriever, которая внедряется в конструктор ICurrentUser вместо получения идентификатора утверждения в методе расширения. Методы расширения идут вразрез с сутью DI и, как правило, полезны только для задач, которые гарантированно не имеют никаких зависимостей (таких как манипуляции со строками или последовательностями). Слабая связанность создания службы также означает, что вы можете легко заменить или расширить реализацию.

Конечно, это подразумевает, что вы также не можете вызывать свойства UserId или UserName вашего класса CurrentUser ни в каком конструкторе. Если какой-либо другой класс зависит от ICurrentUser, вам также может понадобиться ICurrentUserFactory для его безопасного использования.

Абстрактная фабрика спасает при работе со сложными для внедрения зависимостями и решает множество проблем, включая эту.

person NightOwl888    schedule 22.11.2014
comment
Интересно, что я не понял этого в приложениях с интегрированным режимом. Я до сих пор не уверен, почему StructureMap ведет себя так, как в моем примере, но предложенное вами соглашение об использовании комбинации factory с одноэлементным httpcontext и свойствами только для чтения похоже, что это решит мою проблему. Также имеет смысл, учитывая несколько других частей моего приложения и то, как они ведут себя, использовать тот же шаблон в другом месте. Фантастический ответ на то, что я искал. Спасибо :-) - person Nick Albrecht; 24.11.2014