使用CSV Helper在转换值时捕获错误并返回错误列表?

cnh2zyt3  于 2023-05-26  发布在  其他
关注(0)|答案(1)|浏览(129)

我正在编写一个端点,它使用CSV Helper库接收CSV文件,进行一些验证检查/转换,然后使用SQL Bulk Copy将数据持久化到数据库(CSV将有超过一百万条记录)。对于这个端点,我希望返回阅读CSV时发生的错误列表,例如转换出错或值的格式不正确。目前,我正在使用CSV Helper进行一些转换,比如将字符串转换为枚举,将日期转换为有效的UTC日期/时间。我还有一个属性需要是特定的格式,所以我使用Fluent Validation来创建一个检查该格式的规则。我能够很好地抓住所有发生的错误,因为Fluent Validation在根据给定的规则验证给定记录时提供了一个错误集合。
我遇到的问题是当我做转换。我能够进行枚举转换,如果无法将字符串值解析为枚举,则会抛出异常,但这是在我决定收集所有错误并在将所有错误返回给客户端之前继续阅读CSV,以便他们可以编辑CSV并修复错误。
我将使用虚拟字段,因为它仍然会说明我遇到的相同问题:
DummyModel.cs:

public class DummyModel
{
        public string DummyId { get; set; }
        public DateTime DummyDate { get; set; }
        public MyCode DummyCode { get; set; }
        public int? DummyNumberId { get; set; }
        public DateTime? PickUpDate { get; set; }
}

我的DummyModel类的类Map(也在DummyModel.cs中):

public class DummyModelMap : ClassMap<DummyModel>
{
        public DummyModelMap()
        {
            Map(p => p.DummyId).Name("DummyId", "Dummy Id");
            Map(p => p.DummyDate).Name("Date", "Dt").TypeConverterOption.DateTimeStyles(DateTimeStyles.AdjustToUniversal);
            Map(p => p.DummyCode).Name("DummyCode", "Dummy Cd").TypeConverter<DummyCodeEnumConverter<MyCode>>();
            Map(p => p.DummyNumberId).Name("DummyNumberId", "Dummy Number Id").TypeConverter<EmptyStringToIntConverter<int>>();
            Map(p => p.PickUpDate).Name("PickUpDate", "Pick Up Dt").TypeConverterOption.DateTimeStyles(DateTimeStyles.AdjustToUniversal);
        }
}

MyCode枚举(Utilities.cs):

public enum MyCode
{
    ABC = 0,
    DEF = 1,
    GHI = 2,
    JKL = 3,
    M_AND_N = 4 // This will look like M&A in the CSV.
}

我的自定义TypeConverters(Converters.cs):

public class DummyCodeEnumConverter<T> : EnumConverter where T : struct
    {
        public DummyCodeEnumConverter() : base(typeof(T)) { }

        public override object ConvertFromString(string text, IReader row, MemberMapData memberMapData)
        {
            if (!Enum.TryParse(text, out MyCode code))
            {
               /* SPECIAL CASE: The string value (M_AND_N) does not match what will
                * appear in the CSVs that are uploaded (M&A), so we need to check for that here
                * and convert to the correct MyCode enum.
                */
                if (text == "M&A")
                {
                    return MyCode.M_AND_N;
                }

                // If an invalid value is found in the CSV for the Dummy Code column, throw an exception.
                throw new InvalidCastException($"Invalid value to TypeConverter. Type: {typeof(T)} Value: {text}");
            }

            return code;
         }
    }

    /* This custom Type Converter will convert any empty strings
     * to a null value for fields that are integers.
     */
    public class EmptyStringToIntConverter<T> : TypeConverter where T : struct
    {
        public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
        {
            if (!int.TryParse(text, out int result))
            {
                // Convert any empty strings to return a null value for the record's int data field.
                if (text == " " || text == "")
                {
                    return null;
                }

                // If an invalid value is found in the CSV for an int data type column, throw an exception.
                throw new InvalidCastException($"Invalid value to TypeConverter. Type: {typeof(T)} Value: {text}");
            }

            return result;
        }
    }

我的Fluent Validation验证器(Validators):

public class DummyModelValidator : AbstractValidator<DummyModel>
 {
        public DummyModelValidator()
        {
            RuleFor(dm => dm.DummyId).Matches(@"^([A-Za-z0-9]-[A-Za-z0-9]-([A-Za-z0-9]{2})){1,1}$")
                .WithMessage("Dummy Id must be in the following format: B-B-BB (i.e. 2-B-CC).")
                .NotEmpty()
                .WithMessage("Dummy Id cannot be empty.");
        }
 }

我的RecordValidationResult类。这将是返回给客户端的错误集合(它看起来与Fluent Validation的Errors集合中的error对象非常相似):

public class RecordValidationResult
{
    public int RowNumber { get; set; }
    public string PropertyName { get; set; }
    public string Message { get; set; }
    public object AttemptedValue { get; set; }
}

CSV帮助程序配置(DummyModelController.cs):

var errorsList = new List<RecordValidationResult>();

var config = new CsvConfiguration(CultureInfo.InvariantCulture)
                    {
                        // Delimiter may differ from ','.
                        DetectDelimiter = true,
                        IgnoreBlankLines = true,
                        ExceptionMessagesContainRawData = true,

                        /* Add any conversion exceptions that occur while the CSV Reader
                         * parses the file. Fluent Validation will handle other
                         * validations.
                         */
                        ReadingExceptionOccurred = x =>
                        {
                            // Catch any conversion/null errors here and create
                            // a new RecordValidationResult for each and add it
                            // to the errorsList.
                            return false;
                        }
                    };

这是我处理Fluent Validation错误的地方(DummyModelController.cs):

using (var csvReader = new CsvReader(streamReader, config))
                    {
                        //...

                        csvReader.Context.RegisterClassMap<DummyModelMap>();

                        // Validate records and add those that return errors to a list.
                        DummyModelValidator validator = new DummyModelValidator();

                        foreach (var record in csvReader.GetRecords<DummyModel>())
                        {
                            var rowNumber = csvReader.Context.Parser.Row;

                            var results = validator.Validate(record);

                            if (!results.IsValid)
                            {
                                foreach (var failure in results.Errors)
                                {
                                    var vr = new RecordValidationResult()
                                    {
                                        RowNumber = rowNumber,
                                        PropertyName = failure.PropertyName,
                                        AttemptedValue = failure.AttemptedValue,
                                        Message = failure.ErrorMessage,
                                    };

                                    errorsList.Add(vr);
                                }
                            }
                        }

                        // Do not persist data if the errors list is greater than 0.
                        if (errorsList.Count() > 0)
                            return BadRequest(errorsList);

                        //...SQL Bulk Copy stuff...
                    }

那么,在我的自定义TypeConverters中抛出InvalidCastExceptions的地方,我可以做些什么来让CSV Helper配置中的ReadingExceptionOccurred委托可以捕获转换错误,以便我可以使用该行的错误信息创建一个新的RecordValidationResult对象?例如,如果一个给定的字符串,'拜特'没有被解析并转换为MyCode枚举,有没有一种方法可以返回一个RecordValidationResult对象来添加到我的errorsList中,然后继续解析CSV以获取更多的错误?
我已经尝试“欺骗”我的自定义转换器,通过有目的地返回不同的数据类型并使用来自异常和内部异常消息的信息来创建RecordValidationResult对象,从而引发TypeConversion异常。直到我意识到如果我在一行中有多个错误,第一个捕获的错误只会被添加到列表中,其他错误不会。当我搜索不同的属性名以确定在RecordValidationResult对象中放置什么作为我的“PropertyName”值时,代码也非常糟糕。
我还尝试让Fluent Validation做解析工作,比如捕捉字符串是否是无效的DateTime,或者字符串是否可以被解析为MyCode枚举(我已经将DummyModel属性调整为所有字符串),这样我就可以简单地从Errors集合中获取错误,但它在验证时太慢了(比如过去12分钟),我只是停止运行它。我还有另一个自定义转换器,它会将所有“M&N”字符串更改为“M_AND_N”,所以当我稍后进行转换时,它们会这样做,但当我试图操作相同的字符串数据类型时,我最终得到了TypeConversion错误?
我认为最好的方法是保持转换器的原样,但用行错误信息捕获转换错误,将其添加到我的errorsList中,忽略异常并继续下一行。这可能吗?或者我应该考虑将所有这些信息记录到一个文件中,并在完成后向客户端显示?
感谢您的任何帮助/建议!

ifmq2ha2

ifmq2ha21#

也许不完美,但这样的工作会成功吗?

ReadingExceptionOccurred = x =>
{
    errorsList.Add(new RecordValidationResult
    {
        RowNumber = x.Exception.Context.Parser.Row,
        PropertyName = x.Exception.Context.Reader.HeaderRecord[x.Exception.Context.Reader.CurrentIndex],
        Message = x.Exception.InnerException.Message,
        AttemptedValue = x.Exception.Context.Parser.Record[x.Exception.Context.Reader.CurrentIndex]
    });
    return false;
}

对我来说,这将添加一个包含以下数据的RecordValidationResult并继续处理。

RecordValidationResult 
{
  RowNumber = 2, 
  PropertyName = "DummyCode", 
  Message = "Invalid value to TypeConverter. Type: MyCode  Value: BYT",
  AttemptedValue = "BYT"
}

相关问题