I've been reading a lot about this issue here in stackoverflow, however, I can't seem to solve the problem I'm having. I have a simple web api that shows information about movies: The movies itself with a list of actors and a list of directors.
The GET endpoints work just fine showing the information I need. The problem I have is with the POST endpoints since the Actors
and Directors
are being duplicated in my DB (which is a simple SQLite DB).
These are my model classes:
public sealed class Movie
{
public int Id { get; init; }
public string Title { get; init; }
public string Synopsys { get; init; }
public DateOnly ReleaseDate {get; init; }
public Genre Genre { get; init; }
public ICollection<Actor> Actors { get; init; }
public ICollection<Director> Directors { get; init; }
}
public sealed class Actor
{
public int Id { get; init; }
public string Name { get; init; }
public DateOnly DateOfBirth { get; init; }
public string Info { get; set; }
public ICollection<Movie> Movies { get; set; }
}
public sealed class Director
{
public int Id { get; init; }
public string Name { get; init; }
public DateOnly DateOfBirth { get; init; }
public string Info { get; init; }
public ICollection<Movie> Movies { get; init; }
}
Next, I have an Application DB context where I define my db sets and also the many to many relationships to work with EF Core. This section creates the tables Movies, Actors, Directors in the DB as well as the join tables MovieActors and MovieDirectors
public class ApplicationDBContext : DbContext
{
public ApplicationDBContext(DbContextOptions dbContextOptions)
: base(dbContextOptions)
{
}
public DbSet<Movie> Movies { get; set; }
public DbSet<Actor> Actors { get; set; }
public DbSet<Director> Directors { get; set; }
protected override void OnModelCreating (ModelBuilder modelBuilder)
{
modelBuilder.Entity<Movie>()
.HasMany(a => a.Actors)
.WithMany(m => m.Movies)
.UsingEntity(am => am.ToTable("MovieActors"));
modelBuilder.Entity<Movie>()
.HasMany(d => d.Directors)
.WithMany(m=> m.Movies)
.UsingEntity(md => md.ToTable("MovieDirectors"));
}
}
And here I have my repository methods that are being used by the api controller:
public class MovieRepository(ApplicationDBContext context) : IMovieRepository
{
private readonly ApplicationDBContext context = context;
public async Task<Movie> CreateAsync(Movie movie)
{
context.Movies.Add(movie);
await context.SaveChangesAsync();
return movie;
}
}
As you can see the CreateAsync
is tracking the entire Movie
object inserting everything, however, the business rule is the folowing: A movie can't be created without actors and directors, but if the actor or director exists in the DB they won't have to be created.
It's important to know that when calling the Post endpoint the dto submitted by the client don't have ids, so when looking for a Actor
or Director
it will be done by Actor.Name
and Director.Name
properties.
This is the controller:
[Route("api/[controller]")]
[ApiController]
//Changed to use primary constructor
public class MoviesController(IMovieRepository movieRepo) : ControllerBase
{
private readonly IMovieRepository movieRepo = movieRepo;
[HttpPost]
public async Task<IActionResult> CreateMovie([FromBody] CreateMovieDto createMovieDto)
{
var movieModel = createMovieDto.ToMovieFromCreateMovieDto();
if (await movieRepo.Exists(movieModel.Title))
{
return BadRequest("The movie already exists");
}
var movie = await movieRepo.CreateAsync(movieModel);
return CreatedAtAction(nameof(GetMovieById), new {id = movie.Id}, movie.ToMovieDto());
}
}
I have tried to remove the tracking from the DB Sets if the Actor or Director exists with no luck.
I'm a bit lost and can't figure out the correct aproach.
New version of the CreateAsync. PLeae see comment below.
public async Task<Movie> CreateAsync(Movie movie)
{
var actorsInMovie = movie.Actors;
movie.Actors = new List<Actor>();
foreach (var actor in actorsInMovie)
{
var actorEntity = await context.Actors.FirstOrDefaultAsync(a => a.Name == actor.Name);
if (actorEntity is not null)
movie.Actors.Add(actorEntity);
else
movie.Actors.Add(actor);
}
var directorsInMovie = movie.Directors;
movie.Directors = new List<Director>();
foreach (var director in directorsInMovie)
{
var directorEntity = await context.Directors.FirstOrDefaultAsync(d => d.Name == director.Name);
if (directorEntity is not null)
movie.Directors.Add(directorEntity);
else
movie.Directors.Add(director);
}
await context.Movies.AddAsync(movie);
await context.SaveChangesAsync();
return movie;
}
So, I found out that I needed to track the entities. First, fetch them from the database and then call the AddAsync()
and SaveChangesAsync()
methods.
If someone knows a better way of doing this, please let me know. I have put the new code in and EDIT section in the question.