diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index fecd3c2648f..2d15c4e1f22 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -18,6 +18,7 @@ updates:
- dependency-name: Microsoft.Data.SqlClient
- dependency-name: Microsoft.DotNet.PlatformAbstractions
- dependency-name: mod_spatialite
+ - dependency-name: Mono.TextTemplating
- dependency-name: NetTopologySuite*
- dependency-name: Newtonsoft.Json
- dependency-name: SQLitePCLRaw*
diff --git a/Directory.Build.props b/Directory.Build.props
index 5e985fd1e50..94132fb6e9b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -35,10 +35,6 @@
$(NoWarn.Replace(';1591', ''))
-
-
- true
-
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 401c4812c95..368101f46fe 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -1,59 +1,59 @@
-
+ https://github.com/dotnet/runtime
- 4017327955f1d8ddc43980eb1848c52fbb131dfc
+ 7698a9a80c5f6270aa1122d79ce419c7b03f2498
-
+ https://github.com/dotnet/runtime
- 4017327955f1d8ddc43980eb1848c52fbb131dfc
+ 7698a9a80c5f6270aa1122d79ce419c7b03f2498
-
+ https://github.com/dotnet/runtime
- 4017327955f1d8ddc43980eb1848c52fbb131dfc
+ 7698a9a80c5f6270aa1122d79ce419c7b03f2498
-
+ https://github.com/dotnet/runtime
- 4017327955f1d8ddc43980eb1848c52fbb131dfc
+ 7698a9a80c5f6270aa1122d79ce419c7b03f2498
-
+ https://github.com/dotnet/runtime
- 4017327955f1d8ddc43980eb1848c52fbb131dfc
+ 7698a9a80c5f6270aa1122d79ce419c7b03f2498
-
+ https://github.com/dotnet/runtime
- 4017327955f1d8ddc43980eb1848c52fbb131dfc
+ 7698a9a80c5f6270aa1122d79ce419c7b03f2498
-
+ https://github.com/dotnet/runtime
- 4017327955f1d8ddc43980eb1848c52fbb131dfc
+ 7698a9a80c5f6270aa1122d79ce419c7b03f2498
-
+ https://github.com/dotnet/runtime
- 4017327955f1d8ddc43980eb1848c52fbb131dfc
+ 7698a9a80c5f6270aa1122d79ce419c7b03f2498
-
+ https://github.com/dotnet/runtime
- 4017327955f1d8ddc43980eb1848c52fbb131dfc
+ 7698a9a80c5f6270aa1122d79ce419c7b03f2498
-
+ https://github.com/dotnet/runtime
- 4017327955f1d8ddc43980eb1848c52fbb131dfc
+ 7698a9a80c5f6270aa1122d79ce419c7b03f2498
-
+ https://github.com/dotnet/runtime
- 4017327955f1d8ddc43980eb1848c52fbb131dfc
+ 7698a9a80c5f6270aa1122d79ce419c7b03f2498
-
+ https://github.com/dotnet/arcade
- f7136626d0109856df867481219eb7366951985d
+ 8ed47fcae6a5d2d40483ed81858f4ede8eab7ae2
-
+ https://github.com/dotnet/arcade
- f7136626d0109856df867481219eb7366951985d
+ 8ed47fcae6a5d2d40483ed81858f4ede8eab7ae2
diff --git a/eng/Versions.props b/eng/Versions.props
index 1cd42cab6ea..1424e4e05ae 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -15,17 +15,17 @@
False
- 7.0.0-preview.3.22120.1
- 7.0.0-preview.3.22120.1
- 7.0.0-preview.3.22120.1
- 7.0.0-preview.3.22120.1
- 7.0.0-preview.3.22120.1
- 7.0.0-preview.3.22120.1
- 7.0.0-preview.3.22120.1
- 7.0.0-preview.3.22120.1
- 7.0.0-preview.3.22120.1
- 7.0.0-preview.3.22120.1
- 7.0.0-preview.3.22120.1
+ 7.0.0-preview.3.22157.1
+ 7.0.0-preview.3.22157.1
+ 7.0.0-preview.3.22157.1
+ 7.0.0-preview.3.22157.1
+ 7.0.0-preview.3.22157.1
+ 7.0.0-preview.3.22157.1
+ 7.0.0-preview.3.22157.1
+ 7.0.0-preview.3.22157.1
+ 7.0.0-preview.3.22157.1
+ 7.0.0-preview.3.22157.1
+ 7.0.0-preview.3.22157.14.0.1
diff --git a/global.json b/global.json
index d6fcb9e26c5..c6b1bc1bbab 100644
--- a/global.json
+++ b/global.json
@@ -18,7 +18,7 @@
"rollForward": "latestMajor"
},
"msbuild-sdks": {
- "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.22124.4",
- "Microsoft.DotNet.Helix.Sdk": "7.0.0-beta.22124.4"
+ "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.22154.3",
+ "Microsoft.DotNet.Helix.Sdk": "7.0.0-beta.22154.3"
}
}
diff --git a/src/EFCore.Analyzers/InternalUsageDiagnosticAnalyzer.cs b/src/EFCore.Analyzers/InternalUsageDiagnosticAnalyzer.cs
index b4d65f6a132..55faac08b05 100644
--- a/src/EFCore.Analyzers/InternalUsageDiagnosticAnalyzer.cs
+++ b/src/EFCore.Analyzers/InternalUsageDiagnosticAnalyzer.cs
@@ -16,7 +16,8 @@ public sealed class InternalUsageDiagnosticAnalyzer : DiagnosticAnalyzer
private static readonly int EFLen = "EntityFrameworkCore".Length;
private static readonly DiagnosticDescriptor Descriptor
- = new(
+ // HACK: Work around dotnet/roslyn-analyzers#5828 by not using target-typed new
+ = new DiagnosticDescriptor(
Id,
title: AnalyzerStrings.InternalUsageTitle,
messageFormat: AnalyzerStrings.InternalUsageMessageFormat,
diff --git a/src/EFCore.Cosmos/Query/Internal/RandomTranslator.cs b/src/EFCore.Cosmos/Query/Internal/RandomTranslator.cs
index 116a52898ab..fefab0714ff 100644
--- a/src/EFCore.Cosmos/Query/Internal/RandomTranslator.cs
+++ b/src/EFCore.Cosmos/Query/Internal/RandomTranslator.cs
@@ -43,7 +43,7 @@ public virtual SqlExpression Translate(
=> MethodInfo.Equals(method)
? _sqlExpressionFactory.Function(
"RAND",
- Array.Empty(),
+ Enumerable.Empty(),
method.ReturnType)
: null;
}
diff --git a/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs b/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs
index 15a2dfe99cc..e198619bc55 100644
--- a/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs
+++ b/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs
@@ -7,6 +7,8 @@
using Microsoft.EntityFrameworkCore.Migrations.Internal;
using Microsoft.EntityFrameworkCore.Scaffolding;
using Microsoft.EntityFrameworkCore.Scaffolding.Internal;
+using Microsoft.EntityFrameworkCore.TextTemplating;
+using Microsoft.EntityFrameworkCore.TextTemplating.Internal;
namespace Microsoft.EntityFrameworkCore.Design;
@@ -52,7 +54,9 @@ public static IServiceCollection AddEntityFrameworkDesignTimeServices(
.TryAddSingleton(reporter)
.TryAddSingleton()
.TryAddSingleton()
- .TryAddSingleton()
+ .TryAddSingleton()
+ .TryAddSingletonEnumerable()
+ .TryAddSingletonEnumerable()
.TryAddSingleton()
.TryAddSingleton()
.TryAddSingleton()
diff --git a/src/EFCore.Design/Design/Internal/DatabaseOperations.cs b/src/EFCore.Design/Design/Internal/DatabaseOperations.cs
index 0aded8b9714..e6db7ce4479 100644
--- a/src/EFCore.Design/Design/Internal/DatabaseOperations.cs
+++ b/src/EFCore.Design/Design/Internal/DatabaseOperations.cs
@@ -96,7 +96,8 @@ public virtual SavedModelFiles ScaffoldContext(
UseNullableReferenceTypes = _nullable,
ContextDir = MakeDirRelative(outputDir, outputContextDir),
ContextName = dbContextClassName,
- SuppressOnConfiguring = suppressOnConfiguring
+ SuppressOnConfiguring = suppressOnConfiguring,
+ ProjectDir = _projectDir
});
return scaffolder.Save(
diff --git a/src/EFCore.Design/EFCore.Design.csproj b/src/EFCore.Design/EFCore.Design.csproj
index f18008e2f39..55c208b9953 100644
--- a/src/EFCore.Design/EFCore.Design.csproj
+++ b/src/EFCore.Design/EFCore.Design.csproj
@@ -60,6 +60,7 @@
+
diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs
index 533cb70ebf9..42c13f2477e 100644
--- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs
+++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs
@@ -931,7 +931,8 @@ protected virtual void GenerateCheckConstraint(
.Append(", ")
.Append(Code.Literal(checkConstraint.Sql));
- if (checkConstraint.Name != (checkConstraint.GetDefaultName() ?? checkConstraint.ModelName))
+ if (checkConstraint.Name != null
+ && checkConstraint.Name != (checkConstraint.GetDefaultName() ?? checkConstraint.ModelName))
{
stringBuilder
.Append(", c => c.HasName(")
diff --git a/src/EFCore.Design/Properties/DesignStrings.Designer.cs b/src/EFCore.Design/Properties/DesignStrings.Designer.cs
index 1f43f910c25..84c4d344d98 100644
--- a/src/EFCore.Design/Properties/DesignStrings.Designer.cs
+++ b/src/EFCore.Design/Properties/DesignStrings.Designer.cs
@@ -1,5 +1,7 @@
//
+using System;
+using System.Reflection;
using System.Resources;
#nullable enable
@@ -118,7 +120,7 @@ public static string CompiledModelTypeMapping(object? entityType, object? proper
entityType, property, customize, className);
///
- /// The property '{entityType}.{property}' has a value comparer configured using a ValueComparer instance. Instead, create types that inherit from ValueConverter and ValueComparer and use '{method}HasConversion=<ConverterType, ComparerType=>()' or '{method}(Type converterType, Type comparerType)' to configure the value converter and comparer.
+ /// The property '{entityType}.{property}' has a value comparer configured using a ValueComparer instance. Instead, create types that inherit from ValueConverter and ValueComparer and use '{method}=<ConverterType, ComparerType=>()' or '{method}(Type converterType, Type comparerType)' to configure the value converter and comparer.
///
public static string CompiledModelValueComparer(object? entityType, object? property, object? method)
=> string.Format(
@@ -126,7 +128,7 @@ public static string CompiledModelValueComparer(object? entityType, object? prop
entityType, property, method);
///
- /// The property '{entityType}.{property}' has a value converter configured using a ValueConverter instance or inline expressions. Instead, create a type that inherits from ValueConverter and use '{method}HasConversion=<ConverterType=>()' or '{method}(Type converterType)' to configure the value converter.
+ /// The property '{entityType}.{property}' has a value converter configured using a ValueConverter instance or inline expressions. Instead, create a type that inherits from ValueConverter and use '{method}=<ConverterType=>()' or '{method}(Type converterType)' to configure the value converter.
///
public static string CompiledModelValueConverter(object? entityType, object? property, object? method)
=> string.Format(
@@ -199,6 +201,14 @@ public static string DuplicateMigrationName(object? migrationName)
GetString("DuplicateMigrationName", nameof(migrationName)),
migrationName);
+ ///
+ /// The encoding '{encoding}' specified in the output directive will be ignored. EF Core always scaffolds files using the encoding 'utf-8'.
+ ///
+ public static string EncodingIgnored(object? encoding)
+ => string.Format(
+ GetString("EncodingIgnored", nameof(encoding)),
+ encoding);
+
///
/// An error occurred while accessing the database. Continuing without the information provided by the database. Error: {message}
///
@@ -657,6 +667,14 @@ public static string UnhandledEnumValue(object? enumValue)
GetString("UnhandledEnumValue", nameof(enumValue)),
enumValue);
+ ///
+ /// Failed to resolve type for directive processor {name}.
+ ///
+ public static string UnknownDirectiveProcessor(object? name)
+ => string.Format(
+ GetString("UnknownDirectiveProcessor", nameof(name)),
+ name);
+
///
/// Cannot scaffold C# literals of type '{literalType}'. The provider should implement CoreTypeMapping.GenerateCodeLiteral to support using it at design time.
///
@@ -777,3 +795,4 @@ private static string GetString(string name, params string[] formatterNames)
}
}
}
+
diff --git a/src/EFCore.Design/Properties/DesignStrings.resx b/src/EFCore.Design/Properties/DesignStrings.resx
index 0a7e6577b7f..c8e89f0c6b7 100644
--- a/src/EFCore.Design/Properties/DesignStrings.resx
+++ b/src/EFCore.Design/Properties/DesignStrings.resx
@@ -1,17 +1,17 @@
-
@@ -189,6 +189,9 @@
The name '{migrationName}' is used by an existing migration.
+
+ The encoding '{encoding}' specified in the output directive will be ignored. EF Core always scaffolds files using the encoding 'utf-8'.
+
An error occurred while accessing the database. Continuing without the information provided by the database. Error: {message}
@@ -378,6 +381,9 @@ Change your target project to the migrations project by using the Package Manage
Unhandled enum value '{enumValue}'.
+
+ Failed to resolve type for directive processor {name}.
+
Cannot scaffold C# literals of type '{literalType}'. The provider should implement CoreTypeMapping.GenerateCodeLiteral to support using it at design time.
@@ -420,4 +426,4 @@ Change your target project to the migrations project by using the Package Manage
Writing model snapshot to '{file}'.
-
+
\ No newline at end of file
diff --git a/src/EFCore.Design/Scaffolding/IModelCodeGeneratorSelector.cs b/src/EFCore.Design/Scaffolding/IModelCodeGeneratorSelector.cs
index b2d40980104..1e8d73b84a1 100644
--- a/src/EFCore.Design/Scaffolding/IModelCodeGeneratorSelector.cs
+++ b/src/EFCore.Design/Scaffolding/IModelCodeGeneratorSelector.cs
@@ -16,5 +16,16 @@ public interface IModelCodeGeneratorSelector
///
/// The programming language.
/// The .
+ [Obsolete("Use the overload that takes ModelCodeGenerationOptions instead.")]
IModelCodeGenerator Select(string? language);
+
+ ///
+ /// Selects an service for a given set of options.
+ ///
+ /// The options.
+ /// The .
+ IModelCodeGenerator Select(ModelCodeGenerationOptions options)
+#pragma warning disable CS0618 // Type or member is obsolete
+ => Select(options.Language);
+#pragma warning restore CS0618
}
diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs
index 78e4ff45d77..57459df310e 100644
--- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs
+++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs
@@ -558,7 +558,7 @@ private void GenerateIndex(IIndex index)
var lines = new List
{
- $".{nameof(EntityTypeBuilder.HasIndex)}({_code.Lambda(index.Properties, "e")}, {_code.Literal(index.GetDatabaseName())})"
+ $".{nameof(EntityTypeBuilder.HasIndex)}({_code.Lambda(index.Properties, "e")}, {_code.Literal(index.GetDatabaseName()!)})"
};
annotations.Remove(RelationalAnnotationNames.Name);
@@ -845,7 +845,7 @@ private void GenerateManyToMany(ISkipNavigation skipNavigation)
_annotationCodeGenerator.RemoveAnnotationsHandledByConventions(index, indexAnnotations);
lines.Add(
- $"j.{nameof(EntityTypeBuilder.HasIndex)}({_code.Literal(index.Properties.Select(e => e.Name).ToArray())}, {_code.Literal(index.GetDatabaseName())})");
+ $"j.{nameof(EntityTypeBuilder.HasIndex)}({_code.Literal(index.Properties.Select(e => e.Name).ToArray())}, {_code.Literal(index.GetDatabaseName()!)})");
indexAnnotations.Remove(RelationalAnnotationNames.Name);
if (index.IsUnique)
diff --git a/src/EFCore.Design/Scaffolding/Internal/ModelCodeGeneratorSelector.cs b/src/EFCore.Design/Scaffolding/Internal/ModelCodeGeneratorSelector.cs
index 517182fe6ac..5eaff106a3c 100644
--- a/src/EFCore.Design/Scaffolding/Internal/ModelCodeGeneratorSelector.cs
+++ b/src/EFCore.Design/Scaffolding/Internal/ModelCodeGeneratorSelector.cs
@@ -13,6 +13,8 @@ namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal;
///
public class ModelCodeGeneratorSelector : LanguageBasedSelector, IModelCodeGeneratorSelector
{
+ private readonly IEnumerable _templatedModelGenerators;
+
///
/// 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
@@ -20,7 +22,13 @@ public class ModelCodeGeneratorSelector : LanguageBasedSelector
public ModelCodeGeneratorSelector(IEnumerable services)
- : base(services)
- {
- }
+ : base(services.Except(services.OfType()).ToList())
+ => _templatedModelGenerators = services.OfType().ToList();
+
+ ///
+ public virtual IModelCodeGenerator Select(ModelCodeGenerationOptions options)
+ => _templatedModelGenerators
+ .Where(g => options.ProjectDir != null && g.HasTemplates(options.ProjectDir))
+ .LastOrDefault()
+ ?? Select(options.Language);
}
diff --git a/src/EFCore.Design/Scaffolding/Internal/ReverseEngineerScaffolder.cs b/src/EFCore.Design/Scaffolding/Internal/ReverseEngineerScaffolder.cs
index 9d984db84dd..894569031a4 100644
--- a/src/EFCore.Design/Scaffolding/Internal/ReverseEngineerScaffolder.cs
+++ b/src/EFCore.Design/Scaffolding/Internal/ReverseEngineerScaffolder.cs
@@ -114,7 +114,7 @@ public virtual ScaffoldedModel ScaffoldModel(
: DefaultDbContextName;
}
- var codeGenerator = ModelCodeGeneratorSelector.Select(codeOptions.Language);
+ var codeGenerator = ModelCodeGeneratorSelector.Select(codeOptions);
return codeGenerator.GenerateModel(model, codeOptions);
}
diff --git a/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs
new file mode 100644
index 00000000000..4b524c61332
--- /dev/null
+++ b/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs
@@ -0,0 +1,172 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.CodeDom.Compiler;
+using System.Text;
+using Microsoft.EntityFrameworkCore.Design.Internal;
+using Microsoft.EntityFrameworkCore.Internal;
+using Microsoft.EntityFrameworkCore.TextTemplating;
+using Microsoft.EntityFrameworkCore.TextTemplating.Internal;
+
+namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal;
+
+internal class TextTemplatingModelGenerator : TemplatedModelGenerator
+{
+ private readonly ITextTemplating _host;
+ private readonly IOperationReporter _reporter;
+
+ public TextTemplatingModelGenerator(
+ ModelCodeGeneratorDependencies dependencies,
+ ITextTemplating textTemplatingService,
+ IOperationReporter reporter)
+ : base(dependencies)
+ {
+ _host = textTemplatingService;
+ _reporter = reporter;
+ }
+
+ public override bool HasTemplates(string projectDir)
+ => File.Exists(Path.Combine(projectDir, TemplatesDirectory, "DbContext.t4"));
+
+ public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationOptions options)
+ {
+ if (options.ContextName == null)
+ {
+ throw new ArgumentException(
+ CoreStrings.ArgumentPropertyNull(nameof(options.ContextName), nameof(options)), nameof(options));
+ }
+
+ if (options.ConnectionString == null)
+ {
+ throw new ArgumentException(
+ CoreStrings.ArgumentPropertyNull(nameof(options.ConnectionString), nameof(options)), nameof(options));
+ }
+
+ var resultingFiles = new ScaffoldedModel();
+
+ var contextTemplate = Path.Combine(options.ProjectDir!, TemplatesDirectory, "DbContext.t4");
+
+ Check.DebugAssert(_host.Session == null, "Session is not null.");
+ _host.Session = _host.CreateSession();
+ try
+ {
+ _host.Session.Add("Model", model);
+ _host.Session.Add("Options", options);
+ _host.Session.Add("NamespaceHint", options.ContextNamespace ?? options.ModelNamespace);
+ _host.Session.Add("ProjectDefaultNamespace", options.RootNamespace);
+
+ var handler = new TextTemplatingCallback();
+ var generatedCode = ProcessTemplate(contextTemplate, handler);
+
+ var dbContextFileName = options.ContextName + handler.Extension;
+ resultingFiles.ContextFile = new ScaffoldedFile
+ {
+ Path = options.ContextDir != null
+ ? Path.Combine(options.ContextDir, dbContextFileName)
+ : dbContextFileName,
+ Code = generatedCode
+ };
+ }
+ finally
+ {
+ _host.Session = null;
+ }
+
+ var entityTypeTemplate = Path.Combine(options.ProjectDir!, TemplatesDirectory, "EntityType.t4");
+ if (File.Exists(entityTypeTemplate))
+ {
+ foreach (var entityType in model.GetEntityTypes())
+ {
+ // TODO: Should this be handled inside the template?
+ if (CSharpDbContextGenerator.IsManyToManyJoinEntityType(entityType))
+ {
+ continue;
+ }
+
+ _host.Session = _host.CreateSession();
+ try
+ {
+ _host.Session.Add("EntityType", entityType);
+ _host.Session.Add("Options", options);
+ _host.Session.Add("NamespaceHint", options.ModelNamespace);
+ _host.Session.Add("ProjectDefaultNamespace", options.RootNamespace);
+
+ var handler = new TextTemplatingCallback();
+ var generatedCode = ProcessTemplate(entityTypeTemplate, handler);
+ if (string.IsNullOrWhiteSpace(generatedCode))
+ {
+ continue;
+ }
+
+ var entityTypeFileName = entityType.Name + handler.Extension;
+ resultingFiles.AdditionalFiles.Add(
+ new ScaffoldedFile { Path = entityTypeFileName, Code = generatedCode });
+ }
+ finally
+ {
+ _host.Session = null;
+ }
+ }
+ }
+
+ return resultingFiles;
+ }
+
+ private string ProcessTemplate(string inputFile, TextTemplatingCallback handler)
+ {
+ var output = _host.ProcessTemplate(
+ inputFile,
+ File.ReadAllText(inputFile),
+ handler);
+
+ foreach (CompilerError error in handler.Errors)
+ {
+ var builder = new StringBuilder();
+
+ if (!string.IsNullOrEmpty(error.FileName))
+ {
+ builder.Append(error.FileName);
+
+ if (error.Line > 0)
+ {
+ builder
+ .Append("(")
+ .Append(error.Line);
+
+ if (error.Column > 0)
+ {
+ builder
+ .Append(",")
+ .Append(error.Line);
+ }
+ builder.Append(")");
+ }
+
+ builder.Append(" : ");
+ }
+
+ builder
+ .Append(error.IsWarning ? "warning" : "error")
+ .Append(" ")
+ .Append(error.ErrorNumber)
+ .Append(": ")
+ .AppendLine(error.ErrorText);
+
+ if (error.IsWarning)
+ {
+ _reporter.WriteWarning(builder.ToString());
+ }
+ else
+ {
+ _reporter.WriteError(builder.ToString());
+ }
+ }
+
+ if (handler.OutputEncoding != Encoding.UTF8)
+ {
+ _reporter.WriteWarning(DesignStrings.EncodingIgnored(handler.OutputEncoding.WebName));
+ }
+
+ return output;
+ }
+}
diff --git a/src/EFCore.Design/Scaffolding/ModelCodeGenerationOptions.cs b/src/EFCore.Design/Scaffolding/ModelCodeGenerationOptions.cs
index 430a7254dc1..84e6e295ae9 100644
--- a/src/EFCore.Design/Scaffolding/ModelCodeGenerationOptions.cs
+++ b/src/EFCore.Design/Scaffolding/ModelCodeGenerationOptions.cs
@@ -73,4 +73,10 @@ public class ModelCodeGenerationOptions
///
/// The connection string.
public virtual string? ConnectionString { get; set; }
+
+ ///
+ /// Gets or sets the root project directory.
+ ///
+ /// The directory.
+ public virtual string? ProjectDir { get; set; }
}
diff --git a/src/EFCore.Design/Scaffolding/ModelCodeGenerator.cs b/src/EFCore.Design/Scaffolding/ModelCodeGenerator.cs
index 21dcc65c7bc..9d1eb72129b 100644
--- a/src/EFCore.Design/Scaffolding/ModelCodeGenerator.cs
+++ b/src/EFCore.Design/Scaffolding/ModelCodeGenerator.cs
@@ -24,7 +24,7 @@ protected ModelCodeGenerator(ModelCodeGeneratorDependencies dependencies)
/// Gets the programming language supported by this service.
///
/// The language.
- public abstract string Language { get; }
+ public abstract string? Language { get; }
///
/// Dependencies for this service.
diff --git a/src/EFCore.Design/Scaffolding/TemplatedModelGenerator.cs b/src/EFCore.Design/Scaffolding/TemplatedModelGenerator.cs
new file mode 100644
index 00000000000..688aabd9790
--- /dev/null
+++ b/src/EFCore.Design/Scaffolding/TemplatedModelGenerator.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Scaffolding;
+
+///
+/// Base type for model code generators that use templates.
+///
+internal abstract class TemplatedModelGenerator : ModelCodeGenerator
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The dependencies.
+ protected TemplatedModelGenerator(ModelCodeGeneratorDependencies dependencies)
+ : base(dependencies)
+ {
+ }
+
+ ///
+ /// Gets the subdirectory under the project to look for templates in.
+ ///
+ /// The subdirectory.
+ protected static string TemplatesDirectory { get; } = Path.Combine("Templates", "EFCore");
+
+ ///
+ public override string? Language
+ => null;
+
+ ///
+ /// Checks whether the templates required for this generator are present.
+ ///
+ /// The root project directory.
+ /// if the templates are present; otherwise, .
+ public abstract bool HasTemplates(string projectDir);
+}
diff --git a/src/EFCore.Design/TextTemplating/ITextTemplating.cs b/src/EFCore.Design/TextTemplating/ITextTemplating.cs
new file mode 100644
index 00000000000..c0e7a822cd2
--- /dev/null
+++ b/src/EFCore.Design/TextTemplating/ITextTemplating.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.VisualStudio.TextTemplating;
+
+namespace Microsoft.EntityFrameworkCore.TextTemplating;
+
+///
+/// The text template transformation service.
+///
+public interface ITextTemplating : ITextTemplatingSessionHost
+{
+ ///
+ /// Transforms the contents of a text template file to produce the generated text output.
+ ///
+ /// The path of the template file.
+ /// The contents of the template file.
+ /// The callback used to process errors and information.
+ /// The output.
+ string ProcessTemplate(string inputFile, string content, ITextTemplatingCallback? callback = null);
+}
diff --git a/src/EFCore.Design/TextTemplating/ITextTemplatingCallback.cs b/src/EFCore.Design/TextTemplating/ITextTemplatingCallback.cs
new file mode 100644
index 00000000000..b29afc78fb4
--- /dev/null
+++ b/src/EFCore.Design/TextTemplating/ITextTemplatingCallback.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.CodeDom.Compiler;
+using System.Text;
+
+namespace Microsoft.EntityFrameworkCore.TextTemplating;
+
+///
+/// Callback interface to be implemented by clients of that wish to process errors and information.
+///
+public interface ITextTemplatingCallback
+{
+ ///
+ /// Receives errors and warnings.
+ ///
+ /// An error or warning.
+ void ErrorCallback(CompilerError error);
+
+ ///
+ /// Receives the file name extension that is expected for the generated text output.
+ ///
+ /// The extension.
+ void SetFileExtension(string extension);
+
+ ///
+ /// Receives the encoding that is expected for the generated text output.
+ ///
+ /// The encoding.
+ /// A value indicating whether the encoding was specified in the encoding parameter of the output directive.
+ void SetOutputEncoding(Encoding encoding, bool fromOutputDirective);
+}
diff --git a/src/EFCore.Design/TextTemplating/Internal/TextTemplatingCallback.cs b/src/EFCore.Design/TextTemplating/Internal/TextTemplatingCallback.cs
new file mode 100644
index 00000000000..6899abcbe26
--- /dev/null
+++ b/src/EFCore.Design/TextTemplating/Internal/TextTemplatingCallback.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.CodeDom.Compiler;
+using System.Text;
+
+namespace Microsoft.EntityFrameworkCore.TextTemplating.Internal;
+
+///
+/// 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.
+///
+public class TextTemplatingCallback : ITextTemplatingCallback
+{
+ private CompilerErrorCollection? _errors;
+ private string _extension = ".cs";
+ private Encoding _outputEncoding = Encoding.UTF8;
+ private bool _fromOutputDirective;
+
+ ///
+ /// 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.
+ ///
+ public virtual string Extension
+ => _extension;
+
+ ///
+ /// 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.
+ ///
+ public virtual CompilerErrorCollection Errors
+ => _errors ??= new CompilerErrorCollection();
+
+ ///
+ /// 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.
+ ///
+ public virtual Encoding OutputEncoding
+ => _outputEncoding;
+
+ void ITextTemplatingCallback.ErrorCallback(CompilerError error)
+ => Errors.Add(error);
+
+ void ITextTemplatingCallback.SetFileExtension(string extension)
+ => _extension = extension;
+
+ void ITextTemplatingCallback.SetOutputEncoding(Encoding? encoding, bool fromOutputDirective)
+ {
+ if (_fromOutputDirective)
+ {
+ return;
+ }
+
+ _outputEncoding = encoding ?? Encoding.UTF8;
+ _fromOutputDirective = fromOutputDirective;
+ }
+}
diff --git a/src/EFCore.Design/TextTemplating/Internal/TextTemplatingService.cs b/src/EFCore.Design/TextTemplating/Internal/TextTemplatingService.cs
new file mode 100644
index 00000000000..b52984b3ab4
--- /dev/null
+++ b/src/EFCore.Design/TextTemplating/Internal/TextTemplatingService.cs
@@ -0,0 +1,241 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.CodeDom.Compiler;
+using System.Text;
+using Microsoft.EntityFrameworkCore.Internal;
+using Microsoft.VisualStudio.TextTemplating;
+using Engine = Mono.TextTemplating.TemplatingEngine;
+
+namespace Microsoft.EntityFrameworkCore.TextTemplating.Internal;
+
+///
+/// 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.
+///
+public class TextTemplatingService : ITextTemplating, ITextTemplatingEngineHost, IServiceProvider
+{
+ private readonly IServiceProvider _serviceProvider;
+ private ITextTemplatingCallback? _callback;
+ private string? _templateFile;
+
+ ///
+ /// 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.
+ ///
+ public TextTemplatingService(IServiceProvider serviceProvider)
+ => _serviceProvider = serviceProvider;
+
+ ///
+ /// 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.
+ ///
+ public virtual ITextTemplatingSession? Session { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public virtual IList StandardAssemblyReferences { get; } = new string[]
+ {
+ typeof(ITextTemplatingEngineHost).Assembly.Location,
+ typeof(CompilerErrorCollection).Assembly.Location
+ };
+
+ ///
+ /// 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.
+ ///
+ public virtual IList StandardImports { get; } = new[]
+ {
+ "System"
+ };
+
+ ///
+ /// 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.
+ ///
+ public virtual string? TemplateFile
+ => _templateFile;
+
+ ///
+ /// 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.
+ ///
+ public virtual string ProcessTemplate(string inputFile, string content, ITextTemplatingCallback? callback = null)
+ {
+ _templateFile = inputFile;
+ _callback = callback;
+
+ var sessionCreated = false;
+ if (Session == null)
+ {
+ Session = CreateSession();
+ sessionCreated = true;
+ }
+
+ try
+ {
+ return new Engine().ProcessTemplate(content, this);
+ }
+ finally
+ {
+ _templateFile = null;
+ _callback = null;
+
+ if (sessionCreated)
+ {
+ Session = null;
+ }
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ public virtual ITextTemplatingSession CreateSession()
+ => new TextTemplatingSession();
+
+ ///
+ /// 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.
+ ///
+ public virtual object? GetHostOption(string optionName)
+ => null;
+
+ ///
+ /// 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.
+ ///
+ public virtual bool LoadIncludeText(string requestFileName, out string content, out string location)
+ {
+ // TODO: Expand variables?
+ location = ResolvePath(requestFileName);
+ var exists = File.Exists(location);
+ content = exists
+ ? File.ReadAllText(location)
+ : string.Empty;
+
+ return exists;
+ }
+
+ ///
+ /// 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.
+ ///
+ public virtual void LogErrors(CompilerErrorCollection errors)
+ {
+ foreach (CompilerError error in errors)
+ {
+ _callback?.ErrorCallback(error);
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ public virtual AppDomain ProvideTemplatingAppDomain(string content)
+ => AppDomain.CurrentDomain;
+
+ ///
+ /// 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.
+ ///
+ public virtual string ResolveAssemblyReference(string assemblyReference)
+ {
+ try
+ {
+ return Assembly.Load(assemblyReference).Location;
+ }
+ catch
+ {
+ }
+
+ // TODO: Expand variables?
+ return assemblyReference;
+ }
+
+ ///
+ /// 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.
+ ///
+ public virtual Type ResolveDirectiveProcessor(string processorName)
+ => throw new FileNotFoundException(DesignStrings.UnknownDirectiveProcessor(processorName));
+
+ ///
+ /// 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.
+ ///
+ public virtual string ResolveParameterValue(string directiveId, string processorName, string parameterName)
+ => string.Empty;
+
+ ///
+ /// 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.
+ ///
+ public virtual string ResolvePath(string path)
+ => !Path.IsPathRooted(path)
+ ? Path.Combine(Path.GetDirectoryName(TemplateFile)!, path)
+ : path;
+
+ ///
+ /// 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.
+ ///
+ public virtual void SetFileExtension(string extension)
+ => _callback?.SetFileExtension(extension);
+
+ ///
+ /// 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.
+ ///
+ public virtual void SetOutputEncoding(Encoding encoding, bool fromOutputDirective)
+ => _callback?.SetOutputEncoding(encoding, fromOutputDirective);
+
+ ///
+ /// 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.
+ ///
+ public virtual object? GetService(Type serviceType)
+ => _serviceProvider.GetService(serviceType);
+}
diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs
index 3998a996b4a..98000b2490b 100644
--- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs
@@ -51,7 +51,7 @@ public static EntityTypeBuilder ToTable(
{
Check.NotNull(buildAction, nameof(buildAction));
- buildAction(new TableBuilder(entityTypeBuilder));
+ buildAction(new TableBuilder(null, null, entityTypeBuilder));
return entityTypeBuilder;
}
@@ -76,7 +76,7 @@ public static EntityTypeBuilder ToTable(
entityTypeBuilder.Metadata.SetTableName(name);
entityTypeBuilder.Metadata.SetSchema(null);
- buildAction(new TableBuilder(entityTypeBuilder));
+ buildAction(new TableBuilder(name, null, entityTypeBuilder));
return entityTypeBuilder;
}
@@ -192,7 +192,7 @@ public static EntityTypeBuilder ToTable(
entityTypeBuilder.Metadata.SetTableName(name);
entityTypeBuilder.Metadata.SetSchema(schema);
- buildAction(new TableBuilder(entityTypeBuilder));
+ buildAction(new TableBuilder(name, schema, entityTypeBuilder));
return entityTypeBuilder;
}
@@ -281,7 +281,7 @@ public static OwnedNavigationBuilder ToTable(
{
Check.NotNull(buildAction, nameof(buildAction));
- buildAction(new OwnedNavigationTableBuilder(referenceOwnershipBuilder));
+ buildAction(new OwnedNavigationTableBuilder(null, null, referenceOwnershipBuilder));
return referenceOwnershipBuilder;
}
@@ -303,7 +303,7 @@ public static OwnedNavigationBuilder ToTable(referenceOwnershipBuilder));
+ buildAction(new OwnedNavigationTableBuilder(null, null, referenceOwnershipBuilder));
return referenceOwnershipBuilder;
}
@@ -344,7 +344,7 @@ public static OwnedNavigationBuilder ToTable(
referenceOwnershipBuilder.OwnedEntityType.SetTableName(name);
referenceOwnershipBuilder.OwnedEntityType.SetSchema(null);
- buildAction(new OwnedNavigationTableBuilder(referenceOwnershipBuilder));
+ buildAction(new OwnedNavigationTableBuilder(name, null, referenceOwnershipBuilder));
return referenceOwnershipBuilder;
}
@@ -371,7 +371,7 @@ public static OwnedNavigationBuilder ToTable(referenceOwnershipBuilder));
+ buildAction(new OwnedNavigationTableBuilder(name, null, referenceOwnershipBuilder));
return referenceOwnershipBuilder;
}
@@ -423,7 +423,7 @@ public static OwnedNavigationBuilder ToTable(
referenceOwnershipBuilder.OwnedEntityType.SetTableName(name);
referenceOwnershipBuilder.OwnedEntityType.SetSchema(schema);
- buildAction(new OwnedNavigationTableBuilder(referenceOwnershipBuilder));
+ buildAction(new OwnedNavigationTableBuilder(name, schema, referenceOwnershipBuilder));
return referenceOwnershipBuilder;
}
@@ -472,7 +472,7 @@ public static OwnedNavigationBuilder ToTable(referenceOwnershipBuilder));
+ buildAction(new OwnedNavigationTableBuilder(name, schema, referenceOwnershipBuilder));
return referenceOwnershipBuilder;
}
diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs
index 7d609106682..fea3d7d740d 100644
--- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs
@@ -39,7 +39,7 @@ public static class RelationalEntityTypeExtensions
return entityType.GetRootType().GetTableName();
}
- return (entityType as IConventionEntityType)?.GetViewNameConfigurationSource() == null
+ return ((entityType as IConventionEntityType)?.GetViewNameConfigurationSource() == null)
&& ((entityType as IConventionEntityType)?.GetFunctionNameConfigurationSource() == null)
#pragma warning disable CS0618 // Type or member is obsolete
&& ((entityType as IConventionEntityType)?.GetDefiningQueryConfigurationSource() == null)
@@ -254,7 +254,7 @@ public static void SetSchema(this IMutableEntityType entityType, string? value)
public static IEnumerable GetDefaultMappings(this IEntityType entityType)
=> (IEnumerable?)entityType.FindRuntimeAnnotationValue(
RelationalAnnotationNames.DefaultMappings)
- ?? Array.Empty();
+ ?? Enumerable.Empty();
///
/// Returns the tables to which the entity type is mapped.
@@ -264,7 +264,7 @@ public static IEnumerable GetDefaultMappings(this IEntityType
public static IEnumerable GetTableMappings(this IEntityType entityType)
=> (IEnumerable?)entityType.FindRuntimeAnnotationValue(
RelationalAnnotationNames.TableMappings)
- ?? Array.Empty();
+ ?? Enumerable.Empty();
///
/// Returns the name of the view to which the entity type is mapped or if not mapped to a view.
@@ -286,7 +286,7 @@ public static IEnumerable GetTableMappings(this IEntityType entit
return ((entityType as IConventionEntityType)?.GetFunctionNameConfigurationSource() == null)
#pragma warning disable CS0618 // Type or member is obsolete
- && (entityType as IConventionEntityType)?.GetDefiningQueryConfigurationSource() == null
+ && ((entityType as IConventionEntityType)?.GetDefiningQueryConfigurationSource() == null)
#pragma warning restore CS0618 // Type or member is obsolete
&& ((entityType as IConventionEntityType)?.GetSqlQueryConfigurationSource() == null)
? GetDefaultViewName(entityType)
@@ -428,7 +428,7 @@ public static void SetViewSchema(this IMutableEntityType entityType, string? val
public static IEnumerable GetViewMappings(this IEntityType entityType)
=> (IEnumerable?)entityType.FindRuntimeAnnotationValue(
RelationalAnnotationNames.ViewMappings)
- ?? Array.Empty();
+ ?? Enumerable.Empty();
///
/// Gets the default SQL query name that would be used for this entity type when mapped using
@@ -493,7 +493,7 @@ public static void SetSqlQuery(this IMutableEntityType entityType, string? name)
public static IEnumerable GetSqlQueryMappings(this IEntityType entityType)
=> (IEnumerable?)entityType.FindRuntimeAnnotationValue(
RelationalAnnotationNames.SqlQueryMappings)
- ?? Array.Empty();
+ ?? Enumerable.Empty();
///
/// Returns the name of the function to which the entity type is mapped or if not mapped to a function.
@@ -549,7 +549,7 @@ public static void SetFunctionName(this IMutableEntityType entityType, string? n
public static IEnumerable GetFunctionMappings(this IEntityType entityType)
=> (IEnumerable?)entityType.FindRuntimeAnnotationValue(
RelationalAnnotationNames.FunctionMappings)
- ?? Array.Empty();
+ ?? Enumerable.Empty();
///
/// Finds an with the given name.
diff --git a/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs
index 1be45e2ac3b..efeff92fa22 100644
--- a/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs
@@ -21,6 +21,12 @@ public static class RelationalForeignKeyExtensions
/// The foreign key constraint name.
public static string? GetConstraintName(this IReadOnlyForeignKey foreignKey)
{
+ var tableName = foreignKey.DeclaringEntityType.GetTableName();
+ if (tableName == null)
+ {
+ return null;
+ }
+
var annotation = foreignKey.FindAnnotation(RelationalAnnotationNames.Name);
return annotation != null
? (string?)annotation.Value
@@ -39,6 +45,12 @@ public static class RelationalForeignKeyExtensions
in StoreObjectIdentifier storeObject,
in StoreObjectIdentifier principalStoreObject)
{
+ if (storeObject.StoreObjectType != StoreObjectType.Table
+ || principalStoreObject.StoreObjectType != StoreObjectType.Table)
+ {
+ return null;
+ }
+
var annotation = foreignKey.FindAnnotation(RelationalAnnotationNames.Name);
return annotation != null
? (string?)annotation.Value
@@ -50,10 +62,15 @@ public static class RelationalForeignKeyExtensions
///
/// The foreign key.
/// The default constraint name that would be used for this foreign key.
- public static string GetDefaultName(this IReadOnlyForeignKey foreignKey)
+ public static string? GetDefaultName(this IReadOnlyForeignKey foreignKey)
{
var tableName = foreignKey.DeclaringEntityType.GetTableName();
var principalTableName = foreignKey.PrincipalEntityType.GetTableName();
+ if (tableName == null
+ || principalTableName == null)
+ {
+ return null;
+ }
var name = new StringBuilder()
.Append("FK_")
@@ -79,6 +96,12 @@ public static string GetDefaultName(this IReadOnlyForeignKey foreignKey)
in StoreObjectIdentifier storeObject,
in StoreObjectIdentifier principalStoreObject)
{
+ if (storeObject.StoreObjectType != StoreObjectType.Table
+ || principalStoreObject.StoreObjectType != StoreObjectType.Table)
+ {
+ return null;
+ }
+
var propertyNames = foreignKey.Properties.GetColumnNames(storeObject);
var principalPropertyNames = foreignKey.PrincipalKey.Properties.GetColumnNames(principalStoreObject);
if (propertyNames == null
diff --git a/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs b/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs
index 705c7004d33..288071d3225 100644
--- a/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs
@@ -19,8 +19,10 @@ public static class RelationalIndexExtensions
///
/// The index.
/// The name of the index in the database.
- public static string GetDatabaseName(this IReadOnlyIndex index)
- => (string?)index[RelationalAnnotationNames.Name]
+ public static string? GetDatabaseName(this IReadOnlyIndex index)
+ => index.DeclaringEntityType.GetTableName() == null
+ ? null
+ : (string?)index[RelationalAnnotationNames.Name]
?? index.Name
?? index.GetDefaultDatabaseName();
@@ -31,7 +33,9 @@ public static string GetDatabaseName(this IReadOnlyIndex index)
/// The identifier of the store object.
/// The name of the index in the database.
public static string? GetDatabaseName(this IReadOnlyIndex index, in StoreObjectIdentifier storeObject)
- => (string?)index[RelationalAnnotationNames.Name]
+ => storeObject.StoreObjectType != StoreObjectType.Table
+ ? null
+ : (string?)index[RelationalAnnotationNames.Name]
?? index.Name
?? index.GetDefaultDatabaseName(storeObject);
@@ -40,9 +44,14 @@ public static string GetDatabaseName(this IReadOnlyIndex index)
///
/// The index.
/// The default name that would be used for this index.
- public static string GetDefaultDatabaseName(this IReadOnlyIndex index)
+ public static string? GetDefaultDatabaseName(this IReadOnlyIndex index)
{
var tableName = index.DeclaringEntityType.GetTableName();
+ if (tableName == null)
+ {
+ return null;
+ }
+
var baseName = new StringBuilder()
.Append("IX_")
.Append(tableName)
@@ -61,6 +70,11 @@ public static string GetDefaultDatabaseName(this IReadOnlyIndex index)
/// The default name that would be used for this index.
public static string? GetDefaultDatabaseName(this IReadOnlyIndex index, in StoreObjectIdentifier storeObject)
{
+ if (storeObject.StoreObjectType != StoreObjectType.Table)
+ {
+ return null;
+ }
+
var columnNames = index.Properties.GetColumnNames(storeObject);
if (columnNames == null)
{
diff --git a/src/EFCore.Relational/Extensions/RelationalKeyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalKeyExtensions.cs
index 5bb15a3cf93..b1590465495 100644
--- a/src/EFCore.Relational/Extensions/RelationalKeyExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalKeyExtensions.cs
@@ -20,7 +20,10 @@ public static class RelationalKeyExtensions
/// The key.
/// The key constraint name for this key.
public static string? GetName(this IReadOnlyKey key)
- => key.GetName(StoreObjectIdentifier.Table(key.DeclaringEntityType.GetTableName()!, key.DeclaringEntityType.GetSchema()));
+ {
+ var table = StoreObjectIdentifier.Create(key.DeclaringEntityType, StoreObjectType.Table);
+ return !table.HasValue ? null : key.GetName(table.Value);
+ }
///
/// Returns the key constraint name for this key for a particular table.
@@ -29,17 +32,36 @@ public static class RelationalKeyExtensions
/// The identifier of the containing store object.
/// The key constraint name for this key.
public static string? GetName(this IReadOnlyKey key, in StoreObjectIdentifier storeObject)
- => (string?)key[RelationalAnnotationNames.Name]
- ?? key.GetDefaultName(storeObject);
+ {
+ if (storeObject.StoreObjectType != StoreObjectType.Table)
+ {
+ return null;
+ }
+
+ foreach (var containingType in key.DeclaringEntityType.GetDerivedTypesInclusive())
+ {
+ if (StoreObjectIdentifier.Create(containingType, storeObject.StoreObjectType) == storeObject)
+ {
+ return (string?)key[RelationalAnnotationNames.Name] ?? key.GetDefaultName(storeObject);
+ }
+ }
+
+ return null;
+ }
///
/// Returns the default key constraint name that would be used for this key.
///
/// The key.
/// The default key constraint name that would be used for this key.
- public static string GetDefaultName(this IReadOnlyKey key)
+ public static string? GetDefaultName(this IReadOnlyKey key)
{
var tableName = key.DeclaringEntityType.GetTableName();
+ if (tableName == null)
+ {
+ return null;
+ }
+
var name = key.IsPrimaryKey()
? "PK_" + tableName
: new StringBuilder()
@@ -60,6 +82,11 @@ public static string GetDefaultName(this IReadOnlyKey key)
/// The default key constraint name that would be used for this key.
public static string? GetDefaultName(this IReadOnlyKey key, in StoreObjectIdentifier storeObject)
{
+ if (storeObject.StoreObjectType != StoreObjectType.Table)
+ {
+ return null;
+ }
+
string? name;
if (key.IsPrimaryKey())
{
diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableBuilder.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableBuilder.cs
index 0c36a863602..f8c5214cbe4 100644
--- a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableBuilder.cs
+++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableBuilder.cs
@@ -18,11 +18,23 @@ public class OwnedNavigationTableBuilder
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
[EntityFrameworkInternal]
- public OwnedNavigationTableBuilder(OwnedNavigationBuilder ownedNavigationBuilder)
+ public OwnedNavigationTableBuilder(string? name, string? schema, OwnedNavigationBuilder ownedNavigationBuilder)
{
+ Name = name;
+ Schema = schema;
OwnedNavigationBuilder = ownedNavigationBuilder;
}
+ ///
+ /// The specified table name.
+ ///
+ public virtual string? Name { get; }
+
+ ///
+ /// The specified table schema.
+ ///
+ public virtual string? Schema { get; }
+
///
/// The entity type being configured.
///
diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableBuilder`.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableBuilder`.cs
index ff74ddd1933..af93328a20f 100644
--- a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableBuilder`.cs
+++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableBuilder`.cs
@@ -18,8 +18,8 @@ public class OwnedNavigationTableBuilder : OwnedNavigationTableBuilder
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
[EntityFrameworkInternal]
- public OwnedNavigationTableBuilder(OwnedNavigationBuilder referenceOwnershipBuilder)
- : base(referenceOwnershipBuilder)
+ public OwnedNavigationTableBuilder(string? name, string? schema, OwnedNavigationBuilder referenceOwnershipBuilder)
+ : base(name, schema, referenceOwnershipBuilder)
{
}
diff --git a/src/EFCore.Relational/Metadata/Builders/TableBuilder.cs b/src/EFCore.Relational/Metadata/Builders/TableBuilder.cs
index cd913ad8774..8a74222b55d 100644
--- a/src/EFCore.Relational/Metadata/Builders/TableBuilder.cs
+++ b/src/EFCore.Relational/Metadata/Builders/TableBuilder.cs
@@ -18,11 +18,23 @@ public class TableBuilder
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
[EntityFrameworkInternal]
- public TableBuilder(EntityTypeBuilder entityTypeBuilder)
+ public TableBuilder(string? name, string? schema, EntityTypeBuilder entityTypeBuilder)
{
+ Name = name;
+ Schema = schema;
EntityTypeBuilder = entityTypeBuilder;
}
+ ///
+ /// The specified table name.
+ ///
+ public virtual string? Name { get; }
+
+ ///
+ /// The specified table schema.
+ ///
+ public virtual string? Schema { get; }
+
///
/// The entity type being configured.
///
diff --git a/src/EFCore.Relational/Metadata/Builders/TableBuilder`.cs b/src/EFCore.Relational/Metadata/Builders/TableBuilder`.cs
index 2a238815dd9..c02dd30e4f5 100644
--- a/src/EFCore.Relational/Metadata/Builders/TableBuilder`.cs
+++ b/src/EFCore.Relational/Metadata/Builders/TableBuilder`.cs
@@ -19,7 +19,7 @@ public class TableBuilder : TableBuilder
///
[EntityFrameworkInternal]
public TableBuilder(string? name, string? schema, EntityTypeBuilder entityTypeBuilder)
- : base(entityTypeBuilder)
+ : base(name, schema, entityTypeBuilder)
{
}
diff --git a/src/EFCore.Relational/Metadata/IMutableCheckConstraint.cs b/src/EFCore.Relational/Metadata/IMutableCheckConstraint.cs
index 6afcc51acc3..6f7e4349037 100644
--- a/src/EFCore.Relational/Metadata/IMutableCheckConstraint.cs
+++ b/src/EFCore.Relational/Metadata/IMutableCheckConstraint.cs
@@ -19,5 +19,5 @@ public interface IMutableCheckConstraint : IReadOnlyCheckConstraint, IMutableAnn
///
/// Gets or sets the name of the check constraint in the database.
///
- new string Name { get; set; }
+ new string? Name { get; set; }
}
diff --git a/src/EFCore.Relational/Metadata/IReadOnlyCheckConstraint.cs b/src/EFCore.Relational/Metadata/IReadOnlyCheckConstraint.cs
index 5de8906d38d..a9f2f811206 100644
--- a/src/EFCore.Relational/Metadata/IReadOnlyCheckConstraint.cs
+++ b/src/EFCore.Relational/Metadata/IReadOnlyCheckConstraint.cs
@@ -19,7 +19,7 @@ public interface IReadOnlyCheckConstraint : IReadOnlyAnnotatable
///
/// Gets the database name of the check constraint.
///
- string Name { get; }
+ string? Name { get; }
///
/// Returns the default database name that would be used for this check constraint.
@@ -43,8 +43,10 @@ public interface IReadOnlyCheckConstraint : IReadOnlyAnnotatable
///
/// The identifier of the store object.
/// The default name that would be used for this check constraint.
- string GetDefaultName(in StoreObjectIdentifier storeObject)
- => Uniquifier.Truncate(ModelName, EntityType.Model.GetMaxIdentifierLength());
+ string? GetDefaultName(in StoreObjectIdentifier storeObject)
+ => storeObject.StoreObjectType == StoreObjectType.Table
+ ? Uniquifier.Truncate(ModelName, EntityType.Model.GetMaxIdentifierLength())
+ : null;
///
/// Gets the entity type on which this check constraint is defined.
diff --git a/src/EFCore.Relational/Metadata/ITable.cs b/src/EFCore.Relational/Metadata/ITable.cs
index 9a1362af677..21ba462211d 100644
--- a/src/EFCore.Relational/Metadata/ITable.cs
+++ b/src/EFCore.Relational/Metadata/ITable.cs
@@ -51,9 +51,7 @@ public interface ITable : ITableBase
///
/// Gets the check constraints for this table.
///
- IEnumerable CheckConstraints
- => EntityTypeMappings.SelectMany(m => m.EntityType.GetDeclaredCheckConstraints())
- .Distinct((x, y) => x!.Name == y!.Name);
+ IEnumerable CheckConstraints { get; }
///
/// Gets the comment for this table.
diff --git a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs
index 07409a22446..f8655df492d 100644
--- a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs
+++ b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs
@@ -280,15 +280,32 @@ public override bool IsReadOnly
/// 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.
///
- public virtual string Name
+ public virtual string? Name
{
- get => _name ?? ((IReadOnlyCheckConstraint)this).GetDefaultName() ?? ModelName;
+ get => EntityType.GetTableName() == null
+ ? null
+ : _name ?? ((IReadOnlyCheckConstraint)this).GetDefaultName();
set => SetName(value, ConfigurationSource.Explicit);
}
///
public virtual string? GetName(in StoreObjectIdentifier storeObject)
- => _name ?? ((IReadOnlyCheckConstraint)this).GetDefaultName(storeObject) ?? ModelName;
+ {
+ if (storeObject.StoreObjectType != StoreObjectType.Table)
+ {
+ return null;
+ }
+
+ foreach (var containingType in EntityType.GetDerivedTypesInclusive())
+ {
+ if (StoreObjectIdentifier.Create(containingType, storeObject.StoreObjectType) == storeObject)
+ {
+ return _name ?? ((IReadOnlyCheckConstraint)this).GetDefaultName(storeObject);
+ }
+ }
+
+ return null;
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
index 98db63d5249..1c6b8dd5d43 100644
--- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
+++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
@@ -151,7 +151,7 @@ public static IRelationalModel Create(
foreach (var table in databaseModel.Tables.Values)
{
PopulateRowInternalForeignKeys(table);
- PopulateConstraints(table);
+ PopulateConstraints(table, designTime);
if (relationalAnnotationProvider != null)
{
@@ -177,9 +177,9 @@ public static IRelationalModel Create(
if (designTime)
{
- foreach (var checkConstraint in ((ITable)table).CheckConstraints)
+ foreach (var checkConstraint in table.CheckConstraints.Values)
{
- ((AnnotatableBase)checkConstraint).AddAnnotations(
+ checkConstraint.AddAnnotations(
relationalAnnotationProvider.For(checkConstraint, designTime));
}
}
@@ -751,7 +751,7 @@ private static StoreFunction GetOrCreateStoreFunction(IRuntimeDbFunction dbFunct
return storeFunction;
}
- private static void PopulateConstraints(Table table)
+ private static void PopulateConstraints(Table table, bool designTime)
{
var storeObject = StoreObjectIdentifier.Table(table.Name, table.Schema);
foreach (var entityTypeMapping in ((ITable)table).EntityTypeMappings)
@@ -967,6 +967,23 @@ private static void PopulateConstraints(Table table)
tableIndexes.Add(tableIndex);
tableIndex.MappedIndexes.Add(index);
}
+
+ if (designTime)
+ {
+ foreach (var checkConstraint in entityType.GetCheckConstraints())
+ {
+ var name = checkConstraint.GetName(storeObject);
+ if (name == null)
+ {
+ continue;
+ }
+
+ if (!table.CheckConstraints.ContainsKey(name))
+ {
+ table.CheckConstraints.Add(name, (CheckConstraint)checkConstraint);
+ }
+ }
+ }
}
}
diff --git a/src/EFCore.Relational/Metadata/Internal/Table.cs b/src/EFCore.Relational/Metadata/Internal/Table.cs
index 2be86c69d4a..2ed34a918d8 100644
--- a/src/EFCore.Relational/Metadata/Internal/Table.cs
+++ b/src/EFCore.Relational/Metadata/Internal/Table.cs
@@ -113,6 +113,15 @@ public virtual UniqueConstraint? PrimaryKey
public virtual SortedDictionary Indexes { get; }
= new();
+ ///
+ /// 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.
+ ///
+ public virtual SortedDictionary CheckConstraints { get; }
+ = new();
+
///
public virtual bool IsExcludedFromMigrations
=> EntityTypeMappings.First().EntityType.IsTableExcludedFromMigrations();
@@ -174,6 +183,15 @@ IEnumerable ITable.Indexes
get => Indexes.Values;
}
+ ///
+ IEnumerable ITable.CheckConstraints
+ {
+ [DebuggerStepThrough]
+ get => EntityTypeMappings.First().EntityType is RuntimeEntityType
+ ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
+ : CheckConstraints.Values;
+ }
+
///
[DebuggerStepThrough]
IColumn? ITable.FindColumn(string name)
diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
index cba40a8ebbd..7e9ebc3ba4e 100644
--- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
+++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
@@ -1527,7 +1527,7 @@ protected virtual IEnumerable Remove(ICheckConstraint source
var operation = new DropCheckConstraintOperation
{
- Name = source.Name,
+ Name = source.Name!,
Schema = sourceEntityType.GetSchema(),
Table = sourceEntityType.GetTableName()!
};
@@ -2189,7 +2189,7 @@ private IEnumerable GetDataOperations(
var commandBatches = new CommandBatchPreparer(CommandBatchPreparerDependencies)
.BatchCommands(entries, updateAdapter);
- foreach (var commandBatch in commandBatches)
+ foreach (var (commandBatch, _) in commandBatches)
{
InsertDataOperation? batchInsertOperation = null;
foreach (var command in commandBatch.ModificationCommands)
diff --git a/src/EFCore.Relational/Migrations/Operations/AddCheckConstraintOperation.cs b/src/EFCore.Relational/Migrations/Operations/AddCheckConstraintOperation.cs
index 1547f775081..7747f10915a 100644
--- a/src/EFCore.Relational/Migrations/Operations/AddCheckConstraintOperation.cs
+++ b/src/EFCore.Relational/Migrations/Operations/AddCheckConstraintOperation.cs
@@ -46,7 +46,7 @@ public static AddCheckConstraintOperation CreateFrom(ICheckConstraint checkConst
var operation = new AddCheckConstraintOperation
{
- Name = checkConstraint.Name,
+ Name = checkConstraint.Name!,
Sql = checkConstraint.Sql,
Schema = checkConstraint.EntityType.GetSchema(),
Table = checkConstraint.EntityType.GetTableName()!
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index d891f6405b1..a96438e2c9b 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -843,6 +843,18 @@ public static string MissingParameterValue(object? parameter)
GetString("MissingParameterValue", nameof(parameter)),
parameter);
+ ///
+ /// Cannot execute an ModificationCommandBatch which hasn't been completed.
+ ///
+ public static string ModificationCommandBatchAlreadyComplete
+ => GetString("ModificationCommandBatchAlreadyComplete");
+
+ ///
+ /// Cannot execute an ModificationCommandBatch which hasn't been completed.
+ ///
+ public static string ModificationCommandBatchNotComplete
+ => GetString("ModificationCommandBatchNotCompleted");
+
///
/// Cannot save changes for an entity of type '{entityType}' in state '{entityState}'. This may indicate a bug in Entity Framework, please open an issue at https://go.microsoft.com/fwlink/?linkid=2142044. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values of the entity.
///
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index 57b5e40ab57..9ba10958462 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -669,6 +669,12 @@
No value was provided for the required parameter '{parameter}'.
+
+ Cannot add commands to a completed ModificationCommandBatch.
+
+
+ Cannot execute an ModificationCommandBatch which hasn't been completed.
+
Cannot save changes for an entity of type '{entityType}' in state '{entityState}'. This may indicate a bug in Entity Framework, please open an issue at https://go.microsoft.com/fwlink/?linkid=2142044. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values of the entity.
diff --git a/src/EFCore.Relational/Query/Internal/RandomTranslator.cs b/src/EFCore.Relational/Query/Internal/RandomTranslator.cs
index 5fc45186084..9c1ab0e26db 100644
--- a/src/EFCore.Relational/Query/Internal/RandomTranslator.cs
+++ b/src/EFCore.Relational/Query/Internal/RandomTranslator.cs
@@ -43,9 +43,9 @@ public RandomTranslator(ISqlExpressionFactory sqlExpressionFactory)
=> MethodInfo.Equals(method)
? _sqlExpressionFactory.Function(
"RAND",
- Array.Empty(),
+ Enumerable.Empty(),
nullable: false,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
method.ReturnType)
: null;
}
diff --git a/src/EFCore.Relational/Update/IBatchExecutor.cs b/src/EFCore.Relational/Update/IBatchExecutor.cs
index e8209d5328b..20ebf876acc 100644
--- a/src/EFCore.Relational/Update/IBatchExecutor.cs
+++ b/src/EFCore.Relational/Update/IBatchExecutor.cs
@@ -28,17 +28,21 @@ public interface IBatchExecutor
///
/// Executes the commands in the batches against the given database connection.
///
- /// The batches to execute.
+ ///
+ /// A list of value tuples, each of which contains a batch to execute, and whether more batches are available.
+ ///
/// The database connection to use.
/// The total number of rows affected.
int Execute(
- IEnumerable commandBatches,
+ IEnumerable<(ModificationCommandBatch Batch, bool HasMore)> commandBatches,
IRelationalConnection connection);
///
/// Executes the commands in the batches against the given database connection.
///
- /// The batches to execute.
+ ///
+ /// A list of value tuples, each of which contains a batch to execute, and whether more batches are available.
+ ///
/// The database connection to use.
/// A to observe while waiting for the task to complete.
///
@@ -47,7 +51,7 @@ int Execute(
///
/// If the is canceled.
Task ExecuteAsync(
- IEnumerable commandBatches,
+ IEnumerable<(ModificationCommandBatch Batch, bool HasMore)> commandBatches,
IRelationalConnection connection,
CancellationToken cancellationToken = default);
}
diff --git a/src/EFCore.Relational/Update/ICommandBatchPreparer.cs b/src/EFCore.Relational/Update/ICommandBatchPreparer.cs
index 9bfdd8ca3e8..b045c936d51 100644
--- a/src/EFCore.Relational/Update/ICommandBatchPreparer.cs
+++ b/src/EFCore.Relational/Update/ICommandBatchPreparer.cs
@@ -32,8 +32,6 @@ public interface ICommandBatchPreparer
///
/// The entries that represent the entities to be modified.
/// The model data.
- /// The list of batches to execute.
- IEnumerable BatchCommands(
- IList entries,
- IUpdateAdapter updateAdapter);
+ /// A list of value tuples, each of which contains a batch to execute, and whether more batches are available.
+ IEnumerable<(ModificationCommandBatch Batch, bool HasMore)> BatchCommands(IList entries, IUpdateAdapter updateAdapter);
}
diff --git a/src/EFCore.Relational/Update/IUpdateSqlGenerator.cs b/src/EFCore.Relational/Update/IUpdateSqlGenerator.cs
index 3022c1c7e29..9fbaca6a97f 100644
--- a/src/EFCore.Relational/Update/IUpdateSqlGenerator.cs
+++ b/src/EFCore.Relational/Update/IUpdateSqlGenerator.cs
@@ -54,6 +54,26 @@ void AppendNextSequenceValueOperation(
/// The builder to which the SQL fragment should be appended.
void AppendBatchHeader(StringBuilder commandStringBuilder);
+ ///
+ /// Prepends a SQL command for turning on autocommit mode in the database, in case it is off.
+ ///
+ /// The builder to which the SQL should be prepended.
+ void PrependEnsureAutocommit(StringBuilder commandStringBuilder);
+
+ ///
+ /// Appends a SQL command for deleting a row to the commands being built.
+ ///
+ /// The builder to which the SQL should be appended.
+ /// The command that represents the delete operation.
+ /// The ordinal of this command in the batch.
+ /// Returns whether the SQL appended must be executed in a transaction to work correctly.
+ /// The for the command.
+ ResultSetMapping AppendDeleteOperation(
+ StringBuilder commandStringBuilder,
+ IReadOnlyModificationCommand command,
+ int commandPosition,
+ out bool requiresTransaction);
+
///
/// Appends a SQL command for deleting a row to the commands being built.
///
@@ -64,7 +84,22 @@ void AppendNextSequenceValueOperation(
ResultSetMapping AppendDeleteOperation(
StringBuilder commandStringBuilder,
IReadOnlyModificationCommand command,
- int commandPosition);
+ int commandPosition)
+ => AppendDeleteOperation(commandStringBuilder, command, commandPosition, out _);
+
+ ///
+ /// Appends a SQL command for inserting a row to the commands being built.
+ ///
+ /// The builder to which the SQL should be appended.
+ /// The command that represents the delete operation.
+ /// The ordinal of this command in the batch.
+ /// Returns whether the SQL appended must be executed in a transaction to work correctly.
+ /// The for the command.
+ ResultSetMapping AppendInsertOperation(
+ StringBuilder commandStringBuilder,
+ IReadOnlyModificationCommand command,
+ int commandPosition,
+ out bool requiresTransaction);
///
/// Appends a SQL command for inserting a row to the commands being built.
@@ -76,7 +111,22 @@ ResultSetMapping AppendDeleteOperation(
ResultSetMapping AppendInsertOperation(
StringBuilder commandStringBuilder,
IReadOnlyModificationCommand command,
- int commandPosition);
+ int commandPosition)
+ => AppendInsertOperation(commandStringBuilder, command, commandPosition, out _);
+
+ ///
+ /// Appends a SQL command for updating a row to the commands being built.
+ ///
+ /// The builder to which the SQL should be appended.
+ /// The command that represents the delete operation.
+ /// The ordinal of this command in the batch.
+ /// Returns whether the SQL appended must be executed in a transaction to work correctly.
+ /// The for the command.
+ ResultSetMapping AppendUpdateOperation(
+ StringBuilder commandStringBuilder,
+ IReadOnlyModificationCommand command,
+ int commandPosition,
+ out bool requiresTransaction);
///
/// Appends a SQL command for updating a row to the commands being built.
@@ -88,5 +138,6 @@ ResultSetMapping AppendInsertOperation(
ResultSetMapping AppendUpdateOperation(
StringBuilder commandStringBuilder,
IReadOnlyModificationCommand command,
- int commandPosition);
+ int commandPosition)
+ => AppendUpdateOperation(commandStringBuilder, command, commandPosition, out _);
}
diff --git a/src/EFCore.Relational/Update/Internal/BatchExecutor.cs b/src/EFCore.Relational/Update/Internal/BatchExecutor.cs
index 0ac9a116e26..6eb20e22be9 100644
--- a/src/EFCore.Relational/Update/Internal/BatchExecutor.cs
+++ b/src/EFCore.Relational/Update/Internal/BatchExecutor.cs
@@ -49,9 +49,18 @@ public BatchExecutor(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public virtual int Execute(
- IEnumerable commandBatches,
+ IEnumerable<(ModificationCommandBatch Batch, bool HasMore)> commandBatches,
IRelationalConnection connection)
{
+ using var batchEnumerator = commandBatches.GetEnumerator();
+
+ if (!batchEnumerator.MoveNext())
+ {
+ return 0;
+ }
+
+ var (batch, hasMoreBatches) = batchEnumerator.Current;
+
var rowsAffected = 0;
var transaction = connection.CurrentTransaction;
var beganTransaction = false;
@@ -62,7 +71,9 @@ public virtual int Execute(
if (transaction == null
&& transactionEnlistManager?.EnlistedTransaction is null
&& transactionEnlistManager?.CurrentAmbientTransaction is null
- && CurrentContext.Context.Database.AutoTransactionsEnabled)
+ && CurrentContext.Context.Database.AutoTransactionsEnabled
+ // Don't start a transaction if we have a single batch which doesn't require a transaction (single command), for perf.
+ && (hasMoreBatches || batch.RequiresTransaction))
{
transaction = connection.BeginTransaction();
beganTransaction = true;
@@ -79,11 +90,13 @@ public virtual int Execute(
}
}
- foreach (var batch in commandBatches)
+ do
{
+ batch = batchEnumerator.Current.Batch;
batch.Execute(connection);
rowsAffected += batch.ModificationCommands.Count;
}
+ while (batchEnumerator.MoveNext());
if (beganTransaction)
{
@@ -143,10 +156,19 @@ public virtual int Execute(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public virtual async Task ExecuteAsync(
- IEnumerable commandBatches,
+ IEnumerable<(ModificationCommandBatch Batch, bool HasMore)> commandBatches,
IRelationalConnection connection,
CancellationToken cancellationToken = default)
{
+ using var batchEnumerator = commandBatches.GetEnumerator();
+
+ if (!batchEnumerator.MoveNext())
+ {
+ return 0;
+ }
+
+ var (batch, hasMoreBatches) = batchEnumerator.Current;
+
var rowsAffected = 0;
var transaction = connection.CurrentTransaction;
var beganTransaction = false;
@@ -157,7 +179,9 @@ public virtual async Task ExecuteAsync(
if (transaction == null
&& transactionEnlistManager?.EnlistedTransaction is null
&& transactionEnlistManager?.CurrentAmbientTransaction is null
- && CurrentContext.Context.Database.AutoTransactionsEnabled)
+ && CurrentContext.Context.Database.AutoTransactionsEnabled
+ // Don't start a transaction if we have a single batch which doesn't require a transaction (single command), for perf.
+ && (hasMoreBatches || batch.RequiresTransaction))
{
transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
beganTransaction = true;
@@ -174,11 +198,13 @@ public virtual async Task ExecuteAsync(
}
}
- foreach (var batch in commandBatches)
+ do
{
+ batch = batchEnumerator.Current.Batch;
await batch.ExecuteAsync(connection, cancellationToken).ConfigureAwait(false);
rowsAffected += batch.ModificationCommands.Count;
}
+ while (batchEnumerator.MoveNext());
if (beganTransaction)
{
diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs
index 099058647df..9bd866d77dd 100644
--- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs
+++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs
@@ -50,7 +50,7 @@ public CommandBatchPreparer(CommandBatchPreparerDependencies dependencies)
/// 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.
///
- public virtual IEnumerable BatchCommands(
+ public virtual IEnumerable<(ModificationCommandBatch Batch, bool HasMore)> BatchCommands(
IList entries,
IUpdateAdapter updateAdapter)
{
@@ -58,8 +58,10 @@ public virtual IEnumerable BatchCommands(
var commands = CreateModificationCommands(entries, updateAdapter, parameterNameGenerator.GenerateNext);
var sortedCommandSets = TopologicalSort(commands);
- foreach (var independentCommandSet in sortedCommandSets)
+ for (var commandSetIndex = 0; commandSetIndex < sortedCommandSets.Count; commandSetIndex++)
{
+ var independentCommandSet = sortedCommandSets[commandSetIndex];
+
independentCommandSet.Sort(Dependencies.ModificationCommandComparer);
var batch = Dependencies.ModificationCommandBatchFactory.Create();
@@ -83,7 +85,9 @@ public virtual IEnumerable BatchCommands(
batch.ModificationCommands.SelectMany(c => c.Entries), batch.ModificationCommands.Count);
}
- yield return batch;
+ batch.Complete();
+
+ yield return (batch, true);
}
else
{
@@ -92,7 +96,10 @@ public virtual IEnumerable BatchCommands(
foreach (var command in batch.ModificationCommands)
{
- yield return StartNewBatch(parameterNameGenerator, command);
+ batch = StartNewBatch(parameterNameGenerator, command);
+ batch.Complete();
+
+ yield return (batch, true);
}
}
@@ -100,6 +107,8 @@ public virtual IEnumerable BatchCommands(
}
}
+ var hasMoreCommandSets = commandSetIndex < sortedCommandSets.Count - 1;
+
if (batch.ModificationCommands.Count == 1
|| batch.ModificationCommands.Count >= _minBatchSize)
{
@@ -109,16 +118,21 @@ public virtual IEnumerable BatchCommands(
batch.ModificationCommands.SelectMany(c => c.Entries), batch.ModificationCommands.Count);
}
- yield return batch;
+ batch.Complete();
+
+ yield return (batch, hasMoreCommandSets);
}
else
{
Dependencies.UpdateLogger.BatchSmallerThanMinBatchSize(
batch.ModificationCommands.SelectMany(c => c.Entries), batch.ModificationCommands.Count, _minBatchSize);
- foreach (var command in batch.ModificationCommands)
+ for (var commandIndex = 0; commandIndex < batch.ModificationCommands.Count; commandIndex++)
{
- yield return StartNewBatch(parameterNameGenerator, command);
+ var singleCommandBatch = StartNewBatch(parameterNameGenerator, batch.ModificationCommands[commandIndex]);
+ singleCommandBatch.Complete();
+
+ yield return (singleCommandBatch, hasMoreCommandSets || commandIndex < batch.ModificationCommands.Count - 1);
}
}
}
@@ -156,7 +170,7 @@ protected virtual IEnumerable CreateModificationCo
continue;
}
- var mappings = (IReadOnlyCollection)entry.EntityType.GetTableMappings();
+ var mappings = entry.EntityType.GetTableMappings();
IModificationCommand? firstCommands = null;
foreach (var mapping in mappings)
{
diff --git a/src/EFCore.Relational/Update/ModificationCommandBatch.cs b/src/EFCore.Relational/Update/ModificationCommandBatch.cs
index b01c52628af..152e665be45 100644
--- a/src/EFCore.Relational/Update/ModificationCommandBatch.cs
+++ b/src/EFCore.Relational/Update/ModificationCommandBatch.cs
@@ -33,6 +33,16 @@ public abstract class ModificationCommandBatch
///
public abstract bool AddCommand(IReadOnlyModificationCommand modificationCommand);
+ ///
+ /// Indicates that no more commands will be added to this batch, and prepares it for execution.
+ ///
+ public abstract void Complete();
+
+ ///
+ /// Indicates whether the batch requires a transaction in order to execute correctly.
+ ///
+ public abstract bool RequiresTransaction { get; }
+
///
/// Sends insert/update/delete commands to the database.
///
diff --git a/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs b/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs
index b84e25a6b57..1a35f6aa66c 100644
--- a/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs
+++ b/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs
@@ -21,6 +21,8 @@ namespace Microsoft.EntityFrameworkCore.Update;
public abstract class ReaderModificationCommandBatch : ModificationCommandBatch
{
private readonly List _modificationCommands = new();
+ private string? _finalCommandText;
+ private bool _requiresTransaction = true;
///
/// Creates a new instance.
@@ -74,6 +76,11 @@ public override IReadOnlyList ModificationCommands
///
public override bool AddCommand(IReadOnlyModificationCommand modificationCommand)
{
+ if (_finalCommandText is not null)
+ {
+ throw new InvalidOperationException(RelationalStrings.ModificationCommandBatchAlreadyComplete);
+ }
+
if (ModificationCommands.Count == 0)
{
ResetCommandText();
@@ -103,15 +110,35 @@ public override bool AddCommand(IReadOnlyModificationCommand modificationCommand
///
protected virtual void ResetCommandText()
{
- if (CachedCommandText.Length > 0)
- {
- CachedCommandText = new StringBuilder();
- }
+ CachedCommandText.Clear();
UpdateSqlGenerator.AppendBatchHeader(CachedCommandText);
+ _batchHeaderLength = CachedCommandText.Length;
+
+ SetRequiresTransaction(true);
+
LastCachedCommandIndex = -1;
}
+ private int _batchHeaderLength;
+
+ ///
+ /// Whether any SQL has already been added to the batch command text.
+ ///
+ protected virtual bool IsCachedCommandTextEmpty
+ => CachedCommandText.Length == _batchHeaderLength;
+
+ ///
+ public override bool RequiresTransaction
+ => _requiresTransaction;
+
+ ///
+ /// Sets whether the batch requires a transaction in order to execute correctly.
+ ///
+ /// Whether the batch requires a transaction in order to execute correctly.
+ protected virtual void SetRequiresTransaction(bool requiresTransaction)
+ => _requiresTransaction = requiresTransaction;
+
///
/// Checks whether a new command can be added to the batch.
///
@@ -126,18 +153,15 @@ protected virtual void ResetCommandText()
protected abstract bool IsCommandTextValid();
///
- /// Gets the command text for all the commands in the current batch and also caches it
- /// on .
+ /// Processes all unprocessed commands in the batch, making sure their corresponding SQL is populated in
+ /// .
///
- /// The command text.
- protected virtual string GetCommandText()
+ protected virtual void UpdateCachedCommandText()
{
for (var i = LastCachedCommandIndex + 1; i < ModificationCommands.Count; i++)
{
UpdateCachedCommandText(i);
}
-
- return CachedCommandText.ToString();
}
///
@@ -149,22 +173,35 @@ protected virtual void UpdateCachedCommandText(int commandPosition)
{
var newModificationCommand = ModificationCommands[commandPosition];
+ bool requiresTransaction;
+
switch (newModificationCommand.EntityState)
{
case EntityState.Added:
CommandResultSet[commandPosition] =
- UpdateSqlGenerator.AppendInsertOperation(CachedCommandText, newModificationCommand, commandPosition);
+ UpdateSqlGenerator.AppendInsertOperation(
+ CachedCommandText, newModificationCommand, commandPosition, out requiresTransaction);
break;
case EntityState.Modified:
CommandResultSet[commandPosition] =
- UpdateSqlGenerator.AppendUpdateOperation(CachedCommandText, newModificationCommand, commandPosition);
+ UpdateSqlGenerator.AppendUpdateOperation(
+ CachedCommandText, newModificationCommand, commandPosition, out requiresTransaction);
break;
case EntityState.Deleted:
CommandResultSet[commandPosition] =
- UpdateSqlGenerator.AppendDeleteOperation(CachedCommandText, newModificationCommand, commandPosition);
+ UpdateSqlGenerator.AppendDeleteOperation(
+ CachedCommandText, newModificationCommand, commandPosition, out requiresTransaction);
break;
+
+ default:
+ throw new InvalidOperationException(
+ RelationalStrings.ModificationCommandInvalidEntityState(
+ newModificationCommand.Entries[0].EntityType,
+ newModificationCommand.EntityState));
}
+ _requiresTransaction = commandPosition > 0 || requiresTransaction;
+
LastCachedCommandIndex = commandPosition;
}
@@ -175,15 +212,33 @@ protected virtual void UpdateCachedCommandText(int commandPosition)
protected virtual int GetParameterCount()
=> ModificationCommands.Sum(c => c.ColumnModifications.Count);
+ ///
+ public override void Complete()
+ {
+ UpdateCachedCommandText();
+
+ // Some database have a mode where autocommit is off, and so executing a command outside of an explicit transaction implicitly
+ // creates a new transaction (which needs to be explicitly committed).
+ // The below is a hook for allowing providers to turn autocommit on, in case it's off.
+ if (!RequiresTransaction)
+ {
+ UpdateSqlGenerator.PrependEnsureAutocommit(CachedCommandText);
+ }
+
+ _finalCommandText = CachedCommandText.ToString();
+ }
+
///
/// Generates a for the batch.
///
/// The command.
protected virtual RawSqlCommand CreateStoreCommand()
{
+ Check.DebugAssert(_finalCommandText is not null, "_finalCommandText is not null, checked in Execute");
+
var commandBuilder = Dependencies.CommandBuilderFactory
.Create()
- .Append(GetCommandText());
+ .Append(_finalCommandText);
var parameterValues = new Dictionary(GetParameterCount());
@@ -229,6 +284,11 @@ protected virtual RawSqlCommand CreateStoreCommand()
/// The connection to the database to update.
public override void Execute(IRelationalConnection connection)
{
+ if (_finalCommandText is null)
+ {
+ throw new InvalidOperationException(RelationalStrings.ModificationCommandBatchNotComplete);
+ }
+
var storeCommand = CreateStoreCommand();
try
@@ -263,6 +323,11 @@ public override async Task ExecuteAsync(
IRelationalConnection connection,
CancellationToken cancellationToken = default)
{
+ if (_finalCommandText is null)
+ {
+ throw new InvalidOperationException(RelationalStrings.ModificationCommandBatchNotComplete);
+ }
+
var storeCommand = CreateStoreCommand();
try
diff --git a/src/EFCore.Relational/Update/UpdateSqlGenerator.cs b/src/EFCore.Relational/Update/UpdateSqlGenerator.cs
index f18c7f3fc3b..f1768588955 100644
--- a/src/EFCore.Relational/Update/UpdateSqlGenerator.cs
+++ b/src/EFCore.Relational/Update/UpdateSqlGenerator.cs
@@ -53,11 +53,13 @@ protected virtual ISqlGenerationHelper SqlGenerationHelper
/// The builder to which the SQL should be appended.
/// The command that represents the delete operation.
/// The ordinal of this command in the batch.
+ /// Returns whether the SQL appended must be executed in a transaction to work correctly.
/// The for the command.
public virtual ResultSetMapping AppendInsertOperation(
StringBuilder commandStringBuilder,
IReadOnlyModificationCommand command,
- int commandPosition)
+ int commandPosition,
+ out bool requiresTransaction)
{
var name = command.TableName;
var schema = command.Schema;
@@ -72,9 +74,13 @@ public virtual ResultSetMapping AppendInsertOperation(
{
var keyOperations = operations.Where(o => o.IsKey).ToList();
+ requiresTransaction = true;
+
return AppendSelectAffectedCommand(commandStringBuilder, name, schema, readOperations, keyOperations, commandPosition);
}
+ requiresTransaction = false;
+
return ResultSetMapping.NoResultSet;
}
@@ -84,11 +90,13 @@ public virtual ResultSetMapping AppendInsertOperation(
/// The builder to which the SQL should be appended.
/// The command that represents the delete operation.
/// The ordinal of this command in the batch.
+ /// Returns whether the SQL appended must be executed in a transaction to work correctly.
/// The for the command.
public virtual ResultSetMapping AppendUpdateOperation(
StringBuilder commandStringBuilder,
IReadOnlyModificationCommand command,
- int commandPosition)
+ int commandPosition,
+ out bool requiresTransaction)
{
var name = command.TableName;
var schema = command.Schema;
@@ -104,9 +112,13 @@ public virtual ResultSetMapping AppendUpdateOperation(
{
var keyOperations = operations.Where(o => o.IsKey).ToList();
+ requiresTransaction = true;
+
return AppendSelectAffectedCommand(commandStringBuilder, name, schema, readOperations, keyOperations, commandPosition);
}
+ requiresTransaction = false;
+
return AppendSelectAffectedCountCommand(commandStringBuilder, name, schema, commandPosition);
}
@@ -116,11 +128,13 @@ public virtual ResultSetMapping AppendUpdateOperation(
/// The builder to which the SQL should be appended.
/// The command that represents the delete operation.
/// The ordinal of this command in the batch.
+ /// Returns whether the SQL appended must be executed in a transaction to work correctly.
/// The for the command.
public virtual ResultSetMapping AppendDeleteOperation(
StringBuilder commandStringBuilder,
IReadOnlyModificationCommand command,
- int commandPosition)
+ int commandPosition,
+ out bool requiresTransaction)
{
var name = command.TableName;
var schema = command.Schema;
@@ -128,6 +142,8 @@ public virtual ResultSetMapping AppendDeleteOperation(
AppendDeleteCommand(commandStringBuilder, name, schema, conditionOperations);
+ requiresTransaction = false;
+
return AppendSelectAffectedCountCommand(commandStringBuilder, name, schema, commandPosition);
}
@@ -530,6 +546,14 @@ public virtual void AppendBatchHeader(StringBuilder commandStringBuilder)
{
}
+ ///
+ /// Prepends a SQL command for turning on autocommit mode in the database, in case it is off.
+ ///
+ /// The builder to which the SQL should be prepended.
+ public virtual void PrependEnsureAutocommit(StringBuilder commandStringBuilder)
+ {
+ }
+
///
/// Generates SQL that will obtain the next value in the given sequence.
///
diff --git a/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerGeometryCollectionMemberTranslator.cs b/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerGeometryCollectionMemberTranslator.cs
index d42e7c6008a..516cd05ae9a 100644
--- a/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerGeometryCollectionMemberTranslator.cs
+++ b/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerGeometryCollectionMemberTranslator.cs
@@ -46,10 +46,10 @@ public SqlServerGeometryCollectionMemberTranslator(ISqlExpressionFactory sqlExpr
return _sqlExpressionFactory.Function(
instance!,
"STNumGeometries",
- Array.Empty(),
+ Enumerable.Empty(),
nullable: true,
instancePropagatesNullability: true,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
returnType);
}
diff --git a/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerGeometryMemberTranslator.cs b/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerGeometryMemberTranslator.cs
index 508c6a70ba0..ce8ad4c2b4a 100644
--- a/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerGeometryMemberTranslator.cs
+++ b/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerGeometryMemberTranslator.cs
@@ -86,10 +86,10 @@ public SqlServerGeometryMemberTranslator(
return _sqlExpressionFactory.Function(
instance,
functionName,
- Array.Empty(),
+ Enumerable.Empty(),
nullable: true,
instancePropagatesNullability: true,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
returnType,
resultTypeMapping);
}
@@ -131,10 +131,10 @@ public SqlServerGeometryMemberTranslator(
_sqlExpressionFactory.Function(
instance,
"STGeometryType",
- Array.Empty(),
+ Enumerable.Empty(),
nullable: true,
instancePropagatesNullability: true,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
typeof(string)),
whenClauses,
null);
diff --git a/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerMultiLineStringMemberTranslator.cs b/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerMultiLineStringMemberTranslator.cs
index 59f8598680c..c65692b3920 100644
--- a/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerMultiLineStringMemberTranslator.cs
+++ b/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerMultiLineStringMemberTranslator.cs
@@ -46,10 +46,10 @@ public SqlServerMultiLineStringMemberTranslator(ISqlExpressionFactory sqlExpress
return _sqlExpressionFactory.Function(
instance!,
"STIsClosed",
- Array.Empty(),
+ Enumerable.Empty(),
nullable: true,
instancePropagatesNullability: true,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
returnType);
}
diff --git a/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerPolygonMemberTranslator.cs b/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerPolygonMemberTranslator.cs
index 1904aba0a7b..db7e90e0f3b 100644
--- a/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerPolygonMemberTranslator.cs
+++ b/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerPolygonMemberTranslator.cs
@@ -80,10 +80,10 @@ public SqlServerPolygonMemberTranslator(
_sqlExpressionFactory.Function(
instance,
"NumRings",
- Array.Empty(),
+ Enumerable.Empty(),
nullable: true,
instancePropagatesNullability: true,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
returnType),
_sqlExpressionFactory.Constant(1));
}
@@ -98,10 +98,10 @@ public SqlServerPolygonMemberTranslator(
return _sqlExpressionFactory.Function(
instance,
functionName,
- Array.Empty(),
+ Enumerable.Empty(),
nullable: true,
instancePropagatesNullability: true,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
returnType,
resultTypeMapping);
}
diff --git a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs
index 2824ca108ab..c067f3a93de 100644
--- a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs
+++ b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs
@@ -35,11 +35,7 @@ public OwnedNavigationTemporalTableBuilder(OwnedNavigationBuilder referenceOwner
/// The name of the history table.
/// The same builder instance so that multiple calls can be chained.
public virtual OwnedNavigationTemporalTableBuilder UseHistoryTable(string name)
- {
- _referenceOwnershipBuilder.OwnedEntityType.SetHistoryTableName(name);
-
- return this;
- }
+ => UseHistoryTable(name, null);
///
/// Configures a history table for the entity mapped to a temporal table.
diff --git a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder`.cs b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder`.cs
index c5ae7530334..2eb7cc7a7f1 100644
--- a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder`.cs
+++ b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder`.cs
@@ -23,6 +23,18 @@ public OwnedNavigationTemporalTableBuilder(OwnedNavigationBuilder referenceOwner
{
}
+ ///
+ /// Configures a history table for the entity mapped to a temporal table.
+ ///
+ ///
+ /// See Using SQL Server temporal tables with EF Core
+ /// for more information.
+ ///
+ /// The name of the history table.
+ /// The same builder instance so that multiple calls can be chained.
+ public new virtual OwnedNavigationTemporalTableBuilder UseHistoryTable(string name)
+ => (OwnedNavigationTemporalTableBuilder)base.UseHistoryTable(name);
+
///
/// Configures a history table for the entity mapped to a temporal table.
///
@@ -33,6 +45,6 @@ public OwnedNavigationTemporalTableBuilder(OwnedNavigationBuilder referenceOwner
/// The name of the history table.
/// The schema of the history table.
/// The same builder instance so that multiple calls can be chained.
- public new virtual OwnedNavigationTemporalTableBuilder UseHistoryTable(string name, string? schema = null)
+ public new virtual OwnedNavigationTemporalTableBuilder UseHistoryTable(string name, string? schema)
=> (OwnedNavigationTemporalTableBuilder)base.UseHistoryTable(name, schema);
}
diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs
index c7b9f353eff..a62bfc54250 100644
--- a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs
+++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs
@@ -35,11 +35,7 @@ public TemporalTableBuilder(EntityTypeBuilder entityTypeBuilder)
/// The name of the history table.
/// The same builder instance so that multiple calls can be chained.
public virtual TemporalTableBuilder UseHistoryTable(string name)
- {
- _entityTypeBuilder.Metadata.SetHistoryTableName(name);
-
- return this;
- }
+ => UseHistoryTable(name, null);
///
/// Configures a history table for the entity mapped to a temporal table.
diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs
index fd006adbc99..8afbd9aab12 100644
--- a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs
+++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs
@@ -23,6 +23,18 @@ public TemporalTableBuilder(EntityTypeBuilder entityTypeBuilder)
{
}
+ ///
+ /// Configures a history table for the entity mapped to a temporal table.
+ ///
+ ///
+ /// See Using SQL Server temporal tables with EF Core
+ /// for more information and examples.
+ ///
+ /// The name of the history table.
+ /// The same builder instance so that multiple calls can be chained.
+ public new virtual TemporalTableBuilder UseHistoryTable(string name)
+ => (TemporalTableBuilder)base.UseHistoryTable(name);
+
///
/// Configures a history table for the entity mapped to a temporal table.
///
@@ -33,6 +45,6 @@ public TemporalTableBuilder(EntityTypeBuilder entityTypeBuilder)
/// The name of the history table.
/// The schema of the history table.
/// The same builder instance so that multiple calls can be chained.
- public new virtual TemporalTableBuilder UseHistoryTable(string name, string? schema = null)
+ public new virtual TemporalTableBuilder UseHistoryTable(string name, string? schema)
=> (TemporalTableBuilder)base.UseHistoryTable(name, schema);
}
diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs
index 7734540838a..e8c2670cd64 100644
--- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs
+++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs
@@ -264,13 +264,12 @@ public override IEnumerable For(IColumn column, bool designTime)
? periodEndProperty.GetColumnName(storeObjectIdentifier)
: periodEndPropertyName;
- if (column.Name == periodStartColumnName
- || column.Name == periodEndColumnName)
- {
- yield return new Annotation(SqlServerAnnotationNames.IsTemporal, true);
- yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName, periodStartColumnName);
- yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName, periodEndColumnName);
- }
+ // TODO: issue #27459 - we want to avoid having those annotations on every column
+ yield return new Annotation(SqlServerAnnotationNames.IsTemporal, true);
+ yield return new Annotation(SqlServerAnnotationNames.TemporalHistoryTableName, entityType.GetHistoryTableName());
+ yield return new Annotation(SqlServerAnnotationNames.TemporalHistoryTableSchema, entityType.GetHistoryTableSchema());
+ yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName, periodStartColumnName);
+ yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName, periodEndColumnName);
}
}
}
diff --git a/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs
index 9ef5261bad3..f94295e8c5d 100644
--- a/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs
+++ b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs
@@ -104,4 +104,35 @@ public override IEnumerable ForRename(ITable table)
table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
}
}
+
+ ///
+ public override IEnumerable ForRename(IColumn column)
+ {
+ if (column.Table[SqlServerAnnotationNames.IsTemporal] as bool? == true)
+ {
+ yield return new Annotation(SqlServerAnnotationNames.IsTemporal, true);
+
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalHistoryTableName,
+ column.Table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalHistoryTableSchema,
+ column.Table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+
+ if (column[SqlServerAnnotationNames.TemporalPeriodStartColumnName] is string periodStartColumnName)
+ {
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalPeriodStartColumnName,
+ periodStartColumnName);
+ }
+
+ if (column[SqlServerAnnotationNames.TemporalPeriodEndColumnName] is string periodEndColumnName)
+ {
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalPeriodEndColumnName,
+ periodEndColumnName);
+ }
+ }
+ }
}
diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
index 616cbdcc5d1..c51474cd220 100644
--- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
+++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
@@ -1374,7 +1374,7 @@ protected override void Generate(
GenerateIdentityInsert(builder, operation, on: true, model);
var sqlBuilder = new StringBuilder();
- ((SqlServerUpdateSqlGenerator)Dependencies.UpdateSqlGenerator).AppendBulkInsertOperation(
+ ((ISqlServerUpdateSqlGenerator)Dependencies.UpdateSqlGenerator).AppendBulkInsertOperation(
sqlBuilder,
GenerateModificationCommands(operation, model).ToList(),
0);
@@ -2355,21 +2355,47 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema
// if only difference is in temporal annotations being removed or history table changed etc - we can ignore this operation
if (!CanSkipAlterColumnOperation(alterColumnOperation.OldColumn, alterColumnOperation))
{
+ operations.Add(operation);
+
// when modifying a period column, we need to perform the operations as a normal column first, and only later enable period
// removing the period information now, so that when we generate SQL that modifies the column we won't be making them auto generated as period
// (making column auto generated is not allowed in ALTER COLUMN statement)
// in later operation we enable the period and the period columns get set to auto generated automatically
- if (alterColumnOperation[SqlServerAnnotationNames.IsTemporal] as bool? == true
- && alterColumnOperation.OldColumn[SqlServerAnnotationNames.IsTemporal] is null)
+ //
+ // if the column is not period we just remove temporal information - it's no longer needed and could affect the generated sql
+ // we will generate all the necessary operations involved with temporal tables here
+ alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal);
+ alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName);
+ alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName);
+ alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName);
+ alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema);
+
+ // this is the case where we are not converting from normal table to temporal
+ // just a normal modification to a column on a temporal table
+ // in that case we need to double check if we need have disabled versioning earlier in this migration
+ // if so, we need to mirror the operation to the history table
+ if (alterColumnOperation.OldColumn[SqlServerAnnotationNames.IsTemporal] as bool? == true)
{
- alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal);
- alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName);
- alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName);
+ alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal);
+ alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName);
+ alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName);
+ alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName);
+ alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema);
+
+ if (versioningMap.ContainsKey((table, schema)))
+ {
+ var alterHistoryTableColumn = CopyColumnOperation(alterColumnOperation);
+ alterHistoryTableColumn.Table = historyTableName!;
+ alterHistoryTableColumn.Schema = historyTableSchema;
+ alterHistoryTableColumn.OldColumn = CopyColumnOperation(alterColumnOperation.OldColumn);
+ alterHistoryTableColumn.OldColumn.Table = historyTableName!;
+ alterHistoryTableColumn.OldColumn.Schema = historyTableSchema;
+
+ operations.Add(alterHistoryTableColumn);
+ }
// TODO: test what happens if default value just changes (from temporal to temporal)
}
-
- operations.Add(operation);
}
break;
@@ -2396,6 +2422,8 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema
break;
case AddColumnOperation addColumnOperation:
+ operations.Add(addColumnOperation);
+
// when adding a period column, we need to add it as a normal column first, and only later enable period
// removing the period information now, so that when we generate SQL that adds the column we won't be making them auto generated as period
// it won't work, unless period is enabled
@@ -2403,6 +2431,8 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema
if (addColumnOperation[SqlServerAnnotationNames.IsTemporal] as bool? == true)
{
addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal);
+ addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName);
+ addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema);
addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName);
addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName);
@@ -2411,14 +2441,48 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema
{
addColumnOperation.DefaultValue = DateTime.MaxValue;
}
+
+ // when adding (non-period) column to an exisiting temporal table we need to check if we have disabled the period
+ // due to some other operations in the same migration (e.g. delete column)
+ // if so, we need to also add the same column to history table
+ if (addColumnOperation.Name != periodStartColumnName
+ && addColumnOperation.Name != periodEndColumnName)
+ {
+ if (versioningMap.ContainsKey((table, schema)))
+ {
+ var addHistoryTableColumnOperation = CopyColumnOperation(addColumnOperation);
+ addHistoryTableColumnOperation.Table = historyTableName!;
+ addHistoryTableColumnOperation.Schema = historyTableSchema;
+
+ operations.Add(addHistoryTableColumnOperation);
+ }
+ }
+ }
+
+ break;
+
+ case RenameColumnOperation renameColumnOperation:
+ operations.Add(renameColumnOperation);
+
+ // if we disabled period for the temporal table and now we are renaming the column,
+ // we need to also rename this same column in history table
+ if (versioningMap.ContainsKey((table, schema)))
+ {
+ var renameHistoryTableColumnOperation = new RenameColumnOperation
+ {
+ IsDestructiveChange = renameColumnOperation.IsDestructiveChange,
+ Name = renameColumnOperation.Name,
+ NewName = renameColumnOperation.NewName,
+ Table = historyTableName!,
+ Schema = historyTableSchema
+ };
+
+ operations.Add(renameHistoryTableColumnOperation);
}
- operations.Add(addColumnOperation);
break;
default:
- // CreateTableOperation
- // RenameColumnOperation
operations.Add(operation);
break;
}
@@ -2607,6 +2671,7 @@ static bool CanSkipAlterColumnOperation(ColumnOperation first, ColumnOperation s
&& ColumnOperationsOnlyDifferByTemporalTableAnnotation(first, second)
&& ColumnOperationsOnlyDifferByTemporalTableAnnotation(second, first);
+ // don't compare name, table or schema - they are not being set in the model differ (since they should always be the same)
static bool ColumnPropertiesAreTheSame(ColumnOperation first, ColumnOperation second)
=> first.ClrType == second.ClrType
&& first.Collation == second.Collation
@@ -2651,5 +2716,39 @@ static bool ColumnOperationsOnlyDifferByTemporalTableAnnotation(ColumnOperation
|| a.Name == SqlServerAnnotationNames.TemporalPeriodStartColumnName
|| a.Name == SqlServerAnnotationNames.TemporalPeriodEndColumnName);
}
+
+ static TOperation CopyColumnOperation(ColumnOperation source)
+ where TOperation : ColumnOperation, new()
+ {
+ var result = new TOperation
+ {
+ ClrType = source.ClrType,
+ Collation = source.Collation,
+ ColumnType = source.ColumnType,
+ Comment = source.Comment,
+ ComputedColumnSql = source.ComputedColumnSql,
+ DefaultValue = source.DefaultValue,
+ DefaultValueSql = source.DefaultValueSql,
+ IsDestructiveChange = source.IsDestructiveChange,
+ IsFixedLength = source.IsFixedLength,
+ IsNullable = source.IsNullable,
+ IsRowVersion = source.IsRowVersion,
+ IsStored = source.IsStored,
+ IsUnicode = source.IsUnicode,
+ MaxLength = source.MaxLength,
+ Name = source.Name,
+ Precision = source.Precision,
+ Scale = source.Scale,
+ Table = source.Table,
+ Schema = source.Schema
+ };
+
+ foreach (var annotation in source.GetAnnotations())
+ {
+ result.AddAnnotation(annotation.Name, annotation.Value);
+ }
+
+ return result;
+ }
}
}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMemberTranslator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMemberTranslator.cs
index 5a20742cc90..5ce73b14346 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMemberTranslator.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMemberTranslator.cs
@@ -96,17 +96,17 @@ public SqlServerDateTimeMemberTranslator(
case nameof(DateTime.Now):
return _sqlExpressionFactory.Function(
declaringType == typeof(DateTime) ? "GETDATE" : "SYSDATETIMEOFFSET",
- Array.Empty(),
+ Enumerable.Empty(),
nullable: false,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
returnType);
case nameof(DateTime.UtcNow):
var serverTranslation = _sqlExpressionFactory.Function(
declaringType == typeof(DateTime) ? "GETUTCDATE" : "SYSUTCDATETIME",
- Array.Empty(),
+ Enumerable.Empty(),
nullable: false,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
returnType);
return declaringType == typeof(DateTime)
@@ -121,9 +121,9 @@ public SqlServerDateTimeMemberTranslator(
_sqlExpressionFactory.Fragment("date"),
_sqlExpressionFactory.Function(
"GETDATE",
- Array.Empty(),
+ Enumerable.Empty(),
nullable: false,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
typeof(DateTime))
},
nullable: true,
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerNewGuidTranslator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerNewGuidTranslator.cs
index 87036517db0..65b7d074c31 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerNewGuidTranslator.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerNewGuidTranslator.cs
@@ -41,9 +41,9 @@ public SqlServerNewGuidTranslator(ISqlExpressionFactory sqlExpressionFactory)
=> MethodInfo.Equals(method)
? _sqlExpressionFactory.Function(
"NEWID",
- Array.Empty(),
+ Enumerable.Empty(),
nullable: false,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
method.ReturnType)
: null;
}
diff --git a/src/EFCore.SqlServer/Update/Internal/ISqlServerUpdateSqlGenerator.cs b/src/EFCore.SqlServer/Update/Internal/ISqlServerUpdateSqlGenerator.cs
index 4099dd73dec..c7f92fb002a 100644
--- a/src/EFCore.SqlServer/Update/Internal/ISqlServerUpdateSqlGenerator.cs
+++ b/src/EFCore.SqlServer/Update/Internal/ISqlServerUpdateSqlGenerator.cs
@@ -27,5 +27,18 @@ public interface ISqlServerUpdateSqlGenerator : IUpdateSqlGenerator
ResultSetMapping AppendBulkInsertOperation(
StringBuilder commandStringBuilder,
IReadOnlyList modificationCommands,
- int commandPosition);
+ int commandPosition,
+ out bool requiresTransaction);
+
+ ///
+ /// 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.
+ ///
+ ResultSetMapping AppendBulkInsertOperation(
+ StringBuilder commandStringBuilder,
+ IReadOnlyList modificationCommands,
+ int commandPosition)
+ => AppendBulkInsertOperation(commandStringBuilder, modificationCommands, commandPosition, out _);
}
diff --git a/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs b/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs
index c775d2b83f0..18e4ee6d501 100644
--- a/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs
+++ b/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs
@@ -85,7 +85,8 @@ protected override bool IsCommandTextValid()
{
if (--_commandsLeftToLengthCheck < 0)
{
- var commandTextLength = GetCommandText().Length;
+ UpdateCachedCommandText();
+ var commandTextLength = CachedCommandText.Length;
if (commandTextLength >= MaxScriptLength)
{
return false;
@@ -138,28 +139,24 @@ private static int CountParameters(IReadOnlyModificationCommand modificationComm
protected override void ResetCommandText()
{
base.ResetCommandText();
+
_bulkInsertCommands.Clear();
}
- ///
- /// 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.
- ///
- protected override string GetCommandText()
- => base.GetCommandText() + GetBulkInsertCommandText(ModificationCommands.Count);
-
- private string GetBulkInsertCommandText(int lastIndex)
+ private void AppendBulkInsertCommandText(int lastIndex)
{
if (_bulkInsertCommands.Count == 0)
{
- return string.Empty;
+ return;
}
- var stringBuilder = new StringBuilder();
+ var wasCachedCommandTextEmpty = IsCachedCommandTextEmpty;
+
var resultSetMapping = UpdateSqlGenerator.AppendBulkInsertOperation(
- stringBuilder, _bulkInsertCommands, lastIndex - _bulkInsertCommands.Count);
+ CachedCommandText, _bulkInsertCommands, lastIndex - _bulkInsertCommands.Count, out var requiresTransaction);
+
+ SetRequiresTransaction(!wasCachedCommandTextEmpty || requiresTransaction);
+
for (var i = lastIndex - _bulkInsertCommands.Count; i < lastIndex; i++)
{
CommandResultSet[i] = resultSetMapping;
@@ -169,8 +166,19 @@ private string GetBulkInsertCommandText(int lastIndex)
{
CommandResultSet[lastIndex - 1] = ResultSetMapping.LastInResultSet;
}
+ }
- return stringBuilder.ToString();
+ ///
+ /// 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.
+ ///
+ protected override void UpdateCachedCommandText()
+ {
+ base.UpdateCachedCommandText();
+
+ AppendBulkInsertCommandText(ModificationCommands.Count);
}
///
@@ -188,7 +196,9 @@ protected override void UpdateCachedCommandText(int commandPosition)
if (_bulkInsertCommands.Count > 0
&& !CanBeInsertedInSameStatement(_bulkInsertCommands[0], newModificationCommand))
{
- CachedCommandText.Append(GetBulkInsertCommandText(commandPosition));
+ // The new Add command cannot be added to the pending bulk insert commands (e.g. different table).
+ // Write out the pending commands before starting a new pending chain.
+ AppendBulkInsertCommandText(commandPosition);
_bulkInsertCommands.Clear();
}
@@ -198,8 +208,14 @@ protected override void UpdateCachedCommandText(int commandPosition)
}
else
{
- CachedCommandText.Append(GetBulkInsertCommandText(commandPosition));
- _bulkInsertCommands.Clear();
+ // If we have any pending bulk insert commands, write them out before the next non-Add command
+ if (_bulkInsertCommands.Count > 0)
+ {
+ // Note that we don't care about the transactionality of the bulk insert SQL, since there's the additional non-Add
+ // command coming right afterwards, and so a transaction is required in any case.
+ AppendBulkInsertCommandText(commandPosition);
+ _bulkInsertCommands.Clear();
+ }
base.UpdateCachedCommandText(commandPosition);
}
diff --git a/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs b/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs
index 709e7558e76..1d18cee739b 100644
--- a/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs
+++ b/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs
@@ -35,7 +35,8 @@ public SqlServerUpdateSqlGenerator(
public virtual ResultSetMapping AppendBulkInsertOperation(
StringBuilder commandStringBuilder,
IReadOnlyList modificationCommands,
- int commandPosition)
+ int commandPosition,
+ out bool requiresTransaction)
{
var table = StoreObjectIdentifier.Table(modificationCommands[0].TableName, modificationCommands[0].Schema);
if (modificationCommands.Count == 1)
@@ -45,13 +46,16 @@ public virtual ResultSetMapping AppendBulkInsertOperation(
!o.IsKey
|| !o.IsRead
|| o.Property?.GetValueGenerationStrategy(table) == SqlServerValueGenerationStrategy.IdentityColumn)
- ? AppendInsertOperation(commandStringBuilder, modificationCommands[0], commandPosition)
+ // Do a regular INSERT+SELECT for IDENTITY, but not if there are any non-IDENTITY generated columns
+ ? AppendInsertOperation(commandStringBuilder, modificationCommands[0], commandPosition, out requiresTransaction)
+ // If we have a non-identity generated column, do INSERT ... OUTPUT INTO @inserted; SELECT ... FROM @inserted
: AppendInsertOperationWithServerKeys(
commandStringBuilder,
modificationCommands[0],
modificationCommands[0].ColumnModifications.Where(o => o.IsKey).ToList(),
modificationCommands[0].ColumnModifications.Where(o => o.IsRead).ToList(),
- commandPosition);
+ commandPosition,
+ out requiresTransaction);
}
var readOperations = modificationCommands[0].ColumnModifications.Where(o => o.IsRead).ToList();
@@ -59,18 +63,23 @@ public virtual ResultSetMapping AppendBulkInsertOperation(
var keyOperations = modificationCommands[0].ColumnModifications.Where(o => o.IsKey).ToList();
var defaultValuesOnly = writeOperations.Count == 0;
- var nonIdentityOperations = modificationCommands[0].ColumnModifications
- .Where(o => o.Property?.GetValueGenerationStrategy(table) != SqlServerValueGenerationStrategy.IdentityColumn)
+ var writableOperations = modificationCommands[0].ColumnModifications
+ .Where(o =>
+ o.Property?.GetValueGenerationStrategy(table) != SqlServerValueGenerationStrategy.IdentityColumn
+ && o.Property?.GetComputedColumnSql() is null
+ && o.Property?.GetColumnType() is not "rowversion" and not "timestamp")
.ToList();
if (defaultValuesOnly)
{
- if (nonIdentityOperations.Count == 0
+ if (writableOperations.Count == 0
|| readOperations.Count == 0)
{
+ requiresTransaction = modificationCommands.Count > 1;
foreach (var modification in modificationCommands)
{
- AppendInsertOperation(commandStringBuilder, modification, commandPosition);
+ AppendInsertOperation(commandStringBuilder, modification, commandPosition, out var localRequiresTransaction);
+ requiresTransaction = requiresTransaction || localRequiresTransaction;
}
return readOperations.Count == 0
@@ -78,31 +87,36 @@ public virtual ResultSetMapping AppendBulkInsertOperation(
: ResultSetMapping.LastInResultSet;
}
- if (nonIdentityOperations.Count > 1)
+ if (writableOperations.Count > 1)
{
- nonIdentityOperations.RemoveRange(1, nonIdentityOperations.Count - 1);
+ writableOperations.RemoveRange(1, writableOperations.Count - 1);
}
}
if (readOperations.Count == 0)
{
- return AppendBulkInsertWithoutServerValues(commandStringBuilder, modificationCommands, writeOperations);
+ return AppendBulkInsertWithoutServerValues(
+ commandStringBuilder, modificationCommands, writeOperations, out requiresTransaction);
}
if (defaultValuesOnly)
{
return AppendBulkInsertWithServerValuesOnly(
- commandStringBuilder, modificationCommands, commandPosition, nonIdentityOperations, keyOperations, readOperations);
+ commandStringBuilder, modificationCommands, commandPosition, writableOperations, keyOperations, readOperations,
+ out requiresTransaction);
}
if (modificationCommands[0].Entries.SelectMany(e => e.EntityType.GetAllBaseTypesInclusive())
.Any(e => e.IsMemoryOptimized()))
{
- if (!nonIdentityOperations.Any(o => o.IsRead && o.IsKey))
+ requiresTransaction = modificationCommands.Count > 1;
+
+ if (!writableOperations.Any(o => o.IsRead && o.IsKey))
{
foreach (var modification in modificationCommands)
{
- AppendInsertOperation(commandStringBuilder, modification, commandPosition++);
+ AppendInsertOperation(commandStringBuilder, modification, commandPosition++, out var localRequiresTransaction);
+ requiresTransaction = requiresTransaction || localRequiresTransaction;
}
}
else
@@ -110,7 +124,9 @@ public virtual ResultSetMapping AppendBulkInsertOperation(
foreach (var modification in modificationCommands)
{
AppendInsertOperationWithServerKeys(
- commandStringBuilder, modification, keyOperations, readOperations, commandPosition++);
+ commandStringBuilder, modification, keyOperations, readOperations, commandPosition++,
+ out var localRequiresTransaction);
+ requiresTransaction = requiresTransaction || localRequiresTransaction;
}
}
@@ -118,13 +134,15 @@ public virtual ResultSetMapping AppendBulkInsertOperation(
}
return AppendBulkInsertWithServerValues(
- commandStringBuilder, modificationCommands, commandPosition, writeOperations, keyOperations, readOperations);
+ commandStringBuilder, modificationCommands, commandPosition, writeOperations, keyOperations, readOperations,
+ out requiresTransaction);
}
private ResultSetMapping AppendBulkInsertWithoutServerValues(
StringBuilder commandStringBuilder,
IReadOnlyList modificationCommands,
- List writeOperations)
+ List writeOperations,
+ out bool requiresTransaction)
{
Check.DebugAssert(writeOperations.Count > 0, $"writeOperations.Count is {writeOperations.Count}");
@@ -143,6 +161,8 @@ private ResultSetMapping AppendBulkInsertWithoutServerValues(
commandStringBuilder.AppendLine(SqlGenerationHelper.StatementTerminator);
+ requiresTransaction = false;
+
return ResultSetMapping.NoResultSet;
}
@@ -158,7 +178,8 @@ private ResultSetMapping AppendBulkInsertWithServerValues(
int commandPosition,
List writeOperations,
List keyOperations,
- List readOperations)
+ List readOperations,
+ out bool requiresTransaction)
{
AppendDeclareTable(
commandStringBuilder,
@@ -190,6 +211,8 @@ private ResultSetMapping AppendBulkInsertWithServerValues(
commandStringBuilder, readOperations, keyOperations, InsertedTableBaseName, commandPosition, name, schema,
orderColumn: PositionColumnName);
+ requiresTransaction = true;
+
return ResultSetMapping.NotLastInResultSet;
}
@@ -197,28 +220,31 @@ private ResultSetMapping AppendBulkInsertWithServerValuesOnly(
StringBuilder commandStringBuilder,
IReadOnlyList modificationCommands,
int commandPosition,
- List nonIdentityOperations,
+ List writableOperations,
List keyOperations,
- List readOperations)
+ List readOperations,
+ out bool requiresTransaction)
{
AppendDeclareTable(commandStringBuilder, InsertedTableBaseName, commandPosition, keyOperations);
var name = modificationCommands[0].TableName;
var schema = modificationCommands[0].Schema;
- AppendInsertCommandHeader(commandStringBuilder, name, schema, nonIdentityOperations);
+ AppendInsertCommandHeader(commandStringBuilder, name, schema, writableOperations);
AppendOutputClause(commandStringBuilder, keyOperations, InsertedTableBaseName, commandPosition);
- AppendValuesHeader(commandStringBuilder, nonIdentityOperations);
- AppendValues(commandStringBuilder, name, schema, nonIdentityOperations);
+ AppendValuesHeader(commandStringBuilder, writableOperations);
+ AppendValues(commandStringBuilder, name, schema, writableOperations);
for (var i = 1; i < modificationCommands.Count; i++)
{
commandStringBuilder.AppendLine(",");
- AppendValues(commandStringBuilder, name, schema, nonIdentityOperations);
+ AppendValues(commandStringBuilder, name, schema, writableOperations);
}
commandStringBuilder.Append(SqlGenerationHelper.StatementTerminator);
AppendSelectCommand(commandStringBuilder, readOperations, keyOperations, InsertedTableBaseName, commandPosition, name, schema);
+ requiresTransaction = true;
+
return ResultSetMapping.NotLastInResultSet;
}
@@ -399,7 +425,8 @@ private ResultSetMapping AppendInsertOperationWithServerKeys(
IReadOnlyModificationCommand command,
IReadOnlyList keyOperations,
IReadOnlyList readOperations,
- int commandPosition)
+ int commandPosition,
+ out bool requiresTransaction)
{
var name = command.TableName;
var schema = command.Schema;
@@ -415,6 +442,8 @@ private ResultSetMapping AppendInsertOperationWithServerKeys(
AppendValues(commandStringBuilder, name, schema, writeOperations);
commandStringBuilder.Append(SqlGenerationHelper.StatementTerminator);
+ requiresTransaction = true;
+
return AppendSelectCommand(
commandStringBuilder, readOperations, keyOperations, InsertedTableBaseName, commandPosition, name, schema);
}
@@ -515,6 +544,19 @@ public override void AppendBatchHeader(StringBuilder commandStringBuilder)
.Append("SET NOCOUNT ON")
.AppendLine(SqlGenerationHelper.StatementTerminator);
+ ///
+ /// 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.
+ ///
+ public override void PrependEnsureAutocommit(StringBuilder commandStringBuilder)
+ {
+ // SQL Server allows turning off autocommit via the IMPLICIT_TRANSACTIONS setting (see
+ // https://docs.microsoft.com/sql/t-sql/statements/set-implicit-transactions-transact-sql).
+ commandStringBuilder.Insert(0, $"SET IMPLICIT_TRANSACTIONS OFF{SqlGenerationHelper.StatementTerminator}{Environment.NewLine}");
+ }
+
///
/// 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
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteRandomTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteRandomTranslator.cs
index 0670c539e9b..bee733a576a 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteRandomTranslator.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteRandomTranslator.cs
@@ -49,14 +49,14 @@ public SqliteRandomTranslator(ISqlExpressionFactory sqlExpressionFactory)
_sqlExpressionFactory.Divide(
_sqlExpressionFactory.Function(
"random",
- Array.Empty(),
+ Enumerable.Empty(),
nullable: false,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
method.ReturnType),
_sqlExpressionFactory.Constant(9223372036854780000.0))
},
nullable: false,
- argumentsPropagateNullability: Array.Empty(),
+ argumentsPropagateNullability: Enumerable.Empty(),
method.ReturnType)
: null;
}
diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs
index b83d8f0ef41..1f68bb0335a 100644
--- a/src/EFCore/DbContext.cs
+++ b/src/EFCore/DbContext.cs
@@ -482,7 +482,7 @@ protected internal virtual void OnConfiguring(DbContextOptionsBuilder optionsBui
///
///
/// If a model is explicitly set on the options for this context (via )
- /// then this method will not be run.
+ /// then this method will not be run. However, it will still run when creating a compiled model.
///
///
/// See Pre-convention model building in EF Core for more information and
@@ -504,7 +504,7 @@ protected internal virtual void ConfigureConventions(ModelConfigurationBuilder c
///
///
/// If a model is explicitly set on the options for this context (via )
- /// then this method will not be run.
+ /// then this method will not be run. However, it will still run when creating a compiled model.
///
///
/// See Modeling entity types and relationships for more information and
diff --git a/src/EFCore/Metadata/IReadOnlyEntityType.cs b/src/EFCore/Metadata/IReadOnlyEntityType.cs
index 01d84043020..a13bb8dd4ff 100644
--- a/src/EFCore/Metadata/IReadOnlyEntityType.cs
+++ b/src/EFCore/Metadata/IReadOnlyEntityType.cs
@@ -159,7 +159,17 @@ bool IsAssignableFrom(IReadOnlyEntityType derivedType)
{
Check.NotNull(derivedType, nameof(derivedType));
- var baseType = derivedType;
+ if (derivedType == this)
+ {
+ return true;
+ }
+
+ if (!GetDirectlyDerivedTypes().Any())
+ {
+ return false;
+ }
+
+ var baseType = derivedType.BaseType;
while (baseType != null)
{
if (baseType == this)
diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs
index 51448d255e4..e1a419d65a6 100644
--- a/src/EFCore/Metadata/Internal/Model.cs
+++ b/src/EFCore/Metadata/Internal/Model.cs
@@ -458,11 +458,11 @@ public virtual IEnumerable FindEntityTypes(Type type)
/// 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.
///
- public virtual IReadOnlyCollection GetEntityTypes(string name)
+ public virtual IEnumerable GetEntityTypes(string name)
{
var entityType = FindEntityType(name);
return entityType == null
- ? Array.Empty()
+ ? Enumerable.Empty()
: new[] { entityType };
}
diff --git a/src/EFCore/Metadata/RuntimeModel.cs b/src/EFCore/Metadata/RuntimeModel.cs
index 3c41e2924d2..75968bbab15 100644
--- a/src/EFCore/Metadata/RuntimeModel.cs
+++ b/src/EFCore/Metadata/RuntimeModel.cs
@@ -126,7 +126,7 @@ private IEnumerable FindEntityTypes(Type type)
{
var entityType = FindEntityType(GetDisplayName(type));
var result = entityType == null
- ? Array.Empty()
+ ? Enumerable.Empty()
: new[] { entityType };
return _sharedTypes.TryGetValue(type, out var sharedTypes)
diff --git a/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs b/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs
index ac5b86ad43a..7c871ad77d0 100644
--- a/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs
+++ b/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs
@@ -8,6 +8,8 @@
using Microsoft.EntityFrameworkCore.Scaffolding.Metadata;
using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Scaffolding.Internal;
+using Microsoft.EntityFrameworkCore.TextTemplating;
+using Microsoft.EntityFrameworkCore.TextTemplating.Internal;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.EntityFrameworkCore.Design;
@@ -94,7 +96,11 @@ public class UserMigrationsIdGenerator : IMigrationsIdGenerator
Assert.Equal(typeof(CSharpMigrationsGenerator), serviceProvider.GetRequiredService().GetType());
Assert.Equal(
typeof(MigrationsCodeGeneratorSelector), serviceProvider.GetRequiredService().GetType());
- Assert.Equal(typeof(CSharpModelGenerator), serviceProvider.GetRequiredService().GetType());
+ Assert.Equal(typeof(TextTemplatingService), serviceProvider.GetRequiredService().GetType());
+ Assert.Collection(
+ serviceProvider.GetServices(),
+ s => Assert.Equal(typeof(TextTemplatingModelGenerator), s.GetType()),
+ s => Assert.Equal(typeof(CSharpModelGenerator), s.GetType()));
Assert.Equal(typeof(ModelCodeGeneratorSelector), serviceProvider.GetRequiredService().GetType());
Assert.Equal(
typeof(CSharpRuntimeModelCodeGenerator), serviceProvider.GetRequiredService().GetType());
diff --git a/test/EFCore.Design.Tests/Migrations/Design/MigrationsBundleTest.cs b/test/EFCore.Design.Tests/Migrations/Design/MigrationsBundleTest.cs
index 3a2da1d4292..49b9e305c8f 100644
--- a/test/EFCore.Design.Tests/Migrations/Design/MigrationsBundleTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/Design/MigrationsBundleTest.cs
@@ -7,7 +7,7 @@ namespace Microsoft.EntityFrameworkCore.Migrations.Design;
public class MigrationsBundleTest
{
- [Fact]
+ [ConditionalFact]
public void Short_names_are_unique()
{
foreach (var command in GetCommands())
@@ -24,7 +24,7 @@ public void Short_names_are_unique()
}
}
- [Fact]
+ [ConditionalFact]
public void Long_names_are_unique()
{
foreach (var command in GetCommands())
@@ -41,7 +41,7 @@ public void Long_names_are_unique()
}
}
- [Fact]
+ [ConditionalFact]
public void HandleResponseFiles_is_true()
{
var app = new CommandLineApplication { Name = "efbundle" };
@@ -50,7 +50,7 @@ public void HandleResponseFiles_is_true()
Assert.True(app.HandleResponseFiles);
}
- [Fact]
+ [ConditionalFact]
public void AllowArgumentSeparator_is_true()
{
var app = new CommandLineApplication { Name = "efbundle" };
diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
index 0049e6289ee..285844ce4c9 100644
--- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
@@ -1276,7 +1276,7 @@ public virtual void Primary_key_is_stored_in_snapshot()
);
});
- [Fact]
+ [ConditionalFact]
public void HasNoKey_is_handled()
=> Test(
builder => builder.Entity().Ignore(e => e.EntityWithTwoProperties).HasNoKey(),
@@ -2085,7 +2085,7 @@ public virtual void Temporal_table_information_is_stored_in_snapshot()
"Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithStringProperty");
var annotations = temporalEntity.GetAnnotations().ToList();
- Assert.Equal(6, annotations.Count);
+ Assert.Equal(7, annotations.Count);
Assert.Contains(annotations, a => a.Name == SqlServerAnnotationNames.IsTemporal && a.Value as bool? == true);
Assert.Contains(
annotations,
@@ -2147,7 +2147,7 @@ public virtual void Temporal_table_information_is_stored_in_snapshot_minimal_set
"Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithStringProperty");
var annotations = temporalEntity.GetAnnotations().ToList();
- Assert.Equal(6, annotations.Count);
+ Assert.Equal(7, annotations.Count);
Assert.Contains(annotations, a => a.Name == SqlServerAnnotationNames.IsTemporal && a.Value as bool? == true);
Assert.Contains(
annotations,
diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs
index a785ec29210..3b8eaf0a1ba 100644
--- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs
@@ -191,7 +191,8 @@ public void Required_options_to_GenerateModel_are_not_null()
var generator = CreateServices()
.AddSingleton()
.BuildServiceProvider(validateScopes: true)
- .GetRequiredService();
+ .GetServices()
+ .Last(g => g is CSharpModelGenerator);
Assert.StartsWith(
CoreStrings.ArgumentPropertyNull(nameof(ModelCodeGenerationOptions.ContextName), "options"),
@@ -217,7 +218,8 @@ public void Plugins_work()
var generator = CreateServices()
.AddSingleton()
.BuildServiceProvider(validateScopes: true)
- .GetRequiredService();
+ .GetServices()
+ .Last(g => g is CSharpModelGenerator);
var scaffoldedModel = generator.GenerateModel(
new Model(),
diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs
index 7da73280ac2..9023d6205a3 100644
--- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs
@@ -53,6 +53,7 @@ private static IModelCodeGenerator CreateGenerator()
.AddSingleton()
.AddSingleton()
.BuildServiceProvider(validateScopes: true)
- .GetRequiredService();
+ .GetServices()
+ .Last(g => g is CSharpModelGenerator);
}
}
diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorSelectorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorSelectorTest.cs
new file mode 100644
index 00000000000..e05d4609c8d
--- /dev/null
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorSelectorTest.cs
@@ -0,0 +1,119 @@
+// 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.Scaffolding.Internal;
+
+public class ModelCodeGeneratorSelectorTest
+{
+ [ConditionalFact]
+ public void Select_returns_last_service_for_language()
+ {
+ var expected = new TestModelCodeGenerator("C#");
+ var selector = new ModelCodeGeneratorSelector(
+ new[]
+ {
+ new TestModelCodeGenerator("C#"),
+ expected
+ });
+
+ var result = selector.Select(
+ new ModelCodeGenerationOptions
+ {
+ Language = "C#"
+ });
+
+ Assert.Same(expected, result);
+ }
+
+ [ConditionalFact]
+ public void Select_throws_when_no_service_for_language()
+ {
+ var selector = new ModelCodeGeneratorSelector(
+ new[]
+ {
+ new TestModelCodeGenerator("C#")
+ });
+ var options = new ModelCodeGenerationOptions
+ {
+ Language = "VB"
+ };
+
+ var ex = Assert.Throws(
+ () => selector.Select(options));
+
+ Assert.Equal(DesignStrings.NoLanguageService("VB", nameof(IModelCodeGenerator)), ex.Message);
+ }
+
+ [ConditionalFact]
+ public void Select_returns_last_templated_service_with_templates()
+ {
+ var expected = new TestTemplatedModelGenerator(hasTemplates: true);
+ var selector = new ModelCodeGeneratorSelector(
+ new IModelCodeGenerator[]
+ {
+ new TestTemplatedModelGenerator(hasTemplates: true),
+ expected,
+ new TestTemplatedModelGenerator(hasTemplates: false),
+ new TestModelCodeGenerator("C#")
+ });
+
+ var result = selector.Select(
+ new ModelCodeGenerationOptions
+ {
+ Language = "C#",
+ ProjectDir = Directory.GetCurrentDirectory()
+ });
+
+ Assert.Same(expected, result);
+ }
+
+ [ConditionalFact]
+ public void Select_returns_last_service_for_language_when_no_templates()
+ {
+ var expected = new TestModelCodeGenerator("C#");
+ var selector = new ModelCodeGeneratorSelector(
+ new IModelCodeGenerator[]
+ {
+ new TestTemplatedModelGenerator(hasTemplates: false),
+ new TestModelCodeGenerator("C#"),
+ expected
+ });
+
+ var result = selector.Select(
+ new ModelCodeGenerationOptions
+ {
+ Language = "C#"
+ });
+
+ Assert.Same(expected, result);
+ }
+
+ private class TestModelCodeGenerator : ModelCodeGenerator
+ {
+ public TestModelCodeGenerator(string language)
+ : base(new())
+ => Language = language;
+
+ public override string Language { get; }
+
+ public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationOptions options)
+ => throw new NotImplementedException();
+ }
+
+ private class TestTemplatedModelGenerator : TemplatedModelGenerator
+ {
+ private readonly bool _hasTemplates;
+
+ public TestTemplatedModelGenerator(bool hasTemplates)
+ : base(new())
+ => _hasTemplates = hasTemplates;
+
+ public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationOptions options)
+ => throw new NotImplementedException();
+
+ public override bool HasTemplates(string projectDir)
+ => _hasTemplates;
+ }
+}
diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs
index a4f7385ad32..00fec7696b0 100644
--- a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs
@@ -28,7 +28,8 @@ protected void Test(
AddScaffoldingServices(services);
var generator = services.BuildServiceProvider(validateScopes: true)
- .GetRequiredService();
+ .GetServices()
+ .Last(g => g is CSharpModelGenerator);
options.ModelNamespace ??= "TestNamespace";
options.ContextName = "TestDbContext";
diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingModelGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingModelGeneratorTest.cs
new file mode 100644
index 00000000000..c359d40c655
--- /dev/null
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingModelGeneratorTest.cs
@@ -0,0 +1,316 @@
+// 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.Design.Internal;
+using Microsoft.EntityFrameworkCore.Internal;
+using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal;
+using Microsoft.EntityFrameworkCore.TestUtilities.Xunit;
+
+namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal;
+
+[PlatformSkipCondition(TestPlatform.Linux, SkipReason = "CI time out")]
+public class TextTemplatingModelGeneratorTest
+{
+ [ConditionalFact]
+ public void HasTemplates_works_when_templates()
+ {
+ using var projectDir = new TempDirectory();
+
+ var template = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4");
+ Directory.CreateDirectory(Path.GetDirectoryName(template));
+ File.Create(template).Close();
+
+ var generator = CreateGenerator();
+
+ var result = generator.HasTemplates(projectDir);
+
+ Assert.True(result);
+ }
+
+ [ConditionalFact]
+ public void HasTemplates_works_when_no_templates()
+ {
+ using var projectDir = new TempDirectory();
+
+ var generator = CreateGenerator();
+
+ var result = generator.HasTemplates(projectDir);
+
+ Assert.False(result);
+ }
+
+ [ConditionalFact]
+ public void GenerateModel_uses_templates()
+ {
+ using var projectDir = new TempDirectory();
+
+ var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4");
+ Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate));
+ File.WriteAllText(
+ contextTemplate,
+ "My DbContext template");
+
+ File.WriteAllText(
+ Path.Combine(projectDir, "Templates", "EFCore", "EntityType.t4"),
+ "My entity type template");
+
+ var generator = CreateGenerator();
+ var model = new ModelBuilder()
+ .Entity("Entity1", b => { })
+ .FinalizeModel();
+
+ var result = generator.GenerateModel(
+ model,
+ new()
+ {
+ ContextName = "Context",
+ ConnectionString = @"Name=DefaultConnection",
+ ProjectDir = projectDir
+ });
+
+ Assert.Equal("Context.cs", result.ContextFile.Path);
+ Assert.Equal("My DbContext template", result.ContextFile.Code);
+
+ var entityType = Assert.Single(result.AdditionalFiles);
+ Assert.Equal("Entity1.cs", entityType.Path);
+ Assert.Equal("My entity type template", entityType.Code);
+ }
+
+ [ConditionalFact]
+ public void GenerateModel_works_when_no_entity_type_template()
+ {
+ using var projectDir = new TempDirectory();
+
+ var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4");
+ Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate));
+ File.WriteAllText(
+ contextTemplate,
+ "My DbContext template");
+
+ var generator = CreateGenerator();
+ var model = new ModelBuilder()
+ .Entity("Entity1", b => { })
+ .FinalizeModel();
+
+ var result = generator.GenerateModel(
+ model,
+ new()
+ {
+ ContextName = "Context",
+ ConnectionString = @"Name=DefaultConnection",
+ ProjectDir = projectDir
+ });
+
+ Assert.Equal("Context.cs", result.ContextFile.Path);
+ Assert.Equal("My DbContext template", result.ContextFile.Code);
+
+ Assert.Empty(result.AdditionalFiles);
+ }
+
+ [ConditionalFact]
+ public void GenerateModel_sets_session_variables()
+ {
+ using var projectDir = new TempDirectory();
+
+ var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4");
+ Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate));
+ File.WriteAllText(
+ contextTemplate,
+ @"Model not null: <#= Session[""Model""] != null #>
+Options not null: <#= Session[""Options""] != null #>
+NamespaceHint: <#= Session[""NamespaceHint""] #>
+ProjectDefaultNamespace: <#= Session[""ProjectDefaultNamespace""] #>");
+
+ File.WriteAllText(
+ Path.Combine(projectDir, "Templates", "EFCore", "EntityType.t4"),
+ @"EntityType not null: <#= Session[""EntityType""] != null #>
+Options not null: <#= Session[""Options""] != null #>
+NamespaceHint: <#= Session[""NamespaceHint""] #>
+ProjectDefaultNamespace: <#= Session[""ProjectDefaultNamespace""] #>");
+
+ var generator = CreateGenerator();
+ var model = new ModelBuilder()
+ .Entity("Entity1", b => { })
+ .FinalizeModel();
+
+ var result = generator.GenerateModel(
+ model,
+ new()
+ {
+ ContextName = "Context",
+ ConnectionString = @"Name=DefaultConnection",
+ ContextNamespace = "ContextNamespace",
+ ModelNamespace = "ModelNamespace",
+ RootNamespace = "RootNamespace",
+ ProjectDir = projectDir
+ });
+
+ Assert.Equal(
+ @"Model not null: True
+Options not null: True
+NamespaceHint: ContextNamespace
+ProjectDefaultNamespace: RootNamespace",
+ result.ContextFile.Code);
+
+ var entityType = Assert.Single(result.AdditionalFiles);
+ Assert.Equal(
+ @"EntityType not null: True
+Options not null: True
+NamespaceHint: ModelNamespace
+ProjectDefaultNamespace: RootNamespace",
+ entityType.Code);
+ }
+
+ [ConditionalFact]
+ public void GenerateModel_defaults_to_model_namespace_when_no_context_namespace()
+ {
+ using var projectDir = new TempDirectory();
+
+ var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4");
+ Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate));
+ File.WriteAllText(
+ contextTemplate,
+ @"<#= Session[""NamespaceHint""] #>");
+
+ var generator = CreateGenerator();
+ var model = new ModelBuilder()
+ .FinalizeModel();
+
+ var result = generator.GenerateModel(
+ model,
+ new()
+ {
+ ContextName = "Context",
+ ConnectionString = @"Name=DefaultConnection",
+ ModelNamespace = "ModelNamespace",
+ ProjectDir = projectDir
+ });
+
+ Assert.Equal(
+ "ModelNamespace",
+ result.ContextFile.Code);
+ }
+
+ [ConditionalFact]
+ public void GenerateModel_uses_output_extension()
+ {
+ using var projectDir = new TempDirectory();
+
+ var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4");
+ Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate));
+ File.WriteAllText(
+ contextTemplate,
+ @"<#@ output extension="".vb"" #>");
+
+ File.WriteAllText(
+ Path.Combine(projectDir, "Templates", "EFCore", "EntityType.t4"),
+ @"<#@ output extension="".fs"" #>
+My entity type template");
+
+ var generator = CreateGenerator();
+ var model = new ModelBuilder()
+ .Entity("Entity1", b => { })
+ .FinalizeModel();
+
+ var result = generator.GenerateModel(
+ model,
+ new()
+ {
+ ContextName = "Context",
+ ConnectionString = @"Name=DefaultConnection",
+ ProjectDir = projectDir
+ });
+
+ Assert.Equal("Context.vb", result.ContextFile.Path);
+
+ var entityType = Assert.Single(result.AdditionalFiles);
+ Assert.Equal("Entity1.fs", entityType.Path);
+ }
+
+ [ConditionalFact]
+ public void GenerateModel_warns_when_output_encoding()
+ {
+ using var projectDir = new TempDirectory();
+
+ var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4");
+ Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate));
+ File.WriteAllText(
+ contextTemplate,
+ @"<#@ output encoding=""us-ascii"" #>");
+
+ var reporter = new TestOperationReporter();
+ var generator = CreateGenerator(reporter);
+ var model = new ModelBuilder()
+ .FinalizeModel();
+
+ var result = generator.GenerateModel(
+ model,
+ new()
+ {
+ ContextName = "Context",
+ ConnectionString = @"Name=DefaultConnection",
+ ProjectDir = projectDir
+ });
+
+ Assert.Collection(
+ reporter.Messages,
+ x =>
+ {
+ Assert.Equal(LogLevel.Warning, x.Level);
+ Assert.Equal(DesignStrings.EncodingIgnored("us-ascii"), x.Message);
+ });
+ }
+
+ [ConditionalFact]
+ public void GenerateModel_reports_errors()
+ {
+ using var projectDir = new TempDirectory();
+
+ var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4");
+ Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate));
+ File.WriteAllText(
+ contextTemplate,
+ @"<# Warning(""This is a warning"");
+Error(""This is an error""); #>");
+
+ var reporter = new TestOperationReporter();
+ var generator = CreateGenerator(reporter);
+ var model = new ModelBuilder()
+ .FinalizeModel();
+
+ var result = generator.GenerateModel(
+ model,
+ new()
+ {
+ ContextName = "Context",
+ ConnectionString = @"Name=DefaultConnection",
+ ProjectDir = projectDir
+ });
+
+ Assert.Collection(
+ reporter.Messages,
+ x =>
+ {
+ Assert.Equal(LogLevel.Warning, x.Level);
+ Assert.Contains("This is a warning", x.Message);
+ },
+ x =>
+ {
+ Assert.Equal(LogLevel.Error, x.Level);
+ Assert.Contains("This is an error", x.Message);
+ });
+ }
+
+ private static TemplatedModelGenerator CreateGenerator(IOperationReporter reporter = null)
+ {
+ var serviceCollection = new ServiceCollection()
+ .AddEntityFrameworkDesignTimeServices(reporter);
+ new SqlServerDesignTimeServices().ConfigureDesignTimeServices(serviceCollection);
+
+ return serviceCollection
+ .BuildServiceProvider()
+ .GetServices()
+ .OfType()
+ .Last();
+ }
+}
diff --git a/test/EFCore.Design.Tests/TestUtilities/TempDirectory.cs b/test/EFCore.Design.Tests/TestUtilities/TempDirectory.cs
index 06cf980780e..c38c5b2c951 100644
--- a/test/EFCore.Design.Tests/TestUtilities/TempDirectory.cs
+++ b/test/EFCore.Design.Tests/TestUtilities/TempDirectory.cs
@@ -15,6 +15,9 @@ public TempDirectory()
public string Path { get; }
+ public static implicit operator string(TempDirectory dir)
+ => dir.Path;
+
public void Dispose()
=> Directory.Delete(Path, recursive: true);
}
diff --git a/test/EFCore.Design.Tests/TextTemplating/Internal/TextTemplatingServiceTest.cs b/test/EFCore.Design.Tests/TextTemplating/Internal/TextTemplatingServiceTest.cs
new file mode 100644
index 00000000000..64159cb9898
--- /dev/null
+++ b/test/EFCore.Design.Tests/TextTemplating/Internal/TextTemplatingServiceTest.cs
@@ -0,0 +1,183 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.CodeDom.Compiler;
+using Microsoft.EntityFrameworkCore.Internal;
+using Microsoft.EntityFrameworkCore.TestUtilities.Xunit;
+using Microsoft.VisualStudio.TextTemplating;
+
+namespace Microsoft.EntityFrameworkCore.TextTemplating.Internal;
+
+[PlatformSkipCondition(TestPlatform.Linux, SkipReason = "CI time out")]
+public class TextTemplatingServiceTest
+{
+ [ConditionalFact]
+ public void Service_works()
+ {
+ var host = new TextTemplatingService(
+ new ServiceCollection()
+ .AddSingleton("Hello, Services!")
+ .BuildServiceProvider());
+ var callback = new TextTemplatingCallback();
+
+ var result = host.ProcessTemplate(
+ @"T:\test.tt",
+ @"<#@ template hostSpecific=""true"" #><#= ((IServiceProvider)Host).GetService(typeof(string)) #>",
+ callback);
+
+ Assert.Empty(callback.Errors);
+ Assert.Equal("Hello, Services!", result);
+ }
+
+ [ConditionalFact]
+ public void Session_works()
+ {
+ var host = new TextTemplatingService(
+ new ServiceCollection()
+ .BuildServiceProvider());
+ host.Session = new TextTemplatingSession
+ {
+ ["Value"] = "Hello, Session!"
+ };
+ var callback = new TextTemplatingCallback();
+
+ var result = host.ProcessTemplate(
+ @"T:\test.tt",
+ @"<#= Session[""Value""] #>",
+ callback);
+
+ Assert.Empty(callback.Errors);
+ Assert.Equal("Hello, Session!", result);
+ }
+
+ [ConditionalFact]
+ public void Session_works_with_parameter()
+ {
+ var host = new TextTemplatingService(
+ new ServiceCollection()
+ .BuildServiceProvider());
+ host.Session = new TextTemplatingSession
+ {
+ ["Value"] = "Hello, Session!"
+ };
+ var callback = new TextTemplatingCallback();
+
+ var result = host.ProcessTemplate(
+ @"T:\test.tt",
+ @"<#@ parameter name=""Value"" type=""System.String"" #><#= Value #>",
+ callback);
+
+ Assert.Empty(callback.Errors);
+ Assert.Equal("Hello, Session!", result);
+ }
+
+ [ConditionalFact]
+ public void Include_works()
+ {
+ using var dir = new TempDirectory();
+ File.WriteAllText(
+ Path.Combine(dir, "test.ttinclude"),
+ "Hello, Include!");
+
+ var host = new TextTemplatingService(
+ new ServiceCollection()
+ .BuildServiceProvider());
+ var callback = new TextTemplatingCallback();
+
+ var result = host.ProcessTemplate(
+ Path.Combine(dir, "test.tt"),
+ @"<#@ include file=""test.ttinclude"" #>",
+ callback);
+
+ Assert.Empty(callback.Errors);
+ Assert.Equal("Hello, Include!", result);
+ }
+
+ [ConditionalFact]
+ public void Error_works()
+ {
+ var host = new TextTemplatingService(
+ new ServiceCollection()
+ .BuildServiceProvider());
+ var callback = new TextTemplatingCallback();
+
+ host.ProcessTemplate(
+ @"T:\test.tt",
+ @"<# Error(""Hello, Error!""); #>",
+ callback);
+
+ var error = Assert.Single(callback.Errors.Cast());
+ Assert.Equal("Hello, Error!", error.ErrorText);
+ }
+
+ [ConditionalFact]
+ public void Directive_throws_when_processor_unknown()
+ {
+ var host = new TextTemplatingService(
+ new ServiceCollection()
+ .BuildServiceProvider());
+ var callback = new TextTemplatingCallback();
+
+ var ex = Assert.Throws(
+ () => host.ProcessTemplate(
+ @"T:\test.tt",
+ @"<#@ test processor=""TestDirectiveProcessor"" #>",
+ callback));
+
+ Assert.Equal(DesignStrings.UnknownDirectiveProcessor("TestDirectiveProcessor"), ex.Message);
+ }
+
+ [ConditionalFact]
+ public void ResolvePath_work()
+ {
+ using var dir = new TempDirectory();
+
+ var host = new TextTemplatingService(
+ new ServiceCollection()
+ .BuildServiceProvider());
+ var callback = new TextTemplatingCallback();
+
+ var result = host.ProcessTemplate(
+ Path.Combine(dir, "test.tt"),
+ @"<#@ template hostSpecific=""true"" #><#= Host.ResolvePath(""data.json"") #>",
+ callback);
+
+ Assert.Empty(callback.Errors);
+ Assert.Equal(Path.Combine(dir, "data.json"), result);
+ }
+
+ [ConditionalFact]
+ public void Output_works()
+ {
+ var host = new TextTemplatingService(
+ new ServiceCollection()
+ .BuildServiceProvider());
+ var callback = new TextTemplatingCallback();
+
+ host.ProcessTemplate(
+ @"T:\test.tt",
+ @"<#@ output extension="".txt"" encoding=""us-ascii"" #>",
+ callback);
+
+ Assert.Empty(callback.Errors);
+ Assert.Equal(".txt", callback.Extension);
+ Assert.Equal(Encoding.ASCII, callback.OutputEncoding);
+ }
+
+ [ConditionalFact]
+ public void Assembly_works()
+ {
+ var host = new TextTemplatingService(
+ new ServiceCollection()
+ .BuildServiceProvider());
+ var callback = new TextTemplatingCallback();
+
+ var result = host.ProcessTemplate(
+ @"T:\test.tt",
+ @"<#@ assembly name=""Microsoft.EntityFrameworkCore"" #><#= nameof(Microsoft.EntityFrameworkCore.DbContext) #>",
+ callback);
+
+ Assert.Empty(callback.Errors);
+ Assert.Equal("DbContext", result);
+ }
+}
diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs
index 439686def68..b8ecced9d14 100644
--- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs
@@ -935,7 +935,7 @@ public virtual Task Alter_column_remove_comment()
Assert.Null(column.Comment);
});
- [Fact]
+ [ConditionalFact]
public virtual Task Alter_column_set_collation()
=> Test(
builder => builder.Entity("People").Property("Name"),
@@ -951,7 +951,7 @@ public virtual Task Alter_column_set_collation()
}
});
- [Fact]
+ [ConditionalFact]
public virtual Task Alter_column_reset_collation()
=> Test(
builder => builder.Entity("People").Property("Name"),
diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoreValueGenerationModel/StoreValueGenerationContext.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoreValueGenerationModel/StoreValueGenerationContext.cs
new file mode 100644
index 00000000000..37ad0921d58
--- /dev/null
+++ b/test/EFCore.Relational.Specification.Tests/TestModels/StoreValueGenerationModel/StoreValueGenerationContext.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.TestModels.StoreValueGenerationModel;
+
+#nullable enable
+
+public class StoreValueGenerationContext : PoolableDbContext
+{
+ public StoreValueGenerationContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ public DbSet WithSomeDatabaseGenerated
+ => Set(nameof(WithSomeDatabaseGenerated));
+ public DbSet WithSomeDatabaseGenerated2
+ => Set(nameof(WithSomeDatabaseGenerated2));
+
+ public DbSet WithNoDatabaseGenerated
+ => Set(nameof(WithNoDatabaseGenerated));
+ public DbSet WithNoDatabaseGenerated2
+ => Set(nameof(WithNoDatabaseGenerated2));
+
+ public DbSet WithAllDatabaseGenerated
+ => Set(nameof(WithAllDatabaseGenerated));
+ public DbSet WithAllDatabaseGenerated2
+ => Set(nameof(WithAllDatabaseGenerated2));
+}
diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoreValueGenerationModel/StoreValueGenerationData.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoreValueGenerationModel/StoreValueGenerationData.cs
new file mode 100644
index 00000000000..b19365b9f8d
--- /dev/null
+++ b/test/EFCore.Relational.Specification.Tests/TestModels/StoreValueGenerationModel/StoreValueGenerationData.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.TestModels.StoreValueGenerationModel;
+
+#nullable enable
+
+public class StoreValueGenerationData : IEquatable
+{
+ // Generated on add (except for WithNoDatabaseGenerated2)
+ public int Id { get; set; }
+
+ // Generated on update (except for WithNoDatabaseGenerated2)
+ public int Data1 { get; set; }
+
+ // Not generated, except for for WithAllDatabaseGenerated
+ public int Data2 { get; set; }
+
+ public bool Equals(StoreValueGenerationData? other)
+ => other is not null
+ && (ReferenceEquals(this, other)
+ || (Id == other.Id
+ && Data1 == other.Data1
+ && Data2 == other.Data2));
+}
diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/BuildReference.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/BuildReference.cs
index 41437f641f3..e56c51a4b99 100644
--- a/test/EFCore.Relational.Specification.Tests/TestUtilities/BuildReference.cs
+++ b/test/EFCore.Relational.Specification.Tests/TestUtilities/BuildReference.cs
@@ -24,6 +24,7 @@ private BuildReference(IEnumerable references, bool copyLocal
public static BuildReference ByName(string name, bool copyLocal = false)
{
var references = (from l in DependencyContext.Default.CompileLibraries
+ where l.Assemblies.Any(a => IOPath.GetFileNameWithoutExtension(a) == name)
from r in l.ResolveReferencePaths()
where IOPath.GetFileNameWithoutExtension(r) == name
select MetadataReference.CreateFromFile(r)).ToList();
diff --git a/test/EFCore.Relational.Specification.Tests/Update/StoreValueGenerationFixtureBase.cs b/test/EFCore.Relational.Specification.Tests/Update/StoreValueGenerationFixtureBase.cs
new file mode 100644
index 00000000000..b0b27159638
--- /dev/null
+++ b/test/EFCore.Relational.Specification.Tests/Update/StoreValueGenerationFixtureBase.cs
@@ -0,0 +1,91 @@
+// 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.TestModels.StoreValueGenerationModel;
+
+namespace Microsoft.EntityFrameworkCore.Update;
+
+#nullable enable
+
+public abstract class StoreValueGenerationFixtureBase : SharedStoreFixtureBase
+{
+ protected override string StoreName { get; } = "StoreValueGenerationTest";
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
+ {
+ foreach (var name in new[]
+ {
+ nameof(StoreValueGenerationContext.WithNoDatabaseGenerated),
+ nameof(StoreValueGenerationContext.WithNoDatabaseGenerated2)
+ })
+ {
+ modelBuilder
+ .SharedTypeEntity(name)
+ .Property(w => w.Id)
+ .ValueGeneratedNever();
+ }
+
+ foreach (var name in new[]
+ {
+ nameof(StoreValueGenerationContext.WithSomeDatabaseGenerated),
+ nameof(StoreValueGenerationContext.WithSomeDatabaseGenerated2),
+ nameof(StoreValueGenerationContext.WithAllDatabaseGenerated),
+ nameof(StoreValueGenerationContext.WithAllDatabaseGenerated2)
+ })
+ {
+ modelBuilder
+ .SharedTypeEntity(name)
+ .Property(w => w.Data1)
+ .HasComputedColumnSql("80");
+ }
+
+ foreach (var name in new[]
+ {
+ nameof(StoreValueGenerationContext.WithAllDatabaseGenerated),
+ nameof(StoreValueGenerationContext.WithAllDatabaseGenerated2)
+ })
+ {
+ modelBuilder
+ .SharedTypeEntity(name)
+ .Property(w => w.Data2)
+ .HasComputedColumnSql("81");
+ }
+ }
+
+ protected override void Seed(StoreValueGenerationContext context)
+ {
+ context.WithSomeDatabaseGenerated.AddRange(new() { Data2 = 1 }, new() { Data2 = 2 });
+ context.WithSomeDatabaseGenerated2.AddRange(new() { Data2 = 1 }, new() { Data2 = 2 });
+
+ context.WithNoDatabaseGenerated.AddRange(new() { Id = 1, Data1 = 10, Data2 = 20 }, new() { Id = 2, Data1 = 11, Data2 = 21 });
+ context.WithNoDatabaseGenerated2.AddRange(new() { Id = 1, Data1 = 10, Data2 = 20 }, new() { Id = 2, Data1 = 11, Data2 = 21 });
+
+ context.WithAllDatabaseGenerated.AddRange(new(), new());
+ context.WithAllDatabaseGenerated2.AddRange(new(), new());
+
+ context.SaveChanges();
+ }
+
+ protected override void Clean(DbContext context)
+ {
+ var storeValueGenerationContext = CreateContext();
+
+ storeValueGenerationContext.WithSomeDatabaseGenerated.RemoveRange(storeValueGenerationContext.WithSomeDatabaseGenerated);
+ storeValueGenerationContext.WithSomeDatabaseGenerated2.RemoveRange(storeValueGenerationContext.WithSomeDatabaseGenerated2);
+
+ storeValueGenerationContext.WithNoDatabaseGenerated.RemoveRange(storeValueGenerationContext.WithNoDatabaseGenerated);
+ storeValueGenerationContext.WithNoDatabaseGenerated2.RemoveRange(storeValueGenerationContext.WithNoDatabaseGenerated2);
+
+ storeValueGenerationContext.WithAllDatabaseGenerated.RemoveRange(storeValueGenerationContext.WithAllDatabaseGenerated);
+ storeValueGenerationContext.WithAllDatabaseGenerated2.RemoveRange(storeValueGenerationContext.WithAllDatabaseGenerated2);
+
+ storeValueGenerationContext.SaveChanges();
+ }
+
+ protected override bool ShouldLogCategory(string logCategory)
+ => logCategory == DbLoggerCategory.Database.Transaction.Name
+ || logCategory == DbLoggerCategory.Database.Command.Name;
+
+ public TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+}
diff --git a/test/EFCore.Relational.Specification.Tests/Update/StoreValueGenerationTestBase.cs b/test/EFCore.Relational.Specification.Tests/Update/StoreValueGenerationTestBase.cs
new file mode 100644
index 00000000000..5b89e789ace
--- /dev/null
+++ b/test/EFCore.Relational.Specification.Tests/Update/StoreValueGenerationTestBase.cs
@@ -0,0 +1,379 @@
+// 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.TestModels.StoreValueGenerationModel;
+
+namespace Microsoft.EntityFrameworkCore.Update;
+
+#nullable enable
+
+public abstract class StoreValueGenerationTestBase : IClassFixture
+ where TFixture : StoreValueGenerationFixtureBase
+{
+ protected StoreValueGenerationTestBase(TFixture fixture)
+ {
+ Fixture = fixture;
+
+ fixture.Reseed();
+
+ ClearLog();
+ }
+
+ #region Single operation
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Add_with_generated_values(bool async)
+ => Test(EntityState.Added, secondOperationType: null, GeneratedValues.Some, async);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Add_with_no_generated_values(bool async)
+ => Test(EntityState.Added, secondOperationType: null, GeneratedValues.None, async);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Add_with_all_generated_values(bool async)
+ => Test(EntityState.Added, secondOperationType: null, GeneratedValues.All, async);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Modify_with_generated_values(bool async)
+ => Test(EntityState.Modified, secondOperationType: null, GeneratedValues.Some, async);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Modify_with_no_generated_values(bool async)
+ => Test(EntityState.Modified, secondOperationType: null, GeneratedValues.None, async);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Delete(bool async)
+ => Test(EntityState.Deleted, secondOperationType: null, GeneratedValues.Some, async);
+
+ #endregion Single operation
+
+ #region Two operations with same entity type
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Add_Add_with_same_entity_type_and_generated_values(bool async)
+ => Test(EntityState.Added, EntityState.Added, GeneratedValues.Some, async, withSameEntityType: true);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Add_Add_with_same_entity_type_and_no_generated_values(bool async)
+ => Test(EntityState.Added, EntityState.Added, GeneratedValues.None, async, withSameEntityType: true);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Add_Add_with_same_entity_type_and_all_generated_values(bool async)
+ => Test(EntityState.Added, EntityState.Added, GeneratedValues.All, async, withSameEntityType: true);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Modify_Modify_with_same_entity_type_and_generated_values(bool async)
+ => Test(EntityState.Modified, EntityState.Modified, GeneratedValues.Some, async, withSameEntityType: true);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Modify_Modify_with_same_entity_type_and_no_generated_values(bool async)
+ => Test(EntityState.Modified, EntityState.Modified, GeneratedValues.None, async, withSameEntityType: true);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Delete_Delete_with_same_entity_type(bool async)
+ => Test(EntityState.Deleted, EntityState.Deleted, GeneratedValues.Some, async, withSameEntityType: true);
+
+ #endregion Two operations with same entity type
+
+ #region Two operations with different entity types
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Add_Add_with_different_entity_types_and_generated_values(bool async)
+ => Test(EntityState.Added, EntityState.Added, GeneratedValues.Some, async, withSameEntityType: false);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Add_Add_with_different_entity_types_and_no_generated_values(bool async)
+ => Test(EntityState.Added, EntityState.Added, GeneratedValues.None, async, withSameEntityType: false);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Add_Add_with_different_entity_types_and_all_generated_values(bool async)
+ => Test(EntityState.Added, EntityState.Added, GeneratedValues.All, async, withSameEntityType: false);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Modify_Modify_with_different_entity_types_and_generated_values(bool async)
+ => Test(EntityState.Modified, EntityState.Modified, GeneratedValues.Some, async, withSameEntityType: false);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Modify_Modify_with_different_entity_types_and_no_generated_values(bool async)
+ => Test(EntityState.Modified, EntityState.Modified, GeneratedValues.None, async, withSameEntityType: false);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Delete_Delete_with_different_entity_types(bool async)
+ => Test(EntityState.Deleted, EntityState.Deleted, GeneratedValues.Some, async, withSameEntityType: false);
+
+ #endregion Two operations with different entity types
+
+ protected virtual async Task Test(
+ EntityState firstOperationType,
+ EntityState? secondOperationType,
+ GeneratedValues generatedValues,
+ bool async,
+ bool withSameEntityType = true)
+ {
+ await using var context = CreateContext();
+
+ var firstDbSet = generatedValues switch
+ {
+ GeneratedValues.Some => context.WithSomeDatabaseGenerated,
+ GeneratedValues.None => context.WithNoDatabaseGenerated,
+ GeneratedValues.All => context.WithAllDatabaseGenerated,
+ _ => throw new ArgumentOutOfRangeException(nameof(generatedValues))
+ };
+
+ var secondDbSet = secondOperationType is null
+ ? null
+ : (generatedValues, withSameEntityType) switch
+ {
+ (GeneratedValues.Some, true) => context.WithSomeDatabaseGenerated,
+ (GeneratedValues.Some, false) => context.WithSomeDatabaseGenerated2,
+ (GeneratedValues.None, true) => context.WithNoDatabaseGenerated,
+ (GeneratedValues.None, false) => context.WithNoDatabaseGenerated2,
+ (GeneratedValues.All, true) => context.WithAllDatabaseGenerated,
+ (GeneratedValues.All, false) => context.WithAllDatabaseGenerated2,
+ _ => throw new ArgumentOutOfRangeException(nameof(generatedValues))
+ };
+
+ StoreValueGenerationData first;
+ StoreValueGenerationData? second;
+
+ switch (firstOperationType)
+ {
+ case EntityState.Added:
+ switch (generatedValues)
+ {
+ case GeneratedValues.Some:
+ first = new StoreValueGenerationData { Data2 = 1000 };
+ firstDbSet.Add(first);
+ break;
+ case GeneratedValues.None:
+ first = new StoreValueGenerationData { Id = 100, Data1 = 1000, Data2 = 1000 };
+ firstDbSet.Add(first);
+ break;
+ case GeneratedValues.All:
+ first = new StoreValueGenerationData();
+ firstDbSet.Add(first);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(generatedValues));
+ }
+ break;
+
+ case EntityState.Modified:
+ switch (generatedValues)
+ {
+ case GeneratedValues.Some:
+ first = firstDbSet.OrderBy(w => w.Id).First();
+ first.Data2 = 1000;
+ break;
+ case GeneratedValues.None:
+ first = firstDbSet.OrderBy(w => w.Id).First();
+ (first.Data1, first.Data2) = (1000, 1000);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(generatedValues));
+ }
+ break;
+
+ case EntityState.Deleted:
+ switch (generatedValues)
+ {
+ case GeneratedValues.Some:
+ first = firstDbSet.OrderBy(w => w.Id).First();
+ context.Remove(first);
+ break;
+ case GeneratedValues.None:
+ first = firstDbSet.OrderBy(w => w.Id).First();
+ context.Remove(first);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(generatedValues));
+ }
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(firstOperationType));
+ }
+
+ switch (secondOperationType)
+ {
+ case EntityState.Added:
+ switch (generatedValues)
+ {
+ case GeneratedValues.Some:
+ second = new StoreValueGenerationData { Data2 = 1001 };
+ secondDbSet!.Add(second);
+ break;
+ case GeneratedValues.None:
+ second = new StoreValueGenerationData { Id = 101, Data1 = 1001, Data2 = 1001 };
+ secondDbSet!.Add(second);
+ break;
+ case GeneratedValues.All:
+ second = new StoreValueGenerationData();
+ secondDbSet!.Add(second);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(generatedValues));
+ }
+ break;
+
+ case EntityState.Modified:
+ switch (generatedValues)
+ {
+ case GeneratedValues.Some:
+ second = secondDbSet!.OrderBy(w => w.Id).Skip(1).First();
+ second.Data2 = 1001;
+ break;
+ case GeneratedValues.None:
+ second = secondDbSet!.OrderBy(w => w.Id).Skip(1).First();
+ (second.Data1, second.Data2) = (1001, 1001);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(generatedValues));
+ }
+ break;
+
+ case EntityState.Deleted:
+ switch (generatedValues)
+ {
+ case GeneratedValues.Some:
+ second = secondDbSet!.OrderBy(w => w.Id).Skip(1).First();
+ context.Remove(second);
+ break;
+ case GeneratedValues.None:
+ second = secondDbSet!.OrderBy(w => w.Id).Skip(1).First();
+ context.Remove(second);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(generatedValues));
+ }
+ break;
+
+ case null:
+ second = null;
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(firstOperationType));
+ }
+
+ // Execute
+ Fixture.ListLoggerFactory.Clear();
+
+ if (async)
+ {
+ await context.SaveChangesAsync();
+ }
+ else
+ {
+ context.SaveChanges();
+ }
+
+ // Make sure a transaction was created (or not)
+ if (ShouldCreateImplicitTransaction(firstOperationType, secondOperationType, generatedValues, withSameEntityType))
+ {
+ Assert.Contains(Fixture.ListLoggerFactory.Log, l => l.Id == RelationalEventId.TransactionStarted);
+ Assert.Contains(Fixture.ListLoggerFactory.Log, l => l.Id == RelationalEventId.TransactionCommitted);
+ }
+ else
+ {
+ Assert.DoesNotContain(Fixture.ListLoggerFactory.Log, l => l.Id == RelationalEventId.TransactionStarted);
+ Assert.DoesNotContain(Fixture.ListLoggerFactory.Log, l => l.Id == RelationalEventId.TransactionCommitted);
+ }
+
+ // Make sure the updates executed in the expected number of commands
+ Assert.Equal(
+ ShouldExecuteInNumberOfCommands(firstOperationType, secondOperationType, generatedValues, withSameEntityType),
+ Fixture.ListLoggerFactory.Log.Count(l => l.Id == RelationalEventId.CommandExecuted));
+
+ // To make sure generated values have been propagated, re-load the rows from the database and compare
+ context.ChangeTracker.Clear();
+
+ using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents())
+ {
+ if (firstOperationType != EntityState.Deleted)
+ {
+ Assert.Equal(await firstDbSet.FindAsync(first.Id), first);
+ }
+
+ if (second is not null && secondOperationType != EntityState.Deleted)
+ {
+ Assert.Equal(await secondDbSet!.FindAsync(second.Id), second);
+ }
+ }
+ }
+
+ ///
+ /// Providers can override this to specify when should create a transaction, and when not.
+ /// By default, it's assumed that multiple updates always require a transaction, whereas a single update never does.
+ ///
+ protected virtual bool ShouldCreateImplicitTransaction(
+ EntityState firstOperationType,
+ EntityState? secondOperationType,
+ GeneratedValues generatedValues,
+ bool withSameEntityType)
+ {
+ // By default, two changes require a transaction
+ if (secondOperationType is not null)
+ {
+ return true;
+ }
+
+ // Deletes don't ever need to bring back database-generated values
+ if (firstOperationType == EntityState.Deleted)
+ {
+ return false;
+ }
+
+ // By default, assume that fetching back database-generated values requires a transaction
+ return generatedValues != GeneratedValues.None;
+ }
+
+ ///
+ /// Providers can override this to specify how many commands (batches) are used to execute the update.
+ /// By default, it's assumed all operations are batched in one command.
+ ///
+ protected virtual int ShouldExecuteInNumberOfCommands(
+ EntityState firstOperationType,
+ EntityState? secondOperationType,
+ GeneratedValues generatedValues,
+ bool withSameEntityType)
+ => 1;
+
+ protected TFixture Fixture { get; }
+
+ protected StoreValueGenerationContext CreateContext()
+ => Fixture.CreateContext();
+
+ public static IEnumerable