Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #1286: String Collections support in $select. #1282

Open
wants to merge 36 commits into
base: release-8.x
Choose a base branch
from

Conversation

anasik
Copy link

@anasik anasik commented Jul 16, 2024

EF Core 8 comes with support for primtive collections in models.

Naturally, OData should pick up on that support. As of now, if you create a List<string> field in your model and then fetch all the records for that model, it works flawlessly. You see an array of strings in the response.

However, if you try to $select that field, it fails with the following error:

Click to expand error ``` System.NullReferenceException: Object reference not set to an instance of an object. at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.CreateGetValueExpression(ParameterExpression dbDataReader, Int32 index, Boolean nullable, RelationalTypeMapping typeMapping, Type type, IPropertyBase property) at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.VisitExtension(Expression extensionExpression) at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ProcessShaper(Expression shaperExpression, RelationalCommandCache& relationalCommandCache, IReadOnlyList`1& readerColumns, LambdaExpression& relatedDataLoaders, Int32& collectionId) at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.VisitExtension(Expression extensionExpression) at System.Linq.Expressions.ExpressionVisitor.VisitMemberAssignment(MemberAssignment node) at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node) at System.Linq.Expressions.ExpressionVisitor.Visit[T](ReadOnlyCollection`1 nodes, Func`2 elementVisitor) at System.Linq.Expressions.ExpressionVisitor.VisitMemberInit(MemberInitExpression node) at System.Linq.Expressions.ExpressionVisitor.VisitMemberAssignment(MemberAssignment node) at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node) at System.Linq.Expressions.ExpressionVisitor.Visit[T](ReadOnlyCollection`1 nodes, Func`2 elementVisitor) at System.Linq.Expressions.ExpressionVisitor.VisitMemberInit(MemberInitExpression node) at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ProcessShaper(Expression shaperExpression, RelationalCommandCache& relationalCommandCache, IReadOnlyList`1& readerColumns, LambdaExpression& relatedDataLoaders, Int32& collectionId) at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.VisitShapedQuery(ShapedQueryExpression shapedQueryExpression) at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.VisitExtension(Expression extensionExpression) at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.VisitExtension(Expression extensionExpression) at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query) at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async) at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async) at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.b__0() at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler) at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken) at System.Text.Json.Serialization.Converters.IAsyncEnumerableOfTConverter`2.OnWriteResume(Utf8JsonWriter writer, TAsyncEnumerable value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryWrite(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.JsonConverter`1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.SerializeAsync(Stream utf8Json, T rootValue, CancellationToken cancellationToken, Object rootValueBoxed) at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.SerializeAsync(Stream utf8Json, T rootValue, CancellationToken cancellationToken, Object rootValueBoxed) at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.SerializeAsync(Stream utf8Json, T rootValue, CancellationToken cancellationToken, Object rootValueBoxed) at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|30_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext) at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) ```

This happens because in ProjectAsWrapper, we check if it's a collection, and if yes, we wrap it as a collection rather than an element.

  • With SQL Server this works perfectly fine for all collections except for strings.
  • With SQLite, pretty much all primitive collections fail.

That's because queries of the form: Entity().Select( d=> d.StringListProperty.Select(d=> d))) just inherently don't work. It appears to be a bug in EF Core that one can easily recreate without even needing to initialize OData.

However, for our purposes, we can easily get around this by simply wrapping the List as an element as opposed to a collection.

I tested this as much as I could and so far I haven't discovered any negative side-effects of this. It's working perfectly fine and I am even able to filter on the string list using any/all .

Fixes #1286

…per. This gets rid of the excetion resulting from selecting a List<String> field.
@anasik
Copy link
Author

anasik commented Jul 17, 2024

@microsoft-github-policy-service agree

@anasik anasik marked this pull request as ready for review July 17, 2024 18:12
@anasik anasik changed the title Primitive Collections support in $select. String Collections support in $select. Jul 17, 2024
@anasik anasik changed the title String Collections support in $select. Fixes issue#1286: String Collections support in $select. Jul 18, 2024
@anasik anasik changed the title Fixes issue#1286: String Collections support in $select. Fixes #1286: String Collections support in $select. Jul 18, 2024
@xuzhg xuzhg self-requested a review July 18, 2024 18:13
@xuzhg
Copy link
Member

xuzhg commented Jul 18, 2024

Do you mind to add tests

@anasik
Copy link
Author

anasik commented Jul 18, 2024

Do you mind to add tests

I can. I'm just not sure where. Should I create a folder named "Strings" under E2E?

@anasik
Copy link
Author

anasik commented Jul 20, 2024

@xuzhg Since primitive collections only started getting supported as recent as EF Core 8, I'm gonna have to add net8 as a target framework and add a test that only runs on net8. Is that okay?

@xuzhg
Copy link
Member

xuzhg commented Jul 23, 2024

I'm just not sure where. Should I create a folder named "Strings" unde

We have 'Microsoft.AspNetCore.OData.Tests' for Unit test, and

Microsoft.AspNetCore.OData.E2E.Tests for End to End test.

Can you find a folder to add the tests within the E2E test?

@xuzhg
Copy link
Member

xuzhg commented Jul 23, 2024

@xuzhg Since primitive collections only started getting supported as recent as EF Core 8, I'm gonna have to add net8 as a target framework and add a test that only runs on net8. Is that okay?

I am thinking it. Add '.NET8' target framework to test project will run all the test cases on this target framework. Basically, it's ok.
However, We'd make sure the CI/CD can handle it. You can try that and let's review it.

By the way, can we mock the 'EF Core 8' primitive collection? Or can we add a sample project to play the EF Core 8?

@habbes
Copy link
Contributor

habbes commented Jul 24, 2024

@xuzhg AspNetCore OData 9 preview uses .NET 8 as the target framework

@anasik anasik closed this Jul 24, 2024
@anasik anasik reopened this Jul 24, 2024
@anasik
Copy link
Author

anasik commented Jul 24, 2024

I am thinking it. Add '.NET8' target framework to test project will run all the test cases on this target framework. Basically, it's ok. However, We'd make sure the CI/CD can handle it. You can try that and let's review it.

@xuzhg, Yeah I have already added the NET8 target framework locally, I just wanted to ask first.

By the way, can we mock the 'EF Core 8' primitive collection? Or can we add a sample project to play the EF Core 8?

I'm not quite sure what you mean by this but I did add a model for testing purposes that has all primitive collection fields.

Can you find a folder to add the tests within the E2E test?

I decided to just create a new one specifically for Lists. I explored a lot of existing folders and none seemed like a good enough candidate for such a specific case.

…crash on SQLite so added other primitive checks back to SelectExpandBinder.
@anasik
Copy link
Author

anasik commented Jul 25, 2024

@xuzhg,

I have added the test. Assuming you have net8.0 installed on your machine, you can directly run said test through the following command inside the E2E folder:

dotnet test --filter DisplayName~ListsTest -f net8.0

A bunch of observations you could make here:

  • I added SQLite to the project packages and a *.db file. That's a SQLite DB. I had to include it because the bug in question is limited to SQL. I originally encountered it on an SQL Server instance but SQLite seemed much more friendly for automated testing.
  • I also added some more cases to the SelectExpandBinder file. That's because when working with SQLite, I noticed that, unlike SQL Server, it wasn't just List<string> that was failing but also every other primitive collection.
  • For some of the test theories I created, I left the List<Uri> case commented out because it's causing the tests to fail. Even in the original description I added to these PRs, I hade made some observations about some undefined behaviour around the Uri type and like the other problems, it's just a lot more pronounced in SQLite. Sadly, it felt a little too outside the scope of this branch for me to continue investigating just yet.
  • Similarly, for the $filter theory, I also commented out the List<DateTimeOffset> tests. It doesn't seem like they can be passed since the exception thrown insists on using client evaluation.
  • For both of these cases, I left the commented cases stay in case it's possible to pass them later.

I'm gonna maybe see if I can clean up a bit more

@anasik anasik force-pushed the fix/primitive-collections branch from 816ca7d to 411cf73 Compare July 25, 2024 17:00
@WanjohiSammy
Copy link

LGTM

.gitattributes Outdated Show resolved Hide resolved
xuzhg
xuzhg previously approved these changes Aug 19, 2024
@anasik
Copy link
Author

anasik commented Sep 3, 2024

@WanjohiSammy, @xuzhg, I just wanted to ask, are you guys waiting on me for something on this?

/// </summary>
/// <param name="type">The type to validate.</param>
/// <returns>True if type is primitive or known type, otherwise False.</returns>
public static bool IsPrimitiveOrKnownType(Type type)
Copy link

@WanjohiSammy WanjohiSammy Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The primitive types covered here are not sufficient. You can refer to IsPrimitiveType(Type clrType) in ODL which contains all the types that we consider primitive both Nullable or Non-Nullable primitive types. Also looking at this method, you will notice that we do not consider Uri primitive type.

Please use it to rewrite this method.

Copy link
Author

@anasik anasik Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WanjohiSammy, I am well-aware of the correct primitive types but I had to cover all the types that are not only affected by the bug but were also documented to support Primitive Collections in the EF Core 8 change log.

That being said, I took a look at said method it's not immediately evident to me exactly which types of mine you're concerned about. It appears that the only difference between that method and mine is that I explicitly added cases for some types that the other function might still support.

I am almost certain that there's not a single type mentioned there that wasn't first confirmed to be affected.

Since you've already mentioned Uri,

  • Here it is being given express treatment therefore I had to support it.
  • It's also affected by the bug at hand.
  • In fact, this type is actually the most buggy of all, as I have mentioned several times in earlier interactions. In fact, I was hoping this would get merged quickly so I could debug Uri more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the confusion here stems from the fact that these two methods have similar names but serve very different purposes. The core IsPrimitiveType() test for types that can be mapped to EDM primitive types. But this method checks for types that should be treated as primitive collections in the context of EF core, without regards to EDM types. Maybe using a different name, more specific to this use case would alleviate the confusion. But I think it's fine to restrict this method to only the types that matter with respect to the EF core bug.

habbes
habbes previously approved these changes Sep 12, 2024
WanjohiSammy
WanjohiSammy previously approved these changes Sep 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

$select doesn't work on fields of type IList<string>
4 participants