Skip to content

Figure out testing for EF use of Microsoft.SqlServer.Types #10108

New issue

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

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

Already on GitHub? # to your account

Closed
ajcvickers opened this issue Oct 18, 2017 · 5 comments
Closed

Figure out testing for EF use of Microsoft.SqlServer.Types #10108

ajcvickers opened this issue Oct 18, 2017 · 5 comments

Comments

@ajcvickers
Copy link
Contributor

Testing for Microsoft.SqlServer.Types is problematic because:

  • The types are not available on .NET Core
  • Setting up a machine to have the correct versions of the correct types in the correct places is fragile and has traditionally causes C.I./build issues.

For now I have been using this little test app for most of my functional testing:

public class Program
{
    public static void Main()
    {
        var loggerProvider = new MyLoggerProvider();

        using (var context = new TestDbContext())
        {
            // Create UDT columns
            context.Database.EnsureDeleted();
            context.GetService<ILoggerFactory>().AddProvider(loggerProvider);
            context.Database.EnsureCreated();

            // Single save
            context.AddRange(NewFoo(0));
            context.SaveChanges();

            // Batch save
            context.AddRange(NewFoo(1), NewFoo(2));
            context.SaveChanges();
        }

        using (var context = new TestDbContext())
        {
            // Simple query
            var foos = context.Foos.OrderBy(e => e.Id).ToList();
            AssertFoo(foos[0], 0);
            AssertFoo(foos[1], 1);
            AssertFoo(foos[2], 2);

            // Equals translation works for HierarchyId but not for spatial types
            var foo = context.Foos.Single(e => e.HierarchyId.Equals(SqlHierarchyIds[1]));
            AssertFoo(foo, 1);

            // Simple prjection
            var projection = context.Foos.OrderBy(e => e.Id).Skip(1).Select(e => new {foo.Id, foo.Geography1}).First();
            Debug.Assert(projection.Geography1.STEquals(SqlGeographies[1]).IsTrue);

            // FromSql query using spatial function
            var bars = context.Bars.FromSql("SELECT [Id], [Geography1].STDistance([Geography2]) as [Distance] From [Foos]").OrderBy(e => e.Id).ToList();
            AssertDistance(bars[0].Distance, 0);
            AssertDistance(bars[1].Distance, 1);
            AssertDistance(bars[2].Distance, 2);

            // Manipulate and send database updates
            for (var i = 0; i < 3; i++)
            {
                foos[i].Geography1 = SqlGeographies[(i + 1) % 3];
                foos[i].Geography2 = SqlGeographies[(i + 2) % 3];
                foos[i].Geometry = SqlGeometries[(i + 1) % 3];
                foos[i].HierarchyId = SqlHierarchyIds[(i + 1) % 3];
            }
            AssertFoo(foos[0], 0, 1);
            AssertFoo(foos[1], 1, 1);
            AssertFoo(foos[2], 2, 1);

            context.SaveChanges();
        }

        using (var context = new TestDbContext())
        {
            // Query after update
            var foos = context.Foos.OrderBy(e => e.Id).ToList();
            AssertFoo(foos[0], 0, 1);
            AssertFoo(foos[1], 1, 1);
            AssertFoo(foos[2], 2, 1);
        }
    }

    private static void AssertFoo(Foo foo, int index, int offset = 0)
    {
        Debug.Assert((foo.HierarchyId == SqlHierarchyIds[(index + offset) % 3]).IsTrue);
        Debug.Assert(foo.Geometry.STEquals(SqlGeometries[(index + offset) % 3]).IsTrue);
        Debug.Assert(foo.Geography1.STEquals(SqlGeographies[(index + offset) % 3]).IsTrue);
        Debug.Assert(foo.Geography2.STEquals(SqlGeographies[(index + offset + 1) % 3]).IsTrue);
    }

    private static void AssertDistance(double distance, int index) 
        => Debug.Assert(
            Math.Abs(SqlGeographies[index].STDistance(SqlGeographies[(index + 1) % 3]).Value - distance) < 1);

    private static Foo NewFoo(int index)
        => new Foo
        {
            HierarchyId = SqlHierarchyIds[index],
            Geometry = SqlGeometries[index],
            Geography1 = SqlGeographies[index],
            Geography2 = SqlGeographies[(index + 1) % 3]
        };

    public class Bar
    {
        public int Id { get; set; }
        public double Distance { get; set; }
    }

    public class Foo
    {
        public int Id { get; set; }
        public SqlHierarchyId HierarchyId { get; set; }
        public SqlGeometry Geometry { get; set; }
        public SqlGeography Geography1 { get; set; }
        public SqlGeography Geography2 { get; set; }
    }

    public class TestDbContext : DbContext
    {
        public DbSet<Foo> Foos { get; set; }
        public DbSet<Bar> Bars { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .EnableSensitiveDataLogging()
                .UseSqlServer(
                    @"Server=(localdb)\mssqllocaldb;Database=TestTypeEF;ConnectRetryCount=0");
        }
    }

    private static readonly SqlHierarchyId[] SqlHierarchyIds =
    {
        SqlHierarchyId.Parse("/1/1/3/"),
        SqlHierarchyId.Parse("/1/2/3/"),
        SqlHierarchyId.Parse("/2/1/3/")
    };

    private static readonly SqlGeometry[] SqlGeometries =
    {
        SqlGeometry.Point(1, 2, 32768),
        SqlGeometry.Point(10, 12, 32768),
        SqlGeometry.Point(20, 22, 32768)
    };

    private static readonly SqlGeography[] SqlGeographies =
    {
        SqlGeography.Point(77, 88, 4326),
        SqlGeography.Point(7, 8, 4326),
        SqlGeography.Point(45, 21, 4326)
    };
}

public class MyLoggerProvider : ILoggerProvider
{
    public ILogger CreateLogger(string categoryName) => new MyLogger();

    public void Dispose()
    {
    }

    private class MyLogger : ILogger
    {
        public bool IsEnabled(LogLevel logLevel) => true;

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
            Func<TState, Exception, string> formatter) 
            => Console.WriteLine(formatter(state, exception));

        public IDisposable BeginScope<TState>(TState state) => null;
    }
}
ajcvickers added a commit that referenced this issue Oct 18, 2017
…n .NET Framework

Part of issue #1100

This change allows properties of type SqlGeography, SqlGeometry, and HierarchyId to be included in entity classes and mapped by convention to corresponding database columns.

WARNING: This is by no-means full spatial support for EF Core. These are the important limitations:
* It only works when running on .NET Framework. It will not work on .NET Core, since SqlClient for .NET Core does not support these types.
* Query support is limited:
  * LINQ queries that don't use the semantics of the SQL types should work.
  * Queries that need to use the type semantics (e.g. using STDistance) can be done with FromSql queries, but not LINQ
  * See issue #10108 for some test code and issue #10109 for some ideas to make LINQ work better
* The SQL Server spatial types do not provide a good client-side programming experience. A proper .NET spatial library is needed for a good, cross-platform experience.

Note that this change also allows more general mapping of types not directly referenced from the provider, and more specifically UDTs on SQL Server.
@ErikEJ
Copy link
Contributor

ErikEJ commented Oct 18, 2017

Seperate .net fw test project, and privately deploy the libraries (as content) ?

@ajcvickers
Copy link
Contributor Author

Triage: we will put this code into a sample.

@ajcvickers ajcvickers self-assigned this Oct 18, 2017
@ajcvickers ajcvickers added this to the 2.1.0 milestone Oct 18, 2017
ajcvickers added a commit that referenced this issue Oct 20, 2017
…n .NET Framework

Part of issue #1100

This change allows properties of type SqlGeography, SqlGeometry, and HierarchyId to be included in entity classes and mapped by convention to corresponding database columns.

WARNING: This is by no-means full spatial support for EF Core. These are the important limitations:
* It only works when running on .NET Framework. It will not work on .NET Core, since SqlClient for .NET Core does not support these types.
* Query support is limited:
  * LINQ queries that don't use the semantics of the SQL types should work.
  * Queries that need to use the type semantics (e.g. using STDistance) can be done with FromSql queries, but not LINQ
  * See issue #10108 for some test code and issue #10109 for some ideas to make LINQ work better
* The SQL Server spatial types do not provide a good client-side programming experience. A proper .NET spatial library is needed for a good, cross-platform experience.

Note that this change also allows more general mapping of types not directly referenced from the provider, and more specifically UDTs on SQL Server.
@ajcvickers ajcvickers modified the milestones: 2.1.0-preview1, 2.1.0 Jan 17, 2018
@divega divega modified the milestones: 2.1.0-preview2, 2.1.0 Apr 2, 2018
@ajcvickers
Copy link
Contributor Author

Verified that test code still works on current bits. Closing this as remaining work is to document and move to a sample, which is covered by dotnet/EntityFramework.Docs#611

@KyleTraynor
Copy link

When I try to use SqlHierarchyId in one of my classes and then add-migration, i get the error:
The current CSharpHelper cannot scaffold literals of type 'Microsoft.SqlServer.Types.SqlHierarchyId'. Configure your services to use one that can.

I am using EF Core 2.1 with ASPNetCore 2.1 while targeting .net 4.7.1

Any ideas what I need to do to add a column that uses SqlHierarchyId to my existing entity?

Thanks

@ajcvickers
Copy link
Contributor Author

@KyleTraynor Thanks for reporting this--filed as a new issue #12192

@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
@ajcvickers ajcvickers removed their assignment Sep 1, 2024
# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

No branches or pull requests

4 participants