Exposing Related Entities in your Web API

Exposing Related Entities in your Web API

When working on building a powerful API you will find yourself dealing with resources and more resources and these “resources” have “relations” between them and this is powerful as we can leverage the power of relational data to create amazing ways of displaying information or use it. But if you don’t really understand how to define this relationship as I showed you in my last article about Relationships in Entity Framework Core or you don’t know how to make them work together as you will see later in this post, then it can be a nightmare to work with primary keys, constraints, LINQ, etc. 😰

So for that reason, it’s a necessary skill to know how to expose and represent the related entities from your web API resources. This article will teach you how to achieve that while building meaningful endpoints to get the required resources.

Related entities refer to interconnected entities or objects within a data model. In the context of databases and ORMs (Object-Relational Mapping) like Entity Framework Core, related entities represent the associations and relationships between different tables or entities in a database schema.

These relationships come in different flavors, such as one-to-one, one-to-many, or many-to-many. They basically tell us how the pieces fit together. For example, in a blog application, a post may have one author (one-to-one), or an author can have multiple posts (one-to-many). These relationships bring structure and meaning to our data.

Understanding these relationships is like unlocking the secrets of your data model. It allows you to create more complex and intertwined models, giving you the power to fetch and manipulate related data effortlessly. You can easily access information from different entities, perform queries, and build powerful applications.

To sum it up, related entities are the links between data pieces in your database. They give your data model its structure and define how the different parts work together. With a good grasp of these relationships, you can navigate through your data, create efficient queries, and build amazing applications.

Exposing related entities in ASP.NET Core Web API involves making those connections accessible and retrievable to clients who interact with your API. By exposing related entities, you allow clients to access and manipulate interconnected data in a meaningful way.

There are several approaches you can take to achieve this:

  1. Nested Serialization: One way is to include related entities as nested objects within the JSON response. For example, when retrieving a blog post, you can also include the author information as part of the response, making it easy for clients to access both the post and its author in a single request.

  2. Expandable Endpoints: Another approach is to provide expandable endpoints, where clients can specify which related entities they want to include in the response. This gives clients more control over the data they receive, reducing unnecessary data transfer and improving performance.

  3. Hypermedia Links: Hypermedia-driven APIs use links to represent relationships between entities. By including links in your API responses, clients can navigate through related entities by following these links. This approach provides a more dynamic and flexible way to expose related entities.

  4. Custom Endpoints: Depending on your application's requirements, you can create custom endpoints specifically designed to retrieve related entities. For example, you might have an endpoint that returns all the comments associated with a blog post or all the products in a specific category.

When exposing related entities, it's essential to consider factors like performance, data size, and security. You want to strike a balance between providing enough related data for clients to work with and avoiding excessive data transfer that could impact performance.

In this article, we will end up using a mix of those approaches to return the client all the resources needed.

Get the code from GitHub 🐙

As always before we get into the code if you want to follow along with this tutorial make sure you get the code from the GitHub repository and double-check that you are getting the code from the “EfCoreRelationship” branch.

GitHub - Osempu/BlogAPI

Updating the PostRepository class ⤴️

The PostRepository class has been retrieving the posts from the database but it lacks power as it is not returning the author for every repository only the authorId and neither is returning the tags that the Post has so for that reason we will extend the functionality of the repository class updating its methods and adding two new methods.

IRepository class updated ⬆️

public interface IPostRepository 
{
    Task<IEnumerable<Post>> GetPostAsync(QueryParameters parameters);
    Task<Post> GetPostAsync(int id);
    Task<IEnumerable<Post>> GetPostByAuthorId(int id);
    Task<IEnumerable<Post>> GetPostByTagId(int id);
    Task AddAsync(Post post);
    Task EditAsync(Post post);
    Task DeleteAsync(int id);
}

As you can see there are two new methods GetPostByAuthorId and GetPostByTagId this is to retrieve all the posts from the user and all the posts related to a specific tag which is the normal filter functionality of any existing blog.

PostRepository Class 📫

public async Task<IEnumerable<Post>> GetPostAsync(QueryParameters parameters)
{
        //Code ommited for brevity
    var pagedPosts = await allPosts
            .Skip((parameters.PageNumber - 1) * parameters.PageSize)
            .Take(parameters.PageSize)
            .Include(x => x.Author)
            .Include(x => x.Tags)
            .ToListAsync();

    return pagedPosts;
}

public async Task<Post> GetPostAsync(int id)
{
    var post = await context.Posts.AsNoTracking()
                                    .Where(x => x.Id == id)
                                    .Include(x => x.Author)
                                    .Include(x => x.Tags)
                                    .FirstOrDefaultAsync();
    return post;
}

public async Task<IEnumerable<Post>> GetPostByAuthorId(int id)
{
    var posts = await context.Posts.AsNoTracking()
                                    .Where(x => x.AuthorId == id)
                                    .Include(x => x.Author)
                                    .Include(x => x.Tags)
                                    .ToListAsync();
    return posts;
}

public async Task<IEnumerable<Post>> GetPostByTagId(int id)
{
    var posts = await context.Posts.AsNoTracking()
                                    .Include(x => x.Author)
                                    .Include(x => x.Tags)
                                    .Where(x => x.Tags.Any(t => t.Id == id))
                                    .ToListAsync();

    return posts;
}

The first method is the GetPostAsync that we created to retrieve a paginated response for all the posts. Here your work is to include the author of the post and the related tags for every post. So in the final query before calling the ToListAsync you will add two calls to the Include method one for the author and the second for the tags, this way your post response will now include those data sets.

The next method is the GetPostByAuthorId method which will return all the posts belonging to a single author. First, you filter the post from the author with the corresponding Id and then again you call two Include methods to add the author and tag data to the response.

Finally, the GetPostByTagId is similar to the GetPostByAuthorId but you will notice that first there are the two calls to the Include method and then a filter using where to filter by the specified Tag. This must be done this way because the PostTag relation is a many-to-many relationship therefore there is not a single tag to filter from as with the Author but a set of tags and we need to select the one we desire to filter for.

Foreseeing Self-Reference Loop from Property Error ➿

Now you would likely go to the Post controller and update it to test the new changes right? Well, it’s not that simple because if you go and actually do that then you will get a self-reference error because when you try to get a list of posts you will get the author with it, and then you would get the posts again and you will get it’s author and you kinda see where this is going right? Yep, it’s an infinite loop and you know that programming and infinite loops don’t get along in most of cases so to avoid that you will have to use Dtos to return your resources because something you should not do is to return your data models as they are, it’s always a good practice to create some Dtos to only return the data we want to show and avoid this kind of problems and the second thing you must do in order to avoid an infinite loop in your response is to add some configurations to NewtonsoftJson

.AddNewtonsoftJson(options => options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);

The above line of code will prevent the reference loop to continue over and over by ignoring the request to be continuously returning the nested JSON.

Adding some new Dtos 🏭

public record AuthorOnlyResponseDto(int Id, string Name);
public record TagOnlyResponseDto(int Id, string Name, string Description);

As said earlier you will need to use Dtos to return the resources in this case we are returning the author without the collection of Post and the Tag without the collection of Post as well.

Updating PostResponseDto 📬

public record PostResponseDTO(
  [property: JsonPropertyOrder(1)] int Id,
  [property: JsonPropertyOrder(2)] string Title,
  [property: JsonPropertyOrder(3)] string Body,
  [property: JsonPropertyOrder(4)] DateTime CreatedDate,
  [property: JsonPropertyOrder(5)] AuthorOnlyResponseDto Author,
  [property: JsonPropertyOrder(6)] ICollection<TagOnlyResponseDto> Tags);

In this Dto there are some things happening. First, we are adding the JsonPropertyOrder attribute to set the order in which every property will be returned to have a nicely formatted response because otherwise the properties are ordered alphabetically for the Author now we return an AuthorOnlyResponse so we just return the author resource without the nested Post collection and finally, we return a collection of TagOnlyResponseDto to return only the collection of Tag without the nested Post.

Adding the new mapping profiles 🗺️

Now to have your Dtos working add the corresponding mappings to their profile classes as shown below.

public AuthorProfiles()
{
    CreateMap<Author, AuthorOnlyResponseDto>();
}

public TagProfiles()
{
    CreateMap<Tag, TagOnlyResponseDto>();
}

Updating the AuthorsController ✍🏾

After the changes in the PostRepository class there is no need to update the PostsController as all we needed in the controller was to return the author and tags related to the post and that is done in the repository class so now you have to update the Authors and Tags controllers to add an endpoint to return all the post under a certain author and the same for the tags.

[ApiController]
[Route("api/authors")]
public class AuthorsController : ControllerBase
{
    private readonly IAuthorRepository repository;
    private readonly IPostRepository postRepository;
    private readonly IMapper mapper;

    public AuthorsController(IAuthorRepository repository, IPostRepository postRepository, IMapper mapper)
    {
        this.repository = repository;
        this.postRepository = postRepository;
        this.mapper = mapper;
    }

        [HttpGet]
      public async Task<IActionResult> GetAuthor()
      {
          var authors = await repository.GetAsync();
          var authorsDto = mapper.Map<IEnumerable<AuthorOnlyResponseDto>>(authors);
          return Ok(authorsDto);
      }

        [HttpGet("{id:int}")]
        public async Task<IActionResult> GetAuthor(int id)
        {
            var author = await repository.GetAsync(id);
            var authorDto = mapper.Map<AuthorOnlyResponseDto>(author);
            return Ok(authorDto);
        }

        [HttpGet("{id:int}/posts")]
        public async Task<IActionResult> GetPostByAuthorId(int id)
        {
            var posts = await postRepository.GetPostByAuthorId(id);
            var postDto = mapper.Map<IEnumerable<PostResponseDTO>>(posts);

            return Ok(postDto);
        }
}

You will need to add a reference to the IPostRepository interface to get the collection of posts from an author. Then on both Get methods you only have to map the response from Author to an AuthorOnlyResponseDto to return only the author resource and not its nested resources.

Finally, you will have to add a new endpoint names GetPostByAuthorId which will retrieve all the posts from a certain author. First, you have to specify the route as {id:int}/posts so the way of calling this endpoint would be like this api/authors/{authoId}/posts and this will give us all the posts from the specified author. The code is pretty much like both previous get methods but instead of using the author repository here, you will need to call the postRepository.GetPostByAuthorId method and after that map the response into a PostResponseDto.

Updating the TagsController 🏷️

[ApiController]
[Route("api/tags")]
public class TagsController : ControllerBase
{
    private readonly ITagRepository repository;
    private readonly IPostRepository postRepository;
    private readonly IMapper mapper;

    public TagsController(ITagRepository repository, IPostRepository postRepository ,IMapper mapper)
    {
        this.repository = repository;
        this.postRepository = postRepository;
        this.mapper = mapper;
    }

    [HttpGet]
    public async Task<IActionResult> GetTag()
    {
        var tags = await repository.GetAsync();
        var tagsDto = mapper.Map<IEnumerable<TagOnlyResponseDto>>(tags);
        return Ok(tagsDto);
    }

    [HttpGet("{id:int}")]
    public async Task<IActionResult> GetTag(int id)
    {
        var tag = await repository.GetAsync(id);
        var tagDto = mapper.Map<TagOnlyResponseDto>(tag);
        return Ok(tagDto);
    }

    [HttpGet("{id:int}/posts")]
    public async Task<IActionResult> GetPostFromTagId(int id)
    {
        var posts = await postRepository.GetPostByTagId(id);
        var postsDto = mapper.Map<IEnumerable<PostResponseDTO>>(posts);
        return Ok(postsDto);
    }
}

For the TagsController the thing is pretty much the same, you have to inject the IPostRepository interface and then update the get methods mapping the response to a TagOnlyResponseDto and finally add a new endpoint to get all the Posts under a certain Tag.

Testing the API 🧪

Now let’s proceed to test the new endpoints and check out our updated responses.

Getting all the Authors ✍🏾

[
  {
    "id": 4,
    "name": "John Doe"
  },
  {
    "id": 5,
    "name": "Oscar Montenegro"
  },
  {
    "id": 6,
    "name": "Yolanda Montenegro"
  }
]

In the authors controller we are getting only the authors id and name, exposing only the data we ant without exposing the nested collection of post for every author for that we will use the new endpoint.

Getting all the Posts from an Author ✍🏾

[
  {
    "id": 11,
    "title": "Setting up a CI/CD Pipeline",
    "body": "Setup a CI/CD Pipeline using GitHub actions",
    "createdDate": "2023-05-29T13:25:52.0306732",
    "author": {
      "id": 5,
      "name": "Oscar Montenegro"
    },
    "tags": [
      {
        "id": 2,
        "name": "Programming",
        "description": "Topics related to programming"
      }
    ]
  },
  {
    "id": 13,
    "title": "GitHub basics",
    "body": "Check out this amazing series on the GitHub basics to start working on a code repository",
    "createdDate": "2023-05-30T14:25:48.3537618",
    "author": {
      "id": 5,
      "name": "Oscar Montenegro"
    },
    "tags": [
      {
        "id": 1,
        "name": "My first Tag",
        "description": "This is the first Tag"
      },
      {
        "id": 2,
        "name": "Programming",
        "description": "Topics related to programming"
      },
      {
        "id": 4,
        "name": "DevOps",
        "description": "Learn the latest DevOps trends and news"
      }
    ]
  }
]

Here we are getting all the posts for the author with an id of 5 and what makes sense when you get a Post is to get the author from that post and the related tags for that post as well. For that reason, this is the optimal Post response exposing the related and relevant entities. The response is nicely formated presenting the post properties and then exposing the author and finally an array of tags.

Getting all the Tags 🏷️

[
  {
    "id": 1,
    "name": "My first Tag",
    "description": "This is the first Tag"
  },
  {
    "id": 2,
    "name": "Programming",
    "description": "Topics related to programming"
  },
  {
    "id": 4,
    "name": "DevOps",
    "description": "Learn the latest DevOps trends and news"
  }
]

Now the tags are returning only the properties we want to expose on this endpoint being this the name and the description and the collection of posts related to each tag can be retrieved in a separate endpoint.

[
  {
    "id": 11,
    "title": "Setting up a CI/CD Pipeline",
    "body": "Setup a CI/CD Pipeline using GitHub actions",
    "createdDate": "2023-05-29T13:25:52.0306732",
    "author": {
      "id": 5,
      "name": "Oscar Montenegro"
    },
    "tags": [
      {
        "id": 2,
        "name": "Programming",
        "description": "Topics related to programming"
      }
    ]
  },
  {
    "id": 13,
    "title": "GitHub basics",
    "body": "Check out this amazing series on the GitHub basics to start working on a code repository",
    "createdDate": "2023-05-30T14:25:48.3537618",
    "author": {
      "id": 5,
      "name": "Oscar Montenegro"
    },
    "tags": [
      {
        "id": 1,
        "name": "My first Tag",
        "description": "This is the first Tag"
      },
      {
        "id": 2,
        "name": "Programming",
        "description": "Topics related to programming"
      },
      {
        "id": 4,
        "name": "DevOps",
        "description": "Learn the latest DevOps trends and news"
      }
    ]
  }
]

This response is produced after calling the api/tags/2/posts endpoint which returns all the posts related to the programming tag and as before you can see that the response contains the post properties along with the author of the post and the related tags.

Posts Controller 🏤

The response in the posts controller will look the same as before so for that reason I will not post the response to save you from reading the same 😅 but is important to say again that we have formatted the post response just the way we wanted and in a way that makes sense so whenever the client looks for a post or a collection of posts they contain all the needed data to display the post with the author name and the collection of tags for that post.

Conclusion 🌇

To wrap it up, exposing related entities in your web API opens up a world of possibilities for working with interconnected data. By making these relationships accessible to clients, you empower them to effortlessly retrieve, modify, and navigate through linked data.

It's crucial to strike a balance between providing enough related data and optimizing performance when designing your API endpoints and responses. Factors like data size, performance impact, and security requirements should be taken into account to ensure a smooth and efficient experience for API users.

As you continue to develop your web API, make use of the strategies and techniques discussed in this article to effectively expose related entities. Remember to align your approach with the specific requirements and objectives of your application.

Armed with the knowledge and understanding of how to expose related entities, you can take your ASP.NET Core Web API to new heights. By implementing the right approach, you'll create an API that offers seamless navigation, robust data retrieval, and efficient manipulation of interconnected data.

We’ve reached 3K views on Unit Coding 🥳🎉🎊

I’m so happy to share that the last weekend after posting my article about Entity framework relationships I was expecting to reach 1K monthly views but suddenly when I woke up on Saturday I checked and there were more than 2K views only in one night! Man, I was so happy and today we passed the 3K views. I want to thank all of you guys that have been checking out my content and constantly reading, without you this would have not been possible so this is an achievement for all of us that make part of this community.

Also, I want to share with you that this series has somewhere 10 posts before it comes to an end and I’m already getting ready for all the new series I want to launch for example a series on Web App development with Blazor, ASP NET MVC, LINQ and so many more and after finishing this series on web API development I will announce the launch of a surprise to all of you that have been supporting me all along this amazing journey of becoming a writer it will be free so star posted so you don’t lose it.

There are so many words in my mind that I cannot express and so many things have been happening since the beginning of this journey. I’m on my third month unemployed, it’s been hard and I’m almost running out of money (no joke 😨) but I know I’m not out of options and there are some things that I can do before that happens. Well, I think that I must say goodbye for now because I’m making this longer than it should be 😂 thanks for your kind support and beautiful words, remember to follow me on my blog Unit Coding, and on my YouTube channel under the same name and if you like Twitter you can find me there as @OscarOsempu I’m still trying to get constant at twitting but it’s sometimes hard. Thanks and see you in the next chapter! Happy coding! 👋🏾

Did you find this article valuable?

Support Unit Coding by becoming a sponsor. Any amount is appreciated!