I'm using OData v8 with .NET 5 over Entity Framework. I have defined an entity Hour on my model and configured it in a pretty cookie-cutter way as follows, on my DbContext:
public class Hour
{
// Removed non-related properties
public Guid? CustomerId { get; set; }
public string CustomerName { get; set; }
public Guid Id { get; set; }
}
public class HourConfiguration : IEntityTypeConfiguration<Hour>
{
public void Configure(EntityTypeBuilder<Hour> builder)
{
builder.ToTable("Hours");
builder.HasPrimaryKey();
}
}
The client however requires a slight change in the model (corporate restrictions), so I'm projecting it using .Select() into a DTO which is very similar. The main difference is, since CustomerId (and thus CustomerName) are optional, I'd like to put these properties together in a DTO, here, simply, IdNameDto:
public class IdNameDto
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class HourDto
{
public Guid Id { get; set; } // <- PK of the Hour entity
public IdNameDto Customer { get; set; } // <- can be null
}
The aforementioned projection happens in an endpoint as follows, you'll see I've enabled the necessary OData querying:
[HttpGet("odata/hours")]
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public IActionResult Get()
{
IQueryable<Hour> query = readDbContext.Hours
.Select(source =>
new HourDto
{
Id = source.Id,
Customer = source.CustomerId != null
? new IdNameDto
{
Id = source.CustomerId.Value,
Name = source.CustomerName
}
: null
});
return new OkObjectResult(query);
}
Note, that since CustomerId is optional, I'm forced to do a null-check to avoid null-ref exceptions - this is probably the culprit.
And finally, the OData mappings, added in ConfigureServices():
internal static class MvcBuilderExtensions
{
internal static IMvcBuilder AddODataModel(this IMvcBuilder builder, int maxTop = 200)
{
return builder.AddOData(options =>
{
options.EnableQueryFeatures(maxTop);
options.AddRouteComponents("odata", GetEdmModel());
});
}
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EnableLowerCamelCase();
builder.EntitySet<HourDto>("hours");
return builder.GetEdmModel();
}
}
I'd like to think this is a pretty standard setup. As long as I don't use any filters, this works fine and both Customers with Id=null and Id=<GUID> are returned correctly.
Now the main problem, when I try to use the $filter clause to select a specific Customer by Id as such:
/odata/hours?$filter(Customer/Id eq <GUID>) (GUID is of course an actual GUID), the following error occurs:
I've added the full stacktrace here: https://pastebin.com/r3YRX3FB
The error is quite cryptic, but I suspect it has something to with OData thinking the underlying key is a Guid instead of a Guid?. But even that is right, I'm not sure how to solve it.
Any help is greatly appreciated as I've been breaking my head over this for some time.
