ASP.NET Core 3.1 OData序列化NetTopologySuite几何类型循环报错问题
我正在ASP.NET Core中搭配使用OData和NetTopologySuite,当前使用的PostgreSQL数据库包含Geometry类型的字段,通过Npgsql发起的请求运行正常,但OData的序列化结果完全不符合预期,这个问题已经卡了好几天了,恳请大家帮忙。
看起来OData出现了循环序列化的问题,每个返回项按理仅应返回一组XYZ坐标,但实际返回结果出现了无限嵌套的情况,如下所示:
,"shape":{"X":-9.129147,"Y":41.133908999999996,"Z":"NaN","M":"NaN","SRID":4326,"Coordinates":[{"X":-9.129147,"Y":41.133908999999996,"Z":"NaN","M":"NaN","CoordinateValue":{"X":-9.129147,"Y":41.133908999999996,"Z":"NaN","M":"NaN","CoordinateValue":{"X":-9.129147,"Y":41.133908999999996,"Z":"NaN","M":"NaN","CoordinateValue":...无限嵌套...
相关代码
模型(Model)
public class Model_bs_age_depth { [Key] [DatabaseGenerated(DatabaseGeneratedOption.None)] public string age_depth_id { get; set; } public string site_id { get; set; } public string site_type { get; set; } public string site_name { get; set; } public decimal? latitude { get; set; } public decimal? longitude { get; set; } public string country { get; set; } public NetTopologySuite.Geometries.Point shape { get; set; } }
控制器(Controller)
public async Task<ActionResult<IEnumerable<Model_bs_age_depth>>> Getbs_age_depth() { var optionsBuilder = new DbContextOptionsBuilder<StorageBroker>() .UseNpgsql(_connectionString, x => x.UseNetTopologySuite()) .UseLoggerFactory(factoryLogger) .Options; StorageBroker context = new StorageBroker(optionsBuilder); var query = await context.bs_age_depth.ToListAsync(); if (query == null) { // 省略空处理逻辑 } return query; }
Storage Broker
public class StorageBroker : DbContext { private static readonly ILoggerFactory factoryLogger = LoggerFactory.Create(builder => builder.AddConsole())!; public StorageBroker(DbContextOptions<StorageBroker> options) : base(options) { } public DbSet<Model_bs_age_depth> bs_age_depth { get; set; } public DbSet<bs_biozone> bs_biozone { get; set; } public DbSet<DBModel> cd_site { get; set; } }
错误日志
Entity Framework Core 3.1.20 initialized 'StorageBroker' using provider 'Npgsql.EntityFrameworkCore.PostgreSQL' with options: using NetTopologySuite
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (39ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT b.age_depth_id, b.age_epoch_top, b.age_ma, b.age_period_base, b.age_period_top, b.age_stage_base, b.basin_id, b.basin_name, b.boundary, b.country, b.depth, b.depth_f, b.field_id, b.field_name, b.interpretation_version, b.latitude, b.lithostrat_formation, b.lithostrat_group, b.lithostrat_member, b.location, b.longitude, b.product_id, b.product_title, b.product_year, b.province_state, b.region_id, b.region_name, b.shape, b.site_id, b.site_name, b.site_type, b.table_meta_id, b.tvd_base, b.tvd_base_f, b.tvd_top, b.tvd_top_f
FROM bs_age_depth AS b
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
Microsoft.OData.ODataException: The depth limit for entries in nested expanded navigation links was reached. The number of nested expanded entries cannot exceed 100.
这个问题的根本原因是NetTopologySuite的Point类型内置了自引用属性CoordinateValue,OData默认序列化时会遍历类型的所有公共属性,触发无限递归,最终超出嵌套深度限制报错。你可以根据自己的需求选以下任意一种方案解决:
方案1:使用DTO做字段投影(最简单,兼容性最高)
不要直接把数据库实体返回给OData,额外定义一个传输用的DTO类,只保留你需要暴露的字段,避免直接序列化Point对象:
public class Model_bs_age_depth_Dto { public string age_depth_id { get; set; } public string site_id { get; set; } public string site_type { get; set; } public string site_name { get; set; } public decimal? latitude { get; set; } public decimal? longitude { get; set; } public string country { get; set; } // 直接暴露你需要的坐标字段,不返回完整Point对象 public double? ShapeX { get; set; } public double? ShapeY { get; set; } public int? ShapeSRID { get; set; } }
查询时做投影转换:
var query = await context.bs_age_depth .Select(p => new Model_bs_age_depth_Dto { age_depth_id = p.age_depth_id, site_id = p.site_id, // 其他字段逐一赋值,也可以用AutoMapper自动映射 ShapeX = p.shape.X, ShapeY = p.shape.Y, ShapeSRID = p.shape.SRID }).ToListAsync(); return query;
方案2:配置OData EDM模型忽略自引用属性
如果你不想额外维护DTO,可以在构建OData EDM模型时,手动忽略Point类型的自引用属性,避免OData序列化时遍历到循环字段:
// 在你的OData服务配置代码中,构建EDM模型时添加以下逻辑 var modelBuilder = new ODataConventionModelBuilder(); modelBuilder.EntitySet<Model_bs_age_depth>("bs_age_depth"); // 找到Point类型的配置,忽略会触发循环的属性 var pointTypeConfig = modelBuilder.StructuralTypes .First(t => t.ClrType == typeof(NetTopologySuite.Geometries.Point)); pointTypeConfig.Ignore(p => p.CoordinateValue); pointTypeConfig.Ignore(p => p.Coordinates); pointTypeConfig.Ignore(p => p.Geometries); // 用配置好的模型初始化OData var edmModel = modelBuilder.GetEdmModel();
方案3:配置OData空间类型映射
如果你需要返回标准的OData空间格式,可以直接安装Microsoft.AspNetCore.OData.Spatial包启用空间类型支持,OData会自动将NetTopologySuite的Geometry类型映射到内置的空间类型,按照标准规则序列化,不会遍历内部属性。
内容的提问来源于stack exchange,提问作者Mike

