C# .NET解析地理信息的两种矢量空间数据格式SHP与GeoJson
最近项目中遇到了一个需求:前端上传SHP或GeoJson文件,接口接收后对其进行解析取出投影信息和属性集合存入数据库,但是对于很大的文件时,接口先响应返回给前端,将解析任务放在后台执行。
一、前期准备
1.安装必须 NuGet 包
# .NET CLI 安装命令
dotnet add package NetTopologySuite.IO.Esri.Shapefile
dotnet add package NetTopologySuite
dotnet add package NetTopologySuite.IO.GeoJson
2.任务参数类+实体类
/// <summary> /// Shapefile解析任务参数类 /// </summary> public class ShapefileParseTask { /// <summary> /// 记录Id /// </summary> public string ImportId { get; set; } /// <summary> /// 路径 /// </summary> public string TempDirectoryPath { get; set; } = string.Empty; /// <summary> /// 0-SHP文件 1-GeoJSON文件 /// </summary> public string DataType { get; set; } } /// <summary> /// 文件导入记录表 /// </summary> public class FileImport { /// <summary> /// 文件类型 /// </summary> public string FileType { get; set; } /// <summary> /// 状态 /// </summary> public string State { get; set; } /// <summary> /// 导入时间 /// </summary> public DateTime? ImportDate { get; set; } /// <summary> /// 完成时间 /// </summary> public DateTime? CompleteDate { get; set; } } /// <summary> /// 文件导入信息表 /// </summary> public class FileImportInfo : BaseEntity { /// <summary> /// 文件导入记录Id /// </summary> public string FileImportId { get; set; } /// <summary> /// 图层名 /// </summary> public string LayerName { get; set; } /// <summary> /// 投影信息 /// </summary> public string Projection { get; set; } } /// <summary> /// 文件导入属性表 /// </summary> public class FileImportAttribute { /// <summary> /// 文件导入信息Id /// </summary> public string FileImportInfoId { get; set; } /// <summary> /// 属性名 /// </summary> public string AttributeName { get; set; } }
二、核心代码实现
1.异步队列实现(官方 Channel,无阻塞、线程安全)
.NET 内置,无需额外装 NuGet 包,专为异步后台任务设计,完全适配 async/await。
using System.Threading.Channels; /// <summary> /// 异步解析队列接口 /// </summary> public interface IShapefileParseQueue { /// <summary> /// 推入解析任务 /// </summary> /// <param name="task"></param> /// <param name="cancellationToken"></param> /// <returns></returns> ValueTask QueueAsync(ShapefileParseTask task, CancellationToken cancellationToken = default); /// <summary> /// 异步等待并获取任务 /// </summary> /// <param name="cancellationToken"></param> /// <returns></returns> ValueTask<ShapefileParseTask> DequeueAsync(CancellationToken cancellationToken = default); } /// <summary> /// 基于Channel的队列实现 /// </summary> public class ShapefileParseQueue : IShapefileParseQueue { private readonly Channel<ShapefileParseTask> _channel; /// <summary> /// 构造函数 /// </summary> public ShapefileParseQueue() { var options = new UnboundedChannelOptions { SingleReader = true, SingleWriter = false }; _channel = Channel.CreateUnbounded<ShapefileParseTask>(options); } /// <summary> /// 推入解析任务 /// </summary> /// <param name="task"></param> /// <param name="cancellationToken"></param> /// <returns></returns> /// <exception cref="ArgumentNullException"></exception> public async ValueTask QueueAsync(ShapefileParseTask task, CancellationToken cancellationToken = default) { if (task == null) throw new ArgumentNullException(nameof(task)); await _channel.Writer.WriteAsync(task, cancellationToken); } /// <summary> /// 异步等待并获取任务 /// </summary> /// <param name="cancellationToken"></param> /// <returns></returns> public async ValueTask<ShapefileParseTask> DequeueAsync(CancellationToken cancellationToken = default) { var task = await _channel.Reader.ReadAsync(cancellationToken); return task; } }
2.后台解析服务(标准 BackgroundService,全 async 无阻塞)
using Microsoft.Extensions.Hosting; using NetTopologySuite.IO.Esri; using Pipelines.Sockets.Unofficial.Arenas; using Snowflake.Core; /// <summary> /// Shapefile、GeoJson后台解析服务 /// </summary> public class ShapefileParseHostedService : BackgroundService { private readonly IShapefileParseQueue _queue; private readonly IdWorker _idWorker; private readonly IDataGovernanceData _dataGovernanceData; /// <summary> /// 构造函数 /// </summary> /// <param name="queue"></param> /// <param name="idWorker"></param> /// <param name="dataGovernanceData"></param> public ShapefileParseHostedService( IShapefileParseQueue queue, IdWorker idWorker, IDataGovernanceData dataGovernanceData) { _queue = queue; _idWorker = idWorker; _dataGovernanceData = dataGovernanceData; } public override Task StartAsync(CancellationToken cancellationToken) { return base.StartAsync(cancellationToken); } /// <summary> /// /// </summary> /// <param name="stoppingToken"></param> /// <returns></returns> protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 持续监听任务,应用关闭时自动退出 while (!stoppingToken.IsCancellationRequested) { try { var parseTask = await _queue.DequeueAsync(stoppingToken); await ProcessTaskAsync(parseTask, stoppingToken); } catch (OperationCanceledException) { break; } catch (Exception) { await Task.Delay(1000, stoppingToken); } } } /// <summary> /// 解析任务 /// </summary> /// <param name="task"></param> /// <param name="stoppingToken"></param> /// <returns></returns> private async Task ProcessTaskAsync(ShapefileParseTask task, CancellationToken stoppingToken) { var importId = task.ImportId; var tempDir = task.TempDirectoryPath; var dataType = task.DataType; if (dataType == "0") { await ParseShpFilesAsync(importId, tempDir, stoppingToken); } else if (dataType == "1") { await ParseGeoJsonFilesAsync(importId, tempDir, stoppingToken); } } /// <summary> /// 解析 SHP 文件 /// </summary> private async Task ParseShpFilesAsync(string importId, string tempDir, CancellationToken stoppingToken) { try { // 1. 文件分组 var allFiles = Directory.GetFiles(tempDir); var layerGroups = allFiles .GroupBy(f => Path.GetFileName(f).Split('.')[0]) .ToDictionary(g => g.Key, g => g.ToList()); var fileImportInfoList = new List<FileImportInfo>(); var fileImportAttributeList = new List<FileImportAttribute>(); // 2. 遍历解析每个图层 foreach (var group in layerGroups) { var layerName = group.Key; var groupFilePaths = group.Value; // 找到shp主文件 var shpPath = groupFilePaths.FirstOrDefault(f => f.EndsWith(".shp", StringComparison.OrdinalIgnoreCase)); if (string.IsNullOrEmpty(shpPath)) continue; // 读取投影信息 (prj) var prjPath = groupFilePaths.FirstOrDefault(f => f.EndsWith(".prj", StringComparison.OrdinalIgnoreCase)); string projectionWkt = string.Empty; if (!string.IsNullOrEmpty(prjPath)) { projectionWkt = await File.ReadAllTextAsync(prjPath, stoppingToken); } var fileImportInfoId = _idWorker.NextId().ToString(); fileImportInfoList.Add(new FileImportInfo { Id = fileImportInfoId, FileImportId = importId, LayerName = layerName, Projection = projectionWkt }); var attributeFields = new List<string>(); // 解析shp获取属性字段 using var shapefile = Shapefile.OpenRead(shpPath); if (shapefile != null) { attributeFields = shapefile.Fields.Select(field => field.Name).ToList(); } if (attributeFields.Count > 0) { attributeFields.ForEach(field => { fileImportAttributeList.Add(new FileImportAttribute { Id = _idWorker.NextId().ToString(), FileImportInfoId = fileImportInfoId, AttributeName = field }); }); } } _dataGovernanceData.AddFileImportInfo(importId, fileImportInfoList, fileImportAttributeList); } catch (Exception) { _dataGovernanceData.UpdateFileImport(importId); } finally { // 清理临时文件 CleanupTempDirectory(tempDir); } } /// <summary> /// 解析 GeoJSON 文件 /// </summary> private async Task ParseGeoJsonFilesAsync(string importId, string tempDir, CancellationToken stoppingToken) { // 1. 找到所有 .json 或 .geojson 文件 var geoJsonFiles = Directory.GetFiles(tempDir) .Where(f => f.EndsWith(".json", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".geojson", StringComparison.OrdinalIgnoreCase)) .ToList(); if (geoJsonFiles.Count == 0) { throw new Exception("临时目录中未找到 .json 或 .geojson 文件"); } try { var fileImportInfoList = new List<FileImportInfo>(); var fileImportAttributeList = new List<FileImportAttribute>(); // 2. 遍历解析每个 GeoJSON 文件 foreach (var filePath in geoJsonFiles) { var fileName = Path.GetFileNameWithoutExtension(filePath); // 取文件名作为图层名 // 3. 读取并解析 GeoJSON 内容 var geoJsonContent = await File.ReadAllTextAsync(filePath, stoppingToken); var geoJsonReader = new NetTopologySuite.IO.GeoJsonReader(); var geometry = geoJsonReader.Read<NetTopologySuite.Features.FeatureCollection>(geoJsonContent); var fileImportInfoId = _idWorker.NextId().ToString(); fileImportInfoList.Add(new FileImportInfo { Id = fileImportInfoId, FileImportId = importId, LayerName = fileName, Projection = "EPSG:4326" }); List<string> attributeNames = []; if (geometry.Count > 0) { var firstFeature = geometry[0]; if (firstFeature.Attributes != null) { attributeNames = [.. firstFeature.Attributes.GetNames()]; } } if (attributeNames.Count > 0) { attributeNames.ForEach(field => { fileImportAttributeList.Add(new FileImportAttribute { Id = _idWorker.NextId().ToString(), FileImportInfoId = fileImportInfoId, AttributeName = field }); }); } } //同一事务的新增接口 _dataGovernanceData.AddFileImportInfo(importId, fileImportInfoList, fileImportAttributeList); } catch (Exception) {
//更新状态接口 _dataGovernanceData.UpdateFileImport(importId); } finally { // 清理临时文件 CleanupTempDirectory(tempDir); } } /// <summary> /// 删除文件 /// </summary> /// <param name="path"></param> private void CleanupTempDirectory(string path) { if (Directory.Exists(path)) { Directory.Delete(path, true); } } }
3.控制器代码
using Microsoft.AspNetCore.Mvc; using Snowflake.Core; namespace YMW.Gp.AgsInterface.Controllers { /// <summary> /// 数据治理 /// </summary> [Route("ags/[controller]/[action]")] public class DataGovernanceController : BaseController { private readonly IDataGovernanceService _dataGovernanceService; private readonly IdWorker _idWorker; private readonly IShapefileParseQueue _queue; /// <summary> /// 构造函数 /// </summary> public DataGovernanceController(IDataGovernanceService dataGovernanceService, IdWorker idWorker, IShapefileParseQueue queue) { _dataGovernanceService = dataGovernanceService; _idWorker = idWorker; _queue = queue; } /// <summary> /// 提交文件导入 /// </summary> /// <param name="formCollection">文件</param> /// <param name="dataType">数据类型 0SHP 1GeoJson 2中间库</param> /// <returns></returns> [HttpPost] public async Task<ActionResult<string>> UploadShapefile([FromForm] IFormCollection formCollection, string dataType) { var result = string.Empty; var now = DateTime.Now; var files = formCollection.Files; if (files == null || files.Count == 0) return BadRequest("未上传文件"); var importId = _idWorker.NextId().ToString(); var tempDir = Path.Combine(Path.GetTempPath(), "ShapefileImport", importId.ToString()); Directory.CreateDirectory(tempDir); try { if (dataType == "0") { var layerGroups = files .GroupBy(f => { var fileName = f.FileName; var firstDotIndex = fileName.IndexOf('.'); // 没有点/点在首位,直接用完整文件名;否则取第一个点之前的内容作为图层名 return firstDotIndex <= 0 ? fileName : fileName.Substring(0, firstDotIndex); }) .ToDictionary(g => g.Key, g => g.ToList()); foreach (var group in layerGroups) { var layerName = group.Key; var layerFiles = group.Value; // 校验核心文件 var shp = layerFiles.FirstOrDefault(f => f.FileName.EndsWith(".shp", StringComparison.OrdinalIgnoreCase)); var shx = layerFiles.FirstOrDefault(f => f.FileName.EndsWith(".shx", StringComparison.OrdinalIgnoreCase)); var dbf = layerFiles.FirstOrDefault(f => f.FileName.EndsWith(".dbf", StringComparison.OrdinalIgnoreCase)); if (shp == null || shx == null || dbf == null) return BadRequest($"图层 [{layerName}] 缺少 shp/shx/dbf 核心文件"); } } else if (dataType == "1") { var geoJsonFile = files.FirstOrDefault(f => f.FileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase) || f.FileName.EndsWith(".geojson", StringComparison.OrdinalIgnoreCase)); if (geoJsonFile == null) { return BadRequest("GeoJSON格式请上传 .json 或 .geojson 后缀的文件"); } } //保存文件到本地 foreach (var file in files) { if (file.Length <= 0) continue; var savePath = Path.Combine(tempDir, file.FileName); await using var fileStream = new FileStream(savePath, FileMode.Create); await file.CopyToAsync(fileStream); } var importRecord = new FileImport { Id = importId, State = "0", ImportDate = now, FileType = dataType }; //插入记录接口 var insert = _dataGovernanceService.InsertFileImport(importRecord); if (string.IsNullOrEmpty(insert)) { //将任务推入后台队列 await _queue.QueueAsync(new ShapefileParseTask { ImportId = importId, TempDirectoryPath = tempDir, DataType = dataType }); } } catch (Exception ex) { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); result = ex.ToString(); } return string.IsNullOrEmpty(result) ? Ok("提交成功!") : BadRequest(result); } } }
4.Program.cs 注册(必须严格按这个写,不能重复注册)
var builder = WebApplication.CreateBuilder(args); // 1. 添加控制器 builder.Services.AddControllers(); // 2. 注册异步队列(单例,全局唯一,只能注册1次) builder.Services.AddSingleton<IShapefileParseQueue, ShapefileParseQueue>(); // 3. 注册后台解析服务(必须,只能注册1次) builder.Services.AddHostedService<ShapefileParseHostedService>(); // 4. 大文件上传配置(保留你之前的配置) builder.Services.Configure<Microsoft.AspNetCore.Http.Features.FormOptions>(opt => { opt.MultipartBodyLengthLimit = long.MaxValue; }); builder.WebHost.ConfigureKestrel(opt => { opt.Limits.MaxRequestBodySize = long.MaxValue; }); // 5. Swagger配置(调试用,可选) builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // 开发环境启用Swagger if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
根据需求,核心解决接口快速返回 + 后台异步解析 + 状态持久化三大核心问题,采用「接口同步落库 + 文件持久化 + Channel 后台任务队列异步解析」的方案,完美解决大文件解析超时问题,同时保证任务可靠性和数据一致性。

浙公网安备 33010602011771号