简体   繁体   English

Entity Framework Core:此平台不支持 Udt 类型。 (空间数据 - 地理)

[英]Entity Framework Core: Type Udt is not supported on this platform. (Spatial Data - Geography)

I'm experimenting with entity framework core and stumbled upon an error I've never seen before and can't figure out how to fix it.我正在试验实体框架核心,偶然发现了一个我以前从未见过的错误,无法弄清楚如何修复它。 I'm using .net Core Web API 2.0 with EntityFramework Core 2.00-preview2-final我正在使用 .net Core Web API 2.0 和 EntityFramework Core 2.00-preview2-final

Here is a simple example that triggers the error.这是一个触发错误的简单示例。

(concept: simple endpoint to get a user from database) (概念:从数据库中获取用户的简单端点)

Error: System.PlatformNotSupportedException: Type Udt is not supported on this platform.错误:System.PlatformNotSupportedException:此平台不支持 Udt 类型。

Any suggestions?有什么建议吗?

The problem is that I'm using geography in my database but I use it as a string in my model, because entity framework core doesn't support spatial data yet...问题是我在我的数据库中使用 geography 但我在我的模型中将它用作字符串,因为实体框架核心还不支持空间数据......

Any way to keep this cake tasty without getting rid of geography, cause it's an important feature?有什么方法可以在不摆脱地理因素的情况下保持这个蛋糕的美味,因为它是一个重要的特征?

Edit : See my answer for current solution编辑:请参阅我对当前解决方案的回答

Ok here is how I solved it:好的,这是我解决它的方法:

The purpose is to keep geography in Entity Framework Core (without using DbGeography)目的是在 Entity Framework Core 中保留地理(不使用 DbGeography)

1) I created a struct called Location: 1)我创建了一个名为 Location 的结构:

public struct Location
{
    public double Longitude { get; set; }
    public double Latitude { get; set; }
}

2) Add it to your EF Entity Model 2) 将它添加到您的 EF 实体模型

public class User
{
    public Location Location { get; set; }
}

3) Hide it in your modelbuilder 3)将其隐藏在您的模型构建器中

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>().Ignore(x => x.Location);
}

4) Generate a Migration (Add-Migration migrationname) 4)生成迁移(Add-Migration migrationname)

5) Go to your migration file 1231randomnumbers1231_migrationname.cs and add the following (this way we create another column of type geography named Location) and then update your database (update-database): 5) 转到您的迁移文件 1231randomnumbers1231_migrationname.cs 并添加以下内容(这样我们创建另一列名为 Location 的 geography 类型),然后更新您的数据库(update-database):

migrationBuilder.Sql(@"ALTER TABLE [dbo].[User] ADD [Location] geography NULL");

6) (optional) I created a static class to update the db, handy if you have a Location column in mulple tables. 6)(可选)我创建了一个静态类来更新数据库,如果您在多个表中有一个位置列,这很方便。

public static class GeneralDB
{

    public static async Task UpdateLocation(DbContext ctx, string table, Location location, int id)
    {
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");

        string query = String.Format(@"UPDATE [dbo].[{0}] SET Location = geography::STPointFromText('POINT(' + CAST({1} AS VARCHAR(20)) + ' ' + CAST({2} AS VARCHAR(20)) + ')', 4326) WHERE(ID = {3})"
        , table.ToLower(), location.Longitude, location.Latitude, id);
        await ctx.Database.ExecuteSqlCommandAsync(query);
    }
    public static async Task<Location> GetLocation(DbContext ctx, string table, int id)
    {
        Location location = new Location();

        using (var command = ctx.Database.GetDbConnection().CreateCommand())
        {
            string query = String.Format("SELECT Location.Lat AS Latitude, Location.Long AS Longitude FROM [dbo].[{0}] WHERE Id = {1}"
                , table, id);
            command.CommandText = query;
            ctx.Database.OpenConnection();
            using (var result = command.ExecuteReader())
            {
                if (result.HasRows)
                {
                    while (await result.ReadAsync())
                    {
                        location.Latitude = result.GetDouble(0);
                        location.Longitude = result.GetDouble(1);
                    }
                }

            }
        }

        return location;
    }
}

This only works in EF Core 2.0这只适用于 EF Core 2.0

Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");

For EF Core 1.0 you would have to find an alternative way to replace a ',' with '.'.对于 EF Core 1.0,您必须找到一种替代方法来将“,”替换为“.”。 A good old fashion .Replace() method could do the job.一个很好的旧时尚 .Replace() 方法可以完成这项工作。

location.Longitude.ToString().Replace(',', '.')

7) CRUD Examples: 7) CRUD 示例:

7.1: Read 7.1:阅读

public async Task<User> GetByIdAsync(int id)
{
    User user =  await ctx.User.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id);

    user.Location = await GeneralDB.GetLocation(ctx, "user", id);
    return user;
}

7.2: Create 7.2:创建

public async Task<User> CreateAsync(User entity)
{

    ctx.User.Add(entity);
    await ctx.SaveChangesAsync();
    await GeneralDB.UpdateLocation(ctx, "user", entity.Location, entity.Id);
    return entity;  
}

7.3: Update 7.3:更新

public async Task<User> UpdateAsync(User entity)
{
    ctx.User.Attach(entity);
    ctx.Entry<User>(entity).State = EntityState.Modified;
    await ctx.SaveChangesAsync();

    await GeneralDB.UpdateLocation(ctx, "user", entity.Location, entity.Id);

    return entity;
}

UPDATE: Since EF Core 2.2 there is support for spatial data!:更新:从 EF Core 2.2 开始支持空间数据!:

https://docs.microsoft.com/en-us/ef/core/modeling/spatial https://docs.microsoft.com/en-us/ef/core/modeling/spatial


-- ——

Eli, tnx for you're solution. Eli,tnx 为您提供解决方案。 For me it was almost the perfect solution.对我来说,这几乎是完美的解决方案。 I had 2 problems:我有两个问题:

Problems问题

  1. An other application also inserts directly into the database (temporary solution, this will be changed in the future).另一个应用程序也直接插入数据库(临时解决方案,将来会更改)。
  2. When getting the first 50 entities the data must be ordered by distance, so the 50 nearest entities will be returned.获取前 50 个实体时,数据必须按距离排序,因此将返回最近的 50 个实体。

Solutions解决方案

  1. Instead of updating the location table from code I use a trigger on my visit table.我没有从代码更新位置表,而是在访问表上使用触发器。 This trigger will fill or insert, delete or update the Location table.此触发器将填充或插入、删除或更新位置表。 So the create, update, delete function don't have to do anything else than saving the entity.因此,创建、更新、删除功能除了保存实体外无需执行任何其他操作。

    create trigger VisitLocation_trigger on Visit
    after UPDATE, INSERT, DELETE
    as

    if exists(SELECT * from inserted)
        If exists(Select * from deleted)
            BEGIN
                -- UPDATE
                UPDATE visit_location SET location = GEOGRAPHY::Point(Latitude, Longitude, 4326) FROM visit_location JOIN inserted ON visit_location.visitid = inserted.id
            END
        else
            BEGIN
                -- INSERT
                INSERT INTO visit_location SELECT Id, GEOGRAPHY::Point(Latitude, Longitude, 4326) FROM inserted
            END
    else
        BEGIN
            -- DELETE
            declare @visitId int;
            SELECT @visitId = Id from deleted i;
            DELETE visit_location WHERE visit_location.visitid = @visitId 
        end
  1. The query for getting the first 50 must be a raw sql query, that will look like this:获取前 50 个的查询必须是原始 sql 查询,如下所示:


    _context.Visit.FromSql(
        "SELECT TOP 50 v.* " +
        "FROM visit v " +
        "INNER JOIN visit_location vl ON v.id = vl.visitid " +
        "WHERE v.date > {0} " +
        "AND GEOGRAPHY::Point({1},{2}, 4326).STDistance(Location) < {3} " +
        "ORDER BY GEOGRAPHY::Point({1},{2}, 4326).STDistance(Location)",
        startDate, latitude, longitude, radius).ToList();

CRUD CRUD

Read



    public async Task<Visit> GetByIdAsync(int id)
    {
        return await _context.Visit.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id);
    }



     public IList<Visit> GetLastVisitsForHouseIdsByCoordinates(DateTime startDate, double longitude, double latitude, long radius)
        {
            return

                _context.Visit.FromSql("SELECT TOP 50 v.* " +
                                       "FROM visit v " +
                                       "INNER JOIN visit_location vl ON v.id = vl.visitid " +
                                       "WHERE v.IsLastVisit = 1 " +
                                       "AND v.date > {0} " +
                                       "AND GEOGRAPHY::Point({1},{2}, 4326).STDistance(Location) < {3} " +
                                       "ORDER BY GEOGRAPHY::Point({1},{2}, 4326).STDistance(Location)",
                    startDate, latitude, longitude, radius).ToList();
        }
    

Create创建



    public async Task<Visit> CreateAsync(Visit visit)
    {
        _context.Visit.Add(visit);
        await _context.SaveChangesAsync();
        return visit;  
    }

Update更新



    public async Task<Visit> UpdateAsync(Visit visit)
    {
        _context.Visit.Attach(visit);
        _context.Entry(visit).State = EntityState.Modified;
        await _context.SaveChangesAsync();
        return visit;
    }

Delete删除



    public async Task DeleteAsync(Visit visit)
    {
        _dbContext.Remove(entityToUpdate);
        _context.Entry(visit).State = EntityState.Deleted;
        await _context.SaveChangesAsync();
        return visit;
    }

The database models数据库模型


    public class Visit
    {
        public int Id { get; set; }
    
        [Required]
        public VisitStatus Status { get; set; }
    
        [Required]
        public double? Latitude { get; set; }
    
        [Required]
        public double? Longitude { get; set; }
    
        public Location Location { get; set; }
    
        [Required]
        public DateTime Date { get; set; }
    
        public string Street { get; set; }
    
        public int? StreetNumber { get; set; }
    
        public string StreetNumberLetter { get; set; }
    
        public string StreetNumberLetterAddition { get; set; }
    
        public string City { get; set; }
    }
    
    public struct Location
    {
        public double Longitude { get; set; }
        public double Latitude { get; set; }
    }

These solutions work but if you are looking for additional ways here is another solution.这些解决方案有效,但如果您正在寻找其他方法,这里是另一种解决方案。 Since EF core 2 does not support geography types at this time you can use NetTopologySuite for all you server side geography support.由于 EF 核心 2 目前不支持地理类型,您可以使用 NetTopologySuite 来支持所有服务器端地理类型。

When you have a table that needs a geography column add property that EF can map to your table that either has type byte[] or string.当您有一个需要 geography 列的表时,添加 EF 可以映射到您的表的属性,该表的类型为 byte[] 或 string。 Like so:像这样:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NetTopologySuite;
using NetTopologySuite.Geometries;

namespace GeoTest2.Data
{
    public class HomeTown
    {
        private byte[] _pointsWKB;
        public string Name { get; set; }

        public byte[] PointWkb
        {
            get => _pointsWKB;
            set
            {
                if (GeopgraphyFactory.CreatePoint(value) != null)
                    _pointsWKB = value;

                throw new NotImplementedException("How ever you wnat to handle it");
            }
        }

        [NotMapped]
        public Point Point
        {
            get => GeopgraphyFactory.CreatePoint(_pointsWKB);
            set => _pointsWKB = GeopgraphyFactory.PointAsWkb(value);
        }
    }
}

This uses some helpers for creating points where are here:这使用了一些助手来创建点在这里:

using NetTopologySuite.Geometries;

namespace GeoTest2.Data
{
    public static class GeopgraphyFactory
    {
        public static Point CreatePoint(byte[] wkb)
        {
            var reader = new NetTopologySuite.IO.WKBReader();
            var val = reader.Read(wkb);
            return val as Point;
        }

        public static byte[] PointAsWkb(Point point)
        {
            var writer = new NetTopologySuite.IO.WKBWriter();
            return writer.Write(point);
        }

    }
}

As you can see nothing special is going on here.正如你所看到的,这里没有什么特别的。 This code in place you should have Full CRUDS.这段代码就位你应该有完整的 CRUDS。 If you need geography support on the db side as well (like our team did) then you can create a calculated column that uses this data to generate the correct geography type like so:如果您还需要 db 方面的地理支持(就像我们的团队所做的那样),那么您可以创建一个计算列,使用此数据生成正确的地理类型,如下所示:

ALTER TABLE dbo.tableName
    ADD Point AS 
        CONVERT(
            GEOGRAPHY,
            CASE WHEN [GeographyWkb] IS NOT NULL THEN 
                GEOGRAPHY::STGeomFromWKB ( [GeographyWkb], 4326 )
            ELSE NULL
            END)

EF will ignore this calculated column and you will be able to use it db side. EF 将忽略此计算列,您将能够在 db 端使用它。 Now this does leave use to how you would handle spatial queries and that is left to the reader.现在这确实让您使用如何处理空间查询,这留给读者。 There are a number of ways to handle it and some of the answers above show some of them.有多种方法可以处理它,上面的一些答案显示了其中的一些。 It is worth noting if the query is small you could do it in memory with NetTopologySuite, since the library provides support for unions, intersections, etc...值得注意的是,如果查询很小,您可以使用 NetTopologySuite 在内存中进行,因为该库提供了对联合、交叉等的支持......

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 Entity Framework Core 保存 geography::Point 抛出“提供的值不是数据类型 geography 的有效实例” - Entity Framework Core saving geography::Point throws "The supplied value is not a valid instance of data type geography" 此平台不支持 Microsoft.Data.SqlClient - Entity Framework Core 3.1 - Microsoft.Data.SqlClient is not supported on this platform - Entity Framework Core 3.1 空间类型实体框架核心SQL Server - Spatial type Entity Framework Core SQL Server Entity Framework Core 3.1 with NetTopologySuite.Geometries.Point: SqlException: 提供的值不是数据类型地理的有效实例 - Entity Framework Core 3.1 with NetTopologySuite.Geometries.Point: SqlException: The supplied value is not a valid instance of data type geography 实体框架6和空间数据 - Entity framework 6 and spatial data 小巧玲珑的空间地理类型 - Dapper Spatial Geography Type 实体框架核心支持SQL空间数据类型 - DBGeography? - Entity Framework Core support for SQL Spatial Data Types - DBGeography? RSA.FromXmlString &#39;此平台不支持操作。&#39; - RSA.FromXmlString 'Operation is not supported on this platform.' 实体框架核心1.1.0数据类型转换 - Entity Framework Core 1.1.0 Data type conversion 实体框架6代码优先UDT - Entity Framework 6 Code First UDT
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM