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
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
GetAsync()is marked [Obsolete] and is replaced byGetQueryAsync().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.EFCore9.1.0 the extension methodGetAsync(...)was marked[Obsolete](seePR #250)
in favor of
GetQueryAsync(...). In 10.0.0 the method is removedentirely.
The problem — the two methods do not produce the same result. The
recommended
GetQueryAsyncfails to expand an optional 1:1 navigationproperty (same class of issue as
AutoMapper.Extensions.OData#150).
Project
AutoMapper.AspNetCore.OData.EFCore9.1.0 (last version where bothmethods coexist so they can be compared in one project)
Categoryentity with a self-referencing optional 1:1ParentCategory(nullable FKParentId)Category→CategoryDtowithForAllMembers(o => o.ExplicitExpansion())DbSet, mapper andODataQueryOptions:GET /odata/CategoriesObsolete→ usesGetAsync(Obsolete)GET /odata/CategoriesQuery→ usesGetQueryAsyncReproduce
Then issue the same OData query against both endpoints:
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, expandedParentCategoryisnull:{ "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
Mapping configuration
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