Mapping DTOs (Thin Controller Recipes Sample)

Hey! Before we start, keep in mind that this is a sample recipe for the newly available Thin Controller Recipe Set. If you’ve heard that saying “your controllers should be thin!” but felt helpless to take action on it, this is for YOU.

Background

Data transfer objects (DTOs) exist to translate meaning between layers of the a software architecture.

When we work within a layer, like a business domain layer or a user interface layer, we strive to keep the models within that layer as single-focused as possible so that they are simple and easier to work with.

Have you ever used one of those all-in-one kitchen gadgets that claims to do everything under the sun?

These all-in-one gadgets usually come with a giant instruction book and way too many plastic parts to wash, whereas the single-purpose product can be opened and used immediately, without requiring any startup time.

At some point we realized, software is better when it is made up of many, razor-focused, single-purpose components.

Better because it can adapt to change with the least amount of frustration and regression (changes that unintendedly break things in the process.)

In order to facilitate this one-object-one-purpose design, there comes the need for a translation between two layers.
To avoid mixing the code that calculates a bill, for example, with the code that is rendering the HTML of a web page (like an all-in-one kitchen gizmo) we need to involve a middleman to provide the data from the billing calculation layer in a format that makes sense to the user interface layer.

That is the essence of a DTO’s purpose.

Now, in the world of .NET API or MVC controllers, this translation typically comes in the form of request (or input) and response (or view model) DTOs.

In general, we want to limit what can be sent to us to the bare minimum. If we allow too much in, too many options, too much data, consider that the endpoint/action could be doing too much.

We also want to limit what we send back as a response. Give the user exactly what they wanted, no more, no less.

It can also be a security risk. If we send as a response the complete business object, we may be sending IDs or other sensitive information that shouldn’t leave the application walls.

How do we manage all that while keeping controllers thin?

The Ideal

An ideal controller action (with regard to mapping DTOs):

  1. Doesn’t use DTOs if a URL or query string parameter is suitable (abstinence!).

  2. Utilizes small, constrained request and response DTOs.

  3. Delegates mapping to mapper objects that have the sole purpose of mapping.

Why is this the ideal?

First, the obvious but worth noting reason – DTOs shouldn’t be used at all if they aren’t necessary.

You can avoid mapping code altogether if your request endpoint only needs a few pieces of data like an ID passed through the URL or an option included in the query string like ?color=blue.

The same is true of the response. Responding only with an HTTP status code could be perfectly acceptable based on the goals of the endpoint.

Second, when the request or response requires more detail, constrain the objects to limit only what the endpoint needs to process the request, and only what the client needs to understand the result.

If your request DTOs are very large, consider that the endpoint itself could be better split into several smaller ones.

Keeping the response small is also respectful of your clients. Give them only what they need, in a format that makes sense for them.

If they don’t need ID fields, don’t send them.

And finally, when it comes to the mapping itself – the translation of the request into something your controller can work with and the translation of your internal objects with something that is best suited as a response, leave that work to objects whose single responsibility is mapping.

Keeping controllers thin is really a mastering of the single responsibility principle.

By leaving the mapping to the mapper objects and not to the controllers, each object stays razor focused and has only one reason to change.

To achieve the ideal we can:

  1. Make use of URL or query string arguments if there are only a few required inputs.

  2. Break actions up into multiple smaller actions if they are doing more than one thing.

  3. Create mapping objects or utilize a mapping library like AutoMapper.

Where things can go wrong

The obvious misstep here is having any mapping code at all inside your controllers.

If we are abiding to the single responsibility principle so that we have only one reason to change any particular module, it makes sense that we would create objects who are solely designed to map one object to another.

Now let’s look at a few examples that correspond with the numbered steps in the previous section.

Recipes for proper DTO mapping

Using URL parameters and query string arguments in place of DTOs

I know, I know, this recipe is like how abstinence is a form of safe sex. Want to have safe sex? Simply have none at all!

DTOs can definitely be overkill for many controller actions. A simple example is the usual HTTP GET based endpoint that only requires and ID of an entity to return, and perhaps a query string option or two.

We can see that immediately below by using an id route parameter that is then accessible via the action’s id argument.

[HttpGet, Route("api/books/{id:int}")]
public IActionResult GetBookById(int id)
{
    var book = _bookRepository.GetBookById(id);

    return Ok(book);
}

To add one or more query string option, we can add a [FromQuery] attribute to each additional action argument:

[HttpGet, Route("api/books/{id:int}")]
public IActionResult GetBookById(int id, [FromQuery]bool preventRatedR)
{
    var book = _bookRepository.GetBookById(id);

    if (preventRatedR && book.Rating == Models.Rating.R)
    {
        return Unauthorized();
    }

    return Ok(book);
}

That enables clients to hit the endpoint with an optional query string like: api/books/2?preventRatedR=true.

Breaking up large actions

If your controller action is handling multiple tasks and requires a large request object to describe the options of each subtask, there may be an opportunity to break the action up into several smaller actions.

Doing so will make your controllers simpler, your DTOs smaller, and will once again keep to the single responsibility principle for easier future changes.

The trade-off is that your clients may need to make multiple requests instead of one large one.

In this example, we are back to our public library concept, with an application that allows a library maintainer to add new books to a database.

We have a controller method that does two things instead of one. To create a book it must add the book details to the database, but it must also handle modifying a rich author database at the same time.

[HttpPost, Route("api/books/")]
public IActionResult CreateBook([FromBody]CreateBookRequest createBookRequest)
{
    var author = new Models.Author();
    author.FirstName = createBookRequest.Author.FirstName;
    author.LastName = createBookRequest.Author.LastName;
    author.Age = createBookRequest.Author.Age;
    author.Biography = createBookRequest.Author.Biography;

    var savedAuthor = _bookRepository.SaveAuthor(author);

    var book = new Models.Book();
    book.AuthorID = savedAuthor.AuthorID;
    book.Title = createBookRequest.Title;
    book.Rating = Enum.Parse<Models.Rating>(createBookRequest.Rating.ToString());

    var savedBook = _bookRepository.SaveBook(book);

    return Ok(savedBook);
}

Note: The full source code can be found in the included files, in the mapping-DTOs folder.

To handle this complex request, a CreateBookRequest DTO was created to specify exactly what clients need to send to complete the request.

public class CreateBookRequest
{
    public string Title { get; set; }
    public Rating Rating { get; set; }
    public Author Author { get; set; }
}

public enum Rating
{
    Everyone, PG, M, R
}

public class Author
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public string Biography { get; set; }
}

Not only does this violate the single responsibility principle by handling both authors and books, it also has the DTO mapping code directly in the controller action, which we will discuss next, and other issues that you’ll see discussed in later recipes.

To be clear, this example might be simple enough to omit splitting it into two actions, but we are using it to save time. If you notice you are creating request DTOs with multiple levels of nested objects, always ask whether the job might be split up into several smaller actions.

Now, instead of one large request like the above example, let’s break it into a separate author creation action and book creation action.

[HttpPost, Route("api/books/")]
public IActionResult CreateBook([FromBody]CreateBookRequest createBookRequest)
{
    var book = new Book();

    book.Title = createBookRequest.Title;
    book.Rating = Enum.Parse<Models.Rating>(createBookRequest.Rating.ToString());
    book.AuthorID = createBookRequest.AuthorID;

    var savedBook = _bookRepository.SaveBook(book);

    return Ok(savedBook);
}

[HttpPost, Route("api/authors/")]
public IActionResult CreateAuthor([FromBody]CreateAuthorRequest createAuthorRequest)
{
    var author = new Author();

    author.FirstName = createAuthorRequest.FirstName;
    author.LastName = createAuthorRequest.LastName;
    author.Age = createAuthorRequest.Age;
    author.Biography = createAuthorRequest.Biography;

    var savedAuthor = _bookRepository.SaveAuthor(author);

    return Ok(savedAuthor);
}

To enable this, we simplify our CreateBookRequest DTO into one that now requires an already existing author ID, and create a separate DTO for creating authors.

public class CreateBookRequest
{
    public string Title { get; set; }
    public Rating Rating { get; set; }
    //public Author Author { get; set; }
    public int AuthorID { get; set; }
}

public class CreateAuthorRequest
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public string Biography { get; set; }
}

Consider the trade-offs here.

For the sake of a thinner controller, we have added an additional DTO and are now putting the responsibility onto the client to first create the author. Skillful engineering requires thoughtful consideration of things like this.

You may have constraints like a deadline to meet which would make a change like this not as important.

Or you may find during analysis that books could be created far more often authors, and that the system will be adding many books to the same author. This would give good justification to split up the endpoints.

Now let’s thin these controller actions a bit more by delegating the mapping work to separate objects.

Implementing mapping code in separate objects

When it comes down to it, mapping code is mostly just copying a value from one property, to some other similarly named property.

Because of its simplicity, it can be accomplished a number of ways, with the easiest being a basic static class and methods.

Libraries like AutoMapper can also be used to solve this problem. I’ve used AutoMapper in production projects and it has worked well, however in hindsight, I think I would have preferred the control of mapping objects manually. AutoMapper is good for most objects, but if you have very complicated instantiation scenarios, you just might want to do it yourself.

Let’s look at how you would do it without the help of AutoMapper.

First, we’ll create two response DTOs to demonstrate both the input and output mapping process. Recall that although DTOs add additional code, they provide the ability to hide unwanted info from layers (or clients) that don’t need it, or allow additional info that makes more sense to be in one layer than in another.

This is the software concept of encapsulation and information-hiding at work. When objects encapsulate only the properties that make sense for the context they exist in, they are easier to work with and change.

Put bluntly, less extra junk.

public class CreateBookResponse
{
    public string Title { get; set; }
    public int AuthorID { get; set; }
    public int[] AllBooksFromThisAuthor { get; set; }
    public Rating Rating { get; set; }

}

public class CreateAuthorResponse
{
    public int AuthorID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public string Biography { get; set; }
}

These don’t look too different from the original Book and Author domain entities, but we’ve made some minor changes to demonstrate the differences. We’ve removed the BookID field in the response.

You might determine in your analysis of a project that giving the ID to a client is a security risk, or is simply not necessary. DTOs allow that to be removed.

Note: .NET does provide a way to apply attributes to your models to hide fields which may work perfectly well for a small enough project. DTOs give us more fine-grained control and are better for abiding by the single responsibility principle. Attributes generally encourage mixing contexts.

We’ve also added an additional AllBooksFromThisAuthor field to the book response DTO. That is a field we likely wouldn’t need to store explicitly on the domain side since it can be deduced using a SQL query, but we may want to provide it to clients (or the UI layer) if that value was being displayed.

First, we’ll stub out a basic mapper class:

public class Mapper
{
    public Author MapAuthorFrom(CreateAuthorRequest createAuthorRequest)
    {
        //..
    }

    public CreateAuthorResponse MapCreateAuthorResponseFrom(Author author)
    {
        //..
    }

    public Book MapBookFrom(CreateBookRequest createAuthorRequest)
    {
        //..
    }

    public CreateBookResponse MapCreateBookResponseFrom(Book author)
    {
        //..
    }
}

Since mapper classes must contain references to both layer models, they would typically live in the higher layer if you are following an onion style architecture, where references only point inward.

Next, we’ll need to inject the book repository into this class so that it can make use of it to add the extra field to our book response object. To do that, we’ll use dependency injection into the constructor of the mapper, and then inject the mapper itself into the controller’s constructor.

public class Mapper
{
    private readonly IBookRepository _bookRepository;

    public Mapper(IBookRepository bookRepository)
    {
        _bookRepository = bookRepository;
    }
//...
}

public class BooksController : Controller
{
    private IBookRepository _bookRepository;
    private Mapper _mapper;

    public BooksController(IBookRepository bookRepository, Mapper mapper)
    {
        _bookRepository = bookRepository;
        _mapper = mapper;
    }
//...
}

Finally, we’ll comment out the original controller methods and replace them with new ones where we have swapped out the mapping code with use of the mapper classes.

[HttpPost, Route("api/books/")]
public IActionResult CreateBook([FromBody]CreateBookRequest createBookRequest)
{
    var book = _mapper.MapBookFrom(createBookRequest);

    var savedBook = _bookRepository.SaveBook(book);

    return Ok(_mapper.MapCreateBookResponseFrom(savedBook));
}

[HttpPost, Route("api/authors/")]
public IActionResult CreateAuthor([FromBody]CreateAuthorRequest createAuthorRequest)
{
    var author = _mapper.MapAuthorFrom(createAuthorRequest);

    var savedAuthor = _bookRepository.SaveAuthor(author);

    return Ok(_mapper.MapCreateAuthorResponseFrom(savedAuthor));
}

And add the mapping itself into the mapper class methods.

public Author MapAuthorFrom(CreateAuthorRequest createAuthorRequest)
{
    var author = new Author();

    author.FirstName = createAuthorRequest.FirstName;
    author.LastName = createAuthorRequest.LastName;
    author.Age = createAuthorRequest.Age;
    author.Biography = createAuthorRequest.Biography;

    return author;
}

public CreateAuthorResponse MapCreateAuthorResponseFrom(Author author)
{
    var response = new CreateAuthorResponse();

    response.AuthorID = author.AuthorID;
    response.FirstName = author.FirstName;
    response.LastName = author.LastName;
    response.Age = author.Age;
    response.Biography = author.Biography;

    return response;

}

public Book MapBookFrom(CreateBookRequest createBookRequest)
{
    var book = new Book();

    book.Title = createBookRequest.Title;
    book.Rating = Enum.Parse<Models.Rating>(createBookRequest.Rating.ToString());
    book.AuthorID = createBookRequest.AuthorID;

    return book;
}

public CreateBookResponse MapCreateBookResponseFrom(Book book)
{
    var response = new CreateBookResponse();

    response.Title = book.Title;
    response.AuthorID = book.AuthorID;
    response.Rating = Enum.Parse<Enums.Rating>(book.Rating.ToString());

    response.AllBooksFromThisAuthor =
        _bookRepository
        .Books
        .Where(b => b.AuthorID == book.AuthorID)
        .Select(b => b.BookID)
        .ToArray();


    return response;
}

Notice how we made use of the book repository to gather all the additional books an author has written.

An alternative way of doing this could be to pass the book repository to the mapping method explicitly and make the mapper class and its methods all static so you could skip the dependency injection. That would be simpler if you only needed the book repository once like in our example.

Recipe Summary

To avoid mixing contexts (UI concerns and domain concerns, for example) we make use of DTOs to act as the glue between layers.

We saw how this is, in essence, the practice of encapsulation and information hiding, and ultimately a following of the single responsibility principle.

Can you recall the main benefits of such a practice? Go ahead and name them before reading the next line…

Did ya get em’ all?

We prefer to keep objects small and single-focused so that they only have one reason to change. We do it to make our lives easier. Change in software is constant, so limiting the number of changes needed eliminates some of the risk of making a mistake. It also makes the code easier to understand in general.

Small, simple objects that are easier to change is true flexibility.

We saw this in practice above several times.

First, recall our abstinence analogy. The best DTO object is one that isn’t needed in the first place! You don’t need DTOs if an action request or response can be communicated clearly with a single value.

The bare minimum would be a URL parameter for the request and an HTTP status code for the response.

Next we looked at one large controller method that handles more than one thing: creating both authors and their books.

To thin them out, we:

1. Broke them into two smaller actions.

2. Converted the domain Book and Author objects that were originally being used for more than one purpose (request, response, and domain logic) into separate DTOs.

This of course made more objects, but eliminated the reasons they would need to change. If I needed to add some business logic to the Book object, I wouldn’t have to worry about that breaking the request or response handling of the controller.

3. Moved the task of mapping out of the controller and into a mapper object.

With the goal of thin controllers, mapping should be done elsewhere.

Utilize static methods for simplicity, or a library like AutoMapper to delegate all of it outside the controller. Lean towards handmade mappers if you have complex object construction requirements or are a control freak like me.

Useful Resources

AutoMapper documentation

That’s it for the first sample recipe. » Click Here To Get The Rest «.