Quantcast
Channel: asp.net 5 rc1 – .NET Liberty
Viewing all articles
Browse latest Browse all 13

ASP.NET Core: Custom Service Based on Request

$
0
0

main

Recently a .NET Liberty reader asked me an interesting question: is it possible to control the creation of a dependency based on the incoming request? In particular, he wanted to provide a different implementation of a dependency based on the users login.

This is a great question – its actually a fantastic use case for dependency injection. Dependency injection allows me to use objects throughout my application without having to worry about how they are created. It enables me to keep my code base clean and organized – each component can focus on a single responsibility, and any extra functionality it needs can be injected.

A Simple Example

Lets take a look at a simple example. Imagine I wanted to build a control panel for my website. For now, lets say I wanted to have one piece of functionality: deleting a user by name. Here’s what the interface for that looks like:

public interface IControlPanel
{
    string DeleteUser(string name);
}

For the sake of keeping this example simple, the DeleteUser method should return a response message.

Before getting to the implementation, lets talk about the business rules behind deleting a user:

  • Guests cannot delete anybody
  • Normal users can only delete themselves
  • The Administrator can delete anybody

Implementation

Lets see how we could implement these rules. As above, to keep things simple, we’ll determine the logged in user by looking for a request header called loggedInUser (in a real world application, I’d want to make use of Identity services to authenticate users).

public class ControlPanel : IControlPanel
{
    private readonly IHttpContextAccessor _contextAccessor;

    public ControlPanel(IHttpContextAccessor contextAccessor)
    {
        _contextAccessor = contextAccessor;
    }

    public string DeleteUser(string name)
    {
        var currentUser = _contextAccessor.HttpContext.Request.Headers["loggedInUser"];
        // No user logged in: guest
        if (string.IsNullOrEmpty(currentUser))
        {
            return "Guests cannot delete users.";
        }
        // Administrator logged in
        if ("admin" == currentUser)
        {
            return $"Deleted the user {name} as Administrator";
        }
        // Normal user logged in
        if (name == currentUser)
        {
            return $"Deleted user {name} as {currentUser}";
        }
        return $"Cannot delete {name} because {currentUser} is a regular user";
    }
}

Configure Services

Now that we’ve got a basic service implementation, lets add it to the service collection and create a simple API for deleting users:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<IControlPanel, ControlPanel>();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseIISPlatformHandler();

        app.Map("/panel/delete", DeleteUserApi);
        app.Run(async (context) =>
        {
            await context.Response.WriteAsync("Welcome to the demo.");
        });
    }

    public void DeleteUserApi(IApplicationBuilder app)
    {
        app.Run(async (context) =>
        {
            var controlPanel = context.RequestServices.GetRequiredService<IControlPanel>();
            var userToDelete = context.Request.Query["user"];
            await context.Response.WriteAsync(controlPanel.DeleteUser(userToDelete));
        });
    }

    public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}

Testing

Lets test it out using curl:

$ curl -s localhost:49480/panel/delete?user=joe
Guests cannot delete users.
$ curl -s -H "loggedInUser: admin" localhost:49480/panel/delete?user=joe
Deleted the user joe as Administrator
$ curl -s -H "loggedInUser: bob" localhost:49480/panel/delete?user=joe
Cannot delete joe because bob is a regular user
$ curl -s -H "loggedInUser: joe" localhost:49480/panel/delete?user=joe
Deleted user joe as joe

Looks like everything is working as expected.

For this contrived scenario, including all the business logic for the various user types in a single ControlPanel class is fine. However, you can imagine that as more and more functionality is added, the ControlPanel implementation can start to grow quite large and complex.

Multiple IControlPanel Implementations

A nice alternative to this is to make use of ASP.NET Core’s dependency injection (DI) mechanism to provide different implementations of IControlPanel based on the incoming request. First, we can break up the existing ControlPanel into three separate classes – AdminControlPanel, NormalUserControlPanel, and GuestControlPanel:

public class AdminControlPanel : IControlPanel
{
    public string DeleteUser(string name)
    {
        return $"Deleted the user {name} as Administrator";
    }
}

public class NormalUserControlPanel : IControlPanel
{
    private readonly string _name;

    public NormalUserControlPanel(string name)
    {
        _name = name;
    }

    public string DeleteUser(string name)
    {
        // Regular users can only delete themselves
        if (name == _name)
        {
            return $"Deleted user {name} as {_name}";
        }
        return $"Cannot delete {name} because {_name} is a regular user";
    }
}

public class GuestControlPanel : IControlPanel
{
    public string DeleteUser(string name)
    {
        return "Guests cannot delete users.";
    }
}

As you can see, each IControlPanel implementation contains the business logic for that particular user type. Each class on its own is much simpler than the previous monolithic ControlPanel.

Just-In-Time Dependency Injection

Now for the really cool part. We can update the ConfigureServices method to provide a custom IControlPanel implementation based on the incoming request:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
            services.AddTransient<IControlPanel>(serviceProvider =>
            {
                var context = serviceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext;
                var userHeader = context.Request.Headers["loggedInUser"];
                if (string.IsNullOrEmpty(userHeader)) return new GuestControlPanel();
                if ("admin" == userHeader) return new AdminControlPanel();
                return new NormalUserControlPanel(userHeader);
            });
        }

    public void Configure(IApplicationBuilder app)
    {
        app.UseIISPlatformHandler();

        app.Map("/panel/delete", DeleteUserApi);
        app.Run(async (context) =>
        {
            await context.Response.WriteAsync("Welcome to the demo.");
        });
    }

    public void DeleteUserApi(IApplicationBuilder app)
    {
        app.Run(async (context) =>
        {
            var controlPanel = context.RequestServices.GetRequiredService<IControlPanel>();
            var userToDelete = context.Request.Query["user"];
            await context.Response.WriteAsync(controlPanel.DeleteUser(userToDelete));
        });
    }

    public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}

Now if we run the application again, we can verify that the functionality is the exact same as before:

$ curl -s localhost:49480/panel/delete?user=joe
Guests cannot delete users.
$ curl -s -H "loggedInUser: admin" localhost:49480/panel/delete?user=joe
Deleted the user joe as Administrator
$ curl -s -H "loggedInUser: bob" localhost:49480/panel/delete?user=joe
Cannot delete joe because bob is a regular user
$ curl -s -H "loggedInUser: joe" localhost:49480/panel/delete?user=joe
Deleted user joe as joe

Recap

Previously, we were providing a static type name to the AddTransient method. This caused the ASP.NET Core dependency injection system to create an instance of the same type (ControlPanel) each time a IControlPanel was needed.

Later, by making use of the AddTransient overload that accepts a Func instead, the DI system calls this function each time an IControlPanel instance is needed. This Func is essentially a “hook” that allows us to determine just-in-time what type of implementation of IControlPanel to return based on the currently logged in user.

While this is a very simple example, I hope it helps to showcase yet another awesome way we can leverage the ASP.NET Core DI system to help keep our code simple and organized. As our application grows, keeping logic separate helps to keep complexity under check.

Another notable benefit is testability and maintainability: modifying and testing each component on its own is easier and safer to do than when everything exists in a single large object.


Viewing all articles
Browse latest Browse all 13

Latest Images

Trending Articles





Latest Images