19

We have a ASP.NET Web API that provides a REST API for our Single Page Application. We use DTOs/POCOs to pass data through this API.

The problem is now, that these DTOs are getting bigger over time, so now we want to refactor the DTOs.

I am looking for "best practices" how to design a DTO: Currently we have small DTOs that consist only of value-type fields, e.g:

public class UserDto
{
    public int Id { get; set; }

    public string Name { get; set; }
}

Other DTOs use this UserDto by composition, e.g.:

public class TaskDto
{
    public int Id { get; set; }

    public UserDto AssignedTo { get; set; }
}

Also, there are some extended DTOs that are defined by inheriting from others, e.g.:

public class TaskDetailDto : TaskDto
{
    // some more fields
}

Since some DTOs have been used for several endpoints/methods (e.g. GET and PUT), they have been extended incrementally by some fields over time. And due to inheritance and composition other DTOs also got bigger.

My question is now are inheritance and composition not good practices? But when we do not reuse them, it feels like writing the same code multiple times. Is it a bad practice to use a DTO for multiple endpoints/methods, or should there be different DTOs, which only differ in some nuances?

5
  • 11
    I can not tell you what should you do, but to my experience working with DTO, inheritance and composition sooner than later hunt you like a bad coffee. I will never reuse DTO again. Ever. Plus I don't consider similar DTOs to be a DRY violation. Two endpoints that return the same representation can reuse the very same DTOs. Two endpoints that return similar representations are not returning the same DTOs so I make specific DTOs for each. If I were forced to choose, the composition is the less problematic in the long run.
    – Laiv
    Jul 17, 2017 at 14:44
  • @Laiv this is the correct answer to the question, just don't. Not sure why you put it as a comment Jul 17, 2017 at 15:23
  • 3
    @Laiv: What do you use instead? In my experience, people who struggle with this are simply overthinking it. A DTO is just a container for data, and that's all it is. Jul 17, 2017 at 15:26
  • TheCatWhisperer because my arguments would be mainly opinion based. I'm still trying to address this kind of problems in my projects. @RobertHarvey true, don't know why I tend to see things harder than they really are. I'm still working on the solution. I was quite convinced that HAL was the model meant to solve these problems, but reading your answer I realised that I do my DTO too granulars too. So I will put in practice your approach first. The changes are going to be less dramatic than shifting completely to HATEOAS.
    – Laiv
    Jul 17, 2017 at 17:58
  • Try this for a good discussion that might help you: stackoverflow.com/questions/6297322/…
    – johnny
    Jul 17, 2017 at 20:11

4 Answers 4

19

As a best practice, try and make your DTOs as concise as possible. Only return what you need to return. Only use what you need to use. If that means a few extra DTOs, so be it.

In your example, a task contains a user. One probably doesn't need a full user object there, maybe just the name of the user that is assigned the task. We don't need the rest of the user properties.

Let's say you want to reassign a task to a different user, one may be tempted to pass a full user object and a full task object during the re-assignment post. But, what is really needed is just the task Id and the user Id. Those are the only two pieces of information needed to re-assign a task, so model the DTO as such. This usually means there is a separate DTO for every rest call if your striving for a lean DTO model.

Also, sometimes one may need inheritance/composition. Let's say one has a job. A job has multiple tasks. In that case, getting a job may also return the list of tasks for the job as well. So, there is no rule against composition. It depends on what is being modeled.

2
  • As concise as possible also means that I should recreate DTOs if they just differ in some details - doesn't it?
    – officer
    Jul 18, 2017 at 6:46
  • 1
    @officer - In general, yes.
    – Jon Raynor
    Jul 18, 2017 at 14:22
6

Using composition in DTOs is a perfectly fine practice.

Inheritance among concrete types of DTOs is a bad practice.

For one thing, as much as we both do and should abhor duplication, C#'s concise auto-property syntax renders the maintainability impact of duplicate property definitions fairly minimal.

Now let's consider inheritance.

One reason not to use concrete inheritance among DTOs is that certain tools will happily map them to the wrong types.

For example, if you use a database utility like Dapper (I do not recommend it but it is popular) which performs class name -> table name inference, you could easily end up saving a derived type as a concrete base type somewhere in the hierarchy and thereby silently lose data.

More fundamentally, we should not, in principle, use inheritance among DTOs, or for that matter any types, solely to share implementations between them. In other words, unless there is a logical, "is a" relationship between types they should not inherent from one another. Clarity takes precedence over dryness. To my mind, TaskDetail does not sound like a subtype of Task. It could just as easily be a property of a Task or, worse, it could be a supertype of Task.

Now, one thing you may be concerned about is maintaining consistency between the names and types of the properties of various related DTOs.

While inheritance of concrete types would, as a consequence, help to ensure such consistency especially when refactoring, it is much better to use interfaces (or pure virtual base classes in C++) to maintain that kind of consistency.

Consider the following DTOs

interface IIdentity
{
    int Id { get; set; }
}

interface INamed
{
    string Name { get; set; }
}

public class UserDto: IIdentity, INamed
{
    public int Id { get; set; }

    public string Name { get; set; }

    // User specific properties
}

public class TaskDto: IIdentity
{
    public int Id { get; set; }

    // Task specific properties
}
5

It's hard to establish best practices for something as "flexible" or abstract as a DTO. Essentially, DTOs are only objects for data transfer but depending on the destination or the reason for the transfer, you may want to apply different "best practices".

I recommend reading Patterns of Enterprise Application Architecture by Martin Fowler. There's a whole chapter dedicated to patterns, where DTOs get a really detailed section.

Originally, they were "designed" to be used in expensive remote calls, where you would likely to need a lot of data from different parts of you logic; DTOs would make the transfer of data in a single call.

According to the author, DTOs weren't intended to be used in local environments, but some people found a use for them. Usually they're used to gather information from diferent POCOs into a single entity for GUIs, APIs or different layers.

Now, with inheritance, code reuse is more like a side-effect of inheritance rather than its main objective; composition, on the other hand, is implemented with code reuse as the main objective.

Some people recommend the use of composition and inheritance together, using the strengths of both and trying to mitigate their weaknesses. The following is part of my mental process when choosing or creating new DTOs, or any new class/object for that matter:

  • I use inheritance with DTOs inside the same layer or same context. A DTO will never inherit from a POCO, a BLL DTO will never inherit from a DAL DTO, etc.
  • If I find myself trying to hide a field from a DTO, I'll refactor and maybe use composition instead.
  • If very few different fields from a base DTO is all I need, I'll put them in a universal DTO. Universal DTOs are used only internally.
  • A base POCO/DTO will almost never be used for any logic, that way the base answers only to the needs of its children. If I ever need to use the base, I avoid adding any new field that its children will never use.

Some of them maybe not the "best" practices, they work quite well for the projects I've been working on but you need to remember that no size fits all. In the case of the universal DTO you should be careful, my methods signatures look like this:

public void DoSomething(BaseDTO base) {
    //Some code 
}

If any of the methods ever need its own DTO I do inheritance and usually the only change I need to make is the parameter, though sometimes I need to dig deeper for specific cases.

From your comments I get that you're using nested DTOs. If your nested DTOs consist only of a list of other DTOs, I think the best thing to do is unwrap the list.

Depending on the amount of data you need to display or to work with, it may be a good idea to create new DTOs that limit the data; for example, if your UserDTO has a lot of fields and you only need 1 or 2, it may be better to have a DTO with only those fields. Defining the layer, context, usage and utility of a DTO will help a lot when designing it.

1
  • I couldn't add more than 2 links in my answer, here's some more info about local DTOs. I'm aware that its very old information but I think some of it maybe still relevant.
    – IvanGrasp
    Jul 18, 2017 at 15:15
3

Unless your system is based strictly on CRUD operations, your DTO's are too granular. Try creating endpoints that embody business processes or artifacts. This approach maps nicely to a business logic layer, and to Eric Evans' "domain-driven design."

For example, let's say you have an endpoint that returns data for an invoice so that it can be displayed on a screen or form to the end user. In a CRUD model, you would need several calls to your endpoints to assemble the necessary information: name, billing address, shipping address, line items. In a business transaction context, a single DTO from a single endpoint can return all of this information at once.

3
  • Robert, do you mean by an Aggregate Root here?
    – johnny
    Jul 17, 2017 at 19:42
  • Currently our DTOs are designed to provide the whole data for a form, e.g. when displaying a todo list, the TodoListDTO has a List of TaskDTOs. But since we are reusing the TaskDTO, each of them also has a UserDTO. So the problem is the big amount of data that is queried in the backend and sent over the wire.
    – officer
    Jul 18, 2017 at 6:38
  • Is all of the data required? Jul 18, 2017 at 15:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.