0

I have a net core 6 web api endpoint like this (shortened for brevity):

[HttpPut("{id}")]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<ArbitrationCase>> UpdateCaseAsync(int id, [FromBody] ArbitrationCase arbCase)
{

     var orig = await _context.ArbitrationCases
                        .Include(c => c.Arbitrators)
                        .Include(c => c.CPTCodes)
                        .Include(d => d.Notes)
                        .Include(d => d.OfferHistory)
                        .FirstOrDefaultAsync(d => d.Id == arbCase.Id);

     _context.Entry(orig).CurrentValues.SetValues(arbCase);

     // inclusion of the next line will prevent the service from returning a value
     // it will return an Ok - 200 response but the response will be empty
     // var payor = await _context.Payors.FindAsync(arbCase.PayorId.Value);

     await _context.SaveChangesAsync();
     return Ok(orig);
}

As mentioned in the code block above, if I uncomment the line that that fetches payor, the service stops returning the "orig" object. Is there some sort of single-use limit on the _context that I am violating? This seems broken to me.

[Edit] Just to be clear, the Payors.FindAsync(...) method does not generate an error. It will either return a value or null if the Payor record does not exist. The return Ok(orig) line is always hit.

[Edit] The controller inherits from a base class that injects _context. This shouldn't be germane to the issue since the _context works when calling SaveChangesAsync but here is the rest of the controller as well as the base class for completeness:

[Route("[controller]")]
[ApiController]
[Authorize]
public class CasesController : MPBaseController
{
        private readonly ILogger<CasesController> _logger;

        #region Constructor
        public CasesController(ILogger<CasesController> logger, ArbitrationDbContext context) : base(context)
        {

            _logger = logger;
        }
        #endregion

...

And here is the base class:

public class MPBaseController : ControllerBase
{
        protected readonly ArbitrationDbContext _context;

        public MPBaseController(ArbitrationDbContext context)
        {
            _context = context;
        }
}

My suspicion is that because the ArbitrationCase class can have a foreign key reference to a record in the Payors table, loading the Payor record with a separate call does something EF or Net Core doesn't like and causes the response stream back to the client to be terminated / truncated somehow. If anyone knows of a way to see some internals of what happens with the response before it goes back to the client - like maybe the serialization of the "orig" object fails because of something the second _context call does - that would be where I would like to look next.

MPowerGuy
  • 61
  • 1
  • 4
  • "Shortened for brevity" and "like this" are red flags for me. Do you encounter the problem with *exactly* this code in the method, or not? When you have a seemingly unexplainable problem, you aren't outfitted to simplify the code yet because you don't know what code reproduces the problem. A [mre] is invaluable. – Jeroen Mostert Jul 28 '22 at 18:36
  • Yes, this is a minimally reproducible example. – MPowerGuy Jul 28 '22 at 19:16
  • For completeness, where is `_context` coming from? I would expect this to be a private field (ideally `readonly`) injected in the constructor, but you never know. Shenanigans would ensue if it was not unchanged. – Jeroen Mostert Jul 28 '22 at 19:18
  • _"Is there some sort of single-use limit on the _context that I am violating?"_ - no, there is no such thing. – Guru Stron Jul 28 '22 at 19:44
  • 1
    @MPowerGuy _"Yes, this is a minimally reproducible example. "_ - no, it is not. I can't paste in IDE (adding required libraries) and see the same problem as you encounter. – Guru Stron Jul 28 '22 at 19:45
  • Fair enough. I did not paste in the Program.cs file, appsettings.json, the class files for the entities and the DbContext configuration because those get farther away from the conditions that generate the issue. However, are you interested in trying to reproduce the issue if I add the additional files? If so, what else might you want to see? I'm happy to work with anyone in good faith. – MPowerGuy Jul 28 '22 at 19:56
  • In the _ideal_ case, you'd start with a bare-minimum project and add just the lines of code that would be required to get the behavior you're seeing, and then post that code here. I know that's hard to do, and we don't mean to be off-putting by asking for a minimal repro: it's just that there's nothing in the code you've posted that would reasonably produce the behavior you're reporting, so there's no way for anybody to help you based on the details provided. For all we know, an exception is thrown and a custom filter is catching and eating it or something. – StriplingWarrior Jul 28 '22 at 20:05
  • This is an excellent idea and I should have done that. Let me see if I can reproduce with the absolute minimum config. Thank you. – MPowerGuy Jul 28 '22 at 20:07
  • Ah, you know what. I was on the right track with the JSON serialization issue. When I started exploring more of the logs I found this: System.Text.Json.JsonException: A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles. Path: $.PayorEntity.ArbitrationCases.PayorEntity.ArbitrationCases.PayorEntity... Interesting that this only appears when loading the other entity and doesn't generate an exception that causes to req 2 fail – MPowerGuy Jul 28 '22 at 20:30
  • Last comment. This SO question was very helpful in resolving the issue: https://stackoverflow.com/questions/59199593/net-core-3-0-possible-object-cycle-was-detected-which-is-not-supported – MPowerGuy Jul 28 '22 at 20:44
  • @StriplingWarrior Can this question be closed but remain as a reference to others? If not should I delete it? – MPowerGuy Aug 01 '22 at 16:53
  • 1
    It's possible for a question to be closed without deleting it, but often questions are deleted after they've been closed. If you think others are likely to find your question when they experience the same issue, and benefit from what you've found, please add an answer yourself. After a short while you should be able to mark your answer as correct. That doesn't "close" the question, but it marks it as answered. – StriplingWarrior Aug 01 '22 at 17:58

1 Answers1

1

The problem is that the loading of the second entity type, which was a parent entity to the entity I was loading first, triggered a circular reference due to the navigation properties present on the different classes.

When I explored more of the logs I found this: System.Text.Json.JsonException: A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles. Path: $.PayorEntity.ArbitrationCases.PayorEntity.ArbitrationCases.PayorEntity...

Interesting that this only appears when loading the other entity and doesn't generate an exception during failure (which would have made things easier to diagnose). At any rate, the solution was to add the [JsonIgnore] attribute on the navigation properties to break the circular reference. This allowed the serialization of the service result - which apparently happens after the web service method returns "successfully" - to run to completion and fill the service response body properly.

MPowerGuy
  • 61
  • 1
  • 4
  • 1
    You should use dtos in the web application. Using the entities in your controller methods is like opening a can of worms, and causes `EntityTrackingExceptions` – Pieterjan Aug 01 '22 at 20:40
  • I guess I'm not following. In the controller methods, entities are fetched using the DbContext. The context returns an instance of a class. The class has navigation properties exactly as Microsoft describes them to be implemented (virtual methods that return a child class or collection of child classes and integer methods pointing to any foreign keys). Under certain circumstances these navigation properties can result in a circular reference. Using the [JsonIgnore] property attribute prevents the circular loading. This problem seems common when searching. Not sure what dtos stands for. – MPowerGuy Aug 03 '22 at 20:18