diff --git a/azure-pipelines-public.yml b/azure-pipelines-public.yml index 98c9420f928..0cda45d0c05 100644 --- a/azure-pipelines-public.yml +++ b/azure-pipelines-public.yml @@ -176,7 +176,7 @@ stages: - job: macOS enablePublishTestResults: true pool: - vmImage: macOS-13 + vmImage: macOS-15 variables: # Rely on task Arcade injects, not auto-injected build step. - skipComponentGovernanceDetection: true @@ -269,7 +269,7 @@ stages: value: $(_BuildConfig) - ${{ if eq(variables['System.TeamProject'], 'public') }}: - name: HelixTargetQueues - value: Windows.10.Amd64.Open;OSX.13.Amd64.Open;OSX.13.ARM64.Open;Ubuntu.2204.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-20.04-helix-sqlserver-amd64 + value: Windows.10.Amd64.Open;OSX.15.Amd64.Open;OSX.15.ARM64.Open;Ubuntu.2204.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-20.04-helix-sqlserver-amd64 - name: _HelixAccessToken value: '' # Needed for public queues - ${{ if ne(variables['System.TeamProject'], 'public') }}: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8505fdc64a8..30a31e3a5cb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -47,7 +47,9 @@ extends: parameters: featureFlags: autoBaseline: false + usePrefastVersion3: true autoEnableRoslynWithNewRuleset: false + binskimScanAllExtensions: true sdl: sourceAnalysisPool: name: NetCore1ESPool-Svc-Internal @@ -57,6 +59,8 @@ extends: baselineFile: $(Build.SourcesDirectory)\.config\guardian\.gdnbaselines binskim: scanOutputDirectoryOnly: true + analyzeTargetGlob: +:f|**/Microsoft.EntityFrameworkCore*.dll;+:f|**/Microsoft.Data.Sqlite*.dll;+:f|**/ef.exe;+:f|**/dotnet-ef.exe;-:f|**/shims/**/*.exe; + preReleaseVersion: '4.3.1' customBuildTags: - ES365AIMigrationTooling stages: @@ -131,7 +135,7 @@ extends: - job: macOS pool: name: Azure Pipelines - image: macOS-13 + image: macOS-15 os: macOS variables: # Rely on task Arcade injects, not auto-injected build step. @@ -152,6 +156,9 @@ extends: COMPlus_EnableWriteXorExecute: 0 displayName: Build templateContext: + sdl: + binskim: + prereleaseVersion: ' ' outputs: - output: pipelineArtifact displayName: Upload TestResults @@ -190,6 +197,9 @@ extends: - script: eng/common/cibuild.sh --configuration $(_BuildConfig) --prepareMachine $(_InternalRuntimeDownloadArgs) displayName: Build templateContext: + sdl: + binskim: + prereleaseVersion: ' ' outputs: - output: pipelineArtifact displayName: Upload TestResults diff --git a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs index 410f2dd2839..ee3b82baf90 100644 --- a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs @@ -34,6 +34,9 @@ public class ParameterExtractingExpressionVisitor : ExpressionVisitor private static readonly bool UseOldBehavior35100 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35100", out var enabled35100) && enabled35100; + private static readonly bool UseOldBehavior37176 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37176", out var enabled37176) && enabled37176; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -210,8 +213,9 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp switch (method.Name) { case nameof(MemoryExtensions.Contains) - when methodCallExpression.Arguments is [var arg0, var arg1] && - TryUnwrapSpanImplicitCast(arg0, out var unwrappedArg0): + when UseOldBehavior37176 + && methodCallExpression.Arguments is [var arg0, var arg1] + && TryUnwrapSpanImplicitCast(arg0, out var unwrappedArg0): { return Visit( Expression.Call( @@ -219,6 +223,22 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp unwrappedArg0, arg1)); } + // In .NET 10, MemoryExtensions.Contains has an overload that accepts a third, optional comparer, in addition to the older + // overload that accepts two parameters only. + case nameof(MemoryExtensions.Contains) + when !UseOldBehavior37176 + && methodCallExpression.Arguments is [var spanArg, var valueArg, ..] + && (methodCallExpression.Arguments.Count is 2 + || methodCallExpression.Arguments.Count is 3 + && methodCallExpression.Arguments[2] is ConstantExpression { Value: null }) + && TryUnwrapSpanImplicitCast(spanArg, out var unwrappedSpanArg): + { + return Visit( + Expression.Call( + EnumerableMethods.Contains.MakeGenericMethod(method.GetGenericArguments()[0]), + unwrappedSpanArg, valueArg)); + } + case nameof(MemoryExtensions.SequenceEqual) when methodCallExpression.Arguments is [var arg0, var arg1] && TryUnwrapSpanImplicitCast(arg0, out var unwrappedArg0) @@ -231,20 +251,37 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result) { - if (expression is MethodCallExpression + switch (expression) + { + // With newer versions of the SDK, the implicit cast is represented as a MethodCallExpression; + // with older versions, it's a Convert node. + case MethodCallExpression { Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType }, Arguments: [var unwrapped] + } when implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>)): + { + result = unwrapped; + return true; } - && implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition - && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))) - { - result = unwrapped; - return true; - } - result = null; - return false; + case UnaryExpression + { + NodeType: ExpressionType.Convert, + Operand: var unwrapped, + Type: { IsGenericType: true } convertType + } when !UseOldBehavior37176 && convertType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>)): + { + result = unwrapped; + return true; + } + + default: + result = null; + return false; + } } } diff --git a/src/Microsoft.Data.Sqlite.Core/SqliteBlob.cs b/src/Microsoft.Data.Sqlite.Core/SqliteBlob.cs index bbcb2a263ef..7e1088b8cb8 100644 --- a/src/Microsoft.Data.Sqlite.Core/SqliteBlob.cs +++ b/src/Microsoft.Data.Sqlite.Core/SqliteBlob.cs @@ -206,8 +206,12 @@ public virtual int Read(Span buffer) count = (int)(Length - position); } - var rc = sqlite3_blob_read(_blob, buffer.Slice(0, count), (int)position); - SqliteException.ThrowExceptionForRC(rc, _connection.Handle); + // Newer sqlite3_blob_read returns error for 0-byte reads. + if (count > 0) + { + var rc = sqlite3_blob_read(_blob, buffer.Slice(0, count), (int)position); + SqliteException.ThrowExceptionForRC(rc, _connection.Handle); + } _position += count; return count; } @@ -280,8 +284,12 @@ public virtual void Write(ReadOnlySpan buffer) throw new NotSupportedException(Resources.ResizeNotSupported); } - var rc = sqlite3_blob_write(_blob, buffer.Slice(0, count), (int)position); - SqliteException.ThrowExceptionForRC(rc, _connection.Handle); + // Newer sqlite3_blob_write returns error for 0-byte writes. + if (count > 0) + { + var rc = sqlite3_blob_write(_blob, buffer.Slice(0, count), (int)position); + SqliteException.ThrowExceptionForRC(rc, _connection.Handle); + } _position += count; } diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 865f5dc026e..521a504a2c7 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -371,6 +371,34 @@ public virtual Task Column_collection_of_bools_Contains(bool async) async, ss => ss.Set().Where(c => c.Bools.Contains(true))); + // C# 14 first-class spans caused MemoryExtensions.Contains to get resolved instead of Enumerable.Contains. + // The following tests that the various overloads are all supported. + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_on_Enumerable(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => Enumerable.Contains(new[] { 10, 999 }, c.Int))); + + // C# 14 first-class spans caused MemoryExtensions.Contains to get resolved instead of Enumerable.Contains. + // The following tests that the various overloads are all supported. + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_on_MemoryExtensions(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => MemoryExtensions.Contains(new[] { 10, 999 }, c.Int))); + + // Note that we don't test EF 8/9 with .NET 10; this test is here for completeness/documentation purposes. +#if NET10_0_OR_GREATER + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_with_MemoryExtensions_with_null_comparer(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => MemoryExtensions.Contains(new[] { 10, 999 }, c.Int, comparer: null))); +#endif + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Column_collection_Count_method(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index 222b2e9f8c4..e9d34754632 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -462,6 +462,45 @@ await context.Database.SqlQuery($"SELECT [Bools] AS [Value] FROM [Primit .SingleAsync()); } + public override async Task Contains_on_Enumerable(bool async) + { + await base.Contains_on_Enumerable(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] IN (10, 999) +"""); + } + + + public override async Task Contains_on_MemoryExtensions(bool async) + { + await base.Contains_on_MemoryExtensions(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] IN (10, 999) +"""); + } + +#if NET10_0_OR_GREATER + public override async Task Contains_with_MemoryExtensions_with_null_comparer(bool async) + { + await base.Contains_with_MemoryExtensions_with_null_comparer(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] IN (10, 999) +"""); + } +#endif + public override Task Column_collection_Count_method(bool async) => AssertCompatibilityLevelTooLow(() => base.Column_collection_Count_method(async)); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 837646b257c..6d604c275e5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -635,6 +635,45 @@ await context.Database.SqlQuery($"SELECT [Bools] AS [Value] FROM [Primit .SingleAsync()); } + public override async Task Contains_on_Enumerable(bool async) + { + await base.Contains_on_Enumerable(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] IN (10, 999) +"""); + } + + + public override async Task Contains_on_MemoryExtensions(bool async) + { + await base.Contains_on_MemoryExtensions(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] IN (10, 999) +"""); + } + +#if NET10_0_OR_GREATER + public override async Task Contains_with_MemoryExtensions_with_null_comparer(bool async) + { + await base.Contains_with_MemoryExtensions_with_null_comparer(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] IN (10, 999) +"""); + } +#endif + public override async Task Column_collection_Count_method(bool async) { await base.Column_collection_Count_method(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index 9faa7597b62..a9c9d12e50f 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -625,6 +625,45 @@ FROM json_each("p"."Bools") AS "b" """); } + public override async Task Contains_on_Enumerable(bool async) + { + await base.Contains_on_Enumerable(async); + + AssertSql( + """ +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Int" IN (10, 999) +"""); + } + + + public override async Task Contains_on_MemoryExtensions(bool async) + { + await base.Contains_on_MemoryExtensions(async); + + AssertSql( + """ +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Int" IN (10, 999) +"""); + } + +#if NET10_0_OR_GREATER + public override async Task Contains_with_MemoryExtensions_with_null_comparer(bool async) + { + await base.Contains_with_MemoryExtensions_with_null_comparer(async); + + AssertSql( + """ +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Int" IN (10, 999) +"""); + } +#endif + public override async Task Column_collection_Count_method(bool async) { await base.Column_collection_Count_method(async);