I have an ASP.NET Core API that I have written custom middleware for so that I can handle exceptions and write logs in a single spot. The middleware works as required when debugging via Kestrel and submitting a request from a browser or postman however in my test the response body is always a null stream.
Below is the middleware class and the test that I have written, the context.Response.WriteAsync(result) doesn't seem to flush the stream for some reason but I don't know why. Is anyone able to explain?
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Microsoft.Extensions.Logging;
using System.IO;
namespace APP.API.Middleware
{
public class ExceptionHandler
{
private readonly RequestDelegate request;
private readonly ILogger logger;
public ExceptionHandler(RequestDelegate request, ILogger<ExceptionHandler> logger)
{
this.request = request;
this.logger = logger;
}
public async Task Invoke(HttpContext context)
{
try
{
await request(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private Task HandleExceptionAsync(HttpContext context, Exception ex)
{
HttpStatusCode statusCode = HttpStatusCode.InternalServerError;
logger.LogError(ex, "Fatal exception");
var result = JsonConvert.SerializeObject(new { error = ex.Message });
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)statusCode;
return context.Response.WriteAsync(result);
}
}
}
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace APP.Testing.Middleware
{
[TestClass]
public class ExceptionHandler
{
[TestMethod]
public void HandleException()
{
var exceptionHandler = new API.Middleware.ExceptionHandler((innerHttpContext) =>
{
throw new System.Exception("Test exception");
}, new NullLogger<API.Middleware.ExceptionHandler>());
var context = new DefaultHttpContext();
exceptionHandler.Invoke(context).Wait();
context.Response.Body.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(context.Response.Body);
var text = reader.ReadToEnd();
}
}
}
Middleware is software that's assembled into an app pipeline to handle requests and responses. Each component: Chooses whether to pass the request to the next component in the pipeline. Can perform work before and after the next component in the pipeline.
Search for word "middleware" in the top right search box as shown below. Select Middleware Class item and give it a name and click on Add button. This will add a new class for the middleware with extension method as shown below.
Welcome to Stack Overflow!
Your response body is empty, because you are writing to a NullStream
(not to be confused with null
value).
"A Stream with no backing store. Use Null to redirect output to a stream that will not consume any operating system resources. When the methods of Stream that provide writing are invoked on Null, the call simply returns, and no data is written. Null also implements a Read method that returns zero without reading data." - Docs
Default value of Body
property of HttpResponse
is precisely
the NullStream
. In a real scenario when an HTTP request arrives, the NullStream
is replaced with HttpResponseStream
. You won't be able to use it on your own as its accessibility level is set to internal
.
As unit testing is only simulating real scenario, you can just replace the NullStream
with any type of stream you want, for example the MemoryStream
:
var exceptionHandler = new ExceptionHandler((innerHttpContext) =>
{
throw new Exception("Test exception");
}, new NullLogger<ExceptionHandler>());
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream(); // <== Replace the NullStream
await exceptionHandler.Invoke(context);
context.Response.Body.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(context.Response.Body);
var text = reader.ReadToEnd();
Don't forget to add some asserts at the end of your unit tests. After all, you want to perform some checks, right?
Assert.IsFalse(string.IsNullOrEmpty(text));
As @nkosi pointed out, unless you have a really good reason, you should always call asynchronous methods with await
keyword:
await exceptionHandler.Invoke(context);
and mark the method definition with async
and make it return a Task
:
public async Task HandleException()
That way you are avoiding deadlocks.
Something also worth pointing out (but not a necessity) is a naming convention for testing classes. Obviously, you can name it how you like, but keep it mind that when your testing class have the same name as the class you want to test, you end up with unnecessary name ambiguity. Of course you can write full name with namespace (as you did), but with my lazy nature, that's just too much so I'm using different name for testing class, for example ExceptionHandlerTests
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With