C
C#4w ago
iyyel

✅ [.NET Minimal API's] How to design expansion of data through my API?

Hi guys, I am designing a small toy project, basically a flash card application where a user can create sets of flash cards. I have the following models:
public class FlashCard
{
public required Guid Id { get; set; }
public required Guid SetId { get; set; } // <-- A flash card can not exist outside of a set, therefore reference to its set of belonging
public required Guid UserId { get; set; }
public required string Title { get; set; }
public required string Question { get; set; }
public required string Answer { get; set; }
}
public class FlashCard
{
public required Guid Id { get; set; }
public required Guid SetId { get; set; } // <-- A flash card can not exist outside of a set, therefore reference to its set of belonging
public required Guid UserId { get; set; }
public required string Title { get; set; }
public required string Question { get; set; }
public required string Answer { get; set; }
}
public class FlashCardSet
{
public required Guid Id { get; set; }
public required Guid UserId { get; set; }
public required string Title { get; set; }
public required string Description { get; set; }
public required List<Guid> CardIds { get; set; } = [];
}
public class FlashCardSet
{
public required Guid Id { get; set; }
public required Guid UserId { get; set; }
public required string Title { get; set; }
public required string Description { get; set; }
public required List<Guid> CardIds { get; set; } = [];
}
I have a (more or less) CRUD repository for each model and a single FlashCardService of which both repositories are injected into. This works fine. Let's say I have an API endpoint that gets a specific flash card set from its Id, which will return the model above.
public static async Task<IResult> GetSet(Guid setId, IFlashCardSetService service)
{
var result = await service.GetSetAsync(setId);
return result.IsSuccess
? Results.Ok(result.Value!.MapToGetFlashCardSetResponse())
: MapError(result.Error);
}
public static async Task<IResult> GetSet(Guid setId, IFlashCardSetService service)
{
var result = await service.GetSetAsync(setId);
return result.IsSuccess
? Results.Ok(result.Value!.MapToGetFlashCardSetResponse())
: MapError(result.Error);
}
where the service results is mapped to
public record GetFlashCardSetResponse(Guid Id, Guid UserId, string Title, string Description, List<Guid> CardIds);
public record GetFlashCardSetResponse(Guid Id, Guid UserId, string Title, string Description, List<Guid> CardIds);
Now, If a consumer wants to retrieve the data for each flash card, they would have to iterate through the list of card id's and then call a Get API endpoint for each card id. I would argue that this is a bad experience for the consumer. A lot of work to retrieve some simple data. Now, this is where my question arises. What is a proper and idiomatic way of being able to expand the API response with actual flash card data instead of just the ID's? The only (and perhaps obvious) way I have thought about is simply to add a new model, like so
public class FlashCardSetWithCards
{
public required Guid Id { get; set; }
public required Guid UserId { get; set; }
public required string Title { get; set; }
public required string Description { get; set; }
public required List<FlashCard> Cards { get; set; } = [];
}
public class FlashCardSetWithCards
{
public required Guid Id { get; set; }
public required Guid UserId { get; set; }
public required string Title { get; set; }
public required string Description { get; set; }
public required List<FlashCard> Cards { get; set; } = [];
}
and then add a new method in the FlashCardSetService that basically does the iteration I talked about before, which can then be used in the same endpoint, like so:
public static async Task<IResult> GetSet(Guid userId, Guid setId, bool? expandCards, IFlashCardSetService service)
{
if (expandCards is true)
{
var resultWithCards = await service.GetSetWithCardsAsync(userId, setId);
return resultWithCards.IsSuccess
? Results.Ok(resultWithCards.Value!.MapToGetFlashCardSetWithCardsResponse())
: MapError(resultWithCards.Error);
}
else
{
var result = await service.GetSetAsync(userId, setId);
return result.IsSuccess
? Results.Ok(result.Value!.MapToGetFlashCardSetResponse())
: MapError(result.Error);
}
}
public static async Task<IResult> GetSet(Guid userId, Guid setId, bool? expandCards, IFlashCardSetService service)
{
if (expandCards is true)
{
var resultWithCards = await service.GetSetWithCardsAsync(userId, setId);
return resultWithCards.IsSuccess
? Results.Ok(resultWithCards.Value!.MapToGetFlashCardSetWithCardsResponse())
: MapError(resultWithCards.Error);
}
else
{
var result = await service.GetSetAsync(userId, setId);
return result.IsSuccess
? Results.Ok(result.Value!.MapToGetFlashCardSetResponse())
: MapError(result.Error);
}
}
This way, I would say it is a nice experience for the user, as all they would have to do, is the add the
?expandCards=true
?expandCards=true
query parameter to expand the ids to actual card data. What do you guys think? Is there a more clean way to achieve this? I hope it makes sense, thanks.
4 Replies
Angius
Angius4w ago
I'd use another endpoint instead of a query param
iyyel
iyyelOP4w ago
Any particular reason why you prefer this approach? I suppose using another endpoint makes it even more clear what each endpoint is responsible for and thus is a nice separation.
Angius
Angius4w ago
Yes, clarity and separation of concerns If you then want to add, say, a view with paginated cards, or with only card previews, or whatever else, you just add a new endpoint Not another &previewOnly=true
iyyel
iyyelOP4w ago
Those are some good points. Thanks 🙂

Did you find this page helpful?