Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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
like image 794
Zout Avatar asked Jul 09 '15 13:07

Zout


1 Answers

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;
}
like image 152
Zout Avatar answered Oct 25 '22 08:10

Zout