[英]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问题
Solutions解决方案
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
_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.