Announcing Voyager: An Alternative to Controllers in ASP.NET Core
Tags: dotnet, csharp
Published: 2020-06-03 02:14:00

_This post originally appeared at https://smithgeek.com/announcing-voyager/_

---

I'm happy to announce my new project called [Voyager](https://www.github.com/smithgeek/Voyager)!

# What Is It?

Voyager is an alternative way to route requests in ASP.NET core.

# Why Might I Use It?

Controller classes can get pretty big. Each endpoint can have different dependencies they need to be injected. This, of course, means that your constructor parameter list can get long.

With Voyager, you can break apart your giant controller classes into individual small classes for each endpoint. You only have to work with code that applies to your task.

# Getting Started

To get started you first need to install the [Voyager nuget package](https://www.nuget.org/packages/Voyager/).

The next step is to add Voyager in the ConfigureServices function of your startup class. As part of the configuration, you need to tell voyager what assemblies contain your Request and Handler classes.

```cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddVoyager(c =>
    {
        c.AddAssemblyWith<Startup>();
    });
}
```

Next, you need to add some middleware in the Configure function. You can call the `UseVoyagerRouting` and `UseVoyagerEndpoints` extension methods to add the required middleware.

Voyager provides an exception handler middleware that will catch any unhandled exceptions (including [validation exceptions](#Validation)). These will be returned as a 500 response using the [problem details](https://lurumad.github.io/problem-details-an-standard-way-for-specifying-errors-in-http-api-responses-asp.net-core) specification. You can use your own exception handler middleware if you prefer.

A complete example of what a Startup class might look like is included below.

```cs
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseVoyagerExceptionHandler();
        app.UseHttpsRedirection();

        app.UseVoyagerRouting();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
        app.UseVoyagerEndpoints();
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddVoyager(c =>
        {
            c.AddAssemblyWith<Startup>();
        });
    }
}
```

# How It Works

Voyager uses the great [MediatR](https://github.com/jbogard/MediatR) library to do a lot of the heavy lifting. If you're used MediatR before, Voyager will feel familiar to you.

To explain in further detail I'm going to start with a normal controller class and show how we can do the same thing using Voyager.

```cs
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    [HttpGet]
    [Route("{city}")]
    public IEnumerable<GetWeatherForecastResponse> Get([FromRoute] string city, [FromQuery(Name = "d")]int days = 5)
    {
        var rng = new Random();
        return Enumerable.Range(1, days).Select(index => new GetWeatherForecastResponse
        {
            City = city,
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

public class GetWeatherForecastResponse
{
    public string City { get; set; }
    public DateTime Date { get; set; }
    public string Summary { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

An example request that would match this example would be `/WeatherForecast/Kansas City?d=10`

## Request

The first thing we need to do when using Voyager is to create a request object.

```cs
[Route(HttpMethod.Get, "WeatherForecast/{city}")]
public class GetWeatherForecastRequest : EndpointRequest<IEnumerable<GetWeatherForecastResponse>>
{
    [FromRoute]
    public string City { get; set; }

    [FromQuery("d")]
    public int Days { get; set; } = 5;
}
```

Let's go over each part of this code.

### Route

```cs
[Route(HttpMethod.Get, "WeatherForecast/{city}")]
```

We use the `[Route]` attribute to indicate what HTTP method and path is used for this request. You can use the same template syntax in the path that you are used to in controller routing.

### Interface

```cs
public class GetWeatherForecastRequest : EndpointRequest<IEnumerable<GetWeatherForecastResponse>>
```

The request object implements the EndpointRequest interface. The EndpointRequest interfaces also needs the type that will be returned from this request. This is necessary for MediatR to do its work. You could also just implement MediatR's IRequest interface directly if you wish.

```cs
public class GetWeatherForecastRequest : IRequest<ActionResult<<IEnumerable<GetWeatherForecastResponse>>>>
```

### Model Binding

```cs
[FromRoute]
public string City { get; set; }

[FromQuery("d")]
public int Days { get; set; } = 5;
```

You can use the `[FromRoute]` and `[FromQuery]` attributes to tell Voyager where it should get the data from. As you can see in the `[FromQuery]` example, if you want to use a different name for your property, you can pass in the name that Voyager should look for. If you don't provide a name, Voyager will use the name of the property.

If a property does not use a `From` attribute then Voyager will look for the value in the body of the request.

# Handler

Every request must have a corresponding handler class.

```cs
public class GetWeatherForecastHandler : EndpointHandler<GetWeatherForecastRequest, IEnumerable<GetWeatherForecastResponse>>
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public override ActionResult<IEnumerable<GetWeatherForecastResponse>> HandleRequest(GetWeatherForecastRequest request)
    {
        if (request.Days < 1)
        {
            throw new ArgumentException("Days must be greater than 0");
        }
        var rng = new Random();
        return Enumerable.Range(1, request.Days).Select(index => new GetWeatherForecastResponse
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)],
            City = request.City
        })
        .ToArray();
    }
}
```

As you can see this is very similar to the function that was used in the controller class. There are only two lines to point out.

```cs
public class GetWeatherForecastHandler : EndpointHandler<GetWeatherForecastRequest, IEnumerable<GetWeatherForecastResponse>>
```

Here you see that we need to inherit from the EndpointHandler base class and provide the type of our Request class and the type that is being returned.

```cs
public override ActionResult<IEnumerable<GetWeatherForecastResponse>> HandleRequest(GetWeatherForecastRequest request)
```

This is the signature for the function that is called to handle the request.

If you want to use async/await all you have to do is override `HandleRequestAsync`.

## IActionResult

If you prefer to not have a strongly typed return value you can also omit the return type and instead return an IActionResult.

```cs
public class GetWeatherForecastRequest : EndpointRequest
{
    ...
}

public class GetWeatherForecastHandler : EndpointHandler<GetWeatherForecastRequest>
{
    public override IActionResult HandleRequest(GetWeatherForecastRequest request)
    {
        ...
    }
}
```

Providing the return type is useful if you are writing tests or using tools like [OpenApi](https://www.openapis.org/)([Swagger](https://github.com/domaindrivendev/Swashbuckle)).

<a name="Validation"></a>

# Validation

Validation is performed using [Fluent Validation](https://fluentvalidation.net/). Simply add a Validator class for your Request object. Voyager will handle the rest.

```cs
public class GetWeatherForecastRequestValidator : AbstractValidator<GetWeatherForecastRequest>
{
    public GetWeatherForecastRequestValidator()
    {
        RuleFor(request => request.Days).GreaterThanOrEqualTo(1);
    }
}
```

# Authorization

Authorization is handled by the [IAuthorizationService](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-3.1) (using standard AspNet Core [Requirements](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-3.1#requirements) and [Handlers](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-3.1#authorization-handlers)).

Requirements are grouped together into policies. You can make your own policies by creating a class that implements the Policy interface. The interface requires a single GetRequirements function that returns a list of all the requirements that must be satisfied.

```cs
public class AuthenticatedPolicy : Policy
{
    public IList<IAuthorizationRequirement> GetRequirements()
    {
        return new IAuthorizationRequirement[]
        {
            new AuthenticatedRequirement(),
        };
    }
}
```

Voyager provides an `AnonymousPolicy` and `AuthenticatedPolicy` for you.

You apply a policy to a handler by adding the `Enforce` interface and providing a policy.

```cs
public class GetWeatherForecastHandler : EndpointHandler<GetWeatherForecastRequest>, Enforce<AuthenticatedPolicy>
{
    ...
}
```

# Project Info

The project is on [Github](https://github.com/smithgeek/Voyager) so check it out and let me know what you think.