Skip to content

Minimal repro: GetAsync (Obsolete) vs GetQueryAsync (Bug or expected behaviour?) #252

Description

@leoerlandsson

GetAsync()is marked [Obsolete] and is replaced by GetQueryAsync().

The behaviour however is different and we are missing 1:1 expands that were present with the Obsolete method. This breaks our application.

Please see attached repro project.

odata_repro.zip

Summary

In AutoMapper.AspNetCore.OData.EFCore 9.1.0 the extension method
GetAsync(...) was marked [Obsolete] (see
PR #250)
in favor of GetQueryAsync(...). In 10.0.0 the method is removed
entirely
.

The problem — the two methods do not produce the same result. The
recommended GetQueryAsync fails to expand an optional 1:1 navigation
property
(same class of issue as
AutoMapper.Extensions.OData#150).

Project

  • .NET 10 minimal Web API
  • AutoMapper.AspNetCore.OData.EFCore 9.1.0 (last version where both
    methods coexist so they can be compared in one project)
  • EF Core InMemory
  • Single Category entity with a self-referencing optional 1:1
    ParentCategory (nullable FK ParentId)
  • One AutoMapper profile: CategoryCategoryDto with
    ForAllMembers(o => o.ExplicitExpansion())
  • Two OData controllers hitting the exact same DbSet, mapper and
    ODataQueryOptions:
    • GET /odata/CategoriesObsolete → uses GetAsync (Obsolete)
    • GET /odata/CategoriesQuery → uses GetQueryAsync

Reproduce

dotnet run

Then issue the same OData query against both endpoints:

GET /odata/CategoriesObsolete?$filter=parentId ne null&$expand=parentCategory
GET /odata/CategoriesQuery?$filter=parentId ne null&$expand=parentCategory

You can use the provided requests in OData.AutoMapper.Repro.http.

Please see that expanded ParentCategory is missing in the second response.

Actual output

GetAsync (Obsolete) — works:

{
  "value": [
    { "Id": 2, "Name": "Child",      "ParentId": 1, "ParentCategory": { "Id": 1, "Name": "Root",  "ParentId": null } },
    { "Id": 3, "Name": "Grandchild", "ParentId": 2, "ParentCategory": { "Id": 2, "Name": "Child", "ParentId": 1    } }
  ]
}

GetQueryAsyncbroken, expanded ParentCategory is null:

{
  "value": [
    { "Id": 2, "Name": "Child",      "ParentId": 1, "ParentCategory": null },
    { "Id": 3, "Name": "Grandchild", "ParentId": 2, "ParentCategory": null }
  ]
}

Impact

Because the replacement method silently drops optional 1:1 expansions,
we cannot follow AutoMapper.OData.Extensions's guidance to migrate to GetQueryAsync,
and cannot upgrade to 10.0.0 (10.0.0 removes GetAsync).

Source/destination types

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public int? ParentId { get; set; }
    public Category? ParentCategory { get; set; }
    public ICollection<Category> ChildrenCategories { get; set; } = new List<Category>();
}

public class CategoryDto
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public int? ParentId { get; set; }
    public CategoryDto? ParentCategory { get; set; }
    public ICollection<CategoryDto> ChildrenCategories { get; set; } = new List<CategoryDto>();
}

Mapping configuration

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<Category, CategoryDto>()
            .ForAllMembers(o => o.ExplicitExpansion());
    }
}

// registered via:
builder.Services.AddAutoMapper(cfg => cfg.AddProfile<MappingProfile>());

Version: 9.1.0

Expected behavior

 GetQueryAsync  should expand the optional 1:1 navigation property  ParentCategory  just like the obsolete  GetAsync  does, since both are given the same  DbSet ,  IMapper , and  ODataQueryOptions  with  $expand=parentCategory .

Actual behavior

 GetAsync  (Obsolete) correctly returns the expanded  ParentCategory :

{
  "value": [
    { "Id": 2, "Name": "Child",      "ParentId": 1, "ParentCategory": { "Id": 1, "Name": "Root",  "ParentId": null } },
    { "Id": 3, "Name": "Grandchild", "ParentId": 2, "ParentCategory": { "Id": 2, "Name": "Child", "ParentId": 1    } }
  ]
}

 GetQueryAsync  (recommended replacement) returns  ParentCategory: null  for every row, silently dropping the expand:

{
  "value": [
    { "Id": 2, "Name": "Child",      "ParentId": 1, "ParentCategory": null },
    { "Id": 3, "Name": "Grandchild", "ParentId": 2, "ParentCategory": null }
  ]
}

Steps to reproduce

// Seed data
var root  = new Category { Id = 1, Name = "Root",       ParentId = null };
var child = new Category { Id = 2, Name = "Child",      ParentId = 1 };
var grand = new Category { Id = 3, Name = "Grandchild", ParentId = 2 };
db.Categories.AddRange(root, child, grand);
db.SaveChanges();

// Controller A (works): GetAsync
var result = await _db.Categories.AsNoTracking().GetAsync(_mapper, options,
    new QuerySettings { AsyncSettings = new AsyncSettings { CancellationToken = ct } });

// Controller B (broken): GetQueryAsync
var result = await _db.Categories.AsNoTracking().GetQueryAsync(_mapper, options,
    new QuerySettings { AsyncSettings = new AsyncSettings { CancellationToken = ct } });

Issue the same OData request against both endpoints and compare:

GET /odata/CategoriesObsolete?$filter=parentId ne null&$expand=parentCategory
GET /odata/CategoriesQuery?$filter=parentId ne null&$expand=parentCategory

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions