In the whirlwind of new technologies whizzing by us continuously, it is sobering to remember that most of a developer’s role is to get data from one location to another. In the case of HTTP APIs, the response we return represents the data stored in our database de jour. For .NET developers, working with ASP.NET Core and Entity Framework Core are commonplace.
In this post, we’ll try to help folks starting with the combination of ASP.NET Core and Entity Framework Core (EF Core) to deliver HTTP APIs and look at some common approaches to structuring endpoints. We’ll start with a common pitfall from a lack of understanding about EF Core usage and work our way to some solutions.
The Database Model
In most cases, developers reaching for EF Core will be working with a relational database (RDBMS). EF Core supports SQL Server, SQLite, PostgreSQL, MySQL, and Oracle. These database engines have their quirks but generally, work similarly. The advantage of using an RDBMS is that it allows us to model bidirectional relationships that might also include some hierarchical importance.
In this post, we’ll model a Company
and Employee
model.
We can see that a Company
has a collection of Employee
and that an Employee
refers to a Company
. The relationship is cyclical, a normal relationship, but we’ll see how it causes issues later.
Our complete EF Core database context looks like this.
In the data seeding process, we add a new employee who works at a company. Most folks who are familiar with EF Core will be comfortable with this implementation, and those new to EF Core should be able to comprehend the model.
Now, let’s move on to exposing this data through an ASP.NET Core HTTP endpoint.
The ASP.NET Core Endpoints
The most robust way of building HTTP endpoints with ASP.NET Core is by utilizing the Model-View-Controller approach. Developers get extension points for request/response serialization, validation, and authorization. In our case, we’ll be using the JSON serialization provided by System.Text.Json
.
Let’s start with the most common mistake a new EF Core/ASP.NET Core developer might make. If you’ve come to this post via a search, this is likely the answer you need.
EF Core Anti-Pattern Endpoint
Some folks may have seen some demos and samples where an endpoint returns an EF Core model directly. This “works” for simple relational models with non-hierarchical relationships, but it is a bad idea. Let’s make a mistake with our database model and see what happens.
As the comment in the code says, this is a big mistake. EF Core queries are the equivalent of lit dynamite sticks. LINQ queries pack a lot of power, but we need to understand and respect them. Hitting this endpoint, we will see the following exception.
The issue comes from our relationship between Company
and Employee
and the impedance mismatch between our database and the objects we’re expecting to be output over HTTP. In simpler terms, the ASP.NET Core serializer doesn’t know when to stop following the navigation properties of Company
and Employees
, so it keeps following the properties until it throws an exception.
Again, Don’t write code like this! It will explode. If not immediately, it will become an issue as our data model evolves.
EF Core Projection Option
Using EF Core requires a mental model that some developers may gloss over. EF Core can lull some into thinking, “well, this is just C# right?!” Not exactly.
EF Core is a query interface powered by LINQ. While we define types using C# classes, the types and queries represent our database’s entities and concepts (SQL), a subtle yet significant distinction.
Switching to that mindset, we can think about writing a query that expresses our intent and eventual JSON response.
In this solution, we lean on LINQ and anonymous object projection. When running this endpoint, the JSON serializer no longer has a cyclical issue as we’ve terminated the relationship correctly.
Great! We’ve solved our serialization issue, but let’s keep going. A problem with this approach is that our top-level response is an array. The approach leaves no room to evolve our response and add more information to our endpoint.
EF Core Projection With Anonymous Wrapper
In the previous section, we solved our cyclical exception but introduced a structural issue to an endpoint. There’s no way to add metadata to our response, metadata like total item count, pages, cursors, and statistical data.
It’s a good practice to wrap a response in a parent object to evolve the response without breaking clients. Let’s modify our endpoint.
In this altered endpoint, we wrap our results in another anonymous object and set the results to a Results
field. Let’s see how that affects the JSON.
Great! At this point, we could stop. We’ve solved some major stumbling blocks that folks starting with HTTP APIs might encounter. That said, let’s think about strongly-typing our responses.
Strongly-Typed Responses
While the approach in the previous section works, it is prone to developer error. When creating responses, we can make Response
models that we can use across multiple endpoints. We have the added benefit of clearly understanding the schema our API endpoints expose to our consuming clients. Structured models can be helpful for code-generation scenarios using OpenAPI.
Here are the strongly typed response models.
In this case, they are similar to our database models, but they have an opportunity to evolve and change separate from our database technology. A necessary separation as where we read our data could change as our needs evolve. Today we’re reading our data entirely from an RDBMS, but tomorrow we may be aggregating data for this endpoint using a search engine like Elasticsearch.
Let’s see what the endpoint looks like after moving to strongly-typed response models.
As you can see, it’s not very different than our previous approach. This approach begins to benefit us when we have multiple endpoints that return the same response objects. In this case, we may produce a CompanyResponse
from a single response endpoint like /companies/{id}
.
Conclusion
There is an assortment of powerful technologies in the .NET space, but developers can run into problems when using them together. In ASP.NET Core and EF Core implementations, developers might accidentally take the “easy” path and return the EF objects directly from a LINQ Query. The approach is a common mistake, as these EF entities may still connect to the database context. In our case, cyclical relationships can cause the JSON serializer to follow that cyclical relationship until it throws a JsonException
. We walked through several solutions to solve this issue, with all solutions leveraging some idea of query projection. This post shows the basic solution, and developers can expand on it by adding mapping libraries or more architecture if they choose to.
Please visit this GitHub repostiory to see a working sample of the code found in this post.
I hope you found this post useful, and good luck building your HTTP APIs. Please leave a comment below.