简体   繁体   中英

C# Entity Framework Core returns too many related entities on GET request

I'm working on a .NET Core API that uses Entity Framework Core and have the following entities:

  • User which has a list of Appointments & Orders
  • Appointments that can have an order
  • Orders that have a list of parts
  • Parts that just contain a name & price

When I try to do an index request on /order it returns related entities which is good, however, it keeps adding or nesting the related items. I want it to return the related Appointment , User & Parts but not any level deeper than that.

I'm thinking I either made a wrong foreign key in my model/database or my include(s) are wrong.

Below is all my related code, do let me know if I've added too much context.

My models look like this (separated files normally, but in one code block for brevity)

public class User
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set;  }
    public string Phonenumber { get; set;  }
    public string PostalCode { get; set;  }
    public string Housenumber { get; set; }
    public string Streetname { get; set; }
    public string Cityname { get; set; }

    public List<Appointment> Appointments { get; set; }

    public List<Order> Orders { get; set; }

    [JsonIgnore] //Prevents password from being serialized & returned in API response(s)
    public string Password { get; set; }
}

public class Appointment
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public User User { get; set; }
    public Order Order { get; set; }
    public string GeneralProblem { get; set; }

    public enum ProblemType
    {
        Problem1,
        Problem2,
    }

    public Boolean AcceptedAppointment { get; set; }
    public Boolean TermsOfService { get; set; }
    public string AppointmentDue { get; set; }
    public Boolean InQueue { get; set; }
    public DateTime Timestamp { get; set; }
}

public class Order
{
    [Key]
    public int Id { get; set; }
    public Appointment Appointment { get; set; }
    public User User { get; set; }
    public int UserId { get; set; }
    public int AppointmentId { get; set; }
    public float TotalCost { get; set; }

    public List<Part> Parts { get; set; }
}

public class Part
{
    [Key]
    public int Id { get; set; }
    public int OrderId { get; set; }
    public Order Order { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public float Price { get; set; }
}

My ApplicationDbContext looks like this:

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    { }

    public DbSet<User> Users { get; set; }
    public DbSet<Appointment> Appointments { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<Part> Parts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Appointment>()
            .HasOne(a => a.User)
            .WithMany(u => u.Appointments);

        modelBuilder.Entity<Order>()
            .HasOne(o => o.Appointment)
            .WithOne(a => a.Order)
            .HasForeignKey<Order>(b => b.AppointmentId);

        modelBuilder.Entity<Order>()
            .HasMany(o => o.Parts)
            .WithOne(t => t.Order);
    }
}

Currently, I'm trying to index the orders and trying to collect the related entities by doing the following (please note that I use the interface & repository design pattern however these have not been added for brevity)

public class OrderRepository : IOrderRepository
{
    private readonly ApplicationDbContext _context;

    public IEnumerable<Order> FindAll()
    {
        //Adding parts to related order
        var orderContext = _context.Orders
                            .Include(Order => Order.Parts)
                            .Include(Order => Order.Appointment)
                                .ThenInclude(Order => Order.User)
                            .ToList();

        return orderContext ;
    }

I expect the JSON output to look like the following:

{
    "id": 2,
    "appointment": {
        "id": 1,
        "userId": 1,
        "generalProblem": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut orci sit amet nisi egestas semper. ",
        "acceptedAppointment": true,
        "termsOfService": false,
        "appointmentDue": "08/14/2020",
        "inQueue": false,
        "timestamp": "0001-01-01T00:00:00"
    },
    "user": {
        "id": 1,
        "name": "Barry",
        "email": "barry@mail.nl",
        "phonenumber": "12345678",
        "postalCode": "4201JA",
        "housenumber": "44",
        "streetname": "Somewhere",
        "cityname": "NoWhere",
        "appointments": [],
        "orders": [],
        "password": "12345678"
    },
    "userId": 1,
    "appointmentId": 1,
    "totalCost": 155.44,
    "parts": [
        {
            "id": 1,
            "orderId": 2,
            "name": "Test",
            "description": "Test",
            "price": 100.0
        },
        {
            "id": 2,
            "orderId": 2,
            "name": "Wasbak",
            "description": "Grote marmeren wasbak",
            "price": 2000.0
        }
    ]
}

But unfortunately, it turns out like this:

    {
    "id": 2,
    "appointment": {
        "id": 1,
        "userId": 1,
        "user": {
            "id": 1,
            "name": "Barry",
            "email": "barry@mail.nl",
            "phonenumber": "12345678",
            "postalCode": "4201JA",
            "housenumber": "44",
            "streetname": "Somewhere",
            "cityname": "NoWhere",
            "appointments": [
                {
                    "id": 2,
                    "userId": 1,
                    "order": {
                        "id": 4,
                        "userId": 1,
                        "appointmentId": 2,
                        "totalCost": 100.0,
                        "parts": [
                            {
                                "id": 3,
                                "orderId": 4,
                                "name": "Test",
                                "description": "test",
                                "price": 123.0
                            }
                        ]
                    },
                    "generalProblem": "BLABLABLA",
                    "acceptedAppointment": false,
                    "termsOfService": false,
                    "appointmentDue": "08/14/2020",
                    "inQueue": false,
                    "timestamp": "0001-01-01T00:00:00"
                }
            ],
            "orders": [
                {
                    "id": 4,
                    "appointment": {
                        "id": 2,
                        "userId": 1,
                        "generalProblem": "BLABLABLA",
                        "acceptedAppointment": false,
                        "termsOfService": false,
                        "appointmentDue": "08/14/2020",
                        "inQueue": false,
                        "timestamp": "0001-01-01T00:00:00"
                    },
                    "userId": 1,
                    "appointmentId": 2,
                    "totalCost": 100.0,
                    "parts": [
                        {
                            "id": 3,
                            "orderId": 4,
                            "name": "Test",
                            "description": "test",
                            "price": 123.0
                        }
                    ]
                }
            ],
            "password": "12345678"
        },
        "generalProblem": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut orci sit amet nisi egestas semper. ",
        "acceptedAppointment": true,
        "termsOfService": false,
        "appointmentDue": "08/14/2020",
        "inQueue": false,
        "timestamp": "0001-01-01T00:00:00"
    },
    "user": {
        "id": 1,
        "name": "Barry",
        "email": "barry@mail.nl",
        "phonenumber": "12345678",
        "postalCode": "4201JA",
        "housenumber": "44",
        "streetname": "Somewhere",
        "cityname": "NoWhere",
        "appointments": [
            {
                "id": 1,
                "userId": 1,
                "generalProblem": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut orci sit amet nisi egestas semper. ",
                "acceptedAppointment": true,
                "postcode": null,
                "termsOfService": false,
                "appointmentDue": "08/14/2020",
                "inQueue": false,
                "timestamp": "0001-01-01T00:00:00"
            },
            {
                "id": 2,
                "userId": 1,
                "order": {
                    "id": 4,
                    "userId": 1,
                    "appointmentId": 2,
                    "totalCost": 100.0,
                    "parts": [
                        {
                            "id": 3,
                            "orderId": 4,
                            "name": "Test",
                            "description": "test",
                            "price": 123.0
                        }
                    ]
                },
                "generalProblem": "BLABLABLA",
                "acceptedAppointment": false,
                "termsOfService": false,
                "appointmentDue": "08/14/2020",
                "inQueue": false,
                "timestamp": "0001-01-01T00:00:00"
            }
        ],
        "orders": [
            {
                "id": 4,
                "appointment": {
                    "id": 2,
                    "userId": 1,
                    "generalProblem": "BLABLABLA",
                    "acceptedAppointment": false,
                    "termsOfService": false,
                    "appointmentDue": "08/14/2020",
                    "inQueue": false,
                    "timestamp": "0001-01-01T00:00:00"
                },
                "userId": 1,
                "appointmentId": 2,
                "totalCost": 100.0,
                "parts": [
                    {
                        "id": 3,
                        "orderId": 4,
                        "name": "Test",
                        "description": "test",
                        "price": 123.0
                    }
                ]
            }
        ],
        "password": "12345678"
    },
    "userId": 1,
    "appointmentId": 1,
    "totalCost": 155.44,
    "parts": [
        {
            "id": 1,
            "orderId": 2,
            "name": "Test",
            "description": "Test",
            "price": 100.0
        },
        {
            "id": 2,
            "orderId": 2,
            "name": "Wasbak",
            "description": "Grote marmeren wasbak",
            "price": 2000.0
        }
    ]
}

Am I using include wrong? Or are my models setup wrongly?

I think your models are ok, also the eager loading is ok (the Include you're doing). You need to do the joins since you need that data on your response.

The only issue is that you should use DTOs: Automapper for example is a good Nuget package to look at.

With the help of DTOs you could remove this verbosity.

Just to give you an example of how you could proceed.

public class OrderDto
{
    public int Id { get; set; }
    public AppointmentDto Appointment { get; set; }
    public UserDto User { get; set; }
    public int UserId { get; set; }
    public int AppointmentId { get; set; }
    public float TotalCost { get; set; }

    public List<PartDto> Parts { get; set; }
}

public class AppointmentDto
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public string GeneralProblem { get; set; }   
    public Boolean AcceptedAppointment { get; set; }
    public Boolean TermsOfService { get; set; }
    public string AppointmentDue { get; set; }
    public Boolean InQueue { get; set; }
    public DateTime Timestamp { get; set; }
}

public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set;  }
    public string Phonenumber { get; set;  }
    public string PostalCode { get; set;  }
    public string Housenumber { get; set; }
    public string Streetname { get; set; }
    public string Cityname { get; set; }       
}

public class PartDto
{
   public int Id { get; set; }
   public int OrderId { get; set; }
   public Order Order { get; set; }
   public string Name { get; set; }
   public string Description { get; set; }
   public float Price { get; set; }
}

then in your controller I would do something like this:

// GET: api/Orders
[HttpGet("")]
public async Task<ActionResult<OrderDto>> GetAllOrders()
{
    var orders = _orderRepository.FindAll();

    if (orders == null)
    {
        return NotFound();
    }

    return _mapper<IList<OrderDto>>(orders);
}

As you can see I removed from all DTOs all unnecessary navigation properties, so it won't be mapped by .Net Core. In this way you'll get a flatter json. Automapper can be installed via Nuget and just requires you to configure the mapping with few lines on code. Without navigation properties on DTOs all unnecessary joins won't be done, this means better performance and no "circular references".

Ps use as many DTO's you need and remember to add the mapping at the configuration in order to use them. DTOs could be completely different from Model, here's the big pros of Automapper.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM