You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#从数据库提取Blob存为文件时BinaryWriter.Write报越界错误

问题根因

你遇到的报错由明确的逻辑错误触发,除此之外代码中还存在多个会导致文件损坏、资源泄漏的隐藏问题:

  • 直接触发异常的错误:最后写入剩余字节的代码多写了无意义的-1运算。当最后一次调用reader.GetBytes返回的读取字节数retval为0(即Blob内容已经全部读完,无剩余字节)时,(int)retval - 1的计算结果为-1,给BinaryWriter.Write传入负数的count参数会直接触发你看到的参数越界异常;哪怕retval不为0,这个减1操作也会丢失每个文件的最后1个字节,导致导出的文件损坏无法打开。
  • 读取偏移量未重置:你把记录Blob读取位置的startIndex变量定义在了所有文件循环的外层,处理完第一个文件后这个变量不会归零,后续处理其他文件时会从错误的偏移位置开始读取,要么读不到内容要么读出乱码。
  • 其他隐藏问题:
    • 手动调用Close()方法释放资源的写法不可靠,一旦执行过程中抛出异常,文件流、数据库连接、阅读器都不会被正确释放,会出现文件被占用、数据库连接池耗尽的问题
    • 直接用字符串加号拼接文件路径,没有处理路径分隔符,若传入的保存目录末尾不带路径分隔符,会导致目录名和文件名拼接错误
    • 给SqlParameter传值时把Guid转成了字符串,会让数据库执行时发生隐式类型转换,可能导致查询性能下降甚至查不到结果
    • 缓冲区大小设置为100字节太小,大文件导出效率极低
修正后的实现代码
public static void ExtractFiles(string connectionString, List<Guid> itemDetailIds, List<string> filenames, string fileLocation)
{
    Directory.CreateDirectory(fileLocation);
    // 缓冲区调整为更合理的8KB大小
    const int bufferSize = 8192;
    byte[] outbyte = new byte[bufferSize];

    // 提前清理所有非法文件名字符
    for (var i = 0; i < filenames.Count; i++)
    {
        filenames[i] = CleanFileName(filenames[i]);
    }

    const string sql = @"select did.FileExtension, did.Blob 
                        From ItemView v 
                        inner join DocumentItemDetail did on did.ItemDetailID = v.ItemDetailId 
                        Where did.ItemDetailID = @ItemDetailId AND v.Type='Document' AND did.Blob IS NOT NULL";

    // 用using包裹所有实现IDisposable的资源,自动释放,不需要手动调用Close
    using var cnn = new SqlConnection(connectionString);
    using var sqlCommand = new SqlCommand(sql, cnn);
    // 提前定义参数,循环中仅修改参数值,无需每次清空参数集合
    var itemIdParam = sqlCommand.Parameters.Add("@ItemDetailId", SqlDbType.UniqueIdentifier);
    
    cnn.Open();
    for (var i = 0; i < itemDetailIds.Count; i++)
    {
        // 每个文件读取前重置偏移量为0
        long startIndex = 0;
        itemIdParam.Value = itemDetailIds[i];
        
        using var reader = sqlCommand.ExecuteReader(CommandBehavior.SequentialAccess);
        while (reader.Read())
        {
            string fileExt = reader.GetString(0);
            // 用Path.Combine拼接路径,自动处理路径分隔符
            string savePath = Path.Combine(fileLocation, $"{filenames[i]}{fileExt}");
            
            using var fs = new FileStream(savePath, FileMode.OpenOrCreate, FileAccess.Write);
            using var bw = new BinaryWriter(fs);
            
            long retval = reader.GetBytes(1, startIndex, outbyte, 0, bufferSize);
            while (retval == bufferSize)
            {
                bw.Write(outbyte);
                bw.Flush();
                startIndex += bufferSize;
                retval = reader.GetBytes(1, startIndex, outbyte, 0, bufferSize);
            }
            // 移除错误的-1运算,仅当有剩余字节时写入
            if (retval > 0)
            {
                bw.Write(outbyte, 0, (int)retval);
            }
            bw.Flush();
        }
    }
}
修正点说明
  • 所有实现了IDisposable的对象都用using包裹,无论是否抛出异常都会自动释放资源,不会出现资源泄漏
  • 偏移量startIndex放在每个文件的循环内部定义,处理每个文件时都会从0位置开始读取
  • 用Path.Combine处理路径拼接,不需要手动关心目录末尾有没有路径分隔符
  • 参数类型直接指定为UniqueIdentifier对应C#的Guid类型,不需要转字符串,避免隐式类型转换带来的性能问题
  • 最后写入剩余字节前先判断retval是否大于0,避免写入0字节的无效操作,同时去掉了错误的减1逻辑,不会丢失字节也不会传入负数count触发异常
  • 缓冲区大小调整为8KB,大文件导出速度会有明显提升

内容的提问来源于stack exchange,提问作者lross15

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 02:36:32