OData / odata.net

ODataLib: Open Data Protocol - .NET Libraries and Frameworks
https://docs.microsoft.com/odata
Other
687 stars 349 forks source link

Injecting ODataMessageWriterSettings cause an error when self-hosted with OWIN #1210

Open maubin-kl opened 6 years ago

maubin-kl commented 6 years ago

In a unit test we self-host our OData using OWIN to run validations on it. This used to work fine until we needed to inject an ODataMessageWriterSettings when doing the MapODataServiceRoute. See the comment in the code below.

config.MapODataServiceRoute(routeName, routePrefix, builder =>
{
    builder.AddService<IEdmModel>(ServiceLifetime.Singleton, sp => model);
    builder.AddService<IODataPathHandler>(ServiceLifetime.Singleton, sp => new DefaultODataPathHandler());
    builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => routingConventions);
    builder.AddService<HttpMessageHandler>(ServiceLifetime.Singleton, sp => odataNullValueMessageHandler);

    // If you comment out the next 3 lines it fixes the issue.
    builder.AddServicePrototype(new ODataMessageWriterSettings
    {
        LibraryCompatibility = ODataLibraryCompatibility.Version6
    });
});

Now when we make a request to retrieve the $metadata the response is 500 internal server error. System.ObjectDisposedException: 'Cannot access a closed Stream.' in mscorlib.

Assemblies affected

Microsoft.AspNet.OData 6.10 Microsoft.OData.Core 7.4.3 Microsoft.OData.Edm 7.4.3

Reproduce steps

I realise there's custom stuff in the reproduction steps. If a standard OData endpoint doesn't trigger the issue I could create a sample project to try and isolate the issue further.

Unit test

[TestMethod]
public async Task RegisterTest()
{
    using (WebApp.Start("http://localhost:12345", InitializeOData))
    {
        using (var client = new HttpClient())
        {
            // The response is an error 500.
            using (var response = await client.GetAsync("http://localhost:12345/odata4/v5/$metadata"))
            {
                using (var reader = XmlReader.Create(response.Content.ReadAsStreamAsync().Result))
                {
                    var model = CsdlReader.Parse(reader);
                    Assert.IsTrue(model.SchemaElements.Any());
                }
            }
        }
    }
}

private void InitializeOData(IAppBuilder appBuilder)
{
    var webApiConfig = new WebApiConfig();

    var config = new HttpConfiguration();
    config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;

    var controllerSelector = new ODataVersionControllerSelector(config);
    config.Services.Replace(typeof(IHttpControllerSelector), controllerSelector);

    var mappingEngineService = new Mock<IMappingEngineService>(MockBehavior.Default);
    mappingEngineService.SetupGet(mes => mes.MappingEngines).Returns(new Dictionary<string, IMappingEngine>());
    config.Properties.TryAdd(typeof(IMappingEngineService).FullName, mappingEngineService.Object);

    webApiConfig.Register(config);
    config.EnsureInitialized();

    appBuilder.UseWebApi(config);
}

MapODataServiceRoute setup

config.MapODataServiceRoute(routeName, routePrefix, builder =>
{
    builder.AddService<IEdmModel>(ServiceLifetime.Singleton, sp => model);
    builder.AddService<IODataPathHandler>(ServiceLifetime.Singleton, sp => new DefaultODataPathHandler());
    builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => routingConventions);
    builder.AddService<HttpMessageHandler>(ServiceLifetime.Singleton, sp => odataNullValueMessageHandler);

    // If you comment out the next 3 lines it fix the issue.
    builder.AddServicePrototype(new ODataMessageWriterSettings
    {
        LibraryCompatibility = ODataLibraryCompatibility.Latest
    });
});

Expected result

The server respond with a 200 OK and the content of the request.

Actual result

An exception occurs on the server. System.ObjectDisposedException: 'Cannot access a closed Stream.' in mscorlib.

tloten commented 6 years ago

If you change it to be:

builder.AddServicePrototype(new ODataMessageWriterSettings {
    EnableMessageStreamDisposal = false,
    LibraryCompatibility = ODataLibraryCompatibility.Version6
})

it should resolve your issue. This is because the default WebAPI OData settings (set up during the call to MapODataServiceRoute) will set EnableMessageStreamDisposal to false - but the default value when you construct your custom ODataMessageWriterSettings is true.

I suppose a more resilient way would be to try and clone those default settings and apply your changes on top, but I haven't looked into that myself.

maubin-kl commented 6 years ago

@tloten Thank it solved my issue. It's a bit weird that it default to True in the constructor but is set to False when you call the MapODataServiceRoute... Wouldn't have found it without you.

For my own curiosity, are you able to provide an explanation as to why it works when I navigate to the address using my browser, but in my unit test setup with OWIN it fails?

tloten commented 6 years ago

@maubin-kl Hmm, not 100% sure on that. I stumbled across this github issue because I was getting an exception in some logging middleware (which logged request/response bodies by reading the stream) after overriding ODataMessageWriterSettings. But without the logging middleware things worked fine - so I'd guess in your test setup something is doing a bit of a shortcut and not actually going out via the full networking stack (wheras the request from the browser would be).