diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 9264ac97d22..e5f7b03613c 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -17,6 +17,7 @@ updates:
- dependency-name: Microsoft.CSharp
- dependency-name: Microsoft.Data.SqlClient
- dependency-name: Microsoft.DotNet.PlatformAbstractions
+ - dependency-name: Microsoft.SqlServer.Types
- dependency-name: mod_spatialite
- dependency-name: Mono.TextTemplating
- dependency-name: NetTopologySuite*
diff --git a/All.sln b/All.sln
index dcb613bda12..07a3e2b33ea 100644
--- a/All.sln
+++ b/All.sln
@@ -125,6 +125,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.Trimming.Tests", "te
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.Templates", "src\EFCore.Templates\EFCore.Templates.csproj", "{1FE385D8-8F8B-4EC9-A1A9-AFCC38B8546C}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.SqlServer.HierarchyId", "src\EFCore.SqlServer.HierarchyId\EFCore.SqlServer.HierarchyId.csproj", "{8F722A02-71A4-4787-ACD8-FB7D5B7AE648}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.SqlServer.HierarchyId.Tests", "test\EFCore.SqlServer.HierarchyId.Tests\EFCore.SqlServer.HierarchyId.Tests.csproj", "{01F86E65-6448-424C-AAB5-9C6427EF6FD4}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.SqlServer.Abstractions", "src\EFCore.SqlServer.Abstractions\EFCore.SqlServer.Abstractions.csproj", "{3D935B7D-80BD-49AD-BDC9-E1B0C9D9494F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -331,6 +337,18 @@ Global
{1FE385D8-8F8B-4EC9-A1A9-AFCC38B8546C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1FE385D8-8F8B-4EC9-A1A9-AFCC38B8546C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1FE385D8-8F8B-4EC9-A1A9-AFCC38B8546C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8F722A02-71A4-4787-ACD8-FB7D5B7AE648}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8F722A02-71A4-4787-ACD8-FB7D5B7AE648}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8F722A02-71A4-4787-ACD8-FB7D5B7AE648}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8F722A02-71A4-4787-ACD8-FB7D5B7AE648}.Release|Any CPU.Build.0 = Release|Any CPU
+ {01F86E65-6448-424C-AAB5-9C6427EF6FD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {01F86E65-6448-424C-AAB5-9C6427EF6FD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {01F86E65-6448-424C-AAB5-9C6427EF6FD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {01F86E65-6448-424C-AAB5-9C6427EF6FD4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3D935B7D-80BD-49AD-BDC9-E1B0C9D9494F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3D935B7D-80BD-49AD-BDC9-E1B0C9D9494F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3D935B7D-80BD-49AD-BDC9-E1B0C9D9494F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3D935B7D-80BD-49AD-BDC9-E1B0C9D9494F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -386,6 +404,9 @@ Global
{F1B2E5A0-8C74-414A-B262-353FEE325E9F} = {258D5057-81B9-40EC-A872-D21E27452749}
{933C8662-817C-4F45-B98B-6557E28F7BB1} = {258D5057-81B9-40EC-A872-D21E27452749}
{1FE385D8-8F8B-4EC9-A1A9-AFCC38B8546C} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC}
+ {8F722A02-71A4-4787-ACD8-FB7D5B7AE648} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC}
+ {01F86E65-6448-424C-AAB5-9C6427EF6FD4} = {258D5057-81B9-40EC-A872-D21E27452749}
+ {3D935B7D-80BD-49AD-BDC9-E1B0C9D9494F} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {285A5EB4-BCF4-40EB-B9E1-DF6DBCB5E705}
diff --git a/src/EFCore.SqlServer.Abstractions/EFCore.SqlServer.Abstractions.csproj b/src/EFCore.SqlServer.Abstractions/EFCore.SqlServer.Abstractions.csproj
new file mode 100644
index 00000000000..ae4835d4b17
--- /dev/null
+++ b/src/EFCore.SqlServer.Abstractions/EFCore.SqlServer.Abstractions.csproj
@@ -0,0 +1,16 @@
+
+
+
+ netstandard2.0
+ EntityFrameworkCore.SqlServer.HierarchyId.Abstractions
+ Microsoft.EntityFrameworkCore
+ Common abstractions for using hierarchyid with EF Core
+ true
+ true
+
+
+
+
+
+
+
diff --git a/src/EFCore.SqlServer.Abstractions/HierarchyId.cs b/src/EFCore.SqlServer.Abstractions/HierarchyId.cs
new file mode 100644
index 00000000000..67b4084361c
--- /dev/null
+++ b/src/EFCore.SqlServer.Abstractions/HierarchyId.cs
@@ -0,0 +1,217 @@
+using System;
+using System.IO;
+using Microsoft.SqlServer.Types;
+
+namespace Microsoft.EntityFrameworkCore
+{
+ ///
+ /// Represents a position in a hierarchical structure, specifying depth and breadth.
+ ///
+ public class HierarchyId : IComparable
+ {
+ private SqlHierarchyId _value;
+
+ private HierarchyId(SqlHierarchyId value)
+ {
+ if (value.IsNull)
+ throw new ArgumentNullException(nameof(value));
+
+ _value = value;
+ }
+
+ ///
+ /// Gets the root node of the hierarchy.
+ ///
+ /// The root node of the hierarchy.
+ public static HierarchyId GetRoot()
+ => new HierarchyId(SqlHierarchyId.GetRoot());
+
+ ///
+ /// Converts the canonical string representation of a node to a value.
+ ///
+ /// The string representation of a node.
+ /// A value.
+ public static HierarchyId Parse(string input)
+ => Wrap(SqlHierarchyId.Parse(input));
+
+ ///
+ /// Reads a value from the specified reader.
+ ///
+ /// The reader.
+ /// A value.
+ public static HierarchyId Read(BinaryReader reader)
+ {
+ var hid = new SqlHierarchyId();
+ hid.Read(reader);
+ return Wrap(hid);
+ }
+
+ ///
+ /// Writes this value to the specified writer.
+ ///
+ /// The writer.
+ public void Write(BinaryWriter writer)
+ {
+ _value.Write(writer);
+ }
+
+ ///
+ public int CompareTo(object obj)
+ => _value.CompareTo(
+ obj is HierarchyId other
+ ? other._value
+ : obj);
+
+ ///
+ public override bool Equals(object obj)
+ => _value.Equals(
+ obj is HierarchyId other
+ ? other._value
+ : obj);
+
+ ///
+ /// Gets the node levels up the hierarchical tree.
+ ///
+ /// The number of levels to ascend in the hierarchy.
+ /// A value representing the th ancestor of this node or null if is greater than .
+ /// is negative.
+ public HierarchyId GetAncestor(int n)
+ => Wrap(_value.GetAncestor(n));
+
+ ///
+ /// Gets the value of a descendant node that is greater than and less than .
+ ///
+ /// The lower bound.
+ /// The upper bound.
+ /// A value.
+ public HierarchyId GetDescendant(HierarchyId child1, HierarchyId child2)
+ => Wrap(_value.GetDescendant(Unwrap(child1), Unwrap(child2)));
+
+ ///
+ public override int GetHashCode()
+ => _value.GetHashCode();
+
+ ///
+ /// Gets the level of this node in the hierarchical tree.
+ ///
+ /// The depth of this node. The root node is level 0.
+ public short GetLevel()
+ => _value.GetLevel().Value;
+
+ ///
+ /// Gets a value representing the location of a new node that has a path from equal to the path from to this, effectively moving this to the new location.
+ ///
+ /// An ancestor of this node specifying the endpoint of the path segment to be moved.
+ /// The node that represents the new ancestor.
+ /// A value or null if or is null.
+ public HierarchyId GetReparentedValue(HierarchyId oldRoot, HierarchyId newRoot)
+ => Wrap(_value.GetReparentedValue(Unwrap(oldRoot), Unwrap(newRoot)));
+
+ ///
+ /// Gets a value indicating whether this node is a descendant of .
+ ///
+ /// The parent to test against.
+ /// True if this node is in the sub-tree rooted at ; otherwise false.
+ public bool IsDescendantOf(HierarchyId parent)
+ {
+ if (parent == null)
+ return false;
+
+ return _value.IsDescendantOf(parent._value).Value;
+ }
+
+ ///
+ public override string ToString()
+ => _value.ToString();
+
+ ///
+ /// Evaluates whether two nodes are equal.
+ ///
+ /// The first node to compare.
+ /// The second node to compare.
+ /// True if and are equal; otherwise, false.
+ public static bool operator ==(HierarchyId hid1, HierarchyId hid2)
+ {
+ var sh1 = Unwrap(hid1);
+ var sh2 = Unwrap(hid2);
+
+ return sh1.IsNull == sh2.IsNull && sh1.CompareTo(sh2) == 0;
+ }
+
+ ///
+ /// Evaluates whether two nodes are unequal.
+ ///
+ /// The first node to compare.
+ /// The second node to compare.
+ /// True if and are unequal; otherwise, false.
+ public static bool operator !=(HierarchyId hid1, HierarchyId hid2)
+ {
+ var sh1 = Unwrap(hid1);
+ var sh2 = Unwrap(hid2);
+
+ return sh1.IsNull != sh2.IsNull || sh1.CompareTo(sh2) != 0;
+ }
+
+ ///
+ /// Evaluates whether one node is less than another.
+ ///
+ /// The first node to compare.
+ /// The second node to compare.
+ /// True if is less than ; otherwise, false.
+ public static bool operator <(HierarchyId hid1, HierarchyId hid2)
+ {
+ var sh1 = Unwrap(hid1);
+ var sh2 = Unwrap(hid2);
+
+ return !sh1.IsNull && !sh2.IsNull && sh1.CompareTo(sh2) < 0;
+ }
+
+ ///
+ /// Evaluates whether one node is greater than another.
+ ///
+ /// The first node to compare.
+ /// The second node to compare.
+ /// True if is greater than ; otherwise, false.
+ public static bool operator >(HierarchyId hid1, HierarchyId hid2)
+ {
+ var sh1 = Unwrap(hid1);
+ var sh2 = Unwrap(hid2);
+
+ return !sh1.IsNull && !sh2.IsNull && sh1.CompareTo(sh2) > 0;
+ }
+
+ ///
+ /// Evaluates whether one node is less than or equal to another.
+ ///
+ /// The first node to compare.
+ /// The second node to compare.
+ /// True if is less than or equal to ; otherwise, false.
+ public static bool operator <=(HierarchyId hid1, HierarchyId hid2)
+ {
+ var sh1 = Unwrap(hid1);
+ var sh2 = Unwrap(hid2);
+
+ return !sh1.IsNull && !sh2.IsNull && sh1.CompareTo(sh2) <= 0;
+ }
+
+ ///
+ /// Evaluates whether one node is greater than or equal to another.
+ ///
+ /// The first node to compare.
+ /// The second node to compare.
+ /// True if is greater than or equal to ; otherwise, false.
+ public static bool operator >=(HierarchyId hid1, HierarchyId hid2)
+ {
+ var sh1 = Unwrap(hid1);
+ var sh2 = Unwrap(hid2);
+
+ return !sh1.IsNull && !sh2.IsNull && sh1.CompareTo(sh2) >= 0;
+ }
+
+ private static SqlHierarchyId Unwrap(HierarchyId value)
+ => value?._value ?? SqlHierarchyId.Null;
+
+ private static HierarchyId Wrap(SqlHierarchyId value)
+ => value.IsNull ? null : new HierarchyId(value);
+ }
+}
\ No newline at end of file
diff --git a/src/EFCore.SqlServer.HierarchyId/Design/SqlServerHierarchyIdDesignTimeServices.cs b/src/EFCore.SqlServer.HierarchyId/Design/SqlServerHierarchyIdDesignTimeServices.cs
new file mode 100644
index 00000000000..9cfe4fbb710
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Design/SqlServerHierarchyIdDesignTimeServices.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.EntityFrameworkCore.Scaffolding;
+using Microsoft.EntityFrameworkCore.SqlServer.Scaffolding;
+using Microsoft.EntityFrameworkCore.SqlServer.Storage;
+using Microsoft.EntityFrameworkCore.Storage;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Design
+{
+ ///
+ /// Enables configuring design-time services. Tools will automatically discover implementations of this
+ /// interface that are in the startup assembly.
+ ///
+ public class SqlServerHierarchyIdDesignTimeServices : IDesignTimeServices
+ {
+ ///
+ /// Configures design-time services. Use this method to override the default design-time services with your
+ /// own implementations.
+ ///
+ /// The design-time service collection.
+ public virtual void ConfigureDesignTimeServices(IServiceCollection serviceCollection)
+ {
+ serviceCollection
+ .AddSingleton()
+ .AddSingleton();
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer.HierarchyId/EFCore.SqlServer.HierarchyId.csproj b/src/EFCore.SqlServer.HierarchyId/EFCore.SqlServer.HierarchyId.csproj
new file mode 100644
index 00000000000..5fe27346456
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/EFCore.SqlServer.HierarchyId.csproj
@@ -0,0 +1,42 @@
+
+
+
+ net6.0
+ EntityFrameworkCore.SqlServer.HierarchyId
+ Microsoft.EntityFrameworkCore.SqlServer
+ Adds hierarchyid support to the SQL Server EF Core provider
+ true
+ true
+
+
+
+
+ True
+ build
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
diff --git a/src/EFCore.SqlServer.HierarchyId/Extensions/SqlServerHierarchyIdDbContextOptionsBuilderExtensions.cs b/src/EFCore.SqlServer.HierarchyId/Extensions/SqlServerHierarchyIdDbContextOptionsBuilderExtensions.cs
new file mode 100644
index 00000000000..d4f926e8623
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Extensions/SqlServerHierarchyIdDbContextOptionsBuilderExtensions.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure;
+
+namespace Microsoft.EntityFrameworkCore
+{
+ ///
+ /// HierarchyId specific extension methods for .
+ ///
+ public static class SqlServerHierarchyIdDbContextOptionsBuilderExtensions
+ {
+ ///
+ /// Enable HierarchyId mappings.
+ ///
+ /// The builder being used to configure SQL Server.
+ /// The options builder so that further configuration can be chained.
+ public static SqlServerDbContextOptionsBuilder UseHierarchyId(
+ this SqlServerDbContextOptionsBuilder optionsBuilder)
+ {
+ var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure)optionsBuilder).OptionsBuilder;
+
+ var extension = coreOptionsBuilder.Options.FindExtension()
+ ?? new SqlServerHierarchyIdOptionsExtension();
+
+ ((IDbContextOptionsBuilderInfrastructure)coreOptionsBuilder).AddOrUpdateExtension(extension);
+
+ return optionsBuilder;
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer.HierarchyId/Extensions/SqlServerHierarchyIdServiceCollectionExtensions.cs b/src/EFCore.SqlServer.HierarchyId/Extensions/SqlServerHierarchyIdServiceCollectionExtensions.cs
new file mode 100644
index 00000000000..77df6399117
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Extensions/SqlServerHierarchyIdServiceCollectionExtensions.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Query;
+using Microsoft.EntityFrameworkCore.SqlServer.Query.ExpressionTranslators;
+using Microsoft.EntityFrameworkCore.SqlServer.Storage;
+using Microsoft.EntityFrameworkCore.Storage;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ ///
+ /// EntityFrameworkCore.SqlServer.HierarchyId extension methods for .
+ ///
+ public static class SqlServerHierarchyIdServiceCollectionExtensions
+ {
+ ///
+ /// Adds the services required for HierarchyId support in the SQL Server provider for Entity Framework.
+ ///
+ /// The to add services to.
+ /// The same service collection so that multiple calls can be chained.
+ public static IServiceCollection AddEntityFrameworkSqlServerHierarchyId(
+ this IServiceCollection serviceCollection)
+ {
+ new EntityFrameworkRelationalServicesBuilder(serviceCollection)
+ .TryAdd()
+ .TryAdd();
+
+ return serviceCollection;
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer.HierarchyId/Infrastructure/SqlServerHierarchyIdOptionsExtension.cs b/src/EFCore.SqlServer.HierarchyId/Infrastructure/SqlServerHierarchyIdOptionsExtension.cs
new file mode 100644
index 00000000000..1fdbd5265cc
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Infrastructure/SqlServerHierarchyIdOptionsExtension.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Query;
+using Microsoft.EntityFrameworkCore.SqlServer.Properties;
+using Microsoft.EntityFrameworkCore.SqlServer.Query.ExpressionTranslators;
+using Microsoft.EntityFrameworkCore.SqlServer.Storage;
+using Microsoft.EntityFrameworkCore.Storage;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Infrastructure
+{
+ internal class SqlServerHierarchyIdOptionsExtension : IDbContextOptionsExtension
+ {
+ private DbContextOptionsExtensionInfo _info;
+
+ public DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this);
+
+ public virtual void ApplyServices(IServiceCollection services)
+ {
+ services.AddEntityFrameworkSqlServerHierarchyId();
+ }
+
+ public virtual void Validate(IDbContextOptions options)
+ {
+ var internalServiceProvider = options.FindExtension()?.InternalServiceProvider;
+ if (internalServiceProvider != null)
+ {
+ using (var scope = internalServiceProvider.CreateScope())
+ {
+ if (scope.ServiceProvider.GetService>()
+ ?.Any(s => s is SqlServerHierarchyIdMethodCallTranslatorPlugin) != true ||
+ scope.ServiceProvider.GetService>()
+ ?.Any(s => s is SqlServerHierarchyIdTypeMappingSourcePlugin) != true)
+ {
+ throw new InvalidOperationException(Resources.ServicesMissing);
+ }
+ }
+ }
+ }
+
+ private sealed class ExtensionInfo : DbContextOptionsExtensionInfo
+ {
+ public ExtensionInfo(IDbContextOptionsExtension extension)
+ : base(extension)
+ {
+ }
+
+ private new SqlServerHierarchyIdOptionsExtension Extension
+ => (SqlServerHierarchyIdOptionsExtension)base.Extension;
+
+ public override bool IsDatabaseProvider => false;
+
+ public override int GetServiceProviderHashCode() => 0;
+
+ public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other)
+ => other is ExtensionInfo;
+
+ public override void PopulateDebugInfo(IDictionary debugInfo)
+ => debugInfo["SqlServer:" + nameof(SqlServerHierarchyIdDbContextOptionsBuilderExtensions.UseHierarchyId)] = "1";
+
+ public override string LogFragment => "using HierarchyId ";
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer.HierarchyId/Properties/InternalsVisibleTo.cs b/src/EFCore.SqlServer.HierarchyId/Properties/InternalsVisibleTo.cs
new file mode 100644
index 00000000000..5fd03cebc42
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Properties/InternalsVisibleTo.cs
@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo(
+ "EntityFrameworkCore.SqlServer.HierarchyId.Test, PublicKey=00240000048000009400000006020000002400005253413100040000010001006d92138307b5e251bf4918cf751dc83489f4b2d70ac15b04110fd1f78491fe93719d0cd464d103a95fb1c3b1cb21ce0033c94c6f52b325d36360736dea7571bd1074cb2c937cf4fc54526ceb44271c4f44753dbeb5d9b364e4dc57a8988542d3a7edb6575bc35ce7670612bd8f00f2c6899f3e74bd563810fa45f4c5c8b51cd3")]
diff --git a/src/EFCore.SqlServer.HierarchyId/Properties/Resources.Designer.cs b/src/EFCore.SqlServer.HierarchyId/Properties/Resources.Designer.cs
new file mode 100644
index 00000000000..2dd653ff2b1
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Properties/Resources.Designer.cs
@@ -0,0 +1,72 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.EntityFrameworkCore.SqlServer.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to UseHierarchyId requires AddEntityFrameworkSqlServerHierarchyId to be called on the internal service provider used..
+ ///
+ internal static string ServicesMissing {
+ get {
+ return ResourceManager.GetString("ServicesMissing", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer.HierarchyId/Properties/Resources.resx b/src/EFCore.SqlServer.HierarchyId/Properties/Resources.resx
new file mode 100644
index 00000000000..3f088ff9ecd
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Properties/Resources.resx
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ UseHierarchyId requires AddEntityFrameworkSqlServerHierarchyId to be called on the internal service provider used.
+
+
\ No newline at end of file
diff --git a/src/EFCore.SqlServer.HierarchyId/Query/ExpressionTranslators/SqlServerHierarchyIdMethodCallTranslatorPlugin.cs b/src/EFCore.SqlServer.HierarchyId/Query/ExpressionTranslators/SqlServerHierarchyIdMethodCallTranslatorPlugin.cs
new file mode 100644
index 00000000000..92ff81eaefa
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Query/ExpressionTranslators/SqlServerHierarchyIdMethodCallTranslatorPlugin.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore.Query;
+using Microsoft.EntityFrameworkCore.Storage;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Query.ExpressionTranslators
+{
+ internal class SqlServerHierarchyIdMethodCallTranslatorPlugin : IMethodCallTranslatorPlugin
+ {
+ public SqlServerHierarchyIdMethodCallTranslatorPlugin(
+ IRelationalTypeMappingSource typeMappingSource,
+ ISqlExpressionFactory sqlExpressionFactory)
+ {
+ Translators = new IMethodCallTranslator[]
+ {
+ new SqlServerHierarchyIdMethodTranslator(typeMappingSource, sqlExpressionFactory)
+ };
+ }
+
+ public virtual IEnumerable Translators { get; }
+ }
+}
diff --git a/src/EFCore.SqlServer.HierarchyId/Query/ExpressionTranslators/SqlServerHierarchyIdMethodTranslator.cs b/src/EFCore.SqlServer.HierarchyId/Query/ExpressionTranslators/SqlServerHierarchyIdMethodTranslator.cs
new file mode 100644
index 00000000000..0b2af7439a6
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Query/ExpressionTranslators/SqlServerHierarchyIdMethodTranslator.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.EntityFrameworkCore.Query;
+using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+using Microsoft.EntityFrameworkCore.SqlServer.Storage;
+using Microsoft.EntityFrameworkCore.Storage;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Query.ExpressionTranslators
+{
+ internal class SqlServerHierarchyIdMethodTranslator : IMethodCallTranslator
+ {
+ private static readonly IDictionary _methodToFunctionName = new Dictionary
+ {
+ // instance methods
+ { typeof(HierarchyId).GetRuntimeMethod(nameof(HierarchyId.GetAncestor), new[] { typeof(int) }), "GetAncestor" },
+ { typeof(HierarchyId).GetRuntimeMethod(nameof(HierarchyId.GetDescendant), new[] { typeof(HierarchyId), typeof(HierarchyId) }), "GetDescendant" },
+ { typeof(HierarchyId).GetRuntimeMethod(nameof(HierarchyId.GetLevel), Type.EmptyTypes), "GetLevel" },
+ { typeof(HierarchyId).GetRuntimeMethod(nameof(HierarchyId.GetReparentedValue), new[] { typeof(HierarchyId), typeof(HierarchyId) }), "GetReparentedValue" },
+ { typeof(HierarchyId).GetRuntimeMethod(nameof(HierarchyId.IsDescendantOf), new[] { typeof(HierarchyId) }), "IsDescendantOf" },
+ { typeof(object).GetRuntimeMethod(nameof(HierarchyId.ToString), Type.EmptyTypes), "ToString" },
+
+ // static methods
+ { typeof(HierarchyId).GetRuntimeMethod(nameof(HierarchyId.GetRoot), Type.EmptyTypes), "hierarchyid::GetRoot" },
+ { typeof(HierarchyId).GetRuntimeMethod(nameof(HierarchyId.Parse), new[] { typeof(string) }), "hierarchyid::Parse" },
+ };
+
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
+
+ public SqlServerHierarchyIdMethodTranslator(
+ IRelationalTypeMappingSource typeMappingSource,
+ ISqlExpressionFactory sqlExpressionFactory)
+ {
+ _typeMappingSource = typeMappingSource;
+ _sqlExpressionFactory = sqlExpressionFactory;
+ }
+
+ public SqlExpression Translate(
+ SqlExpression instance,
+ MethodInfo method,
+ IReadOnlyList arguments,
+ IDiagnosticsLogger logger)
+ {
+ // instance is null for static methods like Parse
+ const string storeType = SqlServerHierarchyIdTypeMappingSourcePlugin.SqlServerTypeName;
+ var callingType = instance?.Type ?? method.DeclaringType;
+ if (typeof(HierarchyId).IsAssignableFrom(callingType)
+ && _methodToFunctionName.TryGetValue(method, out var functionName))
+ {
+ var typeMappedArguments = new List();
+ foreach (var argument in arguments)
+ {
+ var argumentTypeMapping = typeof(HierarchyId).IsAssignableFrom(argument.Type)
+ ? _typeMappingSource.FindMapping(argument.Type, storeType)
+ : _typeMappingSource.FindMapping(argument.Type);
+ var mappedArgument = _sqlExpressionFactory.ApplyTypeMapping(argument, argumentTypeMapping);
+ typeMappedArguments.Add(mappedArgument);
+ }
+
+ var resultTypeMapping = typeof(HierarchyId).IsAssignableFrom(method.ReturnType)
+ ? _typeMappingSource.FindMapping(method.ReturnType, storeType)
+ : _typeMappingSource.FindMapping(method.ReturnType);
+
+
+ if (instance != null)
+ {
+ var instanceMapping = _typeMappingSource.FindMapping(instance.Type, storeType);
+ instance = _sqlExpressionFactory.ApplyTypeMapping(instance, instanceMapping);
+
+ return _sqlExpressionFactory.Function(
+ instance,
+ functionName,
+ simplify(arguments),
+ nullable: true,
+ instancePropagatesNullability: true,
+ argumentsPropagateNullability: arguments.Select(a => true),
+ method.ReturnType,
+ resultTypeMapping);
+ }
+
+ return _sqlExpressionFactory.Function(
+ functionName,
+ simplify(arguments),
+ nullable: true,
+ argumentsPropagateNullability: arguments.Select(a => true),
+ method.ReturnType,
+ resultTypeMapping);
+ }
+
+ return null;
+ }
+
+ private IEnumerable simplify(IEnumerable arguments)
+ {
+ foreach (var argument in arguments)
+ {
+ if (argument is SqlConstantExpression constant
+ && constant.Value is HierarchyId hierarchyId)
+ {
+ yield return _sqlExpressionFactory.Fragment($"'{hierarchyId}'");
+ }
+ else
+ {
+ yield return argument;
+ }
+ }
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer.HierarchyId/Scaffolding/SqlServerHierarchyIdCodeGeneratorPlugin.cs b/src/EFCore.SqlServer.HierarchyId/Scaffolding/SqlServerHierarchyIdCodeGeneratorPlugin.cs
new file mode 100644
index 00000000000..899f13290b5
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Scaffolding/SqlServerHierarchyIdCodeGeneratorPlugin.cs
@@ -0,0 +1,18 @@
+using System.Reflection;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Scaffolding;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Scaffolding
+{
+ internal class SqlServerHierarchyIdCodeGeneratorPlugin : ProviderCodeGeneratorPlugin
+ {
+ public override MethodCallCodeFragment GenerateProviderOptions()
+ {
+ return new MethodCallCodeFragment(
+ typeof(SqlServerHierarchyIdDbContextOptionsBuilderExtensions).GetRuntimeMethod(
+ nameof(SqlServerHierarchyIdDbContextOptionsBuilderExtensions.UseHierarchyId),
+ new[] { typeof(SqlServerDbContextOptionsBuilder) }));
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer.HierarchyId/Storage/SqlServerHierarchyIdTypeMapping.cs b/src/EFCore.SqlServer.HierarchyId/Storage/SqlServerHierarchyIdTypeMapping.cs
new file mode 100644
index 00000000000..4c08f5d2264
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Storage/SqlServerHierarchyIdTypeMapping.cs
@@ -0,0 +1,155 @@
+using System;
+using System.Data;
+using System.Data.Common;
+using System.Data.SqlTypes;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Threading;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore.Query;
+using Microsoft.EntityFrameworkCore.Storage;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Storage
+{
+ internal class SqlServerHierarchyIdTypeMapping : RelationalTypeMapping
+ {
+ private static readonly MethodInfo _getSqlBytes
+ = typeof(SqlDataReader).GetRuntimeMethod(nameof(SqlDataReader.GetSqlBytes), new[] { typeof(int) });
+
+ private static readonly MethodInfo _parseHierarchyId
+ = typeof(HierarchyId).GetRuntimeMethod(nameof(HierarchyId.Parse), new[] { typeof(string) });
+
+ private static readonly SqlServerHierarchyIdValueConverter _valueConverter = new SqlServerHierarchyIdValueConverter();
+
+ private static Action _sqlDbTypeSetter;
+ private static Action _udtTypeNameSetter;
+
+ public SqlServerHierarchyIdTypeMapping(string storeType, Type clrType)
+ : base(CreateRelationalTypeMappingParameters(storeType, clrType))
+ {
+ }
+
+ private static RelationalTypeMappingParameters CreateRelationalTypeMappingParameters(string storeType, Type clrType)
+ {
+ return new RelationalTypeMappingParameters(
+ new CoreTypeMappingParameters(
+ clrType: clrType,
+ converter: null //this gets the generatecodeliteral to run
+ ),
+ storeType);
+ }
+
+ // needed to implement Clone
+ protected SqlServerHierarchyIdTypeMapping(RelationalTypeMappingParameters parameters)
+ : base(parameters)
+ {
+ }
+
+ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
+ {
+ return new SqlServerHierarchyIdTypeMapping(parameters);
+ }
+
+ protected override void ConfigureParameter(DbParameter parameter)
+ {
+ var type = parameter.GetType();
+ LazyInitializer.EnsureInitialized(ref _sqlDbTypeSetter, () => CreateSqlDbTypeAccessor(type));
+ LazyInitializer.EnsureInitialized(ref _udtTypeNameSetter, () => CreateUdtTypeNameAccessor(type));
+
+ if (parameter.Value == DBNull.Value)
+ {
+ parameter.Value = SqlBytes.Null;
+ }
+
+ _sqlDbTypeSetter(parameter, SqlDbType.Udt);
+ _udtTypeNameSetter(parameter, StoreType);
+ }
+
+ public override MethodInfo GetDataReaderMethod()
+ {
+ return _getSqlBytes;
+ }
+
+ public override Expression GenerateCodeLiteral(object value)
+ {
+ return Expression.Call(
+ _parseHierarchyId,
+ Expression.Constant(value.ToString(), typeof(string))
+ );
+ }
+
+ protected override string GenerateNonNullSqlLiteral(object value)
+ {
+ //this appears to only be called when using the update-database
+ //command, and the value is already a hierarchyid
+ return $"'{value}'";
+ }
+
+ public override DbParameter CreateParameter(DbCommand command, string name, object value, bool? nullable = null, ParameterDirection direction = ParameterDirection.Input)
+ {
+ var parameter = command.CreateParameter();
+ parameter.Direction = ParameterDirection.Input;
+ parameter.ParameterName = name;
+
+ if (Converter != null)
+ {
+ value = Converter.ConvertToProvider(value);
+ }
+
+ parameter.Value = value is null
+ ? DBNull.Value
+ : _valueConverter.ConvertToProvider(value);
+
+ if (nullable.HasValue)
+ {
+ parameter.IsNullable = nullable.Value;
+ }
+
+ ConfigureParameter(parameter);
+
+ return parameter;
+ }
+
+ public override Expression CustomizeDataReaderExpression(Expression expression)
+ {
+ if (expression.Type != _valueConverter.ProviderClrType)
+ {
+ expression = Expression.Convert(expression, _valueConverter.ProviderClrType);
+ }
+
+ return ReplacingExpressionVisitor.Replace(
+ _valueConverter.ConvertFromProviderExpression.Parameters.Single(),
+ expression,
+ _valueConverter.ConvertFromProviderExpression.Body);
+ }
+
+ private static Action CreateSqlDbTypeAccessor(Type paramType)
+ {
+ var paramParam = Expression.Parameter(typeof(DbParameter), "parameter");
+ var valueParam = Expression.Parameter(typeof(SqlDbType), "value");
+
+ return Expression.Lambda>(
+ Expression.Call(
+ Expression.Convert(paramParam, paramType),
+ paramType.GetProperty("SqlDbType").SetMethod,
+ valueParam),
+ paramParam,
+ valueParam).Compile();
+ }
+
+ private static Action CreateUdtTypeNameAccessor(Type paramType)
+ {
+ var paramParam = Expression.Parameter(typeof(DbParameter), "parameter");
+ var valueParam = Expression.Parameter(typeof(string), "value");
+
+ return Expression.Lambda>(
+ Expression.Call(
+ Expression.Convert(paramParam, paramType),
+ paramType.GetProperty("UdtTypeName").SetMethod,
+ valueParam),
+ paramParam,
+ valueParam).Compile();
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer.HierarchyId/Storage/SqlServerHierarchyIdTypeMappingSourcePlugin.cs b/src/EFCore.SqlServer.HierarchyId/Storage/SqlServerHierarchyIdTypeMappingSourcePlugin.cs
new file mode 100644
index 00000000000..10e54dd2189
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Storage/SqlServerHierarchyIdTypeMappingSourcePlugin.cs
@@ -0,0 +1,21 @@
+using System;
+using Microsoft.EntityFrameworkCore.Storage;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Storage
+{
+ internal class SqlServerHierarchyIdTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin
+ {
+ public const string SqlServerTypeName = "hierarchyid";
+
+ public virtual RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo)
+ {
+ var clrType = mappingInfo.ClrType;
+ var storeTypeName = mappingInfo.StoreTypeName;
+
+ return typeof(HierarchyId).IsAssignableFrom(clrType)
+ || SqlServerTypeName.Equals(storeTypeName, StringComparison.OrdinalIgnoreCase)
+ ? new SqlServerHierarchyIdTypeMapping(SqlServerTypeName, clrType ?? typeof(HierarchyId))
+ : null;
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer.HierarchyId/Storage/SqlServerHierarchyIdValueConverter.cs b/src/EFCore.SqlServer.HierarchyId/Storage/SqlServerHierarchyIdValueConverter.cs
new file mode 100644
index 00000000000..cd5593c4f72
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/Storage/SqlServerHierarchyIdValueConverter.cs
@@ -0,0 +1,33 @@
+using System.Data.SqlTypes;
+using System.IO;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Storage
+{
+ internal class SqlServerHierarchyIdValueConverter : ValueConverter
+ {
+ public SqlServerHierarchyIdValueConverter()
+ : base(h => toProvider(h), b => fromProvider(b))
+ {
+ }
+
+ private static SqlBytes toProvider(HierarchyId hid)
+ {
+ using (var memory = new MemoryStream())
+ using (var writer = new BinaryWriter(memory))
+ {
+ hid.Write(writer);
+ return new SqlBytes(memory.ToArray());
+ }
+ }
+
+ private static HierarchyId fromProvider(SqlBytes bytes)
+ {
+ using (var memory = new MemoryStream(bytes.Value))
+ using (var reader = new BinaryReader(memory))
+ {
+ return HierarchyId.Read(reader);
+ }
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer.HierarchyId/build/net6.0/EntityFrameworkCore.SqlServer.HierarchyId.targets b/src/EFCore.SqlServer.HierarchyId/build/net6.0/EntityFrameworkCore.SqlServer.HierarchyId.targets
new file mode 100644
index 00000000000..09f8c908850
--- /dev/null
+++ b/src/EFCore.SqlServer.HierarchyId/build/net6.0/EntityFrameworkCore.SqlServer.HierarchyId.targets
@@ -0,0 +1,46 @@
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ $(IntermediateOutputPath)EFCoreSqlServerHierarchyId$(DefaultLanguageSourceExtension)
+
+
+
+
+
+
+ CompileBefore
+
+
+
+
+ CompileAfter
+
+
+
+
+
+
+ Compile
+
+
+
+
+
+
+ <_Parameter1>Microsoft.EntityFrameworkCore.SqlServer.Design.SqlServerHierarchyIdDesignTimeServices, EntityFrameworkCore.SqlServer.HierarchyId
+ <_Parameter2>Microsoft.EntityFrameworkCore.SqlServer
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/CSharpDbContextGeneratorTest.cs b/test/EFCore.SqlServer.HierarchyId.Tests/CSharpDbContextGeneratorTest.cs
new file mode 100644
index 00000000000..dbb660d1f41
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/CSharpDbContextGeneratorTest.cs
@@ -0,0 +1,62 @@
+using System;
+using Microsoft.EntityFrameworkCore.Internal;
+using Microsoft.EntityFrameworkCore.Scaffolding;
+using Microsoft.EntityFrameworkCore.SqlServer.Internal;
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer;
+
+public class CSharpDbContextGeneratorTest : ModelCodeGeneratorTestBase
+{
+ [ConditionalFact]
+ public void Generates_context_with_UseHierarchyId()
+ => Test(
+ modelBuilder =>
+ {
+ modelBuilder.Entity(
+ "Patriarch",
+ b =>
+ {
+ b.Property("Id");
+ b.HasKey("Id");
+ b.Property("Name");
+ });
+ },
+ new ModelCodeGenerationOptions {UseDataAnnotations = false},
+ code =>
+ {
+ AssertFileContents(
+ @"using System;
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore;
+
+namespace TestNamespace;
+
+public partial class TestDbContext : DbContext
+{
+ public TestDbContext()
+ {
+ }
+
+ public TestDbContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ public virtual DbSet Patriarch { get; set; }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
+ => optionsBuilder.UseSqlServer(""Initial Catalog=TestDatabase"", x => x.UseHierarchyId());
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ OnModelCreatingPartial(modelBuilder);
+ }
+
+ partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
+}
+",
+ code.ContextFile);
+ });
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/CSharpEntityTypeGeneratorTest.cs b/test/EFCore.SqlServer.HierarchyId.Tests/CSharpEntityTypeGeneratorTest.cs
new file mode 100644
index 00000000000..4dd7005a6ca
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/CSharpEntityTypeGeneratorTest.cs
@@ -0,0 +1,125 @@
+using System.Linq;
+using Microsoft.EntityFrameworkCore.Scaffolding;
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer;
+
+public class CSharpEntityTypeGeneratorTest : ModelCodeGeneratorTestBase
+{
+ [ConditionalFact]
+ public void Class_with_HierarchyId_key_is_generated()
+ => Test(
+ modelBuilder =>
+ {
+ modelBuilder.Entity(
+ "Patriarch",
+ b =>
+ {
+ b.Property("Id");
+ b.HasKey("Id");
+ b.Property("Name");
+ });
+ },
+ new ModelCodeGenerationOptions { UseDataAnnotations = true },
+ code =>
+ {
+ AssertFileContents(
+ @"using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+
+namespace TestNamespace;
+
+public partial class Patriarch
+{
+ [Key]
+ public HierarchyId Id { get; set; }
+
+ public string Name { get; set; }
+}
+",
+ code.AdditionalFiles.Single(f => f.Path == "Patriarch.cs"));
+ });
+
+ [ConditionalFact]
+ public void Class_with_HierarchyId_property_is_generated()
+ => Test(
+ modelBuilder =>
+ {
+ modelBuilder.Entity(
+ "Patriarch",
+ b =>
+ {
+ b.Property("Id");
+ b.HasKey("Id");
+ b.Property("Name");
+ b.Property("Hierarchy");
+ });
+ },
+ new ModelCodeGenerationOptions { UseDataAnnotations = true },
+ code =>
+ {
+ AssertFileContents(
+ @"using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+
+namespace TestNamespace;
+
+public partial class Patriarch
+{
+ [Key]
+ public int Id { get; set; }
+
+ public HierarchyId Hierarchy { get; set; }
+
+ public string Name { get; set; }
+}
+",
+ code.AdditionalFiles.Single(f => f.Path == "Patriarch.cs"));
+ });
+
+ [ConditionalFact]
+ public void Class_with_multiple_HierarchyId_properties_are_generated()
+ => Test(
+ modelBuilder =>
+ {
+ modelBuilder.Entity(
+ "Patriarch",
+ b =>
+ {
+ b.Property("Id");
+ b.HasKey("Id");
+ b.Property("Name");
+ b.Property("Hierarchy");
+ });
+ },
+ new ModelCodeGenerationOptions { UseDataAnnotations = true },
+ code =>
+ {
+ AssertFileContents(
+ @"using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+
+namespace TestNamespace;
+
+public partial class Patriarch
+{
+ [Key]
+ public HierarchyId Id { get; set; }
+
+ public HierarchyId Hierarchy { get; set; }
+
+ public string Name { get; set; }
+}
+",
+ code.AdditionalFiles.Single(f => f.Path == "Patriarch.cs"));
+ });
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/DesignTimeServicesTests.cs b/test/EFCore.SqlServer.HierarchyId.Tests/DesignTimeServicesTests.cs
new file mode 100644
index 00000000000..65cd1c3d3a2
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/DesignTimeServicesTests.cs
@@ -0,0 +1,24 @@
+using Microsoft.EntityFrameworkCore.Scaffolding;
+using Microsoft.EntityFrameworkCore.SqlServer.Design;
+using Microsoft.EntityFrameworkCore.SqlServer.Scaffolding;
+using Microsoft.EntityFrameworkCore.SqlServer.Storage;
+using Microsoft.EntityFrameworkCore.Storage;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer
+{
+ public class DesignTimeServicesTests
+ {
+ [Fact]
+ public void ConfigureDesignTimeServices_works()
+ {
+ var serviceCollection = new ServiceCollection();
+ new SqlServerHierarchyIdDesignTimeServices().ConfigureDesignTimeServices(serviceCollection);
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ Assert.IsType(serviceProvider.GetService());
+ Assert.IsType(serviceProvider.GetService());
+ }
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/EFCore.SqlServer.HierarchyId.Tests.csproj b/test/EFCore.SqlServer.HierarchyId.Tests/EFCore.SqlServer.HierarchyId.Tests.csproj
new file mode 100644
index 00000000000..9ac69ed5adb
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/EFCore.SqlServer.HierarchyId.Tests.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net7.0
+ false
+ EntityFrameworkCore.SqlServer.HierarchyId.Test
+ Microsoft.EntityFrameworkCore.SqlServer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/MigrationTests.cs b/test/EFCore.SqlServer.HierarchyId.Tests/MigrationTests.cs
new file mode 100644
index 00000000000..deddd3596e0
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/MigrationTests.cs
@@ -0,0 +1,63 @@
+using System;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.EntityFrameworkCore.Design.Internal;
+using Microsoft.EntityFrameworkCore.Migrations.Design;
+using Microsoft.EntityFrameworkCore.SqlServer.Test.Models.Migrations;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer
+{
+ public class MigrationTests
+ {
+ private delegate string MigrationCodeGetter(string migrationName, string rootNamespace);
+ private delegate string SnapshotCodeGetter(string rootNamespace);
+
+ [Fact]
+ public void Migration_and_snapshot_generate_with_typed_array()
+ {
+ using var db = new TypedArraySeedContext();
+ ValidateMigrationAndSnapshotCode(db, db.GetExpectedMigrationCode, db.GetExpectedSnapshotCode);
+ }
+
+ [Fact]
+ public void Migration_and_snapshot_generate_with_anonymous_array()
+ {
+ using var db = new AnonymousArraySeedContext();
+ ValidateMigrationAndSnapshotCode(db, db.GetExpectedMigrationCode, db.GetExpectedSnapshotCode);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Uses internal efcore apis")]
+ private static void ValidateMigrationAndSnapshotCode(
+ DbContext context,
+ MigrationCodeGetter migrationCodeGetter,
+ SnapshotCodeGetter snapshotCodeGetter)
+ {
+ const string migrationName = "MyMigration";
+ const string rootNamespace = "MyApp.Data";
+
+ var expectedMigration = migrationCodeGetter(migrationName, rootNamespace);
+ var expectedSnapshot = snapshotCodeGetter(rootNamespace);
+
+ var reporter = new OperationReporter(
+ new OperationReportHandler(
+ m => Console.WriteLine($" error: {m}"),
+ m => Console.WriteLine($" warn: {m}"),
+ m => Console.WriteLine($" info: {m}"),
+ m => Console.WriteLine($"verbose: {m}")));
+
+ var assembly = System.Reflection.Assembly.GetExecutingAssembly();
+
+ //this works because we have placed the DesignTimeServicesReferenceAttribute
+ //in the test project's properties, which simulates
+ //the nuget package's build target
+ var migration = new DesignTimeServicesBuilder(assembly, assembly, reporter, Array.Empty())
+ .Build(context)
+ .GetRequiredService()
+ .ScaffoldMigration(migrationName, rootNamespace);
+
+ Assert.Equal(expectedMigration, migration.MigrationCode);
+ Assert.Equal(expectedSnapshot, migration.SnapshotCode);
+ }
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/ModelCodeGeneratorTestBase.cs b/test/EFCore.SqlServer.HierarchyId.Tests/ModelCodeGeneratorTestBase.cs
new file mode 100644
index 00000000000..c1d87785da7
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/ModelCodeGeneratorTestBase.cs
@@ -0,0 +1,66 @@
+using System;
+using Microsoft.EntityFrameworkCore.Design.Internal;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.Scaffolding;
+using Microsoft.EntityFrameworkCore.TestUtilities;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer;
+
+#pragma warning disable EF1001
+
+public abstract class ModelCodeGeneratorTestBase
+{
+ protected void Test(
+ Action buildModel,
+ ModelCodeGenerationOptions options,
+ Action assertScaffold)
+ {
+ var designServices = new ServiceCollection();
+ AddModelServices(designServices);
+
+ var modelBuilder = SqlServerTestHelpers.Instance.CreateConventionBuilder(customServices: designServices);
+ modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion);
+ buildModel(modelBuilder);
+
+ var model = modelBuilder.FinalizeModel(designTime: true, skipValidation: true);
+
+ var services = CreateServices();
+ AddScaffoldingServices(services);
+
+ var generator = services.BuildServiceProvider(validateScopes: true)
+ .GetRequiredService();
+
+ options.ModelNamespace ??= "TestNamespace";
+ options.ContextName = "TestDbContext";
+ options.ConnectionString = "Initial Catalog=TestDatabase";
+
+ var scaffoldedModel = generator.GenerateModel(
+ model,
+ options);
+ assertScaffold(scaffoldedModel);
+ }
+
+ private static IServiceCollection CreateServices()
+ {
+ var testAssembly = typeof(ModelCodeGeneratorTestBase).Assembly;
+ var reporter = new TestOperationReporter();
+ var services = new DesignTimeServicesBuilder(testAssembly, testAssembly, reporter, new string[0])
+ .CreateServiceCollection("Microsoft.EntityFrameworkCore.SqlServer");
+ return services;
+ }
+
+ protected virtual void AddModelServices(IServiceCollection services)
+ {
+ }
+
+ protected virtual void AddScaffoldingServices(IServiceCollection services)
+ {
+ }
+
+ protected static void AssertFileContents(
+ string expectedCode,
+ ScaffoldedFile file)
+ => Assert.Equal(expectedCode, file.Code, ignoreLineEndingDifferences: true);
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/NullabilityTests.cs b/test/EFCore.SqlServer.HierarchyId.Tests/NullabilityTests.cs
new file mode 100644
index 00000000000..dc717930a68
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/NullabilityTests.cs
@@ -0,0 +1,80 @@
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer
+{
+ public class NullabilityTests
+ {
+ [Fact]
+ public void Null_against_null()
+ {
+ Assert.True((HierarchyId)null == (HierarchyId)null);
+ Assert.False((HierarchyId)null != (HierarchyId)null);
+ Assert.False((HierarchyId)null > (HierarchyId)null);
+ Assert.False((HierarchyId)null >= (HierarchyId)null);
+ Assert.False((HierarchyId)null < (HierarchyId)null);
+ Assert.False((HierarchyId)null <= (HierarchyId)null);
+ }
+
+ [Fact]
+ public void Null_against_nonNull()
+ {
+ var hid = HierarchyId.GetRoot();
+ Assert.False(hid == (HierarchyId)null);
+ Assert.False((HierarchyId)null == hid);
+
+ Assert.True(hid != (HierarchyId)null);
+ Assert.True((HierarchyId)null != hid);
+
+ Assert.False(hid > (HierarchyId)null);
+ Assert.False((HierarchyId)null > hid);
+
+ Assert.False(hid >= (HierarchyId)null);
+ Assert.False((HierarchyId)null >= hid);
+
+ Assert.False(hid < (HierarchyId)null);
+ Assert.False((HierarchyId)null < hid);
+
+ Assert.False(hid <= (HierarchyId)null);
+ Assert.False((HierarchyId)null <= hid);
+ }
+
+ [Fact]
+ public void NullOnly_aggregates_equalTo_null()
+ {
+ var hid = (HierarchyId)null;
+ var collection = new[] { (HierarchyId)null, (HierarchyId)null, };
+ var min = collection.Min();
+ var max = collection.Max();
+
+ Assert.True(hid == min);
+ Assert.True(min == hid);
+ Assert.False(hid != min);
+ Assert.False(min != hid);
+
+ Assert.True(hid == max);
+ Assert.True(max == hid);
+ Assert.False(hid != max);
+ Assert.False(max != hid);
+ }
+
+ [Fact]
+ public void Aggregates_including_nulls_equalTo_nonNull()
+ {
+ var hid = HierarchyId.GetRoot();
+ var collection = new[] { (HierarchyId)null, (HierarchyId)null, HierarchyId.GetRoot(), HierarchyId.GetRoot(), };
+ var min = collection.Min();
+ var max = collection.Max();
+
+ Assert.True(hid == min);
+ Assert.True(min == hid);
+ Assert.False(hid != min);
+ Assert.False(min != hid);
+
+ Assert.True(hid == max);
+ Assert.True(max == hid);
+ Assert.False(hid != max);
+ Assert.False(max != hid);
+ }
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/Properties/EFCoreSqlServerHierarchyId.cs b/test/EFCore.SqlServer.HierarchyId.Tests/Properties/EFCoreSqlServerHierarchyId.cs
new file mode 100644
index 00000000000..4b04f36c8db
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/Properties/EFCoreSqlServerHierarchyId.cs
@@ -0,0 +1,18 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+using System;
+using System.Reflection;
+
+[assembly: Microsoft.EntityFrameworkCore.Design.DesignTimeServicesReferenceAttribute("Microsoft.EntityFrameworkCore.SqlServer.Design.SqlServerHierarchyIdDesignTimeServ" +
+ "ices, EntityFrameworkCore.SqlServer.HierarchyId", "Microsoft.EntityFrameworkCore.SqlServer")]
+
+// Generated by the MSBuild WriteCodeFragment class.
+
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/QueryTests.cs b/test/EFCore.SqlServer.HierarchyId.Tests/QueryTests.cs
new file mode 100644
index 00000000000..e07e3fa6676
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/QueryTests.cs
@@ -0,0 +1,343 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.EntityFrameworkCore.SqlServer.Test.Models;
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer
+{
+ public class QueryTests : IDisposable
+ {
+ private readonly AbrahamicContext _db;
+
+ public QueryTests()
+ {
+ _db = new AbrahamicContext();
+ _db.Database.EnsureDeleted();
+ _db.Database.EnsureCreated();
+ }
+
+ [Fact]
+ public void GetLevel_can_translate()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where p.Id.GetLevel() == 0
+ select p.Name);
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Name] FROM [Patriarchy] AS [p] WHERE [p].[Id].GetLevel() = CAST(0 AS smallint)"),
+ condense(_db.Sql));
+
+ Assert.Equal(new[] { "Abraham" }, results);
+ }
+
+ [Fact]
+ public void IsDescendantOf_can_translate()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where p.Id.GetLevel() == 3
+ select p.Id.IsDescendantOf(p.Id.GetAncestor(1)));
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Id].IsDescendantOf([p].[Id].GetAncestor(1)) FROM [Patriarchy] AS [p] WHERE [p].[Id].GetLevel() = CAST(3 AS smallint)"),
+ condense(_db.Sql));
+
+ Assert.All(results, b => Assert.True(b));
+ }
+
+ [Fact]
+ public void GetAncestor_0_can_translate()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where p.Id.GetLevel() == 0
+ select p.Id.GetAncestor(0));
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Id].GetAncestor(0) FROM [Patriarchy] AS [p] WHERE [p].[Id].GetLevel() = CAST(0 AS smallint)"),
+ condense(_db.Sql));
+
+ Assert.All(results, h => Assert.Equal(HierarchyId.GetRoot(), h));
+ }
+
+ [Fact]
+ public void GetAncestor_1_can_translate()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where p.Id.GetLevel() == 1
+ select p.Id.GetAncestor(1));
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Id].GetAncestor(1) FROM [Patriarchy] AS [p] WHERE [p].[Id].GetLevel() = CAST(1 AS smallint)"),
+ condense(_db.Sql));
+
+ Assert.All(results, h => Assert.Equal(HierarchyId.GetRoot(), h));
+ }
+
+ [Fact]
+ public void GetAncestor_2_can_translate()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where p.Id.GetLevel() == 2
+ select p.Id.GetAncestor(2));
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Id].GetAncestor(2) FROM [Patriarchy] AS [p] WHERE [p].[Id].GetLevel() = CAST(2 AS smallint)"),
+ condense(_db.Sql));
+
+ Assert.All(results, h => Assert.Equal(HierarchyId.GetRoot(), h));
+ }
+
+ [Fact]
+ public void GetAncestor_3_can_translate()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where p.Id.GetLevel() == 3
+ select p.Id.GetAncestor(3));
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Id].GetAncestor(3) FROM [Patriarchy] AS [p] WHERE [p].[Id].GetLevel() = CAST(3 AS smallint)"),
+ condense(_db.Sql));
+
+ Assert.All(results, h => Assert.Equal(HierarchyId.GetRoot(), h));
+ }
+
+ [Fact]
+ public void GetAncestor_of_root_returns_null()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where p.Id.GetLevel() == 0
+ select p.Id.GetAncestor(1));
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Id].GetAncestor(1) FROM [Patriarchy] AS [p] WHERE [p].[Id].GetLevel() = CAST(0 AS smallint)"),
+ condense(_db.Sql));
+
+ Assert.Equal(new HierarchyId[] { null }, results);
+ }
+
+ [Fact]
+ public void GetDescendent_can_translate()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where p.Id.GetLevel() == 0
+ select p.Id.GetDescendant(null, null));
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Id].GetDescendant(NULL, NULL) FROM [Patriarchy] AS [p] WHERE [p].[Id].GetLevel() = CAST(0 AS smallint)"),
+ condense(_db.Sql));
+
+ Assert.Equal(new[] { HierarchyId.Parse("/1/") }, results);
+ }
+
+ [Fact]
+ public void HierarchyId_can_be_sent_as_parameter()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where p.Id == HierarchyId.Parse("/1/")
+ select p.Name);
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Name] FROM [Patriarchy] AS [p] WHERE [p].[Id] = '/1/'"),
+ condense(_db.Sql));
+
+ Assert.Equal(new[] { "Isaac" }, results);
+ }
+
+ [Fact]
+ public void Converted_HierarchyId_can_be_sent_as_parameter()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.ConvertedPatriarchy
+ where p.HierarchyId == HierarchyId.Parse("/1/").ToString()
+ select p.Name);
+
+ Assert.Equal(
+ condense(@"SELECT [c].[Name] FROM [ConvertedPatriarchy] AS [c] WHERE [c].[HierarchyId] = '/1/'"),
+ condense(_db.Sql));
+
+ Assert.Equal(new[] { "Isaac" }, results);
+ }
+
+ [Fact]
+ public void Can_insert_HierarchyId()
+ {
+ using (_db.Database.BeginTransaction())
+ {
+ var entities = new List
+ {
+ new() { Id = HierarchyId.Parse("/2/1/"), Name = "Thrór" },
+ new() { Id = HierarchyId.Parse("/2/2/"), Name = "Thráin II" },
+ new() { Id = HierarchyId.Parse("/3/"), Name = "Thorin Oakenshield" }
+ };
+
+ _db.AddRange(entities);
+ _db.SaveChanges();
+ _db.ChangeTracker.Clear();
+
+ var queried = _db.Patriarchy.Where(e => e.Name.StartsWith("Th")).OrderBy(e => e.Id).ToList();
+
+ Assert.Equal(3, queried.Count);
+
+ Assert.Equal(HierarchyId.Parse("/2/1/"), queried[0].Id);
+ Assert.Equal("Thrór", queried[0].Name);
+
+ Assert.Equal(HierarchyId.Parse("/2/2/"), queried[1].Id);
+ Assert.Equal("Thráin II", queried[1].Name);
+
+ Assert.Equal(HierarchyId.Parse("/3/"), queried[2].Id);
+ Assert.Equal("Thorin Oakenshield", queried[2].Name);
+ }
+ }
+
+ [Fact]
+ public void Can_insert_and_update_converted_HierarchyId()
+ {
+ using (_db.Database.BeginTransaction())
+ {
+ var entities = new List
+ {
+ new() { HierarchyId = HierarchyId.Parse("/2/1/").ToString(), Name = "Thrór" },
+ new() { HierarchyId = HierarchyId.Parse("/2/2/").ToString(), Name = "Thráin II" },
+ new() { HierarchyId = HierarchyId.Parse("/3/").ToString(), Name = "Thorin Oakenshield" }
+ };
+
+ _db.AddRange(entities);
+ _db.SaveChanges();
+ _db.ChangeTracker.Clear();
+
+ var queried = _db.ConvertedPatriarchy.Where(e => e.Name.StartsWith("Th")).OrderBy(e => e.Id).ToList();
+
+ Assert.Equal(3, queried.Count);
+
+ Assert.Equal(HierarchyId.Parse("/2/1/").ToString(), queried[0].HierarchyId);
+ Assert.Equal("Thrór", queried[0].Name);
+
+ Assert.Equal(HierarchyId.Parse("/2/2/").ToString(), queried[1].HierarchyId);
+ Assert.Equal("Thráin II", queried[1].Name);
+
+ Assert.Equal(HierarchyId.Parse("/3/").ToString(), queried[2].HierarchyId);
+ Assert.Equal("Thorin Oakenshield", queried[2].Name);
+
+ queried[2].HierarchyId = "/3/1/";
+
+ _db.SaveChanges();
+ _db.ChangeTracker.Clear();
+
+ queried = _db.ConvertedPatriarchy.Where(e => e.Name.StartsWith("Th")).OrderBy(e => e.Id).ToList();
+
+ Assert.Equal(3, queried.Count);
+
+ Assert.Equal(HierarchyId.Parse("/2/1/").ToString(), queried[0].HierarchyId);
+ Assert.Equal("Thrór", queried[0].Name);
+
+ Assert.Equal(HierarchyId.Parse("/2/2/").ToString(), queried[1].HierarchyId);
+ Assert.Equal("Thráin II", queried[1].Name);
+
+ Assert.Equal(HierarchyId.Parse("/3/1/").ToString(), queried[2].HierarchyId);
+ Assert.Equal("Thorin Oakenshield", queried[2].Name);
+ }
+ }
+
+ [Fact]
+ public void HierarchyId_get_ancestor_of_level_is_root()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where p.Id.GetAncestor(p.Id.GetLevel()) == HierarchyId.GetRoot() // HierarchyId.Parse("/1/") // HierarchyId.Parse(p.Id.ToString()).GetAncestor(HierarchyId.Parse(p.Id.ToString()).GetLevel())
+ select p.Name);
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Name] FROM [Patriarchy] AS [p] WHERE [p].[Id].GetAncestor(CAST([p].[Id].GetLevel() AS int)) = '/'"),
+ condense(_db.Sql));
+
+ var all = Enumerable.ToList(
+ from p in _db.Patriarchy
+ select p.Name);
+
+ Assert.Equal(all, results);
+ }
+
+ [Fact]
+ public void HierarchyId_can_call_method_on_parameter()
+ {
+ var isaac = HierarchyId.Parse("/1/");
+
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where isaac.IsDescendantOf(p.Id)
+ select p.Name);
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Name] FROM [Patriarchy] AS [p] WHERE @__isaac_0.IsDescendantOf([p].[Id]) = CAST(1 AS bit)"),
+ condense(_db.Sql));
+
+ Assert.Equal(new[] { "Abraham", "Isaac" }, results);
+ }
+
+ [Fact]
+ public void ToString_can_translate()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where p.Id.GetLevel() == 1
+ select p.Id.ToString());
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Id].ToString() FROM [Patriarchy] AS [p] WHERE [p].[Id].GetLevel() = CAST(1 AS smallint)"),
+ condense(_db.Sql));
+
+ Assert.Equal(new[] { "/1/" }, results);
+ }
+
+ [Fact]
+ public void ToString_can_translate_redux()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where EF.Functions.Like(p.Id.ToString(), "%/1/")
+ select p.Name);
+
+ Assert.Equal(
+ condense(@"SELECT [p].[Name] FROM [Patriarchy] AS [p] WHERE [p].[Id].ToString() LIKE N'%/1/'"),
+ condense(_db.Sql));
+
+ Assert.Equal(new[] { "Isaac", "Jacob", "Reuben" }, results);
+ }
+
+ [Fact]
+ public void Parse_can_translate()
+ {
+ var results = Enumerable.ToList(
+ from p in _db.Patriarchy
+ where p.Id == HierarchyId.GetRoot()
+ select HierarchyId.Parse(p.Id.ToString()));
+
+ Assert.Equal(
+ condense(@"SELECT hierarchyid::Parse([p].[Id].ToString()) FROM [Patriarchy] AS [p] WHERE [p].[Id] = '/'"),
+ condense(_db.Sql));
+
+ Assert.Equal(new[] { HierarchyId.Parse("/") }, results);
+ }
+
+ public void Dispose()
+ {
+ _db.Dispose();
+ }
+
+ // replace whitespace with a single space
+ private static string condense(string str)
+ {
+ var split = str.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
+ return string.Join(" ", split);
+ }
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/RelationalScaffoldingModelFactoryTest.cs b/test/EFCore.SqlServer.HierarchyId.Tests/RelationalScaffoldingModelFactoryTest.cs
new file mode 100644
index 00000000000..987d6865a1e
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/RelationalScaffoldingModelFactoryTest.cs
@@ -0,0 +1,121 @@
+using Microsoft.EntityFrameworkCore.Design.Internal;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.Scaffolding;
+using Microsoft.EntityFrameworkCore.Scaffolding.Metadata;
+using Microsoft.EntityFrameworkCore.TestUtilities;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer;
+
+#pragma warning disable EF1001
+
+public class RelationalScaffoldingModelFactoryTest
+{
+ private readonly IScaffoldingModelFactory _factory;
+ private readonly TestOperationReporter _reporter;
+
+ private static readonly DatabaseModel Database;
+ private static readonly DatabaseTable Table;
+ private static readonly DatabaseColumn IdColumn;
+ private static readonly DatabasePrimaryKey IdPrimaryKey;
+
+ static RelationalScaffoldingModelFactoryTest()
+ {
+ Database = new DatabaseModel();
+ Table = new DatabaseTable { Database = Database, Name = "Foo" };
+ IdColumn = new DatabaseColumn
+ {
+ Table = Table,
+ Name = "Id",
+ StoreType = "int"
+ };
+ IdPrimaryKey = new DatabasePrimaryKey
+ {
+ Table = Table,
+ Name = "IdPrimaryKey",
+ Columns = { IdColumn }
+ };
+ }
+
+ public RelationalScaffoldingModelFactoryTest()
+ {
+ _reporter = new TestOperationReporter();
+
+ var assembly = typeof(RelationalScaffoldingModelFactoryTest).Assembly;
+ _factory = new DesignTimeServicesBuilder(assembly, assembly, _reporter, new string[0])
+ .CreateServiceCollection("Microsoft.EntityFrameworkCore.SqlServer")
+ .AddSingleton()
+ .BuildServiceProvider(validateScopes: true)
+ .GetRequiredService();
+
+ _reporter.Clear();
+ }
+
+ [ConditionalFact]
+ public void Loads_HierarchyId_columns()
+ {
+ var info = new DatabaseModel
+ {
+ Tables =
+ {
+ new DatabaseTable
+ {
+ Database = Database,
+ Name = "Jobs",
+ Columns =
+ {
+ IdColumn,
+ new DatabaseColumn
+ {
+ Table = Table,
+ Name = "occupation",
+ StoreType = "nvarchar(max)",
+ DefaultValueSql = "\"dev\""
+ },
+ new DatabaseColumn
+ {
+ Table = Table,
+ Name = "salary",
+ StoreType = "int",
+ IsNullable = true
+ },
+ new DatabaseColumn
+ {
+ Table = Table,
+ Name = "hierarchy",
+ StoreType = "HierarchyId"
+ }
+ },
+ PrimaryKey = IdPrimaryKey
+ }
+ }
+ };
+
+ var entityType =
+ (EntityType)_factory.Create(info, new ModelReverseEngineerOptions { NoPluralize = true }).FindEntityType("Jobs");
+
+ Assert.Collection(
+ entityType.GetProperties(),
+ pk =>
+ {
+ Assert.Equal("Id", pk.Name);
+ Assert.Equal(typeof(int), pk.ClrType);
+ },
+ column =>
+ {
+ Assert.Equal("hierarchy", column.GetColumnName());
+ Assert.Equal(typeof(HierarchyId), column.ClrType);
+ },
+ column =>
+ {
+ Assert.Equal("occupation", column.GetColumnName());
+ Assert.Equal(typeof(string), column.ClrType);
+ },
+ column =>
+ {
+ Assert.Equal("salary", column.GetColumnName());
+ Assert.Equal(typeof(int?), column.ClrType);
+ });
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/Test/Logging/TestLogger.cs b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Logging/TestLogger.cs
new file mode 100644
index 00000000000..3906af28440
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Logging/TestLogger.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Test.Logging
+{
+ public class TestLogger : ILogger
+ {
+ public string Sql { get; set; }
+
+ public IDisposable BeginScope(TState state)
+ => null;
+
+ public bool IsEnabled(LogLevel logLevel)
+ => true;
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ if (eventId != RelationalEventId.CommandExecuting)
+ return;
+
+ var structure = (IReadOnlyList>)state;
+ var commandText = (string)structure.FirstOrDefault(i => i.Key == "commandText").Value;
+
+ Sql = commandText;
+ }
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/Test/Logging/TestLoggerFactory.cs b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Logging/TestLoggerFactory.cs
new file mode 100644
index 00000000000..0d2f8efd9bb
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Logging/TestLoggerFactory.cs
@@ -0,0 +1,21 @@
+using System;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Test.Logging
+{
+ class TestLoggerFactory : ILoggerFactory
+ {
+ public TestLogger Logger { get; }
+ = new TestLogger();
+
+ public void AddProvider(ILoggerProvider provider)
+ => throw new NotImplementedException();
+
+ public ILogger CreateLogger(string categoryName)
+ => Logger;
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/AbrahamicContext.cs b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/AbrahamicContext.cs
new file mode 100644
index 00000000000..8b3f94d9aa6
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/AbrahamicContext.cs
@@ -0,0 +1,69 @@
+using Microsoft.EntityFrameworkCore.SqlServer.Test.Logging;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Test.Models
+{
+ class AbrahamicContext : DbContext
+ {
+ readonly TestLoggerFactory _loggerFactory
+ = new TestLoggerFactory();
+
+ public DbSet Patriarchy { get; set; }
+ public DbSet ConvertedPatriarchy { get; set; }
+
+ public string Sql
+ => _loggerFactory.Logger.Sql;
+
+ protected override void OnConfiguring(DbContextOptionsBuilder options)
+ => options
+ .UseSqlServer(
+ @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=HierarchyIdTests",
+ x => x.UseHierarchyId())
+ .UseLoggerFactory(_loggerFactory);
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasData(
+ new Patriarch { Id = HierarchyId.GetRoot(), Name = "Abraham" },
+ new Patriarch { Id = HierarchyId.Parse("/1/"), Name = "Isaac" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/"), Name = "Jacob" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/1/"), Name = "Reuben" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/2/"), Name = "Simeon" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/3/"), Name = "Levi" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/4/"), Name = "Judah" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/5/"), Name = "Issachar" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/6/"), Name = "Zebulun" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/7/"), Name = "Dan" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/8/"), Name = "Naphtali" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/9/"), Name = "Gad" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/10/"), Name = "Asher" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/11.1/"), Name = "Ephraim" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/11.2/"), Name = "Manasseh" },
+ new Patriarch { Id = HierarchyId.Parse("/1/1/12/"), Name = "Benjamin" });
+
+ modelBuilder.Entity(b =>
+ {
+ b.Property(e => e.HierarchyId)
+ .HasConversion(v => HierarchyId.Parse(v), v => v.ToString());
+
+ b.HasData(
+ new ConvertedPatriarch { Id = 1, HierarchyId = HierarchyId.GetRoot().ToString(), Name = "Abraham" },
+ new ConvertedPatriarch { Id = 2, HierarchyId = HierarchyId.Parse("/1/").ToString(), Name = "Isaac" },
+ new ConvertedPatriarch { Id = 3, HierarchyId = HierarchyId.Parse("/1/1/").ToString(), Name = "Jacob" },
+ new ConvertedPatriarch { Id = 4, HierarchyId = HierarchyId.Parse("/1/1/1/").ToString(), Name = "Reuben" },
+ new ConvertedPatriarch { Id = 5, HierarchyId = HierarchyId.Parse("/1/1/2/").ToString(), Name = "Simeon" },
+ new ConvertedPatriarch { Id = 6, HierarchyId = HierarchyId.Parse("/1/1/3/").ToString(), Name = "Levi" },
+ new ConvertedPatriarch { Id = 7, HierarchyId = HierarchyId.Parse("/1/1/4/").ToString(), Name = "Judah" },
+ new ConvertedPatriarch { Id = 8, HierarchyId = HierarchyId.Parse("/1/1/5/").ToString(), Name = "Issachar" },
+ new ConvertedPatriarch { Id = 9, HierarchyId = HierarchyId.Parse("/1/1/6/").ToString(), Name = "Zebulun" },
+ new ConvertedPatriarch { Id = 10, HierarchyId = HierarchyId.Parse("/1/1/7/").ToString(), Name = "Dan" },
+ new ConvertedPatriarch { Id = 11, HierarchyId = HierarchyId.Parse("/1/1/8/").ToString(), Name = "Naphtali" },
+ new ConvertedPatriarch { Id = 12, HierarchyId = HierarchyId.Parse("/1/1/9/").ToString(), Name = "Gad" },
+ new ConvertedPatriarch { Id = 13, HierarchyId = HierarchyId.Parse("/1/1/10/").ToString(), Name = "Asher" },
+ new ConvertedPatriarch { Id = 14, HierarchyId = HierarchyId.Parse("/1/1/11.1/").ToString(), Name = "Ephraim" },
+ new ConvertedPatriarch { Id = 15, HierarchyId = HierarchyId.Parse("/1/1/11.2/").ToString(), Name = "Manasseh" },
+ new ConvertedPatriarch { Id = 16, HierarchyId = HierarchyId.Parse("/1/1/12/").ToString(), Name = "Benjamin" });
+ });
+ }
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/ConvertedPatriarch.cs b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/ConvertedPatriarch.cs
new file mode 100644
index 00000000000..a5ca81ce2b1
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/ConvertedPatriarch.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.EntityFrameworkCore.SqlServer.Test.Models
+{
+ class ConvertedPatriarch
+ {
+ public int Id { get; set; }
+ public string HierarchyId { get; set; }
+ public string Name { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/Migrations/AnonymousArraySeedContext.cs b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/Migrations/AnonymousArraySeedContext.cs
new file mode 100644
index 00000000000..b448b23ca89
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/Migrations/AnonymousArraySeedContext.cs
@@ -0,0 +1,197 @@
+using Microsoft.EntityFrameworkCore.SqlServer.Storage;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Test.Models.Migrations
+{
+ internal sealed class AnonymousArraySeedContext : MigrationContext
+ {
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ RemoveVariableModelAnnotations(modelBuilder);
+
+ modelBuilder.Entity().HasData(
+ new { Id = HierarchyId.GetRoot(), Name = "Eddard Stark" },
+ new { Id = HierarchyId.Parse("/1/"), Name = "Robb Stark" },
+ new { Id = HierarchyId.Parse("/2/"), Name = "Jon Snow" });
+
+ modelBuilder.Entity(b =>
+ {
+ b.Property(e => e.HierarchyId)
+ .HasConversion(v => HierarchyId.Parse(v), v => v.ToString());
+
+ b.HasData(
+ new ConvertedPatriarch { Id = 1, HierarchyId = HierarchyId.GetRoot().ToString(), Name = "Eddard Stark" },
+ new ConvertedPatriarch { Id = 2, HierarchyId = HierarchyId.Parse("/1/").ToString(), Name = "Robb Stark" },
+ new ConvertedPatriarch { Id = 3, HierarchyId = HierarchyId.Parse("/2/").ToString(), Name = "Jon Snow" });
+ });
+ }
+
+ public override string GetExpectedMigrationCode(string migrationName, string rootNamespace)
+ {
+ return $@"using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
+
+namespace {rootNamespace}.Migrations
+{{
+ ///
+ public partial class {migrationName} : Migration
+ {{
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {{
+ migrationBuilder.CreateTable(
+ name: ""{nameof(ConvertedTestModels)}"",
+ columns: table => new
+ {{
+ Id = table.Column(type: ""int"", nullable: false),
+ HierarchyId = table.Column<{nameof(HierarchyId)}>(type: ""hierarchyid"", nullable: true),
+ Name = table.Column(type: ""nvarchar(max)"", nullable: true)
+ }},
+ constraints: table =>
+ {{
+ table.PrimaryKey(""PK_ConvertedTestModels"", x => x.Id);
+ }});
+
+ migrationBuilder.CreateTable(
+ name: ""{nameof(TestModels)}"",
+ columns: table => new
+ {{
+ {nameof(Patriarch.Id)} = table.Column<{nameof(HierarchyId)}>(type: ""hierarchyid"", nullable: false),
+ {nameof(Patriarch.Name)} = table.Column(type: ""nvarchar(max)"", nullable: true)
+ }},
+ constraints: table =>
+ {{
+ table.PrimaryKey(""PK_{nameof(TestModels)}"", x => x.{nameof(Patriarch.Id)});
+ }});
+
+ migrationBuilder.InsertData(
+ table: ""ConvertedTestModels"",
+ columns: new[] {{ ""Id"", ""HierarchyId"", ""Name"" }},
+ values: new object[,]
+ {{
+ {{ 1, Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/""), ""Eddard Stark"" }},
+ {{ 2, Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/1/""), ""Robb Stark"" }},
+ {{ 3, Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/2/""), ""Jon Snow"" }}
+ }});
+
+ migrationBuilder.InsertData(
+ table: ""TestModels"",
+ columns: new[] {{ ""Id"", ""Name"" }},
+ values: new object[,]
+ {{
+ {{ Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/""), ""Eddard Stark"" }},
+ {{ Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/1/""), ""Robb Stark"" }},
+ {{ Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/2/""), ""Jon Snow"" }}
+ }});
+ }}
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {{
+ migrationBuilder.DropTable(
+ name: ""ConvertedTestModels"");
+
+ migrationBuilder.DropTable(
+ name: ""{nameof(TestModels)}"");
+ }}
+ }}
+}}
+";
+ }
+
+ public override string GetExpectedSnapshotCode(string rootNamespace)
+ {
+ return $@"//
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using {ThisType.Namespace};
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace {rootNamespace}.Migrations
+{{
+ [DbContext(typeof({ThisType.Name}))]
+ partial class {ThisType.Name}ModelSnapshot : ModelSnapshot
+ {{
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {{
+#pragma warning disable 612, 618
+
+ modelBuilder.Entity(""{ModelType2.FullName}"", b =>
+ {{
+ b.Property(""Id"")
+ .ValueGeneratedOnAdd()
+ .HasColumnType(""int"");
+
+ b.Property(""HierarchyId"")
+ .HasColumnType(""hierarchyid"");
+
+ b.Property(""Name"")
+ .HasColumnType(""nvarchar(max)"");
+
+ b.HasKey(""Id"");
+
+ b.ToTable(""ConvertedTestModels"");
+
+ b.HasData(
+ new
+ {{
+ Id = 1,
+ HierarchyId = Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/""),
+ Name = ""Eddard Stark""
+ }},
+ new
+ {{
+ Id = 2,
+ HierarchyId = Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/1/""),
+ Name = ""Robb Stark""
+ }},
+ new
+ {{
+ Id = 3,
+ HierarchyId = Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/2/""),
+ Name = ""Jon Snow""
+ }});
+ }});
+
+ modelBuilder.Entity(""{ModelType1.FullName}"", b =>
+ {{
+ b.Property<{nameof(HierarchyId)}>(""{nameof(Patriarch.Id)}"")
+ .HasColumnType(""{SqlServerHierarchyIdTypeMappingSourcePlugin.SqlServerTypeName}"");
+
+ b.Property(""{nameof(Patriarch.Name)}"")
+ .HasColumnType(""nvarchar(max)"");
+
+ b.HasKey(""{nameof(Patriarch.Id)}"");
+
+ b.ToTable(""{nameof(TestModels)}"");
+
+ b.HasData(
+ new
+ {{
+ {nameof(Patriarch.Id)} = Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/""),
+ {nameof(Patriarch.Name)} = ""Eddard Stark""
+ }},
+ new
+ {{
+ {nameof(Patriarch.Id)} = Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/1/""),
+ {nameof(Patriarch.Name)} = ""Robb Stark""
+ }},
+ new
+ {{
+ {nameof(Patriarch.Id)} = Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/2/""),
+ {nameof(Patriarch.Name)} = ""Jon Snow""
+ }});
+ }});
+#pragma warning restore 612, 618
+ }}
+ }}
+}}
+";
+ }
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/Migrations/MigrationContext.cs b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/Migrations/MigrationContext.cs
new file mode 100644
index 00000000000..940d136502f
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/Migrations/MigrationContext.cs
@@ -0,0 +1,48 @@
+using System;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Test.Models.Migrations
+{
+ internal abstract class MigrationContext : DbContext
+ where TEntity1 : class
+ where TEntity2 : class
+ {
+ protected Type ModelType1 { get; } = typeof(TEntity1);
+ protected Type ModelType2 { get; } = typeof(TEntity2);
+
+ private Type _thisType;
+ protected Type ThisType => _thisType ??= GetType();
+
+ public DbSet TestModels { get; set; }
+ public DbSet ConvertedTestModels { get; set; }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder options)
+ => options
+ .UseSqlServer(
+ @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=HierarchyIdMigrationTests",
+ x => x.UseHierarchyId());
+
+ ///
+ /// Removes annotations from the model that can
+ /// change between versions of ef.
+ /// This should be called during OnModelCreating
+ ///
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Uses internal efcore apis.")]
+ protected void RemoveVariableModelAnnotations(ModelBuilder modelBuilder)
+ {
+ var model = modelBuilder.Model;
+
+ //the values of these could change between versions
+ //so get rid of them for the tests
+ model.RemoveAnnotation(CoreAnnotationNames.ProductVersion);
+ model.RemoveAnnotation(RelationalAnnotationNames.MaxIdentifierLength);
+ model.RemoveAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy);
+ }
+
+ public abstract string GetExpectedMigrationCode(string migrationName, string rootNamespace);
+ public abstract string GetExpectedSnapshotCode(string rootNamespace);
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/Migrations/TypedArraySeedContext.cs b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/Migrations/TypedArraySeedContext.cs
new file mode 100644
index 00000000000..6b04e71c517
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/Migrations/TypedArraySeedContext.cs
@@ -0,0 +1,197 @@
+using Microsoft.EntityFrameworkCore.SqlServer.Storage;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Test.Models.Migrations
+{
+ internal sealed class TypedArraySeedContext : MigrationContext
+ {
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ RemoveVariableModelAnnotations(modelBuilder);
+
+ modelBuilder.Entity().HasData(
+ new Patriarch { Id = HierarchyId.GetRoot(), Name = "Eddard Stark" },
+ new Patriarch { Id = HierarchyId.Parse("/1/"), Name = "Robb Stark" },
+ new Patriarch { Id = HierarchyId.Parse("/2/"), Name = "Jon Snow" });
+
+ modelBuilder.Entity(b =>
+ {
+ b.Property(e => e.HierarchyId)
+ .HasConversion(v => HierarchyId.Parse(v), v => v.ToString());
+
+ b.HasData(
+ new ConvertedPatriarch { Id = 1, HierarchyId = HierarchyId.GetRoot().ToString(), Name = "Eddard Stark" },
+ new ConvertedPatriarch { Id = 2, HierarchyId = HierarchyId.Parse("/1/").ToString(), Name = "Robb Stark" },
+ new ConvertedPatriarch { Id = 3, HierarchyId = HierarchyId.Parse("/2/").ToString(), Name = "Jon Snow" });
+ });
+ }
+
+ public override string GetExpectedMigrationCode(string migrationName, string rootNamespace)
+ {
+ return $@"using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
+
+namespace {rootNamespace}.Migrations
+{{
+ ///
+ public partial class {migrationName} : Migration
+ {{
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {{
+ migrationBuilder.CreateTable(
+ name: ""{nameof(ConvertedTestModels)}"",
+ columns: table => new
+ {{
+ {nameof(ConvertedPatriarch.Id)} = table.Column(type: ""int"", nullable: false),
+ {nameof(ConvertedPatriarch.HierarchyId)} = table.Column<{nameof(HierarchyId)}>(type: ""hierarchyid"", nullable: true),
+ {nameof(ConvertedPatriarch.Name)} = table.Column(type: ""nvarchar(max)"", nullable: true)
+ }},
+ constraints: table =>
+ {{
+ table.PrimaryKey(""PK_{nameof(ConvertedTestModels)}"", x => x.{nameof(ConvertedPatriarch.Id)});
+ }});
+
+ migrationBuilder.CreateTable(
+ name: ""{nameof(TestModels)}"",
+ columns: table => new
+ {{
+ {nameof(Patriarch.Id)} = table.Column<{nameof(HierarchyId)}>(type: ""hierarchyid"", nullable: false),
+ {nameof(Patriarch.Name)} = table.Column(type: ""nvarchar(max)"", nullable: true)
+ }},
+ constraints: table =>
+ {{
+ table.PrimaryKey(""PK_{nameof(TestModels)}"", x => x.{nameof(Patriarch.Id)});
+ }});
+
+ migrationBuilder.InsertData(
+ table: ""ConvertedTestModels"",
+ columns: new[] {{ ""Id"", ""HierarchyId"", ""Name"" }},
+ values: new object[,]
+ {{
+ {{ 1, {typeof(HierarchyId).FullName}.Parse(""/""), ""Eddard Stark"" }},
+ {{ 2, {typeof(HierarchyId).FullName}.Parse(""/1/""), ""Robb Stark"" }},
+ {{ 3, {typeof(HierarchyId).FullName}.Parse(""/2/""), ""Jon Snow"" }}
+ }});
+
+ migrationBuilder.InsertData(
+ table: ""TestModels"",
+ columns: new[] {{ ""Id"", ""Name"" }},
+ values: new object[,]
+ {{
+ {{ {typeof(HierarchyId).FullName}.Parse(""/""), ""Eddard Stark"" }},
+ {{ {typeof(HierarchyId).FullName}.Parse(""/1/""), ""Robb Stark"" }},
+ {{ {typeof(HierarchyId).FullName}.Parse(""/2/""), ""Jon Snow"" }}
+ }});
+ }}
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {{
+ migrationBuilder.DropTable(
+ name: ""ConvertedTestModels"");
+
+ migrationBuilder.DropTable(
+ name: ""{nameof(TestModels)}"");
+ }}
+ }}
+}}
+";
+ }
+
+ public override string GetExpectedSnapshotCode(string rootNamespace)
+ {
+ return $@"//
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using {ThisType.Namespace};
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace {rootNamespace}.Migrations
+{{
+ [DbContext(typeof({ThisType.Name}))]
+ partial class {ThisType.Name}ModelSnapshot : ModelSnapshot
+ {{
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {{
+#pragma warning disable 612, 618
+
+ modelBuilder.Entity(""{ModelType2.FullName}"", b =>
+ {{
+ b.Property(""Id"")
+ .ValueGeneratedOnAdd()
+ .HasColumnType(""int"");
+
+ b.Property(""HierarchyId"")
+ .HasColumnType(""hierarchyid"");
+
+ b.Property(""Name"")
+ .HasColumnType(""nvarchar(max)"");
+
+ b.HasKey(""Id"");
+
+ b.ToTable(""ConvertedTestModels"");
+
+ b.HasData(
+ new
+ {{
+ Id = 1,
+ HierarchyId = Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/""),
+ Name = ""Eddard Stark""
+ }},
+ new
+ {{
+ Id = 2,
+ HierarchyId = Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/1/""),
+ Name = ""Robb Stark""
+ }},
+ new
+ {{
+ Id = 3,
+ HierarchyId = Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/2/""),
+ Name = ""Jon Snow""
+ }});
+ }});
+
+ modelBuilder.Entity(""{ModelType1.FullName}"", b =>
+ {{
+ b.Property<{nameof(HierarchyId)}>(""{nameof(Patriarch.Id)}"")
+ .HasColumnType(""{SqlServerHierarchyIdTypeMappingSourcePlugin.SqlServerTypeName}"");
+
+ b.Property(""{nameof(Patriarch.Name)}"")
+ .HasColumnType(""nvarchar(max)"");
+
+ b.HasKey(""{nameof(Patriarch.Id)}"");
+
+ b.ToTable(""{nameof(TestModels)}"");
+
+ b.HasData(
+ new
+ {{
+ {nameof(Patriarch.Id)} = Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/""),
+ {nameof(Patriarch.Name)} = ""Eddard Stark""
+ }},
+ new
+ {{
+ {nameof(Patriarch.Id)} = Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/1/""),
+ {nameof(Patriarch.Name)} = ""Robb Stark""
+ }},
+ new
+ {{
+ {nameof(Patriarch.Id)} = Microsoft.EntityFrameworkCore.HierarchyId.Parse(""/2/""),
+ {nameof(Patriarch.Name)} = ""Jon Snow""
+ }});
+ }});
+#pragma warning restore 612, 618
+ }}
+ }}
+}}
+";
+ }
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/Patriarch.cs b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/Patriarch.cs
new file mode 100644
index 00000000000..2e7712a4c61
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Models/Patriarch.cs
@@ -0,0 +1,8 @@
+namespace Microsoft.EntityFrameworkCore.SqlServer.Test.Models
+{
+ class Patriarch
+ {
+ public HierarchyId Id { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/Test/Utilities/FakeScaffoldingModelFactory.cs b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Utilities/FakeScaffoldingModelFactory.cs
new file mode 100644
index 00000000000..e73193686fe
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Utilities/FakeScaffoldingModelFactory.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.EntityFrameworkCore.Design.Internal;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Scaffolding;
+using Microsoft.EntityFrameworkCore.Scaffolding.Internal;
+using Microsoft.EntityFrameworkCore.Scaffolding.Metadata;
+
+namespace Microsoft.EntityFrameworkCore.TestUtilities;
+
+#pragma warning disable EF1001
+
+public class FakeScaffoldingModelFactory : RelationalScaffoldingModelFactory
+{
+ public FakeScaffoldingModelFactory(
+ IOperationReporter reporter,
+ ICandidateNamingService candidateNamingService,
+ IPluralizer pluralizer,
+ ICSharpUtilities cSharpUtilities,
+ IScaffoldingTypeMapper scaffoldingTypeMapper,
+ IModelRuntimeInitializer modelRuntimeInitializer)
+ : base(reporter, candidateNamingService, pluralizer, cSharpUtilities, scaffoldingTypeMapper,
+ modelRuntimeInitializer)
+ {
+ }
+
+ public override IModel Create(DatabaseModel databaseModel, ModelReverseEngineerOptions options)
+ {
+ foreach (var sequence in databaseModel.Sequences)
+ {
+ sequence.Database = databaseModel;
+ }
+
+ foreach (var table in databaseModel.Tables)
+ {
+ table.Database = databaseModel;
+
+ foreach (var column in table.Columns)
+ {
+ column.Table = table;
+ }
+
+ if (table.PrimaryKey != null)
+ {
+ table.PrimaryKey.Table = table;
+ FixupColumns(table, table.PrimaryKey.Columns);
+ }
+
+ foreach (var index in table.Indexes)
+ {
+ index.Table = table;
+ FixupColumns(table, index.Columns);
+ }
+
+ foreach (var uniqueConstraints in table.UniqueConstraints)
+ {
+ uniqueConstraints.Table = table;
+ FixupColumns(table, uniqueConstraints.Columns);
+ }
+
+ foreach (var foreignKey in table.ForeignKeys)
+ {
+ foreignKey.Table = table;
+ FixupColumns(table, foreignKey.Columns);
+
+ if (foreignKey.PrincipalTable is DatabaseTableRef tableRef)
+ {
+ foreignKey.PrincipalTable = databaseModel.Tables
+ .First(t => t.Name == tableRef.Name && t.Schema == tableRef.Schema);
+ }
+
+ FixupColumns(foreignKey.PrincipalTable, foreignKey.PrincipalColumns);
+ }
+ }
+
+ return base.Create(databaseModel, options);
+ }
+
+ private static void FixupColumns(DatabaseTable table, IList columns)
+ {
+ for (var i = 0; i < columns.Count; i++)
+ {
+ if (columns[i] is DatabaseColumnRef columnRef)
+ {
+ columns[i] = table.Columns.First(c => c.Name == columnRef.Name);
+ }
+
+ columns[i].Table = table;
+ }
+ }
+}
+
+internal class DatabaseTableRef : DatabaseTable
+{
+ public DatabaseTableRef(string name, string schema = null)
+ {
+ Name = name;
+ Schema = schema;
+ }
+
+ public override DatabaseModel Database
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public override string Comment
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public override DatabasePrimaryKey PrimaryKey
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public override IList Columns
+ => throw new NotImplementedException();
+
+ public override IList UniqueConstraints
+ => throw new NotImplementedException();
+
+ public override IList Indexes
+ => throw new NotImplementedException();
+
+ public override IList ForeignKeys
+ => throw new NotImplementedException();
+}
+
+internal class DatabaseColumnRef : DatabaseColumn
+{
+ public DatabaseColumnRef(string name)
+ {
+ Name = name;
+ }
+
+ public override DatabaseTable Table
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public override bool IsNullable
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public override string StoreType
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public override string DefaultValueSql
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public override string ComputedColumnSql
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public override string Comment
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public override ValueGenerated? ValueGenerated
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/Test/Utilities/SqlServerTestHelpers.cs b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Utilities/SqlServerTestHelpers.cs
new file mode 100644
index 00000000000..f74e93f88ad
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/Test/Utilities/SqlServerTestHelpers.cs
@@ -0,0 +1,25 @@
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.EntityFrameworkCore.TestUtilities;
+
+#pragma warning disable EF1001
+
+public class SqlServerTestHelpers : TestHelpers
+{
+ private SqlServerTestHelpers()
+ {
+ }
+
+ public static SqlServerTestHelpers Instance { get; } = new();
+
+ public override IServiceCollection AddProviderServices(IServiceCollection services)
+ => services.AddEntityFrameworkSqlServer();
+
+ public override DbContextOptionsBuilder UseProviderOptions(DbContextOptionsBuilder optionsBuilder)
+ => optionsBuilder.UseSqlServer(new SqlConnection("Database=DummyDatabase"));
+
+ public override LoggingDefinitions LoggingDefinitions { get; } = new SqlServerLoggingDefinitions();
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/TypeMappingTests.cs b/test/EFCore.SqlServer.HierarchyId.Tests/TypeMappingTests.cs
new file mode 100644
index 00000000000..79bfa933a51
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/TypeMappingTests.cs
@@ -0,0 +1,56 @@
+using System;
+using Microsoft.EntityFrameworkCore.SqlServer.Storage;
+using Microsoft.EntityFrameworkCore.Storage;
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer
+{
+ public class TypeMappingTests
+ {
+ [Fact]
+ public void Maps_int_column()
+ {
+ var mapping = CreateMapper().FindMapping(
+ new RelationalTypeMappingInfo(
+ storeTypeName: "int",
+ storeTypeNameBase: "int",
+ unicode: null,
+ size: null,
+ precision: null,
+ scale: null));
+
+ Assert.Null(mapping);
+ }
+
+ [Fact]
+ public void Maps_hierarchyid_column()
+ {
+ var mapping = CreateMapper().FindMapping(
+ new RelationalTypeMappingInfo(
+ storeTypeName: SqlServerHierarchyIdTypeMappingSourcePlugin.SqlServerTypeName,
+ storeTypeNameBase: SqlServerHierarchyIdTypeMappingSourcePlugin.SqlServerTypeName,
+ unicode: null,
+ size: null,
+ precision: null,
+ scale: null));
+
+ AssertMapping(mapping);
+ }
+
+ private static void AssertMapping(
+ RelationalTypeMapping mapping)
+ {
+ AssertMapping(typeof(T), mapping);
+ }
+
+ private static void AssertMapping(
+ Type type,
+ RelationalTypeMapping mapping)
+ {
+ Assert.Same(type, mapping.ClrType);
+ }
+
+ private static IRelationalTypeMappingSourcePlugin CreateMapper()
+ => new SqlServerHierarchyIdTypeMappingSourcePlugin();
+ }
+}
diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/WrapperTests.cs b/test/EFCore.SqlServer.HierarchyId.Tests/WrapperTests.cs
new file mode 100644
index 00000000000..c6a7c62a304
--- /dev/null
+++ b/test/EFCore.SqlServer.HierarchyId.Tests/WrapperTests.cs
@@ -0,0 +1,15 @@
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer
+{
+ public class WrapperTests
+ {
+ [Fact]
+ public void GetAncestor_returns_null_when_too_high()
+ => Assert.Null(HierarchyId.Parse("/1/").GetAncestor(2));
+
+ [Fact]
+ public void GetReparentedValue_returns_null_when_newRoot_is_null()
+ => Assert.Null(HierarchyId.Parse("/1/").GetReparentedValue(HierarchyId.GetRoot(), newRoot: null));
+ }
+}