0

I've been trying to get away from using AutoMapper, since everyone seems to think it's the devil. Because of this, I've been putting all of my mapping into the DBO and DTO models that I'm handing back and forth. However, this quickly causes a looping problem within the EF framework when it tries to link the objects together.

/* Program.cs File */
using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddJsonOptions(options => {
        options.JsonSerializerOptions.MaxDepth = 256;
        options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
    });

builder.Services.AddOpenApi();

builder.Services.AddDbContext<DBContext>(options => {
    options.UseSqlServer("Name=ConnectionStrings:Orders");
});

builder.Services
    .AddScoped<IOrderActor, OrderActor>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

app.MapControllers();

app.Run();
public class Order {
    public Order() {
        ID = 0;
        Name = string.Empty;
        OrderDate = DateTime.Now;
        
        Items = [];
    }
    public Order(OrderDTO dto) {
        ID = dto.ID;
        Name = dto.Name;
        OrderDate = dto.OrderDate;
        
        Items = dto.Items.Select(i => new OrderItem(i));
    }

    public int ID {get; set;}
    public string Name {get; set;} = null!;
    public DateTime OrderDate {get; set;} = null!;
    
    public IEnumerable<OrderItem> Items {get; set;} = [];
}

public class OrderDTO {
    public OrderDTO() {
        ID = 0;
        Name = string.Empty;
        OrderDate = DateTime.Now;W
        
        Items = [];
    }
    public OrderDTO(Order dbo) {
        ID = dbo.ID;
        Name = dbo.Name;
        OrderDate = dbo.OrderDate;
        
        Items = dbo.Items.Select(i => new OrderItemDTO(i));
    }

    public int ID {get; set;}
    public string Name {get; set;} = null!;
    public DateTime OrderDate {get; set;} = null!;
    
    public IEnumerable<OrderItemDTO> Items {get; set;} = [];
}

public class OrderItem {
    public OrderItem() {
        ID = 0;
        Name = string.Empty;
        Price = 0;
        
        Order = new Order();
    }
    public OrderItem(OrderItemDTO dto) {
        ID = dto.ID;
        Name = dto.Name;
        Price = dto.Price;
        
        Order = new Order(dto.Order);
    }

    public int ID {get; set;}
    public string Name {get; set;}
    public decimal Price {get; set;}
    
    public Order Order {get; set;} = null!;
}

public class OrderItemDTO {
    public OrderItemDTO() {
        ID = 0;
        Name = string.Empty;
        Price = 0;
        
        Order = new OrderDTO();
    }
    public OrderItemDTO(OrderItem dbo) {
        ID = dbo.ID;
        Name = dbo.Name;
        Price = dbo.Price;
        
        Order = new OrderDTO(dbo.Order);
    }

    public int ID {get; set;}
    public string Name {get; set;}
    public decimal Price {get; set;}
    
    public OrderDTO Order {get; set;} = null!;
}

I have a web service that is trying to output a list of orders with their order items like so:

public class OrderActor(DBContext context) {
    public async Task<List<OrderDTO>> GetAllOrders() {
        return await context.Orders
            .AsNoTracking()
            .Select(o => new OrderDTO(o))
            .ToListAsync();
    }
}

[ApiController]
[Route("[controller]")]
public class OrderController(OrderActor actor) : ControllerBase {
    [HttpGet]
    [Route("GetAllOrders")]
    public async Task<List<OrderDTO>> GetAllOrders() {
        return await actor.GetAllOrders();
    }
}

Since I have OpenAPI set up, it's trying to rip through this and as it does, it hits this problem.

System.Text.Json.JsonReaderException: The maximum configured depth of 64 has been exceeded. Cannot read next JSON object.

So, am I doing the mapping wrong? And if so, how should the mapping work between my DBO and DTO?

EDIT: So, much of this seems to be coming from using OpenAPI and Scalar. When my Scalar frontend starts up, that's what fails the entire thing. So maybe Scalar is doing something to try and set up the model list?

14
  • It seems you are getting this error on controller please try this in startup. builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.MaxDepth = 256; options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; }); Commented Apr 18 at 20:04
  • I'll add my Program.cs file above to show my options. It's not reading that MaxDepth, however...still specifying 64. Commented Apr 18 at 20:51
  • Try to use this. await context.Orders .AsNoTracking() .Select(o => new { o.Id, o.OrderDate, o.TotalAmount // Only flat fields }) .ToListAsync(); Commented Apr 18 at 21:07
  • Projection could help you to avoid this issue. Commented Apr 18 at 21:08
  • 1
    While bi-directional references can make sense in certain entity scenarios (not Order-OrderItem mind-you, single direction references should be sufficient as order items wouldn't really be referenced without their orders) for DTOs you should definitely avoid bi-directional references. Orders should have a collection of OrderItems and that is it, no OrderItem.Order. Serializers will serialize order.OrderItems then for each OrderItem, the order, and so-on to a max depth or exception. For DTOs especially YAGNI so remove them to avoid the headache. Commented Apr 19 at 6:41

1 Answer 1

3

The issue you're encountering may be because of references between your Order and OrderItem classes which causes infinite recursion during serialization.

To solve this problem, you can use projection in LINQ:

public async Task<List<OrderDTO>> GetAllOrders() {
    return await context.Orders
        .AsNoTracking()
        .Select(o => new OrderDTO {
            ID = o.ID,
            Name = o.Name,
            OrderDate = o.OrderDate,
            Items = o.Items.Select(i => new OrderItemDTO {
                ID = i.ID,
                Name = i.Name,
                Price = i.Price,
                OrderId = o.ID 
            }).ToList()
        })
        .ToListAsync();
}
Sign up to request clarification or add additional context in comments.

3 Comments

Yes, but ProjectTo would greatly simpliy that :)
If this is the case, does that mean I'll have to update my constructors to not automatically accept item children like it is right now? That'd be fine to do, but I've personally never used Projection or ProjectTo.
No, if it works with Select, it works with ProjectTo.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.