Understanding ASP.Net -Part3- Building Reusable and Configurable Middlewares
Introduction
Hello and welcome to part3 of Understanding ASP.Net with
Owin and Katana series, I this series we are learning about new owin and katana
features in ASP.Net 4 and above. So, if you are new to OWIN then please go back
and check other parts as well where we discussed all the details about what
OWIN, what benefits it provides and why we should use it.
- Understanding
ASP.Net - Part1- Owin and Katana Introduction
- Understanding
ASP.Net -Part2- Building an Owin Pipeline
In this part, we will create a reusable OWIN middleware that
we can configure and use in different projects and even in a different OWIN
implementation other than Project Katana.
Creating Middleware
We will refactor our delegate based debug middleware that we
created in the last part and turn it into a reusable and configurable
middleware by creating a middleware class. Now create a folder named “Middlewares”
where we’ll put our middleware classes.
With this in place create a class with name DebugMiddlware
in Middlewares folder.
Before we turn this class into and OWIN middleware I just
want to mention that if you look around on the internet about how to implement
a katana base OWIN middleware with a reusable pattern then you’ll get to see a
lot of peoples will show you doing this by creating a class inheriting from
base class OwinMiddleware. OwinMiddleware
class comes with project katana. Creating middleware using that pattern is fine
but only limitation is that you can’t use this middleware in different OWIN implementation
other than project katana.
So, we are not going to use this pattern but instead we’ll
declare our very own AppFunc that takes
IDictionary<string, object> and returns a Task.
using AppFunc = Func<IDictionary<string, object>, Task>;
public class DebugMiddleware
{
}
Now we need to
create constructor of this class that takes a single parameter of type AppFunc named
as next.
public class DebugMiddleware
{
private AppFunc _next;
public DebugMiddleware(AppFunc next)
{
_next = next;
}
}
This next variable is used to invoke the next middleware in
the pipeline so that’s why we named it to next.
Now we need to invoke this middleware. To invoke a
middleware we need to create a public method named “Invoke” that takes IDictionary<string,object> and
returns a Task
. As its returns a task so we’ll mark it with async
keyword to tell the C# compiler this method will perform an asynchronous
operation. The benefit of marking the
method with async is that we don’t have to
manually return the task instead await
keyword will do the job for us.
public async Task Invoke(IDictionary<string,object>
environment)
{
//all the
functionality goes here
}
In the Invoke method we’ll put all the logic of our
middleware and will call the AppFunc to invoke the next middleware in pipeline.
Since we are building a debug middleware so let’s add some tracing logic that
we have already implemented in our delegate based middleware in part2.
public async Task Invoke(IDictionary<string,object>
environment)
{
var owinContext = new OwinContext(environment);
Debug.WriteLine("Request: " + owinContext.Request.Path);
await _next(environment);//will forward
request to next middleware
/*at this point
responce headers will be sent client
/and response body is about to
sent*/
Debug.WriteLine("Responce Status Code:" + owinContext.Response.StatusCode);
}
Now last thing is to plug in middleware into the pipeline
using IAppBuilder from Startup.cs
class
public class Startup
{
/*IAppBuilder
object is used to plugin middlewares
to build a pipeline*/
public void
Configuration(IAppBuilder app)
{
app.Use<DebugMiddleware>();
app.Use(async (context, nextMiddleWare) =>
{
await context.Response.WriteAsync("Peace
be on world.");
});
}
}
Finally press F5 to run the app with debugger attached. To
make sure our Debug middleware has run successfully we need to see trace
messages written to output window.
Now we have a reusable custom OWIN middleware running in
pipeline, but still we have something to add into pattern.
Configuring Middleware
In most of the time we would like our middleware to do
different things in different situations. To introduce that functionality, we
need an options class. An options class is plane C# class that have name of
middleware plus “options” as suffix. In our case we’ll create class named
“DebugMiddlewareOptions”.
public class DebugMiddlewareOptions
{
}
The options class will contain things that we need to
configure in our middleware. Let’s say we need to configure two things in our
middleware so we’ll have two properties in our options class.
public class DebugMiddlewareOptions
{
public Action<IOwinContext> OnIncomingRequest { get; set; }
public Action<IOwinContext> OnOutgoingResponse { get; set; }
}
OnIncomingRequest will
be called when middleware will receive request and OnOutgoingResponse
will be called when response will be on its way to client. Now we need
our middleware class to take this options class instance through constructor
for configuration to work. We’ll have a second parameter of Options class in
DebugMiddleware’s constructor that’s
already taking AppFunc parameter.
private AppFunc _next;
private DebugMiddlewareOptions _options;
public DebugMiddleware(AppFunc next, DebugMiddlewareOptions options)
{
_next = next;
_options = options;
}
Now instead of directly outputting response to output window
from Invoke method we’ll refactor it to use DebugMiddlewareOptions
delegates.
public async Task Invoke(IDictionary<string,object>
environment)
{
var owinContext = new OwinContext(environment);
_options.OnIncomingRequest(owinContext);
await _next(environment);//will forward
request to next middleware
/*at this point
responce headers will be sent client
/and response body is about to
sent*/
_options.OnOutgoingResponse(owinContext);
}
We should provide default functionality to both delegates of
DebugMiddlewareOptions class if in some case
options class didn’t provide a call back.
public DebugMiddleware(AppFunc next, DebugMiddlewareOptions options)
{
_next = next;
_options = options;
if (_options.OnIncomingRequest == null)
_options.OnIncomingRequest =
(ctx) => {
Debug.WriteLine("Request: " +
ctx.Request.Path);
};
if (_options.OnOutgoingResponse == null)
_options.OnOutgoingResponse =
(ctx) => {
Debug.WriteLine("Responce Status Code:" +
ctx.Response.StatusCode);
};
}
We’ll provide instance of options class to middleware with app.Use<DebugMiddleware> method by the when we’ll plug in middleware
to pipeline in Startup.cs class.
app.Use<DebugMiddleware>(new DebugMiddlewareOptions());
Run the app with debugger attached and see the output window
to make sure debug middleware runs.
At this point we need to add
some configuration to our middleware to change its way to work. Let’s turn this
middleware into a performance mintoring module that will output total time elapsed
by a request to complete the response. We’ll add the functionality to
middleware by providing options call back methods for both OnOutgoingResponse and OnIncomingRequest delegates from Startup.cs class.
app.Use<DebugMiddleware>(new DebugMiddlewareOptions() {
OnIncomingRequest = (ctx) =>
{
var stopWatch = new Stopwatch();
stopWatch.Start();
//put the stop into environment dictionary
ctx.Environment["StopWatch"] = stopWatch;
},
OnOutgoingResponse = (ctx)=>
{
var stopWatch = (Stopwatch)ctx.Environment["StopWatch"];
Debug.Write($"Time Elapsed: {stopWatch.ElapsedMilliseconds} milliseconds");
}
});
The middleware adds a running
stopwatch into environment dictionary from OnIncomingRequest
and get it back from OnOutgoingResponse to
output total millicseconds elapsed during the completion of request.
Press F5 to run the app with
debugger attached and look at the output ot make sure configuration worked
properly.
48 milliseconds are because
of extra overhead when you first time run the app but if you refresh from
browser window after the app is running you will see a much faster response.
Congratulations finally we have a fully configurable and
reusable middleware running into our pipeline.
Source Code
Startup.cs
public class Startup
{
/*IAppBuilder
object is used to plugin middlewares
to build a pipeline*/
public void
Configuration(IAppBuilder app)
{
app.Use<DebugMiddleware>(new DebugMiddlewareOptions() {
OnIncomingRequest = (ctx) =>
{
var stopWatch = new Stopwatch();
stopWatch.Start();
//put the stop into environment dictionary
ctx.Environment["StopWatch"] = stopWatch;
},
OnOutgoingResponse = (ctx)=>
{
var stopWatch = (Stopwatch)ctx.Environment["StopWatch"];
Debug.Write($"Time Elapsed: {stopWatch.ElapsedMilliseconds} milliseconds");
}
});
app.Use(async (context, nextMiddleWare) =>
{
await context.Response.WriteAsync("Peace
be on world.");
});
}
}
DebugMiddleware.cs
using Microsoft.Owin;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
namespace OwinPipeline.Middlewares
{
using AppFunc = Func<IDictionary<string, object>, Task>;
public class DebugMiddleware
{
private AppFunc _next;
private DebugMiddlewareOptions _options;
public DebugMiddleware(AppFunc next, DebugMiddlewareOptions options)
{
_next = next;
_options = options;
if (_options.OnIncomingRequest == null)
_options.OnIncomingRequest =
(ctx) => {
Debug.WriteLine("Request: " +
ctx.Request.Path);
};
if (_options.OnOutgoingResponse == null)
_options.OnOutgoingResponse =
(ctx) => {
Debug.WriteLine("Responce Status Code:" +
ctx.Response.StatusCode);
};
}
public async Task Invoke(IDictionary<string,object>
environment)
{
var owinContext = new OwinContext(environment);
_options.OnIncomingRequest(owinContext);
await _next(environment);//will forward
request to next middleware
/*at this point
responce headers will be sent client
/and response body is about to
sent*/
_options.OnOutgoingResponse(owinContext);
}
}
}
DebugMiddlewareOptions.cs
using Microsoft.Owin;
using System;
namespace OwinPipeline
{
public class DebugMiddlewareOptions
{
public Action<IOwinContext> OnIncomingRequest { get; set; }
public Action<IOwinContext> OnOutgoingResponse { get; set; }
}
}
Comments
Post a Comment