StructureMap causes Stack Empty exception in Web API Help Pages ModelDescriptionLink.cshtml
Asked Answered
D

1

7

I have a Web API project which uses StructureMap for its DI. It's been working fine for awhile, but I'm having some issues with the Web API help pages (Microsoft.AspNet.WebApi.HelpPage) where InvalidOperationExceptions are being thrown as a result of an empty stack.

I created a new Web API project with the help pages, and it works fine until I add the StructureMap.WebApi2 package, whereas the previously mentioned exception is being thrown here, inside ModelDescriptionLink.cshtml

else if (modelDescription is CollectionModelDescription)
{
    var collectionDescription = modelDescription as CollectionModelDescription;
    var elementDescription = collectionDescription.ElementDescription;
    @:Collection of @Html.DisplayFor(m => elementDescription.ModelType, "ModelDescriptionLink", new { modelDescription = elementDescription })
}
else
{
    @Html.DisplayFor(m => modelDescription)
}

It's being thrown at @:Collection of @Html.DisplayFor(m => elementDescription.ModelType, "ModelDescriptionLink", new { modelDescription = elementDescription }) when it tries to display the link to the resource description for his this model.

This is a slimmed down route that still causes the exception:

[Route("Test")]
public IHttpActionResult Post([FromBody] IEnumerable<MySimpleModel> models)
{
    return null;
}

Attempting to visit the documentation for this route at http://localhost:21966/Help/Api/POST-Test causes the exception:

Exception image

I was only able to find one example of someone having the same problem and their solutions were to switch from StructureMap to Ninject or to avoid the exception with null checks.

Here's the top of the stack trace:

[InvalidOperationException: Stack empty.]
System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource resource) +52
System.Collections.Generic.Stack`1.Peek() +6693321
System.Web.WebPages.WebPageBase.get_Output() +51
System.Web.WebPages.WebPageBase.GetOutputWriter() +35
System.Web.WebPages.WebPageExecutingBase.BeginContext(String virtualPath, Int32 startPosition, Int32 length, Boolean isLiteral) +50
ASP._Page_Areas_HelpPage_Views_Help_DisplayTemplates_ModelDescriptionLink_cshtml.Execute() in c:...ModelDescriptionLink.cshtml:28
System.Web.WebPages.WebPageBase.ExecutePageHierarchy() +271
System.Web.Mvc.WebViewPage.ExecutePageHierarchy() +122
System.Web.WebPages.WebPageBase.ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage) +145
System.Web.Mvc.RazorView.RenderView(ViewContext viewContext, TextWriter writer, Object instance) +695
System.Web.Mvc.BuildManagerCompiledView.Render(ViewContext viewContext, TextWriter writer) +382
System.Web.Mvc.Html.ActionCacheViewItem.Execute(HtmlHelper html, ViewDataDictionary viewData) +278

By catching the exception in this place, it pops up later on in HelpPageApiModel.cshtml on a nearly identical line: @Html.DisplayFor(m => m.ResourceDescription.ModelType, "ModelDescriptionLink", new { modelDescription = Model.ResourceDescription }). This is the top of that stack trace:

[InvalidOperationException: Stack empty.]
System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource resource) +52
System.Collections.Generic.Stack`1.Pop() +6667365
System.Web.WebPages.WebPageBase.PopContext() +66
System.Web.WebPages.WebPageBase.ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage) +154
Diesel answered 9/7, 2015 at 13:10 Comment(0)
D
1

Still not sure why StructureMap.WebApi2 is having this problem, but my solution was basically to rewrite the dependency resolution without that library. This blog post was quite helpful as to a way to implement it without using IDependencyScope

Edit: I was asked to show an example. It's basically the solution posted on that blog, but I hope it will be helpful anyway.

Add a custom controller activator for structuremap:

public class StructureMapWebApiControllerActivator : IHttpControllerActivator
{
    private readonly IContainer _container;

    public StructureMapWebApiControllerActivator(IContainer container)
    {
        _container = container;
    }

    public IHttpController Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)
    {
        var nested = _container.GetNestedContainer();
        var instance = nested.GetInstance(controllerType) as IHttpController;
        request.RegisterForDispose(nested);
        return instance;
    }
}

Here's what my container initialization looks like:

public class IoC
{
    public static IContainer InitializeContainer()
    {
        IContainer container = new Container
        (
            c => c.Scan
            (
                scan =>
                {
                    scan.Assembly("assembly1");
                    scan.Assembly("assembly2");
                    scan.LookForRegistries();
                    scan.WithDefaultConventions();
                }
            )
        );
        return container;
    }
}

and then wherever you're setting up your HttpConfiguration (for such as calling MapHttpAttributeRoutes()), replace the controller activator with your new structure map one:

HttpConfig.MapHttpAttributeRoutes();
var container = IoC.InitializeContainer();
HttpConfig.Services.Replace(typeof(IHttpControllerActivator), new StructureMapWebApiControllerActivator(container));

In my case I'm using OWIN so I'm not using the GlobalConfiguration.Configuration HttpConfiguration, but the idea is the same. I believe there is normally a WebApiConfig class which has a method that's passed the HttpConfiguration, so you could do the replacement there.

Edit again for changed parts of HelpController.cs:

Originally StructureMap was trying to use the second constructor (which had an argument of HttpConfiguration), causing it to be unable to create an instance of the HelpController. I fixed it by marking the second constructor as protected.

I also changed the reference from GlobalConfiguration's HttpConfiguration to my own (from my Startup class) in the first constructor's call because I'm using Owin.

public HelpController(): this(Startup.HttpConfig)
{
    logger.Trace("Help controller created");
}

protected HelpController(HttpConfiguration config)
{
    Configuration = config;
}
Diesel answered 9/7, 2015 at 17:30 Comment(8)
Can you (or someone) post a working solution here? I tried the referenced blog post without success.Uther
@Uther Sure, I just added some code showing how I got it workingDiesel
thanks! I have pretty much the exact thing in my solution but still am seeing the Stack empty. exception in ModelDescriptionLink.cshtml. :(Uther
Did you totally remove the StructureMap.WebApi2 package? Maybe try creating a new Web API project with the MVC help pages, and try using StructureMap with the method I described to help distinguish where the problem liesDiesel
did you change HelpController at all? Did you add HttpConfiguration to StructureMap somewhere?Uther
I did. Pretty minor changes and I don't think they affected this particular issue, but I'll add to my answer with the changed bits of the HelpController anywayDiesel
I think my issue is related to also have StructureMap.Mvc installed - removing that seems to fix things.Uther
@Diesel So I assume you're not using the StructureMap.MVC5 package either, is that right?Officiary

© 2022 - 2024 — McMap. All rights reserved.