Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to round trip an object with relation using asp .net web api, entity framework and json (code first)?

Using a .NET client app, I am trying to post an object A that includes a collection of objects B via asp .net web api JSON, have that create a LocalDB database, and store the data. Then fetch object A again.

The app includes 3 projects. A asp .net mvc 4 web api project, a .Net console app and a .Net class library. Both the asp .net app and console app reference the class library, which includes the class definitions for objects A and B.

Class library:

public class Actor
{
    public Actor()
    {
        this.Movies = new HashSet<Movie>();
    }

    public int ActorID { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Movie> Movies { get; set; }
}

public class Movie
{
    public Movie()
    {
        this.Actors = new HashSet<Actor>();
    }

    public int MovieID { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Actor> Actors { get; set; }
}

Console App:

Movie movie = new Movie()
{
    Name = "Dr. No"
};

Actor actor = new Actor()
{
    Name = "Sean Connery"
};

movie.Actors.Add(actor);

using (HttpClient client = new HttpClient())
{
    client.BaseAddress = new Uri("http://localhost:3654");
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    var response = client.PostAsJsonAsync<Movie>("api/movies", movie).Result;
    response.EnsureSuccessStatusCode();

    response = client.GetAsync("api/movies/1").Result;
    response.EnsureSuccessStatusCode();

    Movie newMovie = response.Content.ReadAsAsync<Movie>().Result;
}

asp .net mvc DbContext:

public class MediaContext : DbContext
{
    public MediaContext()
    {
        this.Configuration.LazyLoadingEnabled = false;
    }

    public DbSet<Movie> Movies { get; set; }
    public DbSet<Actor> Actors { get; set; }
}

Problem #1: It seems JSON doesn't like the circular reference, however if I don't add the collection to both objects, EF5 does not create a MoviesActors table to hold the reference.

Problem #2: Even if I add the reference in the controller, when I return that object, it doesn't return it with the Actors. E.g. I expected something like

Movie
{
   MovieID = "1",
   Name = "???",
   Actors[] = { 1 }
}

But instead, Actors is just null.

Update: Here is the self-referencing exception:

ExceptionMessage=Self referencing loop detected with type 'System.Data.Entity.DynamicProxies.Movie_157D88BDC89E46A7CE4875C2970C7BBFB893972095EFA0745C2261AACC007969'. Path '[0].Actors[0].Movies'.

I managed to work around this exception using the method at How did I solve the Json serializing circular reference error? and just disabling the proxy. That solves problem #1. However when I get the Movies, they still come back with no Actors, even using Include("Actors"). I can see the reference has been created correctly in the intermediate table in the LocalDb.

Update 2

FINALLY figured this out, answer below.

Thanks so much!

like image 469
Luke Kim Avatar asked Jan 15 '23 19:01

Luke Kim


2 Answers

After much searching I was finally able to resolve my issue. Originally I tried adding a [ScriptIgnore] tag to the Movies set, however I was using EF5 final which changed the default serializer to JSON .NET. I finally found that [ScriptIgnore] didn't work and I had to set [JsonIgnore] like this:

public class Actor
{
    public int ActorID { get; set; }
    public string Name { get; set; }

    [JsonIgnore]
    public virtual ICollection<Movie> Movies { get; set; }
}

public class Movie
{
    public int MovieID { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Actor> Actors { get; set; }
}

Doing that combined with the .Include("Actors") eager loading in the Get method finally solved my issue.

In summary I needed to:

  1. Have the virtual ICollection references on both objects to create the intermediate table.
  2. Add [JsonIgnore] to the Movies collection reference to solve the circular reference.
  3. Turn off the proxy to solve my 500 error.
  4. Eager load the Actors in the GetMovies/GetMovie method using .Include("Actors")

Additionally I found for subsequent PUTs of the Movie object, where the Actors collection had changed, I had to manually change the RelationShip state on each child Actor to either Added/Deleted to make sure the related collection was updated.

like image 134
Luke Kim Avatar answered Jan 26 '23 01:01

Luke Kim


About your comment:

Also, I'd be completely happy to remove the public virtual ICollection<Movie> Movies { get; set; } from the Actor class, however in doing that, EF wasn't generating the intermediate table in the LocalDb.

If you remove one of the collections EF assumes by convention that your relationship is a one-to-many relationship (which doesn't have an intermediate table, but only a foreign key in one of the tables) in contrast to the model with two collections where EF creates a many-to-many relationship (that has an intermediate table).

However, you can override this convention using Fluent API and tell EF explicitly that the remaining collection belongs to a many-to-many relationship instead of a one-to-many. Having this model...

public class Actor
{
    //...
    public int ActorID { get; set; }
    public string Name { get; set; }
}

public class Movie
{
    //...
    public int MovieID { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Actor> Actors { get; set; }
}

...you can define a many-to-many relationship this way:

public class MediaContext : DbContext
{
    //...
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Movie>()
            .HasMany(m => m.Actors)
            .WithMany()
            .Map(a =>
            {
                a.MapLeftKey("MovieID");
                a.MapRightKey("ActorID");
                a.ToTable("MovieActors"); // intermediate table
            });
    }
}
like image 37
Slauma Avatar answered Jan 25 '23 23:01

Slauma