Skip to content

Instantly share code, notes, and snippets.

@pielegacy
Last active March 11, 2018 01:42
Show Gist options
  • Save pielegacy/f9b6d23980a92a368a03751451905d65 to your computer and use it in GitHub Desktop.
Save pielegacy/f9b6d23980a92a368a03751451905d65 to your computer and use it in GitHub Desktop.

Configure By Example

Learn the power of ASP.NET Core's Startup.Configure method with examples.

What is the Configure(IApplicationBuilder app, IHostingEnvironment env) method?

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.

Disclaimer

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).

Running these at Home

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

1. Hello World

Snippet

app.Run(async context => await context.Response.WriteAsync("Hello World!"));

Notes

  • The HttpContext object is the heart of all HTTP related operations in ASP.NET Core,
    • context exposes objects such as the current HTTP Request and Response.
    • The current HttpContext can be accessed anywhere in an ASP.NET Core application using HttpContext.Current.
  • app.Run takes in the current HttpContext as a parameter.
  • Using the response object this will return the string "Hello World!".

2. Return a Response based on the Request

Snippet

app.Run(async context => await context.Response.WriteAsync($"Hello {context.Request.Query["name"]}"));

Notes

  • 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

3. Return the User Agent of the browser

Snippet

app.Run(async context => await context.Response.WriteAsync($"Your user agent is: {context.Request.Headers["User-Agent"]}"));

Notes

  • As with the Query object, the Request & Response object has a Headers object which can be used to read and write to the headers

4. Mapping different routes

Snippet

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"));
});

Notes

  • The Map method on the IApplicationBuilder interface provides a way of "branching" the request pipeline based on the requested path
    • This method exposes a second IApplicationBuilder (which I have called endpoint), allowing all the functionality of app to be executed inside a separate scope
  • There is no difference to how the Run method is used inside a Map, so we can return plain text the same as we would previously.

5. Mapping based on HttpContext

Snippet

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!"))
);

Notes

  • The MapWhen method allows for filtering based on the current HttpContext
    • 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.

6. Return data from Memory

Snippet

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());
    })
);

Notes

  • The counter variable is saved in memory, the value is not lost until the application is stopped.
  • POSTing will increment the value, for all clients.

6. Return data from File

Snippet

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");
    })
);

Notes

  • There's no limitations to the C# code you can execute in the HTTP Pipeline
  • Posting to /?data=Some value will save "Some Value" to data.txt
  • This data is persistent too, restarting the app doesn't clear the value.

7. Reading from Request Body

Snippet

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");
        }
    })
);

Notes

  • The Body of the Request is a Stream, so you must read it with a StreamReader.
  • You can test this by POSTing plain text to the API endpoint

8. GET Data in a REST full fashion

Snippet

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]);
    })
);

Notes

  • 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.

9 POST Data to an In-Memory Collection

Snippet

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");
        }
    })
);

Notes

  • As with the file save, you can POST plain text to the endpoint and it will save to the collection.

10. Setting Response Code

Snippet

app.Run(async (context) => {
    context.Response.StatusCode = 401;
    await context.Response.WriteAsync("Unauthorized");
});

Notes

  • The Response object can be edited directly, thus the setting of the status code.

11. Basic Middleware

Snippet

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"));

Notes

  • The .Use method on IApplicationBuilder provider can be used to develop custom middleware for the HTTP Pipeline
  • Middleware, as the name implies, is executed in between the HTTP Request and Response.
  • 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.

12. Intercept a Response

Snippet

app.Use(async (context, next) =>
{
    context.Response.StatusCode = 500;
    await context.Response.WriteAsync("Something Broke");
});

app.Run(async (context) =>
{
    await context.Response.WriteAsync("Success");
});

Notes

  • As next.Invoke() is never being called, every request is constantly intercepted.

13. Basic Bearer Token Authentication

Snippet

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");
});

Notes

  • 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment