C
C#ā€¢12mo ago
joren

How are DTO utilized

So I've been writing some end points for my learning project, and I stumbled on DTO. The concept I understand, limit view you return for privacy reasons, security reasons, having a smaller payload, etc. How does it work in practice though, I saw that auto mapper is a thing used, which is awesome, that I can use and understand easily. However, what's best practice, when you have a DTO for a model, does the endpoint always return the DTO? Or do you have two endpoints, one that return the actual model, and one the DTO?
86 Replies
joren
jorenOPā€¢12mo ago
Or am i completely missing the point of DTO's, and should they only be used if we always only want to return the DTO, if thats the case just always returning the DTO model makes sense, and having different endpoints for the model and its DTO would not make sense.
Pobiega
Pobiegaā€¢12mo ago
Your endpoints traditionally only take DTOs You'll have a different DTO for Create vs Read vs Update you map from entity to DTO, but rarely the other way.
joren
jorenOPā€¢12mo ago
wait so if i were to update, I wouldnt use a DTO i'd guess most people use PUT for that
Pobiega
Pobiegaā€¢12mo ago
you'd still use a DTO ie, CreatePersonDto and UpdatePersonDto the idea is that you don't want re-use within your DTOs they should be separate so you have distinct control over them
joren
jorenOPā€¢12mo ago
Pastebin
using AutoMapper;using Microsoft.AspNetCore.Mvc;using _1._1._4._3__...
Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.
joren
jorenOPā€¢12mo ago
I see, makes sense, same case when using an auto mapper?
Pobiega
Pobiegaā€¢12mo ago
... generic controller? šŸ¤®
joren
jorenOPā€¢12mo ago
well you know my background
Pobiega
Pobiegaā€¢12mo ago
Do I?
joren
jorenOPā€¢12mo ago
C++, I've told you before when we talked about generics
Pobiega
Pobiegaā€¢12mo ago
Sorry, I help hundreds of people per month :p Can't remember each discussion
joren
jorenOPā€¢12mo ago
that's okay, anyway, generic controllers are not the way?
Pobiega
Pobiegaā€¢12mo ago
Well, imho not
joren
jorenOPā€¢12mo ago
cant I just make them overridable though? or hide them
Pobiega
Pobiegaā€¢12mo ago
But maybe its the right call for you. Idk
joren
jorenOPā€¢12mo ago
if i dont like them
Pobiega
Pobiegaā€¢12mo ago
I've never written a "pure CRUD" web api thou
joren
jorenOPā€¢12mo ago
I mean, im new to wep api's I've got no idea this is a test project to learn
Pobiega
Pobiegaā€¢12mo ago
which is the only usecase generic controllers makes sense for, imho where you need 2-* controllers with the exact same endpoints and code, only changing what entity they handle
joren
jorenOPā€¢12mo ago
makes sense
Pobiega
Pobiegaā€¢12mo ago
but to answer your question
joren
jorenOPā€¢12mo ago
so in my example, does the auto mapper solve all my issues? or do I need a seperate DTO
Pobiega
Pobiegaā€¢12mo ago
Automapper takes care of Entity -> DTO
joren
jorenOPā€¢12mo ago
so I create one DTO, and the rest is taking care of by the auto mapper?
Pobiega
Pobiegaā€¢12mo ago
but as said, best practice is to have one DTO per endpoint that is meaningfully different no? You create several DTOs
joren
jorenOPā€¢12mo ago
for one model?
Pobiega
Pobiegaā€¢12mo ago
yes One for read, one for create, one for update, etc
joren
jorenOPā€¢12mo ago
that pains me
Pobiega
Pobiegaā€¢12mo ago
update must have an ID, for example while create does not, and read might need it most likely it will, but maybe not (if your system uses slugs etc)
joren
jorenOPā€¢12mo ago
im just a littel confused as to how my current setup works then
Pobiega
Pobiegaā€¢12mo ago
well you certainly can use only a single DTO but then you get no benefits at all from having it, imho its the same as if you used your entity :p
joren
jorenOPā€¢12mo ago
yes but how is that decided though
// PUT: api/[controller]/5
[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, TDto dto)
// PUT: api/[controller]/5
[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, TDto dto)
Pobiega
Pobiegaā€¢12mo ago
wdym?
joren
jorenOPā€¢12mo ago
take this for instance, this is in my base controller the TDto is passed, the type depends on template param meters of the BaseController public abstract class BaseController<TEntity, TRepository, TDto> in other words this makes no sense
Pobiega
Pobiegaā€¢12mo ago
yeah, thats why I said generic controllers are garbo
joren
jorenOPā€¢12mo ago
it'd be limited to one TDto, per controller
Pobiega
Pobiegaā€¢12mo ago
even for a PURE crud app, I'd say you need 3 dtos read/create/update and thats assuming you want fully editable models
joren
jorenOPā€¢12mo ago
so I can throw away my controller nonsense that's where im getting at
Pobiega
Pobiegaā€¢12mo ago
Thats my opinion, yes
joren
jorenOPā€¢12mo ago
generic part of it I can make it non generic, just an interface i guess just so I have my mapper and repository there for each controller
Pobiega
Pobiegaā€¢12mo ago
could still have it be generic, just not for the dto and not have the methods so you implement those in the child classes
joren
jorenOPā€¢12mo ago
true actually mhm, thanks, ill just fix that real quick and come back with some follow up questions
Pobiega
Pobiegaā€¢12mo ago
Alternatively make it be generic over 3 dto types šŸ™‚
joren
jorenOPā€¢12mo ago
I need to stop trying to make everything a generic so lets practice writing non generic code for once šŸ˜‰
Pobiega
Pobiegaā€¢12mo ago
hah sure šŸ™‚
joren
jorenOPā€¢12mo ago
@Pobiega I suppose its a silly idea having my base controller define the standard end points most tables have? Like, Get, Post, etc based on the primary key
Pobiega
Pobiegaā€¢12mo ago
depends?
joren
jorenOPā€¢12mo ago
not define, declare I mean
Pobiega
Pobiegaā€¢12mo ago
If you need them to always exist, then yes but what if you don
joren
jorenOPā€¢12mo ago
I dont know if that's normal though in my pokemon application its like, I suppose so
Pobiega
Pobiegaā€¢12mo ago
base controllers are not normal, as far as I'm concerned but I dont write CRUD apps, again very few of my controllers are identical
joren
jorenOPā€¢12mo ago
fair enough, I think its a silly idea then i wont lie
Pobiega
Pobiegaā€¢12mo ago
sure, a lot of them might have an /entity/{id} route, but not all
joren
jorenOPā€¢12mo ago
ye, its just a stupid idea ill just do it when I know it's 100% needed for all my controllers
Pobiega
Pobiegaā€¢12mo ago
hm there is an alternative to controllers btw
joren
jorenOPā€¢12mo ago
oh?
Pobiega
Pobiegaā€¢12mo ago
yeah, something called "minimal APIs" you literally map a route and a verb to a function in its simplest form There is a neat "framework" library that wraps that functionality very nicely called "FastEndpoints" I personally use that when I make new APIs, instead of controllers
joren
jorenOPā€¢12mo ago
Ill give their documentation a read, might be more suitable for me I hate this MCV (or whatever its called) pattern Management Control View? Model View Controller
Pobiega
Pobiegaā€¢12mo ago
yup
joren
jorenOPā€¢12mo ago
[Route("api/[controller]")]
[ApiController]
public class PokemonController : BaseController<Pokemon, EfCorePokemonRepository>
{
public PokemonController(EfCorePokemonRepository repository, IMapper autoMapper) : base(repository, autoMapper)
{
}
// GET: api/Pokemon
[HttpGet]
public async Task<ActionResult<IEnumerable<PokemonDTO>>> Get()
{
var items = await _repository.GetAll();
var dtoItems = _mapper.Map<IEnumerable<PokemonDTO>>(items);
return Ok(dtoItems);
}

// ...

// PUT: api/Pokemon/5
[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, PokemonDTO dto)
{
var entity = _mapper.Map<Pokemon>(dto);
entity.Id = id;

if (id != entity.Id)
{
return BadRequest();
}

await _repository.Update(entity);
return NoContent();
}
[Route("api/[controller]")]
[ApiController]
public class PokemonController : BaseController<Pokemon, EfCorePokemonRepository>
{
public PokemonController(EfCorePokemonRepository repository, IMapper autoMapper) : base(repository, autoMapper)
{
}
// GET: api/Pokemon
[HttpGet]
public async Task<ActionResult<IEnumerable<PokemonDTO>>> Get()
{
var items = await _repository.GetAll();
var dtoItems = _mapper.Map<IEnumerable<PokemonDTO>>(items);
return Ok(dtoItems);
}

// ...

// PUT: api/Pokemon/5
[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, PokemonDTO dto)
{
var entity = _mapper.Map<Pokemon>(dto);
entity.Id = id;

if (id != entity.Id)
{
return BadRequest();
}

await _repository.Update(entity);
return NoContent();
}
so this is currently my Pokemon controller, now it's still using the same DTO, for each endpoint so the idea is that I have different DTO's defined based on the end point correct?
PokemonCreateDTO // PUT too?
PokemonReadDTO // GET req
PokemonUpdateDTO // PUT req (and one more that i forgot)
PokemonDeleteDTO // DELETE req
PokemonCreateDTO // PUT too?
PokemonReadDTO // GET req
PokemonUpdateDTO // PUT req (and one more that i forgot)
PokemonDeleteDTO // DELETE req
Post is to create, iirc
Pobiega
Pobiegaā€¢12mo ago
something like that, ye the delete one might not need a dTO since its probably just an ID šŸ™‚ and thats given by the url(route)
joren
jorenOPā€¢12mo ago
ahhhhhhh its for the payload, we can make it smaller since the rest is irrelevant
Pobiega
Pobiegaā€¢12mo ago
ye
joren
jorenOPā€¢12mo ago
makes sense you are one smart individual, let me make some DTO's and ill send them here so you can double check if you want is it common practice to have all the TDO's in the same file? or more like the java route, where each class has its own file I feel like that'd be so overkill, considering the sizes of TDO's in general
Pobiega
Pobiegaā€¢12mo ago
I'd rather say I'm one experienced individual šŸ˜„ I've gone through all the pain points of not using DTOs, reusing a single DTO etc you can have either. when using Fast Endpoints I keep em in the same file as the endpoint itself when using controllers I usually bunch em up in a "Dtos.cs" file next to the controller
joren
jorenOPā€¢12mo ago
So this is my model:
public class Pokemon : IModelEntity
{
public int Id { get; set; }

public string Name { get; set; }

public DateTime BirthDate { get; set; }

public ICollection<Review> Reviews { get; set; }

public ICollection<PokemonOwner> PokemonOwners { get; set; }
public ICollection<PokemonCategory> PokemonCategories { get; set; }
}
public class Pokemon : IModelEntity
{
public int Id { get; set; }

public string Name { get; set; }

public DateTime BirthDate { get; set; }

public ICollection<Review> Reviews { get; set; }

public ICollection<PokemonOwner> PokemonOwners { get; set; }
public ICollection<PokemonCategory> PokemonCategories { get; set; }
}
Now let's say I wish to add a new Pokemon, it has to have a PokemonCategories assigned, doesnt need reviews but it could for instance? Same with owner, it could have a owner at some point so should I have a DTO for when u update the owner of a pokemon, or when a review is added wait those dont need DTO's, as it'd be a single field thinkingpensive
Pobiega
Pobiegaā€¢12mo ago
have the optional props be nullale in the dto
joren
jorenOPā€¢12mo ago
that translates to my API properly? i suppose so I guess
Pobiega
Pobiegaā€¢12mo ago
if PokemonCategory etc is also an entity, you'd have DTOs for them too btw
joren
jorenOPā€¢12mo ago
ye
Pobiega
Pobiegaā€¢12mo ago
well, in some way ye
joren
jorenOPā€¢12mo ago
it is a DTO for this controller or just in general you mean? for PokemonCategory
Pobiega
Pobiegaā€¢12mo ago
depends could be either its own for this controller, if it needs a special data subset or you re-use the "read" dto from the category endpoints thats more niche thou, and sort of violates the idea of DTOs I'd say that most of the time, you'd have a special sub-dto for this dto
joren
jorenOPā€¢12mo ago
how'd you make that? another class, and then use that class inside the DTO?
Pobiega
Pobiegaā€¢12mo ago
ye assuming source and destination have the same property names, and both dtos are mapped to their respective entity, automapper understands it
joren
jorenOPā€¢12mo ago
my model:
public class PokemonCategory
{
public int PokemonId { get; set; }

public int CategoryId { get; set; }

public Pokemon Pokemon { get; set; }
public Category Category { get; set; }
}
public class PokemonCategory
{
public int PokemonId { get; set; }

public int CategoryId { get; set; }

public Pokemon Pokemon { get; set; }
public Category Category { get; set; }
}
and then
public class PokemonCategorySubDTO
{
public int PokemonId { get; set; }

public int CategoryId { get; set; }
}

public class PokemonCreateDTO
{
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public ICollection<Review>? Reviews { get; set; }

public ICollection<PokemonOwner>? PokemonOwners { get; set; }
public ICollection<PokemonCategorySubDTO> PokemonCategories { get; set; }
}
public class PokemonCategorySubDTO
{
public int PokemonId { get; set; }

public int CategoryId { get; set; }
}

public class PokemonCreateDTO
{
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public ICollection<Review>? Reviews { get; set; }

public ICollection<PokemonOwner>? PokemonOwners { get; set; }
public ICollection<PokemonCategorySubDTO> PokemonCategories { get; set; }
}
Pobiega
Pobiegaā€¢12mo ago
yep!
joren
jorenOPā€¢12mo ago
and the reason I do this is, when I try to create, it will require me for the information I have in my PokemonCategorySubDTO, rather than the model itself?
[HttpPost]
public async Task<ActionResult<PokemonGetDTO>> Post(PokemonCreateDTO dto)
{
var entity = _mapper.Map<Pokemon>(dto);

await _repository.Add(entity);

var createdDto = _mapper.Map<PokemonGetDTO>(entity);
return CreatedAtAction(nameof(Get), new { id = entity.Id }, createdDto);
}
[HttpPost]
public async Task<ActionResult<PokemonGetDTO>> Post(PokemonCreateDTO dto)
{
var entity = _mapper.Map<Pokemon>(dto);

await _repository.Add(entity);

var createdDto = _mapper.Map<PokemonGetDTO>(entity);
return CreatedAtAction(nameof(Get), new { id = entity.Id }, createdDto);
}
So now this Post request, will require all fields except those that are nullable correct?
Pobiega
Pobiegaā€¢12mo ago
it wont be "required" unless you mark it as such with the required keyword
joren
jorenOPā€¢12mo ago
ah fair, makes sense how does my end point look, does it look good?
Pobiega
Pobiegaā€¢12mo ago
sure, yeah
joren
jorenOPā€¢12mo ago
now when I add a pokemon, it looks like this:
{
"name": "string",
"birthDate": "2023-12-09T18:50:55.382Z",
"reviews": [
{
"id": 0,
"title": "string",
"text": "string",
"reviewer": {
"id": 0,
"firstName": "string",
"lastName": "string",
"reviews": [
"string"
{
"name": "string",
"birthDate": "2023-12-09T18:50:55.382Z",
"reviews": [
{
"id": 0,
"title": "string",
"text": "string",
"reviewer": {
"id": 0,
"firstName": "string",
"lastName": "string",
"reviews": [
"string"
and a bunch more, seems like a large input to be given to add a pokemon... if I wanted to limit it to just adding a pokemon I'd change my DTO to just the pokemon data
Pobiega
Pobiegaā€¢12mo ago
yep
joren
jorenOPā€¢12mo ago
but my relations might get missed that way thats a risk I carry I guess I should mark whatever is required as required though that doesnt stop me from writing a incomplete DTO...
Pobiega
Pobiegaā€¢12mo ago
no, but ASP wont allow it the model binder will say "nah, you missed a required prop"
joren
jorenOPā€¢12mo ago
I see, makes sense
Want results from more Discord servers?
Add your server