diff --git a/IRaCIS.Core.Application/Helper/OtherTool/DicomSortHelper.cs b/IRaCIS.Core.Application/Helper/OtherTool/DicomSortHelper.cs index ee63f5c53..ab0d67063 100644 --- a/IRaCIS.Core.Application/Helper/OtherTool/DicomSortHelper.cs +++ b/IRaCIS.Core.Application/Helper/OtherTool/DicomSortHelper.cs @@ -129,7 +129,7 @@ public sealed class DicomMaskOptions /// 灰度图像的遮挡像素值,默认 0。 /// 注意:这表示写入的原始像素值,不保证视觉上一定为黑色。 /// - public ushort GrayscaleMaskValue { get; init; } = 0; + //public ushort GrayscaleMaskValue { get; init; } = 0; /// /// 彩色图像的遮挡值,默认 [0,0,0]。 @@ -147,6 +147,19 @@ public sealed class DicomMaskOptions /// 是否更新 BurnedInAnnotation 为 NO。 /// public bool UpdateBurnedInAnnotationToNo { get; init; } = false; + + + /// + /// 是否启用自动灰度遮挡值。 + /// true 时,根据 PhotometricInterpretation + BitsStored + PixelRepresentation 自动选择更合理的值。 + /// + public bool AutoSelectGrayscaleMaskValue { get; init; } = true; + + /// + /// 当 AutoSelectGrayscaleMaskValue=false 时,使用该值。 + /// 当为 null 且 AutoSelectGrayscaleMaskValue=false 时,默认 0。 + /// + public int? GrayscaleMaskValue { get; init; } } @@ -188,7 +201,7 @@ public static class DicomPixelMasker ValidateDataset(originalDataset); - var originalTransferSyntax = originalFile.FileMetaInfo.TransferSyntax; + var originalTs = originalFile.FileMetaInfo.TransferSyntax; var originalPhotometric = originalDataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty); var rows = originalDataset.GetSingleValue(DicomTag.Rows); @@ -198,36 +211,32 @@ public static class DicomPixelMasker var samplesPerPixel = originalDataset.GetSingleValue(DicomTag.SamplesPerPixel); var pixelRepresentation = originalDataset.GetSingleValueOrDefault(DicomTag.PixelRepresentation, (ushort)0); var planarConfiguration = originalDataset.GetSingleValueOrDefault(DicomTag.PlanarConfiguration, (ushort)0); - //log?.Invoke($"Rows={rows}, Cols={cols}, BitsAllocated={bitsAllocated}, BitsStored={bitsStored}, SamplesPerPixel={samplesPerPixel}, PixelRepresentation={pixelRepresentation}, PlanarConfiguration={planarConfiguration}"); + Console.WriteLine($"Rows={rows}, Cols={cols}, BitsAllocated={bitsAllocated}, BitsStored={bitsStored}, SamplesPerPixel={samplesPerPixel}, PixelRepresentation={pixelRepresentation}, PlanarConfiguration={planarConfiguration}"); + EnsureSupportedPhotometric(originalPhotometric, samplesPerPixel); // 1. 转为工作用的未压缩 DICOM var workingFile = await EnsureUncompressedAsync(originalFile, cancellationToken).ConfigureAwait(false); - var workingDataset = workingFile.Dataset; - // 2. 修改像素 - MaskPixelDataInPlace(workingDataset, regionList, options); - + MaskPixelDataInPlace(workingFile.Dataset, regionList, options); // 3. 保持原 PhotometricInterpretation if (!string.IsNullOrWhiteSpace(originalPhotometric)) { - workingDataset.AddOrUpdate(DicomTag.PhotometricInterpretation, originalPhotometric); + workingFile.Dataset.AddOrUpdate(DicomTag.PhotometricInterpretation, originalPhotometric); } - // 4. 可选更新 BurnedInAnnotation if (options.UpdateBurnedInAnnotationToNo) { - workingDataset.AddOrUpdate(DicomTag.BurnedInAnnotation, "NO"); + workingFile.Dataset.AddOrUpdate(DicomTag.BurnedInAnnotation, "NO"); } - - // 5. 转回原始 TransferSyntax + // 5. 编码回原始传输语法 var finalFile = await ReEncodeToOriginalTransferSyntaxAsync( workingFile, - originalTransferSyntax, + originalTs, options.StrictKeepTransferSyntax, cancellationToken).ConfigureAwait(false); - finalFile.FileMetaInfo.TransferSyntax = originalTransferSyntax; + finalFile.FileMetaInfo.TransferSyntax = originalTs; if (output.CanSeek) output.SetLength(0); @@ -236,6 +245,7 @@ public static class DicomPixelMasker if (output.CanSeek) output.Position = 0; + } /// @@ -254,6 +264,37 @@ public static class DicomPixelMasker _ = dataset.GetSingleValue(DicomTag.SamplesPerPixel); } + private static void EnsureSupportedPhotometric(string photometric, int samplesPerPixel, Action? log = null) + { + if (samplesPerPixel == 1) + { + if (string.Equals(photometric, "MONOCHROME1", StringComparison.OrdinalIgnoreCase) || + string.Equals(photometric, "MONOCHROME2", StringComparison.OrdinalIgnoreCase)) + { + return; + } + throw new NotSupportedException($"Unsupported grayscale PhotometricInterpretation: {photometric}"); + } + if (samplesPerPixel == 3) + { + if (string.Equals(photometric, "RGB", StringComparison.OrdinalIgnoreCase) || + string.Equals(photometric, "YBR_FULL", StringComparison.OrdinalIgnoreCase)) + { + return; + } + // 对 YBR_FULL_422 不建议直接按当前布局改 + if (string.Equals(photometric, "YBR_FULL_422", StringComparison.OrdinalIgnoreCase) || + string.Equals(photometric, "YBR_PARTIAL_422", StringComparison.OrdinalIgnoreCase) || + string.Equals(photometric, "YBR_PARTIAL_420", StringComparison.OrdinalIgnoreCase)) + { + throw new NotSupportedException( + $"PhotometricInterpretation={photometric} uses subsampling layout and is not safely supported by this pixel masking implementation."); + } + throw new NotSupportedException($"Unsupported color PhotometricInterpretation: {photometric}"); + } + throw new NotSupportedException($"Unsupported SamplesPerPixel={samplesPerPixel}"); + } + /// /// 转为工作用的未压缩 DICOM /// @@ -288,59 +329,35 @@ public static class DicomPixelMasker /// /// 转回原始 TransferSyntax /// - /// + /// /// /// /// /// /// private static async Task ReEncodeToOriginalTransferSyntaxAsync( - DicomFile uncompressedFile, - DicomTransferSyntax originalTransferSyntax, - bool strictKeepTransferSyntax, - CancellationToken cancellationToken) + DicomFile workingFile, + DicomTransferSyntax originalTransferSyntax, + bool strictKeepTransferSyntax, + CancellationToken cancellationToken) { - if (!originalTransferSyntax.IsEncapsulated) - { - if (uncompressedFile.FileMetaInfo.TransferSyntax == originalTransferSyntax) - { - return uncompressedFile; - } - - try - { - var transcoder = new DicomTranscoder( - uncompressedFile.FileMetaInfo.TransferSyntax, - originalTransferSyntax); - - return await Task.Run(() => transcoder.Transcode(uncompressedFile), cancellationToken) - .ConfigureAwait(false); - } - catch (Exception ex) - { - throw new InvalidOperationException( - $"Failed to convert dataset back to original uncompressed TransferSyntax {originalTransferSyntax.UID.UID}.", - ex); - } - } - + if (workingFile.FileMetaInfo.TransferSyntax == originalTransferSyntax) + return workingFile; try { - var currentTs = uncompressedFile.FileMetaInfo.TransferSyntax; + var currentTs = workingFile.FileMetaInfo.TransferSyntax; var transcoder = new DicomTranscoder(currentTs, originalTransferSyntax); - return await Task.Run(() => transcoder.Transcode(uncompressedFile), cancellationToken) - .ConfigureAwait(false); + return await Task.Run(() => transcoder.Transcode(workingFile), cancellationToken).ConfigureAwait(false); } catch (Exception ex) { if (strictKeepTransferSyntax) { throw new InvalidOperationException( - $"Failed to re-encode DICOM back to original compressed TransferSyntax {originalTransferSyntax.UID.UID}.", + $"Failed to re-encode DICOM back to original TransferSyntax {originalTransferSyntax.UID.UID}.", ex); } - - return uncompressedFile; + return workingFile; } } @@ -351,77 +368,66 @@ public static class DicomPixelMasker /// /// private static void MaskPixelDataInPlace( - DicomDataset dataset, - IReadOnlyList regions, - DicomMaskOptions options) + DicomDataset dataset, + IReadOnlyList regions, + DicomMaskOptions options) { var rows = dataset.GetSingleValue(DicomTag.Rows); var cols = dataset.GetSingleValue(DicomTag.Columns); var bitsAllocated = dataset.GetSingleValue(DicomTag.BitsAllocated); + var bitsStored = dataset.GetSingleValueOrDefault(DicomTag.BitsStored, bitsAllocated); var samplesPerPixel = dataset.GetSingleValue(DicomTag.SamplesPerPixel); var pixelRepresentation = dataset.GetSingleValueOrDefault(DicomTag.PixelRepresentation, (ushort)0); var photometric = dataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty); var planarConfiguration = dataset.GetSingleValueOrDefault(DicomTag.PlanarConfiguration, (ushort)0); - var pixelData = DicomPixelData.Create(dataset, false); var frameCount = pixelData.NumberOfFrames; - var framesToProcess = ResolveFrames(frameCount, options.FrameIndices); - - var newFrames = new List(frameCount); - + var replacementFrames = new List(frameCount); for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { var frame = pixelData.GetFrame(frameIndex); - var data = frame.Data.ToArray(); - + var buffer = frame.Data; + var bytes = buffer.ToArray(); if (framesToProcess.Contains(frameIndex)) { ApplyMaskToFrame( - data, - rows, - cols, - bitsAllocated, - samplesPerPixel, - pixelRepresentation, - photometric, - planarConfiguration, - regions, - options); + frameData: bytes, + rows: rows, + cols: cols, + bitsAllocated: bitsAllocated, + bitsStored: bitsStored, + samplesPerPixel: samplesPerPixel, + pixelRepresentation: pixelRepresentation, + photometric: photometric, + planarConfiguration: planarConfiguration, + regions: regions, + options: options); } - - newFrames.Add(new MemoryByteBuffer(data)); + replacementFrames.Add(new MemoryByteBuffer(bytes)); } - - ReplacePixelDataFrames(dataset, pixelData, newFrames); + ReplacePixelDataFrames(dataset, pixelData, replacementFrames); } private static HashSet ResolveFrames(int frameCount, IReadOnlyCollection? frameIndices) { if (frameIndices == null || frameIndices.Count == 0) - { return Enumerable.Range(0, frameCount).ToHashSet(); - } - var result = new HashSet(); - foreach (var i in frameIndices) + foreach (var index in frameIndices) { - if (i < 0 || i >= frameCount) - throw new ArgumentOutOfRangeException(nameof(frameIndices), $"Frame index {i} out of range [0, {frameCount - 1}]"); - - result.Add(i); + if (index < 0 || index >= frameCount) + throw new ArgumentOutOfRangeException(nameof(frameIndices), $"Frame index {index} out of range [0, {frameCount - 1}]"); + result.Add(index); } - return result; } - private static void ReplacePixelDataFrames( DicomDataset dataset, DicomPixelData sourcePixelData, IReadOnlyList frames) { dataset.Remove(DicomTag.PixelData); - var newPixelData = DicomPixelData.Create(dataset, true); //newPixelData.BitsAllocated = sourcePixelData.BitsAllocated; newPixelData.BitsStored = sourcePixelData.BitsStored; @@ -432,7 +438,6 @@ public static class DicomPixelMasker newPixelData.Height = sourcePixelData.Height; newPixelData.Width = sourcePixelData.Width; newPixelData.PhotometricInterpretation = sourcePixelData.PhotometricInterpretation; - foreach (var frame in frames) { newPixelData.AddFrame(frame); @@ -440,63 +445,106 @@ public static class DicomPixelMasker } private static void ApplyMaskToFrame( - byte[] frameData, - int rows, - int cols, - int bitsAllocated, - int samplesPerPixel, - ushort pixelRepresentation, - string photometric, - ushort planarConfiguration, - IReadOnlyList regions, - DicomMaskOptions options) + byte[] frameData, + int rows, + int cols, + int bitsAllocated, + int bitsStored, + int samplesPerPixel, + ushort pixelRepresentation, + string photometric, + ushort planarConfiguration, + IReadOnlyList regions, + DicomMaskOptions options) { if (samplesPerPixel == 1) { if (bitsAllocated == 8) { - //8 位灰度 - ApplyMask_Grayscale8(frameData, rows, cols, regions, (byte)Math.Min(options.GrayscaleMaskValue, byte.MaxValue)); + byte maskValue8 = ResolveGrayscaleMaskValue8(photometric, options); + ApplyMask_Grayscale8(frameData, rows, cols, regions, maskValue8); return; } - - // 16 位灰度 if (bitsAllocated == 16) { - ApplyMask_Grayscale16(frameData, rows, cols, regions, options.GrayscaleMaskValue); + ushort maskValue16 = ResolveGrayscaleMaskValue16( + photometric, + bitsStored, + pixelRepresentation, + options); + ApplyMask_Grayscale16(frameData, rows, cols, regions, maskValue16); return; } - - throw new NotSupportedException($"Unsupported grayscale BitsAllocated={bitsAllocated}, Photometric={photometric}."); + throw new NotSupportedException( + $"Unsupported grayscale image: BitsAllocated={bitsAllocated}, Photometric={photometric}"); } - - //RGB 彩图 if (samplesPerPixel == 3) { if (bitsAllocated != 8) { - throw new NotSupportedException($"Unsupported color image: SamplesPerPixel=3 but BitsAllocated={bitsAllocated}."); + throw new NotSupportedException( + $"Unsupported color image: SamplesPerPixel=3, BitsAllocated={bitsAllocated}, Photometric={photometric}"); } - if (planarConfiguration == 0) { ApplyMask_Color8_Interleaved(frameData, rows, cols, regions, options.ColorMaskValue); return; } - if (planarConfiguration == 1) { ApplyMask_Color8_Planar(frameData, rows, cols, regions, options.ColorMaskValue); return; } - - throw new NotSupportedException($"Unsupported PlanarConfiguration={planarConfiguration}, Photometric={photometric}."); + throw new NotSupportedException( + $"Unsupported PlanarConfiguration={planarConfiguration}, Photometric={photometric}"); } - throw new NotSupportedException( - $"Unsupported image format: SamplesPerPixel={samplesPerPixel}, BitsAllocated={bitsAllocated}, PixelRepresentation={pixelRepresentation}, Photometric={photometric}."); + $"Unsupported format: SamplesPerPixel={samplesPerPixel}, BitsAllocated={bitsAllocated}, Photometric={photometric}"); + } + private static byte ResolveGrayscaleMaskValue8(string photometric, DicomMaskOptions options) + { + if (!options.AutoSelectGrayscaleMaskValue) + { + var value = options.GrayscaleMaskValue ?? 0; + return (byte)Math.Clamp(value, byte.MinValue, byte.MaxValue); + } + if (string.Equals(photometric, "MONOCHROME1", StringComparison.OrdinalIgnoreCase)) + return byte.MaxValue; + return byte.MinValue; + } + private static ushort ResolveGrayscaleMaskValue16( + string photometric, + int bitsStored, + ushort pixelRepresentation, + DicomMaskOptions options) + { + if (!options.AutoSelectGrayscaleMaskValue) + { + var value = options.GrayscaleMaskValue ?? 0; + return unchecked((ushort)value); + } + bool isSigned = pixelRepresentation == 1; + bool mono1 = string.Equals(photometric, "MONOCHROME1", StringComparison.OrdinalIgnoreCase); + bitsStored = Math.Clamp(bitsStored, 1, 16); + if (!isSigned) + { + ushort min = 0; + ushort max = bitsStored == 16 + ? ushort.MaxValue + : (ushort)((1 << bitsStored) - 1); + return mono1 ? max : min; + } + else + { + // signed range based on BitsStored + // minSigned = -(1 << (bitsStored - 1)) + // maxSigned = (1 << (bitsStored - 1)) - 1 + int minSigned = -(1 << (bitsStored - 1)); + int maxSigned = (1 << (bitsStored - 1)) - 1; + short selected = mono1 ? (short)maxSigned : (short)minSigned; + return unchecked((ushort)selected); + } } - private static void ApplyMask_Grayscale8( byte[] data, int rows, @@ -508,18 +556,16 @@ public static class DicomPixelMasker { var (left, top, right, bottom) = ClipRegion(region, cols, rows); if (left >= right || top >= bottom) continue; - for (int y = top; y < bottom; y++) { - int rowOffset = y * cols; + int offset = y * cols; for (int x = left; x < right; x++) { - data[rowOffset + x] = maskValue; + data[offset + x] = maskValue; } } } } - private static void ApplyMask_Grayscale16( byte[] data, int rows, @@ -531,22 +577,19 @@ public static class DicomPixelMasker { var (left, top, right, bottom) = ClipRegion(region, cols, rows); if (left >= right || top >= bottom) continue; - for (int y = top; y < bottom; y++) { - int rowOffset = y * cols; + int offset = y * cols; for (int x = left; x < right; x++) { - int pixelIndex = rowOffset + x; + int pixelIndex = offset + x; int byteIndex = pixelIndex * 2; - data[byteIndex] = (byte)(maskValue & 0xFF); data[byteIndex + 1] = (byte)((maskValue >> 8) & 0xFF); } } } } - private static void ApplyMask_Color8_Interleaved( byte[] data, int rows, @@ -556,23 +599,19 @@ public static class DicomPixelMasker { if (color == null || color.Length < 3) throw new ArgumentException("ColorMaskValue must contain at least 3 bytes."); - byte c0 = color[0]; byte c1 = color[1]; byte c2 = color[2]; - foreach (var region in regions) { var (left, top, right, bottom) = ClipRegion(region, cols, rows); if (left >= right || top >= bottom) continue; - for (int y = top; y < bottom; y++) { for (int x = left; x < right; x++) { int pixelIndex = y * cols + x; int byteIndex = pixelIndex * 3; - data[byteIndex] = c0; data[byteIndex + 1] = c1; data[byteIndex + 2] = c2; @@ -580,7 +619,6 @@ public static class DicomPixelMasker } } } - private static void ApplyMask_Color8_Planar( byte[] data, int rows, @@ -590,18 +628,14 @@ public static class DicomPixelMasker { if (color == null || color.Length < 3) throw new ArgumentException("ColorMaskValue must contain at least 3 bytes."); - int planeSize = rows * cols; - byte c0 = color[0]; byte c1 = color[1]; byte c2 = color[2]; - foreach (var region in regions) { var (left, top, right, bottom) = ClipRegion(region, cols, rows); if (left >= right || top >= bottom) continue; - for (int y = top; y < bottom; y++) { int rowOffset = y * cols; @@ -615,8 +649,10 @@ public static class DicomPixelMasker } } } - - private static (int left, int top, int right, int bottom) ClipRegion(MaskRegion region, int imageWidth, int imageHeight) + private static (int left, int top, int right, int bottom) ClipRegion( + MaskRegion region, + int imageWidth, + int imageHeight) { int left = Math.Max(0, region.X); int top = Math.Max(0, region.Y); diff --git a/IRaCIS.Core.Application/Helper/OtherTool/WeComNotifier.cs b/IRaCIS.Core.Application/Helper/OtherTool/WeComNotifier.cs index 7af0b625c..b9f991f43 100644 --- a/IRaCIS.Core.Application/Helper/OtherTool/WeComNotifier.cs +++ b/IRaCIS.Core.Application/Helper/OtherTool/WeComNotifier.cs @@ -1,13 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using IdentityModel; using Newtonsoft.Json; using RestSharp; +using System; +using System.Collections.Generic; +using System.Linq; using System.Net; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; using System.Threading.Tasks; -using IdentityModel; namespace IRaCIS.Core.Application.Helper.OtherTool; @@ -31,6 +32,11 @@ public static class WeComNotifier public static async Task SendAlertAsync(string webhook, WeComAlert alert) { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + await Task.CompletedTask; + } + try { var client = new RestClient(); diff --git a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml index d3dbc1581..5ca2defe6 100644 --- a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml +++ b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml @@ -16157,12 +16157,6 @@ 要处理的帧,null 或空表示全部帧。 - - - 灰度图像的遮挡像素值,默认 0。 - 注意:这表示写入的原始像素值,不保证视觉上一定为黑色。 - - 彩色图像的遮挡值,默认 [0,0,0]。 @@ -16180,6 +16174,18 @@ 是否更新 BurnedInAnnotation 为 NO。 + + + 是否启用自动灰度遮挡值。 + true 时,根据 PhotometricInterpretation + BitsStored + PixelRepresentation 自动选择更合理的值。 + + + + + 当 AutoSelectGrayscaleMaskValue=false 时,使用该值。 + 当为 null 且 AutoSelectGrayscaleMaskValue=false 时,默认 0。 + + 验证dicom tag @@ -16200,7 +16206,7 @@ 转回原始 TransferSyntax - + @@ -17206,17 +17212,17 @@ - ���� + 质疑 - һ���Ժ˲� + 一致性核查 - ���� + 复制 diff --git a/IRaCIS.Core.Application/TestService.cs b/IRaCIS.Core.Application/TestService.cs index 119543e55..861ea57fb 100644 --- a/IRaCIS.Core.Application/TestService.cs +++ b/IRaCIS.Core.Application/TestService.cs @@ -137,10 +137,7 @@ namespace IRaCIS.Core.Application.Service { new MaskRegion(0, 0, 200, 200) }; - var options = new DicomMaskOptions - { - StrictKeepTransferSyntax = true - }; + foreach (var inputPath in Directory.EnumerateFiles(sourceDir, "*.dcm", SearchOption.TopDirectoryOnly)) { var relativePath = Path.GetRelativePath(sourceDir, inputPath); @@ -152,7 +149,19 @@ namespace IRaCIS.Core.Application.Service } await using var input = File.OpenRead(inputPath); await using var output = File.Create(outputPath); - await DicomPixelMasker.MaskAsync(input, output, regions, options); + + try + { + await DicomPixelMasker.MaskAsync(input, output, regions); + + } + catch(Exception ex) + { + // 跳过该文件 + + Console.WriteLine($"error: {ex.Message}"); + } + Console.WriteLine($"Done: {relativePath}"); } diff --git a/IRaCIS.Core.Domain/Reading/ReadingCriterion/ReadingQuestionCriterionTrial.cs b/IRaCIS.Core.Domain/Reading/ReadingCriterion/ReadingQuestionCriterionTrial.cs index d27815e83..ca3df7293 100644 --- a/IRaCIS.Core.Domain/Reading/ReadingCriterion/ReadingQuestionCriterionTrial.cs +++ b/IRaCIS.Core.Domain/Reading/ReadingCriterion/ReadingQuestionCriterionTrial.cs @@ -239,7 +239,6 @@ public enum ReadingOrder } -[ComplexType] public class DefaultSegmentNameDto { public string SegmentationName { get; set; } =string.Empty; diff --git a/IRaCIS.Core.Infra.EFCore/Context/IRaCISDBContext.cs b/IRaCIS.Core.Infra.EFCore/Context/IRaCISDBContext.cs index 700581332..7beb13475 100644 --- a/IRaCIS.Core.Infra.EFCore/Context/IRaCISDBContext.cs +++ b/IRaCIS.Core.Infra.EFCore/Context/IRaCISDBContext.cs @@ -104,10 +104,10 @@ public class IRaCISDBContext : DbContext modelBuilder.Entity(entity => { //默认SegmentName - entity.OwnsOne(x => x.DefaultSegmentName, ownedNavigationBuilder => - { - ownedNavigationBuilder.ToJson(); - }); + entity.Property(e => e.DefaultSegmentName).HasConversion( + v => v == null ? null : JsonConvert.SerializeObject(v), + v => string.IsNullOrEmpty(v) ? new DefaultSegmentNameDto() : JsonConvert.DeserializeObject(v) + ); }); #region pgsql codefirst 配置 暂时屏蔽