Skip to content

Commit

Permalink
Merge branch 'release/9.0-staging' => 'release/9.0' (#35743)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndriySvyryd authored Mar 6, 2025
2 parents a43de14 + 67f52d8 commit 873d1a5
Show file tree
Hide file tree
Showing 18 changed files with 335 additions and 82 deletions.
26 changes: 26 additions & 0 deletions src/EFCore.Design/Design/Internal/AppServiceProviderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,30 @@ private IServiceProvider CreateEmptyServiceProvider()

return new ServiceCollection().BuildServiceProvider();
}

/// <summary>
/// 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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static void SetEnvironment(IOperationReporter reporter)
{
var aspnetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var dotnetEnvironment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
var environment = aspnetCoreEnvironment
?? dotnetEnvironment
?? "Development";
if (aspnetCoreEnvironment == null)
{
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", environment);
}

if (dotnetEnvironment == null)
{
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", environment);
}

reporter.WriteVerbose(DesignStrings.UsingEnvironment(environment));
}
}
3 changes: 3 additions & 0 deletions src/EFCore.Design/Design/Internal/DatabaseOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Design.Internal;
/// </summary>
public class DatabaseOperations
{
private readonly IOperationReporter _reporter;
private readonly string _projectDir;
private readonly string? _rootNamespace;
private readonly string? _language;
Expand All @@ -34,6 +35,7 @@ public DatabaseOperations(
bool nullable,
string[]? args)
{
_reporter = reporter;
_projectDir = projectDir;
_rootNamespace = rootNamespace;
_language = language;
Expand Down Expand Up @@ -73,6 +75,7 @@ public virtual SavedModelFiles ScaffoldContext(
? Path.GetFullPath(Path.Combine(_projectDir, outputContextDir))
: outputDir;

AppServiceProviderFactory.SetEnvironment(_reporter);
var services = _servicesBuilder.Build(provider);
using var scope = services.CreateScope();

Expand Down
17 changes: 1 addition & 16 deletions src/EFCore.Design/Design/Internal/DbContextOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -503,22 +503,7 @@ private IDictionary<Type, Func<DbContext>> FindContextTypes(string? name = null,
{
_reporter.WriteVerbose(DesignStrings.FindingContexts);

var aspnetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var dotnetEnvironment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
var environment = aspnetCoreEnvironment
?? dotnetEnvironment
?? "Development";
if (aspnetCoreEnvironment == null)
{
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", environment);
}

if (dotnetEnvironment == null)
{
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", environment);
}

_reporter.WriteVerbose(DesignStrings.UsingEnvironment(environment));
AppServiceProviderFactory.SetEnvironment(_reporter);

var contexts = new Dictionary<Type, Func<DbContext>?>();

Expand Down
59 changes: 57 additions & 2 deletions src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ public class ExpressionTreeFuncletizer : ExpressionVisitor
private static readonly bool UseOldBehavior35111 =
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35111", out var enabled35111) && enabled35111;

private static readonly bool UseOldBehavior35656 =
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35656", out var enabled35656) && enabled35656;

private static readonly bool UseOldBehavior35100 =
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35100", out var enabled35100) && enabled35100;

private static readonly MethodInfo ReadOnlyCollectionIndexerGetter = typeof(ReadOnlyCollection<Expression>).GetProperties()
.Single(p => p.GetIndexParameters() is { Length: 1 } indexParameters && indexParameters[0].ParameterType == typeof(int)).GetMethod!;

Expand Down Expand Up @@ -971,6 +977,51 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCall)
}
}

// .NET 10 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans").
// Unfortunately, the LINQ interpreter does not support ref structs, so we rewrite e.g. MemoryExtensions.Contains to
// Enumerable.Contains here. See https://github.com/dotnet/runtime/issues/109757.
if (method.DeclaringType == typeof(MemoryExtensions) && !UseOldBehavior35100)
{
switch (method.Name)
{
case nameof(MemoryExtensions.Contains)
when methodCall.Arguments is [var arg0, var arg1] && TryUnwrapSpanImplicitCast(arg0, out var unwrappedArg0):
{
return Visit(
Call(
EnumerableMethods.Contains.MakeGenericMethod(methodCall.Method.GetGenericArguments()[0]),
unwrappedArg0, arg1));
}

case nameof(MemoryExtensions.SequenceEqual)
when methodCall.Arguments is [var arg0, var arg1]
&& TryUnwrapSpanImplicitCast(arg0, out var unwrappedArg0)
&& TryUnwrapSpanImplicitCast(arg1, out var unwrappedArg1):
return Visit(
Call(
EnumerableMethods.SequenceEqual.MakeGenericMethod(methodCall.Method.GetGenericArguments()[0]),
unwrappedArg0, unwrappedArg1));
}

static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result)
{
if (expression is MethodCallExpression
{
Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType },
Arguments: [var unwrapped]
}
&& implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition
&& (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>)))
{
result = unwrapped;
return true;
}

result = null;
return false;
}
}

// Regular/arbitrary method handling from here on

// First, visit the object and all arguments, saving states as well
Expand Down Expand Up @@ -2132,8 +2183,12 @@ static Expression RemoveConvert(Expression expression)
}
}

private static Expression ConvertIfNeeded(Expression expression, Type type)
=> expression.Type == type ? expression : Convert(expression, type);
private Expression ConvertIfNeeded(Expression expression, Type type)
=> expression.Type == type
? expression
: UseOldBehavior35656
? Convert(expression, type)
: Visit(Convert(expression, type));

private bool IsGenerallyEvaluatable(Expression expression)
=> _evaluatableExpressionFilter.IsEvaluatableExpression(expression, _model)
Expand Down
82 changes: 79 additions & 3 deletions src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ namespace Microsoft.Data.Sqlite
/// <seealso href="https://docs.microsoft.com/dotnet/standard/data/sqlite/async">Async Limitations</seealso>
public partial class SqliteConnection : DbConnection
{
private static readonly bool UseOldBehavior35715 =
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35715", out var enabled35715) && enabled35715;

internal const string MainDatabaseName = "main";

private const int SQLITE_WIN32_DATA_DIRECTORY_TYPE = 1;
Expand All @@ -48,6 +51,8 @@ public partial class SqliteConnection : DbConnection
private static readonly StateChangeEventArgs _fromClosedToOpenEventArgs = new StateChangeEventArgs(ConnectionState.Closed, ConnectionState.Open);
private static readonly StateChangeEventArgs _fromOpenToClosedEventArgs = new StateChangeEventArgs(ConnectionState.Open, ConnectionState.Closed);

private static string[]? NativeDllSearchDirectories;

static SqliteConnection()
{
Type.GetType("SQLitePCL.Batteries_V2, SQLitePCLRaw.batteries_v2")
Expand Down Expand Up @@ -624,11 +629,82 @@ public virtual void LoadExtension(string file, string? proc = null)

private void LoadExtensionCore(string file, string? proc)
{
var rc = sqlite3_load_extension(Handle, utf8z.FromString(file), utf8z.FromString(proc), out var errmsg);
if (rc != SQLITE_OK)
if (UseOldBehavior35715)
{
var rc = sqlite3_load_extension(Handle, utf8z.FromString(file), utf8z.FromString(proc), out var errmsg);
if (rc != SQLITE_OK)
{
throw new SqliteException(Resources.SqliteNativeError(rc, errmsg.utf8_to_string()), rc, rc);
}
}
else
{
SqliteException? firstException = null;
foreach (var path in GetLoadExtensionPaths(file))
{
var rc = sqlite3_load_extension(Handle, utf8z.FromString(path), utf8z.FromString(proc), out var errmsg);
if (rc == SQLITE_OK)
{
return;
}

if (firstException == null)
{
// We store the first exception so that error message looks more obvious if file appears in there
firstException = new SqliteException(Resources.SqliteNativeError(rc, errmsg.utf8_to_string()), rc, rc);
}
}

if (firstException != null)
{
throw firstException;
}
}
}

private static IEnumerable<string> GetLoadExtensionPaths(string file)
{
// we always try original input first
yield return file;

string? dirName = Path.GetDirectoryName(file);

// we don't try to guess directories for user, if they pass a path either absolute or relative - they're on their own
if (!string.IsNullOrEmpty(dirName))
{
throw new SqliteException(Resources.SqliteNativeError(rc, errmsg.utf8_to_string()), rc, rc);
yield break;
}

bool shouldTryAddingLibPrefix = !file.StartsWith("lib", StringComparison.Ordinal) && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

if (shouldTryAddingLibPrefix)
{
yield return $"lib{file}";
}

NativeDllSearchDirectories ??= GetNativeDllSearchDirectories();

foreach (string dir in NativeDllSearchDirectories)
{
yield return Path.Combine(dir, file);

if (shouldTryAddingLibPrefix)
{
yield return Path.Combine(dir, $"lib{file}");
}
}
}

private static string[] GetNativeDllSearchDirectories()
{
string? searchDirs = AppContext.GetData("NATIVE_DLL_SEARCH_DIRECTORIES") as string;

if (string.IsNullOrEmpty(searchDirs))
{
return [];
}

return searchDirs!.Split([ Path.PathSeparator ], StringSplitOptions.RemoveEmptyEntries);
}

/// <summary>
Expand Down
70 changes: 68 additions & 2 deletions test/EFCore.Design.Tests/Design/Internal/DatabaseOperationsTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Internal;

namespace Microsoft.EntityFrameworkCore.Design.Internal;

public class DatabaseOperationsTest
Expand All @@ -10,16 +12,80 @@ public void Can_pass_null_args()
{
// Even though newer versions of the tools will pass an empty array
// older versions of the tools can pass null args.
CreateOperations(null);
}

[ConditionalFact]
public void ScaffoldContext_throws_exceptions_for_invalid_context_name()
{
ValidateContextNameInReverseEngineerGenerator("Invalid!CSharp*Class&Name");
ValidateContextNameInReverseEngineerGenerator("1CSharpClassNameCannotStartWithNumber");
ValidateContextNameInReverseEngineerGenerator("volatile");
}

private void ValidateContextNameInReverseEngineerGenerator(string contextName)
{
var operations = CreateOperations([]);

Assert.Equal(
DesignStrings.ContextClassNotValidCSharpIdentifier(contextName),
Assert.Throws<ArgumentException>(
() => operations.ScaffoldContext(
"Microsoft.EntityFrameworkCore.SqlServer",
"connectionstring",
"",
"",
dbContextClassName: contextName,
null,
null,
"FakeNamespace",
contextNamespace: null,
useDataAnnotations: false,
overwriteFiles: true,
useDatabaseNames: false,
suppressOnConfiguring: true,
noPluralize: false))
.Message);
}

[ConditionalFact]
[SqlServerConfiguredCondition]
public void ScaffoldContext_sets_environment()
{
var operations = CreateOperations([]);
operations.ScaffoldContext(
"Microsoft.EntityFrameworkCore.SqlServer",
TestEnvironment.DefaultConnection,
"",
"",
dbContextClassName: nameof(TestContext),
schemas: ["Empty"],
null,
null,
contextNamespace: null,
useDataAnnotations: false,
overwriteFiles: true,
useDatabaseNames: false,
suppressOnConfiguring: true,
noPluralize: false);

Assert.Equal("Development", Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"));
Assert.Equal("Development", Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"));
}

private static DatabaseOperations CreateOperations(string[] args)
{
var assembly = MockAssembly.Create(typeof(TestContext));
_ = new TestDatabaseOperations(
var operations = new DatabaseOperations(
new TestOperationReporter(),
assembly,
assembly,
"projectDir",
"RootNamespace",
"C#",
nullable: false,
args: null);
args: args);
return operations;
}

public class TestContext : DbContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2453,7 +2453,7 @@ public void InsertDataOperation_required_empty_array()
Assert.Single(o.Columns);
Assert.Equal(1, o.Values.GetLength(0));
Assert.Equal(1, o.Values.GetLength(1));
Assert.Equal([], (string[])o.Values[0, 0]);
Assert.Equal(new string[0], (string[])o.Values[0, 0]);
});

[ConditionalFact]
Expand All @@ -2478,7 +2478,7 @@ public void InsertDataOperation_required_empty_array_composite()
Assert.Equal(1, o.Values.GetLength(0));
Assert.Equal(3, o.Values.GetLength(1));
Assert.Null(o.Values[0, 1]);
Assert.Equal([], (string[])o.Values[0, 2]);
Assert.Equal(new string[0], (string[])o.Values[0, 2]);
});

[ConditionalFact]
Expand Down
Loading

0 comments on commit 873d1a5

Please sign in to comment.