Learn the power of ASP.NET Core's Startup.Configure method with examples.
The Configure
method in Startup.cs
is essentially a fundamental part of Microsoft's OWIN implementation
Katana. To sum up what OWIN and Katana are:
The Open Web Interface for .NET (OWIN) is a standard interface specification which aims to "decouple .NET Web Servers and applications".
Older versions of ASP.NET were not server agnostic, they had to be ran on IIS and a large amount of their functionality only worked with IIS as the host.
This was a dumb move, meant that ASP.NET applications as they were could not be ran on anything besides Windows Server and IIS.
Remnants of this can be seen in ASP.NET projects that have the global.asax
file and the web.config
file (especially the latter as that is essentially an IIS config file).
OWIN basically provided an interface to remodel ASP.NET applications to be less reliant on an IIS server and basically allow for applications to be self hosting or at least hosted on another server.
This is Microsoft's official implementation of OWIN. Katana was a .NET Framework (not Core) set of libraries that provided everything you needed to work with OWIN in ASP.NET. Katana provides heaps of middleware for authentication, authorization, routing, template generation, API endpoints.
Katana basically allows you to write web applications that look like ASP.NET Core but using older versions of ASP.NET.
When ASP.NET Core was being developed, a tonne was pulled from Katana and you'll notice that looking at old Katana source code invokes a very similar look to ASP.NET Core code.
With this background in mind, the Configure
method can basically be summed up as the method in which all of the
middlware for your ASP.NET Core application is added to the pipeline.
When you write a controller for a WebAPI, you are essentially writing up data that is used by this pipeline to generate API endpoints.
When you add a view to a Razor page, you are mapping templates to endpoint which the pipeline uses to generate web pages.
All the bells and whistles of ASP.NET Core are just abstractions of the methods provided by the app
object in the
Configure
method, and to show this off lets look at some examples.
Whilst you could very well write an entire ASP.NET Core application in your Startup.cs
file, do not do that. This
article aims to showcase just how many of the features of ASP.NET Core work at a "lower level" (still not really that
low). That being said, some of these concepts you may implement exactly as you see here (using Map
to scope parts of
your application and writing middleware by providing a delegate in the Use
method are both practices you'll see in
professional ASP.NEt development).
Create a folder and run:
dotnet new web
Once that's loaded, open up your Startup.cs
. Clear everything in the Configure
method. To run any of these samples,
paste the related code snippet into the Configure
method and run the following in Powershell/Cmd/Bash:
dotnet run
app.Run(async context => await context.Response.WriteAsync("Hello World!"));
- The
HttpContext
object is the heart of all HTTP related operations in ASP.NET Core,context
exposes objects such as the current HTTPRequest
andResponse
.- The current
HttpContext
can be accessed anywhere in an ASP.NET Core application usingHttpContext.Current
.
app.Run
takes in the currentHttpContext
as a parameter.- Using the response object this will return the string
"Hello World!"
.
app.Run(async context => await context.Response.WriteAsync($"Hello {context.Request.Query["name"]}"));
- In this example we get data from the query string of the request and use it to return a dynamic response.
- Appending
?name=Alex
(or whatever your name is) to the app URL will return a greting with the name
app.Run(async context => await context.Response.WriteAsync($"Your user agent is: {context.Request.Headers["User-Agent"]}"));
- As with the
Query
object, theRequest
&Response
object has aHeaders
object which can be used to read and write to the headers
app.Map("/Home", (endpoint) => {
endpoint.Run(async context => await context.Response.WriteAsync("This is the home page"));
});
app.Map("/About", (endpoint) => {
endpoint.Run(async context => await context.Response.WriteAsync("This is the about page"));
});
- The
Map
method on theIApplicationBuilder
interface provides a way of "branching" the request pipeline based on the requested path- This method exposes a second
IApplicationBuilder
(which I have calledendpoint
), allowing all the functionality ofapp
to be executed inside a separate scope
- This method exposes a second
- There is no difference to how the
Run
method is used inside aMap
, so we can return plain text the same as we would previously.
app.MapWhen(
(context) => context.Request.Method == "GET",
(endpoint) => endpoint.Run(async context => await context.Response.WriteAsync("Nice GET!"))
);
app.MapWhen(
(context) => context.Request.Method == "POST",
(endpoint) => endpoint.Run(async context => await context.Response.WriteAsync("The POST with the most!"))
);
- The
MapWhen
method allows for filtering based on the currentHttpContext
- If the context fulfils the requirements of the predicate, the provided pipeline will be used.
- Test this with Postman, send the two separate requests to the server and look at the output.
int counter = 0;
app.MapWhen(
(context) => context.Request.Method == "GET",
(endpoint) => endpoint.Run(async context => await context.Response.WriteAsync(counter.ToString()))
);
app.MapWhen(
(context) => context.Request.Method == "POST",
(endpoint) => endpoint.Run(async context => {
counter++;
await context.Response.WriteAsync(counter.ToString());
})
);
- The
counter
variable is saved in memory, the value is not lost until the application is stopped. POST
ing will increment the value, for all clients.
if (!System.IO.File.Exists("data.txt"))
System.IO.File.Create("data.txt");
app.MapWhen(
(context) => context.Request.Method == "GET",
(endpoint) => endpoint.Run(async context =>
await context.Response.WriteAsync(System.IO.File.ReadAllText("data.txt")))
);
app.MapWhen(
(context) => context.Request.Method == "POST",
(endpoint) => endpoint.Run(async context =>
{
System.IO.File.WriteAllText("data.txt", context.Request.Query["data"]);
await context.Response.WriteAsync($"{context.Request.Query["data"]} saved to file");
})
);
- There's no limitations to the C# code you can execute in the HTTP Pipeline
- Posting to
/?data=Some value
will save"Some Value"
todata.txt
- This data is persistent too, restarting the app doesn't clear the value.
if (!System.IO.File.Exists("data.txt"))
System.IO.File.Create("data.txt");
app.MapWhen(
(context) => context.Request.Method == "GET",
(endpoint) => endpoint.Run(async context =>
await context.Response.WriteAsync(System.IO.File.ReadAllText("data.txt")))
);
app.MapWhen(
(context) => context.Request.Method == "POST",
(endpoint) => endpoint.Run(async context =>
{
using (var reader = new StreamReader(context.Request.Body))
{
string contents = await reader.ReadToEndAsync();
System.IO.File.WriteAllText("data.txt", contents);
await context.Response.WriteAsync($"{contents} saved to file");
}
})
);
- The
Body
of theRequest
is aStream
, so you must read it with aStreamReader
. - You can test this by
POST
ing plain text to the API endpoint
var data = new List<string>() {
"Item #1",
"Item #2",
"Item #3"
};
app.MapWhen(
(context) => context.Request.Method == "GET",
(endpoint) => endpoint.Run(async context => {
var indexParsed = int.TryParse(context.Request.Path.ToString().Replace("/", ""), out int index);
await context.Response.WriteAsync(data[index]);
})
);
- Going to
/0
will return the first item in the array,/1
the second and so on. - The
Request.Path
object when parsed as a string will include the/
so we have to remove it.
var data = new List<string>();
app.MapWhen(
(context) => context.Request.Method == "GET",
(endpoint) => endpoint.Run(async context => {
var indexParsed = int.TryParse(context.Request.Path.ToString().Replace("/", ""), out int index);
await context.Response.WriteAsync(data[index]);
})
);
app.MapWhen(
(context) => context.Request.Method == "POST",
(endpoint) => endpoint.Run(async context =>
{
using (var reader = new StreamReader(context.Request.Body))
{
string contents = await reader.ReadToEndAsync();
data.Add(contents);
await context.Response.WriteAsync($"{contents} saved in-memory");
}
})
);
- As with the file save, you can POST plain text to the endpoint and it will save to the collection.
app.Run(async (context) => {
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized");
});
- The
Response
object can be edited directly, thus the setting of the status code.
app.Use(async (context, next) => {
context.Response.Headers.Add("Custom-Header", "Hello");
await next.Invoke();
});
app.Run(async (context) => await context.Response.WriteAsync("Check Browser Headers"));
- The
.Use
method onIApplicationBuilder
provider can be used to develop custom middleware for the HTTP Pipeline - Middleware, as the name implies, is executed in between the HTTP
Request
andResponse
. - The call to
next.Invoke()
will move onto the next middleware in the pipeline or the request. - Opening the web app, and checking the Response headers in browser will show that there is a
Custom-Header
added to the response.
app.Use(async (context, next) =>
{
context.Response.StatusCode = 500;
await context.Response.WriteAsync("Something Broke");
});
app.Run(async (context) =>
{
await context.Response.WriteAsync("Success");
});
- As
next.Invoke()
is never being called, every request is constantly intercepted.
string token = "secret";
app.Use(async (context, next) =>
{
if (!context.Request.Headers.ContainsKey("Authorization")
|| context.Request.Headers["Authorization"] != $"Bearer {token}")
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized");
}
else
await next.Invoke();
});
app.Run(async (context) =>
{
await context.Response.WriteAsync("Success");
});
- This is a very simple implementation of bearer token authorization.
- Using Postman, send a GET request with the Authorization set to "Bearer Token" and the token to
secret