The complete code is spanned through those posts:
- APIs with Entity Framework Core
- APIs with Entity Framework Core: POST
- APIs with Entity Framework Core: PUT
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.

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.