AutoMapper / AutoMapper.Extensions.OData

Creates LINQ expressions from ODataQueryOptions and executes the query.
MIT License
140 stars 38 forks source link

Selected properties not respected - returns all properties #180

Closed andyfurniss4 closed 1 year ago

andyfurniss4 commented 1 year ago

Got a bit of an issue with the select parameter. I'm not sure if I've set something up wrong but it doesn't seem to be excluding fields that I have not explicitly requested. I've tried debugging through the AutoMapper code and it seems to know that I only want one property - I can see it build the lambda for that property in the QueryableExtensions.GetQueryable method - but somewhere between that and executing the SQL, it loses track.

Versions

AutoMapper.AspNetCore.OData.EFCore: 4.0.0 Microsoft.EntityFrameworkCore.SqlServer: 6.0.16

Source/destination types

public class Thing : IThing
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public ICollection<ChildThing> ChildThings { get; set; }
    public Guid GroupId { get; set; }

    [NotMapped] IEnumerable<IThing> IThing.ChildThings => ChildThings;
}

public class ChildThing : IChildThing 
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public Guid ThingId { get; set; }
}

public class ThingDto : IThing
{
    public Guid Id { get; set; }
    public string Name { get; set; };
    public IEnumerable<IThing> Thing  { get; set; }
}

public class ChildThingDto : IChildThing
{
    public Guid Id { get; set; }
    public short Number { get; set; }
}

Mapping configuration

public class AutoMapperProfile : Profile
{
    public AutoMapperProfile()
    {
        CreateMap<Infrastructure.Entities.Thing, ThingDto>()
            .ForMember(t => t.ChildThings, o => o.ExplicitExpansion());
        CreateMap<Infrastructure.Entities.ChildThing, ChildThingDto>();
    }
}

Setup

var builder = WebApplication
    .CreateBuilder(args);

builder.Services.AddDbContext<ThingDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("Database"));
    options.LogTo(log =>
    {
        if (log.Contains(RelationalEventId.CommandExecuting.Id.ToString()))
        {
            Console.WriteLine(log);
        }
    });
});
builder.Services.AddAutoMapper(typeof(Program).Assembly);

builder.Services
    .AddControllers()
    .AddOData(options =>
    {
        options.EnableQueryFeatures().AddRouteComponents("", GetEdmModel()).Filter().Select().Expand();
    })
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.ApplyDefaults();
        options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
    });

var app = builder.Build();
app.MapControllers();

app.Run();

IEdmModel GetEdmModel()
{
    var edmBuilder = new ODataConventionModelBuilder();
    edmBuilder.EnableLowerCamelCase();
    edmBuilder.EntitySet<ThingDto>("Things");
    edmBuilder.EntitySet<ChildThingDto>("ChildThings");
    return edmBuilder.GetEdmModel();
}

Controller

[ApiController]
[Route("[controller]")]
public class ThingsController : ODataController
{
    private readonly ThingDbContext _thingDbContext;
    private readonly IMapper _mapper;

    public ThingsController(ThingDbContext thingDbContext, IMapper mapper)
    {
        _thingDbContext = thingDbContext;
        _mapper = mapper;
    }

    [HttpGet]
    public async Task<IActionResult> Query(ODataQueryOptions<ThingDto> options)
    {
        var query = await _thingDbContext.Things.GetQueryAsync(_mapper, options);
        var result = await query.ToListAsync();
        return Ok(result);
    }
}

Query

http://localhost:5100/things?select=id

SQL executed

SELECT [t].[Id], [t].[Name]
FROM [Things] AS [t]

Expected behavior

Array of objects, with only the ID value of each object returned. Database only queried for Id value (not Name).

Actual behavior

Array of objects, with both the ID and Name values of each object returned. E.g.

[
    {
        "id": "3a268b28-589b-41a2-83a3-5e9262987926",
        "name": "Thing 1"
    },
    {
        "id": "b2e5620e-b624-438f-9404-d275ee038fae",
        "name": "Thing 2"
    }
]
BlaiseD commented 1 year ago

That usually means ExplicitExpansion is missing somewhere in your profile.

You might find the tests helpful.

andyfurniss4 commented 1 year ago

@BlaiseD My understanding was that you only needed to set ExplicitExpansion for complex types. However, I just changed my profile from:

public class AutoMapperProfile : Profile
{
    public AutoMapperProfile()
    {
        CreateMap<Infrastructure.Entities.Thing, ThingDto>()
            .ForMember(t => t.ChildThings, o => o.ExplicitExpansion());
        CreateMap<Infrastructure.Entities.ChildThing, ChildThingDto>();
    }
}

To this:

public class AutoMapperProfile : Profile
{
    public AutoMapperProfile()
    {
        CreateMap<Infrastructure.Entities.Thing, ThingDto>()
            .ForAllMembers(o => o.ExplicitExpansion());
        CreateMap<Infrastructure.Entities.ChildThing, ChildThingDto>();
    }
}

And now it works! So I guess my assumption was wrong. Does it apply to reference types of all kinds?

I have another question. I'm trying to apply a filter within an expand. I'd expect this filter to apply to the expanding collection so that only one child object is returned/queryed, however it's still returning all of them.

Query

http://localhost:5100/things?$expand=childThings($filter=number eq 1)

SQL

SELECT [t].[Id], [t].[Name], [c].[Id], [c].[Number]
FROM [Things] AS [t]
LEFT JOIN [ChildThings] AS [c] ON [t].[Id] = [c].[ThingId]
ORDER BY [t].[Id]

Mapping Profile

I have enabled ExplicitExpansion for all members of both objects in my profile.

public class AutoMapperProfile : Profile
{
    public AutoMapperProfile()
    {
        CreateMap<Infrastructure.Entities.Thing, ThingDto>()
            .ForAllMembers(o => o.ExplicitExpansion());
        CreateMap<Infrastructure.Entities.ChildThing, ChildThingDto>()
            .ForAllMembers(o => o.ExplicitExpansion());
    }
}

I can see from the unit tests that this should work but I must have missed something. Any ideas?

Thank you very much for your help.

BlaiseD commented 1 year ago

Not for the libraries in this repository - see the ReadMe. The expansions for value types are automatically added unless a $select is specified.