APIs with Entity Framework Core: PUT

APIs with Entity Framework Core

Continuing the topic, I want to give a complete example of minimal APIs in Blazor with Entity Framework Core with complex objects.I always struggle to have a solution working when my model has dependencies with other object. Here I show my test and my code. The code is in NET9. In the Microsoft documentation, there are some examples, but it is not complex enough. A few days ago, I posted about another problem I had with NET9 and Entity Framework Core.

The full source code of this post is available on GitHub. If you have any questions, please comment below or post in the forum.

The complete code is spanned through those posts:

This is the most difficult part of the implementation. There are a few steps to do to avoid errors and get the code working.

The initial code

First, the code created by the Scaffolded Item in Visual Studio has generated this code for the PUT verb in the minimal API:

group.MapPut("/{id}", async Task<Results<Ok, NotFound>> 
    (long id, Domain.Client client, MyDbContext db) =>
    {
        var affected = await db.Clients
            .Where(model => model.Id == id)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(m => m.Id, client.Id)
                .SetProperty(m => m.FirstName, client.FirstName)
                .SetProperty(m => m.LastName, client.LastName)
            );
        return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound();
    })
    .WithName("UpdateClient")
    .WithOpenApi();

This code takes into consideration only to update the record not the dependencies of the record. If I pass the json with the channels like in the following example

{
    "id": 3,
	"firstname": "UpdateTest1",
	"lastname": "UpdateTest2",
	"channels": [
		{
			"Id": 3,
			"name": "Other"
		},
		{
			"Id": 2,
			"name": "Bing"
		}
	]
}

Entity Framework Core raises an error because it can’t update the record. Here is the screenshot of the error.

The error occurs when I try to update - APIs with Entity Framework Core: PUT
The error occurs when I try to update

When an object – like in my case the Client class – is updated, there are a few things to take into consideration:

  • do I have to update only the record?
  • do I have to update the dependencies?
  • how to add or remove the dependencies from an object?

So, the solution is a bit longer than expected.

Add the Domain mapper

First, I have to add a mapping between objects. In this case, the mapping will be between the same object. Why? When the PUT method receives the request, we have to be able to match the properties from the parameter with the properties of the record from the database. This is because we want to have the latest representation of the object and then apply the changes.

So, the first step is to add a new project for mapping the Domain objects with DTO (Data Transfer Object). In this case, I don’t have DTOs but only the model from the Domain.

To do the mapping, I use AutoMapper. AutoMapper is an object-object mapper. Object-object mapping works by transforming an input object of one type into an output object of a different type. What makes AutoMapper interesting is that it provides some interesting conventions to take the dirty work out of figuring out how to map type A to type B. As long as type B follows AutoMapper’s established convention, almost zero configuration is needed to map two types.

So, instead of creating the mapper manually, I use this tool. In this simple example could be easy and quicker to map myself the object. Because I want to use this project as a reference, I add everything I need.

Add the registration service

As I did for the persistence layer, I’m creating a file called MapperRegistration that contains all the required configurations for this layer, in particular for AutoMapper. The file is quite simple:

public static class MapperRegistration
{
    public static IServiceCollection AddMapperServices(this IServiceCollection services)
    {
        services.AddAutoMapper(Assembly.GetExecutingAssembly());
        return services;
    }
}

Once this file is created, I can call this function in the server project in the Program.cs and use it with like

builder.Services.AddMapperServices();

Nice and easy.

Add the AutoMapper profile

Now, the Profile explains to AutoMapper what models are a match. In the case of this project, the models are Client and Channel. Here is the code for this file

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<Client, Client>()
            .ForMember(x => x.Channels, opt => opt.Ignore());
        CreateMap<Channel, Channel>();
    }
}

Although the name of this file can be anything, by convention the name is MappingProfile. The definition of the mapping is in the Create.Map that explains to AutoMapper what model can be map to what other object. Usually, you have a Domain model and a DTO (Data Transfer Object). To simplify, in this project, the minimal APIs receive as a parameter an object in the Domain structure.

Add the mapper to the server

Atter all of it, I can add the mapper to the server project. So, I open the _Program.cs_ and add this line

builder.Services.AddMapperServices();

before builder.Build();. Now, I am ready to implement the logic in the minimal API.

Update the PUT verb for Client

Now, this is the most complicated part of the project. When the application has to update the Client object, I have to bear in mind that the dependencies for Channel can be added or removed for the update. Also, I have to remember that only the changes to an object database can be saved in the database again. If I try to save an update for an object that is not coming from the database or I add elements from another object that is not coming from the database, I will face an error of different nature.

So, as a general rule, you can update an object in the database with only values from the database. What I have to do now is:

  • read the full object from the database
  • identify the updated channels from the PUT parameter
  • identity the current channels in the database
  • identify the channels to add
  • identify the channels to remove
  • save the object in the database

Read the full object

This is quite straightforward. This is the code

var localClient = await db.Clients
    .Include((c => c.Channels))
    .FirstAsync(model => model.Id == id);

Here the code is reading from the database the full Client object with all the Channels.

Map the new object

After that, I have to map the new object from the parameter of the function with the record from the database. Because I need an AutoMapper instance, I have to change the signature of the function like the following code:

group.MapPut("/{id}", async Task<Results<Ok, NotFound>> (long id, Domain.Client client, 
    MyDbContext db,
    IMapper mapper) =>
    {
        // ...
    }

So, I added IMapper mapper to get the instance. Now, I can map the object like

mapper.Map(client, localClient);

Now, AutoMapper using the reflection is mapping all the properties of the object from the parameter of the function with the record from the database apart from the Channels properties as we set before.

What to save and what to remove

So, the next step is to identify if there is any change in the Channels object. I have to list what channels to add to the record. I also have to list what to remove from the record.

var updatedChannelIds = client.Channels.Select(c => c.Id).ToList();
var currentChannelIds = localClient.Channels.Select(c => c.Id).ToList();
var channelIdsToAdd = updatedChannelIds.Except(currentChannelIds);
var channelIdsToRemove = currentChannelIds.Except(updatedChannelIds);

Now that I know the IDs of the channels to add and remove, I can implement this part.

Remove channels

First, I am going to remove the channels from the real record using the channel’s records from the database. I convert the query into a list to avoid future errors.

if (channelIdsToRemove.Any())
{
    var channelsToRemove =
        localClient.Channels.Where(c => channelIdsToRemove.Contains(c.Id)).ToList();
    foreach (var channel in channelsToRemove)
        localClient.Channels.Remove(channel);
}

Add channels

Next step is to add from the database the new list of channels.

if (channelIdsToAdd.Any())
{
    var channelsToAdd = await db.Channels.Where(c => channelIdsToAdd.Contains(c.Id)).ToListAsync();
    foreach (var channel in channelsToAdd)
        localClient.Channels.Add(channel);
}

Save the updated record

await db.SaveChangesAsync();
return TypedResults.Ok();

Video

Finally, if you want to follow me in the creation of this project, watch the following video.

Wrap up

Finally, I have a decent project to use as a future reference based on minimal APIs. The PUT implementation was a bit longer. It was necessary because updating a record with dependencies can be tricky.

Related posts

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.