Search code examples
c#asynchronousowinmediatypeformatter

Custom async MediaTypeFormatter returning Cannot access a closed Stream


I'm experiencing strange behavior with a custom MediaTypeFormatter, but only with an Owin Self-hosted WebApi.

With a standard .NET WebApi project hosted in IIS, the same formatter works fine.

Stepping through the program, CanWriteType in the Formatter is getting called multiple times. The exception logger is firing with an exception of Cannot access a closed stream.

Any insight would be very helpful! Removing the async nature of this formatter results in it working fine so It's mostly likely some weird threading issue. I'd like to stay with an asynchronous formatter and not use the BufferedMediaTypeFormatter if at all possible.

Stack Trace:

at System.IO.__Error.StreamIsClosed()
   at System.IO.MemoryStream.Seek(Int64 offset, SeekOrigin loc)
   at System.Net.Http.HttpContent.<>c__DisplayClass21_0.<LoadIntoBufferAsync>b__0(Task copyTask)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Owin.HttpMessageHandlerAdapter.<BufferResponseContentAsync>d__13.MoveNext()

Startup.cs:

namespace ConsoleApplication1
{

    public class TraceExceptionLogger : ExceptionLogger
    {
        public override void Log(ExceptionLoggerContext context)
        {
           Trace.TraceError(context.ExceptionContext.Exception.ToString());
        }
    }

    public class Startup
    {
        // This code configures Web API. The Startup class is specified as a type
        // parameter in the WebApp.Start method.
        public void Configuration(IAppBuilder appBuilder)
        {
            // Configure Web API for self-host. 
            HttpConfiguration config = new HttpConfiguration();

            // Add in a simple exception tracer so we can see what is causing the 500 Internal Server Error
            config.Services.Add(typeof(IExceptionLogger), new TraceExceptionLogger());

            // Add in the custom formatter
            config.Formatters.Add(new JsonFhirAsyncFormatter());

            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            config.Formatters.Add(new JsonFhirAsyncFormatter());

            appBuilder.UseWebApi(config);
        }
    }
}

Formatter:

namespace ConsoleApplication1
{
    public class JsonFhirAsyncFormatter : MediaTypeFormatter
    {
        public JsonFhirAsyncFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json+fhir"));
        }

        public override bool CanWriteType(Type type)
        {
            return true;
        }

        public override bool CanReadType(Type type)
        {
            return false;
        }

        public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
        {
            using (StreamWriter streamwriter = new StreamWriter(writeStream))
            {
                await streamwriter.WriteAsync("{\"test\":\"test\"}");
            }
        }
    }
}

Controller:

namespace ConsoleApplication1.Controllers
{
    [RoutePrefix("test")]
    public class TestController : ApiController
    {
        [HttpGet]
        [Route("")]
        public async Task<string> Test()
        {
            // Eventually this will using an await method
            return "test";
        }
    }
}

Program.cs:

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            WebApp.Start<Startup>("http://localhost:9000");
            Console.ReadLine();
        }
    }
}

Solution

  • Maybe you could try this:

    public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
        {
            using (StreamWriter streamwriter = new StreamWriter(writeStream))
            {
                return streamwriter.WriteAsync("{\"test\":\"test\"}");
            }
        }
    

    This is then not activating the Async stuff, and allows the Async to occur when needed.

    However in my server (sqlonfhir) this function is implemented as

     public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
        {
            StreamWriter writer = new StreamWriter(writeStream);
            JsonWriter jsonwriter = new JsonTextWriter(writer);
            if (type == typeof(OperationOutcome))
            {
                Resource resource = (Resource)value;
                FhirSerializer.SerializeResource(resource, jsonwriter);
            }
            else if (typeof(Resource).IsAssignableFrom(type))
            {
                if (value != null)
                {
                    Resource r = value as Resource;
                    SummaryType st = SummaryType.False;
                    if (r.UserData.ContainsKey("summary-type"))
                        st = (SummaryType)r.UserData["summary-type"];
                    FhirSerializer.SerializeResource(r, jsonwriter, st);
                }
            }
            writer.Flush();
            return Task.FromResult<object>(null); // Task.CompletedTask // When we update to .net 4.6;
        }
    

    (And yes, I do my unit testing using OWIN self testing with this code too)