2

I have this simple controller in a NetCore 2 Entity Framework API app that is supposed to get a list of nationalparks based on the stateID:

[HttpGet("ListNationalParksByState/{stateId}")]
public async Task<ActionResult<IEnumerable<NationalParkList>>> GetNationalParksForState(Guid stateID)
{
    var nationalparkList = await _context.NationalParkList.Where(n => n.StateId == stateID).ToListAsync();

    return nationalparkList;
}

But my returned JSON only shows this:

  [{"state":{"nationalparkList":[

However, when I set a breakpoint in that controller, it shows it found 3 nationalparks (I'm not sure was Castle.Proxies is):

  [0] {Castle.Proxies.NationalParkListProxy}
  [1] {Castle.Proxies.NationalParkListProxy}
  [2] {Castle.Proxies.NationalParkListProxy}

Expanding those shows the 3 nationalparks and all their properties.

Here is my NationalParkList model:

public partial class NationalParkList
{
    public NationalParkList()
    {
        NationalParkLinks = new HashSet<NationalParkLinks>();
    }

    public string NationalParkId { get; set; }
    public Guid StateId { get; set; }
    public string NationalParkTitle { get; set; }
    public string NationalParkText { get; set; }

    public virtual StateList State { get; set; }
    public virtual ICollection<NationalParkLinks> NationalParkLinks { get; set; }
}

Here is how it's defined in my dbcontext:

modelBuilder.Entity<NationalParkList>(entity =>
        {
            entity.HasKey(e => e.NationalParkId)
                .HasName("PK_NationalParkList");

            entity.ToTable("nationalparkList");

            entity.Property(e => e.NationalParkId)
                .HasColumnName("nationalparkID")
                .HasMaxLength(50)
                .ValueGeneratedNever();

            entity.Property(e => e.StateId).HasColumnName("stateID");

            entity.Property(e => e.NationalParkText).HasColumnName("nationalparkText");

            entity.Property(e => e.NationalParkTitle)
                .HasColumnName("nationalparkTitle")
                .HasMaxLength(3000);

            entity.HasOne(d => d.State)
                .WithMany(p => p.NationalParkList)
                .HasForeignKey(d => d.StateId)
                .HasConstraintName("FK_nationalparkList_stateList");
        });

I'm not getting any errors, I'm just not getting any data.

Does anyone see why I'd get no data when I hit this controller?

Thanks!

Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73
SkyeBoniwell
  • 6,345
  • 12
  • 81
  • 185
  • 2
    [Related data and serialization](https://learn.microsoft.com/en-us/ef/core/querying/related-data#related-data-and-serialization) – Ivan Stoev Aug 19 '19 at 19:06
  • @IvanStoev I see now. Is there a way to 'exclude' the data that I don't need? In this case, I don't want to see the NationalParkLinks and StateLists data. Thanks! – SkyeBoniwell Aug 19 '19 at 19:22
  • 2
    An API is inherently an anti-corruption layer. As such, you should not ever return actual entities, nor accept actual entities. Rather, you should work with an expose DTO classes, which you then map back and forth to your entities. This allows your database to evolve independent of your API and vice versa. Otherwise, you're tightly-coupling your clients to your database, and you might as well just not have an API at all. – Chris Pratt Aug 19 '19 at 19:33
  • 1
    See https://stackoverflow.com/questions/21554977/should-services-always-return-dtos-or-can-they-also-return-domain-models – Jota.Toledo Aug 19 '19 at 19:50
  • 1
    IMO you should always map your entity classes to lower-level objects, never expose them in your web layer. A non-purist approach would be to add decorators to your class properties to configure the serialization process. – Jota.Toledo Aug 19 '19 at 19:53
  • @ChrisPratt Chris, what are you thoughts on the accepted answer to this question(specifically the 'when not to use' part? https://stackoverflow.com/questions/21554977/should-services-always-return-dtos-or-can-they-also-return-domain-models - thanks – SkyeBoniwell Aug 19 '19 at 19:59
  • 2
    I'd say it's BS. There's no good conditions for not using a DTO. Who cares if it's a new solution? That's actually more argument for going with DTOs now, rather than trying to retrofit them later, and having to deprecate your API and move clients off of it. Entities are for one thing: moving data to/from the database. The minute you start using them for anything more than that, you're breaking SOLID. – Chris Pratt Aug 19 '19 at 22:06

1 Answers1

2

As discussed in the comments, most likely the existence of cycles in your object graph is causing the serialization to malfunction. Im surprised that you arent getting exceptions during run time if this is the case.

AFAIK you have two options:

Decorate props in your class that you dont want to be serialized with [JsonIgnore] or similar, in order to avoid cycles in your object graphs.

The biggest issue with this approach that I see is its inflexibility: consider 2 endpoints A, B and an entity class Foo with multiple properties, including both x,y; A needs all props of Foo except x and B all props except y. How would you handle this having only one possible serialization configuration for class Foo?

Furthermore, from a purist point of view, adding such decorators increases the responsabilities/knowledge of an entity class with stuff unrelated to the business logic.

Map your entities into lower-level objects AKA DTOs

Another approach is to map your entities into instances of (mostly) behaviorless classes that can be considered data transfer objects. In most web/controller layers you will see data objects going in and out. In your case for example, you could refactor into the following:

public class NationalParkListData
{
    public string Id { get; set; }
    public Guid StateId { get; set; }
    public string Title { get; set; }
    public string Text { get; set; }

    // Depending on how rest-compliant your approach is, you 
    // might include non-collection nested objects or not
    public StateListData State { get; set; }
    public int NationalParkLinksCount { get; set; }
}

[HttpGet("/states/{stateId:guid}/national-parks")]
public async Task<IActionResult> GetNationalParksForState(Guid stateId, CancellationToken cancellationToken)
{
    var stateNationalParks = await _context.NationalParkList
                                   .Where(n => n.StateId == stateId)
                                   .ToListAsync(cancellationToken);
    IEnumerable<NationalParkListData> result = // your mapper logic
    return this.Ok(result);
}

In this case, you can easily notice that the issue mentioned in the previous approach does not exist as its handled by the mapping layer. For implementing a mapping layer the most common approach is to use libraries like AutoMapper.

Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73