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: Category → CategoryDto 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    } }
  ]
}

GetQueryAsync — broken, 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

  1. changed the title [-]Minimal repro: `GetAsync` (Obsolete) vs `GetQueryAsync` in `AutoMapper.AspNetCore.OData.EFCore`[/-] [+]Minimal repro: `GetAsync` (Obsolete) vs `GetQueryAsync`[/+] on Aug 31, 2026
  2. changed the title [-]Minimal repro: `GetAsync` (Obsolete) vs `GetQueryAsync`[/-] [+]Minimal repro: `GetAsync` (Obsolete) vs `GetQueryAsync` (Bug?)[/+] on Aug 31, 2026
  3. changed the title [-]Minimal repro: `GetAsync` (Obsolete) vs `GetQueryAsync` (Bug?)[/-] [+]Minimal repro: `GetAsync` (Obsolete) vs `GetQueryAsync` (Bug or expected behaviour?)[/+] on Aug 31, 2026
  4. BlaiseD commented on Sep 1, 2026

    @BlaiseD
    Member

    This thread recommends separate DTO types for self referencing entities. GetAsync referenced code (old ideas) which have been removed from the expression mapping library which this one depends on. If you get the same results using AutoMapper's ProjectTo (without this library) then it's probably the same issue. If you get it to succeed then we'll have a bug to address. In short we want to stay away from GetAsync and improve GetQueryAsync if we have to.

  5. leoerlandsson commented on Sep 2, 2026

    @leoerlandsson
    Author

    Hi,

    Thanks for you response. I can confirm that this is upstream in AutoMapper.
    I've attached an updated repro project.

    However, switching from [Obsolete] GetAsync to GetQueryAsync is not a 1:1 update. It introduces breaking changes (like this one), both for this example and some others we've found (where Includes() and included data are dropped from the result, also probably because the new GetQueryAsync uses ProjectTo).

    If this is intended behaviour, the Issue can be closed, and we'll have to find some kind of workarounds switching from GetAsync to GetQueryAsync in our application.

    odata_repro_with_projectto.zip

  6. BlaiseD commented on Sep 2, 2026

    @BlaiseD
    Member

    Removing the MapIncludes behavior from the expression mapping library and GetAsync as a consequence is intended. MapIncludes derived the mapped parent of flattened DTO members for expansion. ProjectTo expansions handle flattened members without further mapping and has millions more users i.e. continuing to maintain MapIncludes isn't worth the trouble if ProjectTo can achieve the same behavior even though code changes will be needed for some use cases.

  7. leoerlandsson commented on Sep 2, 2026

    @leoerlandsson
    Author

    Ok, and thanks. The removal of MapIncludes also explains why we are missing the (on backend manually) included entities.

    I understand the behaviour of GetQueryAsync will not be changed and we'll have to find a way around that going forward switching from [Obsolete] GetAsync to GetQueryAsync.

    Thanks again for your prompt response.

  8. leoerlandsson commented on Sep 2, 2026

    @leoerlandsson
    Author

    Workaround / rewrite of code implemented for the removed MapIncludes.

    The rewrite involves, in this case, separately loading the data and adding the Includes needed in a separate query and then populating the OData response with it before responding. Sure, two hits to the database, but for us an acceptable compromise.

    If anyone needs an example, just give me a ping.

    You can close this Issue @BlaiseD. Thanks again.

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