Błąd "possible object cycle was detected" w relacji One-to-Many

0

Hej,

problem dotyczy dokumentu zamówienia i jego pozycji
przykład

public class Order
{
    public int Id { get; set; }
    
    public int CustomerId { get; set; }
    public virtual Customer Customer { get; set; }        
    
    public int TargetCustomerId { get; set; }
    public virtual Customer TargetCustomer { get; set; }
    
    public int AddressId { get; set; }
    public virtual Address Address { get; set; }
    
    public DateTime RealizationDate { get; set; }
    public DateTime DueDate { get; set; }
    public string? Comments { get; set; }

    public int UserId { get; set; }
    public virtual User User { get; set; }

    public virtual List<OrderPosition> Positions { get; set; }
}

public class OrderPosition
{
    public int Id { get; set; }

    public int SkuId { get; set; }
    public virtual Sku Sku { get; set; }
    
    [Column(TypeName = "decimal(11,4)")]
    public decimal quantity { get; set; }

    public int OrderId { get; set; }
    public virtual Order Order { get; set; }
}

próba wywołania

var order = _dbContext.Orders.Where(c => c.Id == id)
  .Include(user => user.User)
  .Include(address => address.Address)
  .Include(customer => customer.Customer)
  .Include(targetCustomer => targetCustomer.TargetCustomer)
  .Include(lines => lines.Positions)
  .ThenInclude(sku => sku.Sku)
  .FirstOrDefault();

daje exception possible object cycle was detected. W debugu kręcę się w kółko o ordera po pozycje i z powrotem.
wygooglałem taką konfigurację aby to ignorować

builder.Services.AddControllers()
    .AddJsonOptions(options =>
        options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles
    );

ale czy to jest rozwiązanie?
co robię nie tak? wydaję się standardowe złączenie jeden do wielu ...

robiąc same pozycje

_dbContext.OrderPositions.Where(c => c.OrderId == 1).Include(sku => sku.Sku);

mogę dostać się do nagłówka zamówienia, nie do końca potrzebne
czy pozbyć się

public virtual Order Order { get; set; }

z postions? nie wiem jak przy zapisie potem....

0

Możesz pokazać zadanie z migracji?

0
gswidwa1 napisał(a):

Możesz pokazać zadanie z migracji?

using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace SalesManager.Infrastructure.Migrations
{
    public partial class AddOrder : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Orders",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    CustomerId = table.Column<int>(type: "int", nullable: false),
                    TargetCustomerId = table.Column<int>(type: "int", nullable: false),
                    AddressId = table.Column<int>(type: "int", nullable: false),
                    RealizationDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                    DueDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                    Comments = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    UserId = table.Column<int>(type: "int", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Orders", x => x.Id);
                    table.ForeignKey(
                        name: "FK_Orders_Users_UserId",
                        column: x => x.UserId,
                        principalTable: "Users",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateTable(
                name: "OrderPositions",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    SkuId = table.Column<int>(type: "int", nullable: false),
                    quantity = table.Column<decimal>(type: "decimal(11,4)", nullable: false),
                    OrderId = table.Column<int>(type: "int", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_OrderPositions", x => x.Id);
                    table.ForeignKey(
                        name: "FK_OrderPositions_Orders_OrderId",
                        column: x => x.OrderId,
                        principalTable: "Orders",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                name: "IX_OrderPositions_OrderId",
                table: "OrderPositions",
                column: "OrderId");

            migrationBuilder.CreateIndex(
                name: "IX_OrderPositions_SkuId",
                table: "OrderPositions",
                column: "SkuId");

            migrationBuilder.CreateIndex(
                name: "IX_Orders_AddressId",
                table: "Orders",
                column: "AddressId");

            migrationBuilder.CreateIndex(
                name: "IX_Orders_CustomerId",
                table: "Orders",
                column: "CustomerId");

            migrationBuilder.CreateIndex(
                name: "IX_Orders_TargetCustomerId",
                table: "Orders",
                column: "TargetCustomerId");

            migrationBuilder.CreateIndex(
                name: "IX_Orders_UserId",
                table: "Orders",
                column: "UserId");
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "OrderPositions");

            migrationBuilder.DropTable(
                name: "Orders");
        }
    }
}
0

Na moje oko jest tutaj coś nie tak w kwestii relacji/konfiguracji, pewnie masz odwołania pomiędzy obiektami i wtedy nic dziwnego, że serializer JSONa kręci Ci się w koło... Podpowiem - może być to związane z brakiem poprawnej konfiguracji OnModelCreating lub jej brakiem.

https://www.learnentityframeworkcore.com/configuration/one-to-many-relationship-configuration
https://learn.microsoft.com/pl-pl/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key

0

Przy generowaniu json-a, generujesz tekst. Generując tekst nie ma możliwości 'odwołania' się do innego obiektu. Więc kręci się to w kółko Order zawiera OrderPoistion. OrderPosition zawiera Order. Order zawiera OrderPosition. OrderPosition zawiera Order itd.

Serializer próbuję budować coś takiego:

{
  Order: {
    OrderPosition :{
      Order :{
        OrderPosition {
          Order: {
            OrderPosition{
          }
      }    
    }
  }
}

Najlepszy rozwiązaniem byłoby DTO, Data Transfer Object. Czyli encję bazodanowe mapujesz (np. za pomocą AutoMapera) do DTO (które nie posiada zapętlenia).

0

dzieki
Czy możesz podać przykład jak to powinno wyglądać z użyciem AutoMappera lub linq?
Używam tez Dapper. Muszę sprawdzić czy Dapper potrafi query rozbić na dto: naglowek i pozycje.

0

Trochę prymitywny kod, oczywiście wszystko powinno być porozbijane na osobne pliki i znajdować się w osobnych warstwach aplikacji, ale ogólnie pokazuję jakbym to zrobił:

using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations.Schema;
using System.Net;

IMapper mapper = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Order, OrderResponse>();
    cfg.CreateMap<OrderPosition, OrderPositionResponse>();
}).CreateMapper();

using (AppDbContext context = new AppDbContext())
{
    Order order = new Order()
    {
        Id = 1,
        Comments = "",
        DueDate = DateTime.Now,
        RealizationDate = DateTime.Now,
    };

    order.Positions = new List<OrderPosition>()
    {
         new OrderPosition(){ Order = order, quantity= 1 },
         new OrderPosition(){ Order = order, quantity= 1 },
         new OrderPosition(){ Order = order, quantity= 1 }
    };

    context.Orders.Add(order);
    context.SaveChanges();

    var ordersFromQuery = context.Orders.Where(c => c.Id == 1)
              .Include(lines => lines.Positions)
              .FirstOrDefault();

    //To wywala błąd ze względu na zapętlenie
    //string output = JsonConvert.SerializeObject(ordersFromQuery);

    OrderResponse response = mapper.Map<OrderResponse>(ordersFromQuery);
    string output = JsonConvert.SerializeObject(response, Formatting.Indented);

    Console.WriteLine(output);  
}

public class Order
{
    public int Id { get; set; }
    public DateTime RealizationDate { get; set; }
    public DateTime DueDate { get; set; }
    public string? Comments { get; set; }
    public virtual List<OrderPosition> Positions { get; set; }
}

public class OrderPosition
{
    public int Id { get; set; }

    [Column(TypeName = "decimal(11,4)")]
    public decimal quantity { get; set; }

    public int OrderId { get; set; }
    public virtual Order Order { get; set; }
}

public class AppDbContext : DbContext
{
    public virtual DbSet<Order> Orders { get; set; }
    public virtual DbSet<OrderPosition> Positions { get; set; } 

    public AppDbContext()
    {
        //Database.EnsureDeleted();
        Database.EnsureCreated();
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.LogTo(Console.WriteLine);
        optionsBuilder.UseSqlite("Data Source=mydb.db;");
    }
}

public class OrderResponse
{
    public int Id { get; set; }
    public DateTime RealizationDate { get; set; }
    public DateTime DueDate { get; set; }
    public string? Comments { get; set; }
    public virtual List<OrderPositionResponse> Positions { get; set; }
}

public class OrderPositionResponse
{
    public int Id { get; set; }
    public decimal quantity { get; set; }
}


1 użytkowników online, w tym zalogowanych: 0, gości: 1