0x05-综合示例,导出CSV
现在,我们来完成一个稍微复杂一点的场景用例。
将实体导出为 CSV 文件
为了使下文的示例更加符合生产实际,我们在这里引入一个具体的场景。
我们需要将实体导出为 CSV 文件。
CSV 文件一般包含两个部分。
第一个部分是文件的头部。头部中包含了每一列的列名。
第二个部分是内容部分。内容部分每行都是一条记录。
不论是头部还是内容部分每个属性之间都使用逗号进行分隔。
这里我们使用前文使用到的 OrderInfo
进行演示:
public class OrderInfo
{
public int OrderId { get; set; }
public string Buyer { get; set; }
public decimal TotalPrice { get; set; }
}
则导出的 CSV 样例如下:
OrderId,Buyer,TotalPrice
1,yueluo,99999
2,newbe36524,36524
3,traceless,123456
分析实现思路
这个业务场景的实现方式可以非常多样化,此处我们简要将逻辑分为以下部分:
- 使用 object visitor 访问
OrderInfo
的所有属性 - 将所有属性传递给 CSV Writer 进行输出
实现 CSV 写入器
首先,我们先添加第 2 步所需要的 CSV 写入器:
public interface ICsvWriter
{
ICsvWriter WriteHeader(string header);
ICsvWriter FinishHead();
ICsvWriter WriteCell(string cell);
ICsvWriter FinishRow();
}
public class CsvWriter : ICsvWriter
{
public string Separator { get; set; } = ",";
private readonly TextWriter _writer;
public CsvWriter(
TextWriter writer)
{
_writer = writer;
}
private bool _firstHead = true;
public ICsvWriter WriteHeader(string header)
{
if (_firstHead)
{
_firstHead = false;
}
else
{
_writer.Write(Separator);
}
_writer.Write(header);
return this;
}
public ICsvWriter FinishHead()
{
_writer.WriteLine();
return this;
}
private bool _firstCell = true;
public ICsvWriter WriteCell(string cell)
{
if (_firstCell)
{
_firstCell = false;
}
else
{
_writer.Write(Separator);
}
_writer.Write(cell);
return this;
}
public ICsvWriter FinishRow()
{
_firstCell = true;
_writer.WriteLine();
return this;
}
}
有了这样一个基础的CsvWriter
,我们便可以首先来生成一个样例中的表格:
var sb = new StringBuilder();
var csvWriter = new CsvWriter(new StringWriter(sb));
csvWriter.WriteHeader("OrderId")
.WriteHeader("Buyer")
.WriteHeader("TotalPrice")
.FinishHead()
.WriteCell("1")
.WriteCell("yueluo")
.WriteCell("99999")
.FinishRow()
.WriteCell("2")
.WriteCell("newbe36524")
.WriteCell("36524")
.FinishRow()
.WriteCell("3")
.WriteCell("traceless")
.WriteCell("123456")
.FinishRow();
Console.WriteLine(sb.ToString());
输出表头
现在,我们使用第一个 object visitor 来调用上文的 CsvWriter 来输出表头:
var sb = new StringBuilder();
var csvWriter = new CsvWriter(new StringWriter(sb));
default(OrderInfo)
.V()
.WithExtendObject<OrderInfo, CsvWriter>()
.ForEach((name, value, w) => w.WriteHeader(name))
.Run(new OrderInfo(), csvWriter);
csvWriter.FinishHead();
Console.WriteLine(sb.ToString());
这样我就会得到如下的结果:
OrderId,Buyer,TotalPrice
输出表行
现在,我们在增加一个 object visitor 来输出表的每行内容:
var rowWriter = default(OrderInfo)
.V()
.WithExtendObject<OrderInfo, CsvWriter>()
.ForEach((name, value, w) => w.WriteCell(value != null ? value.ToString() : ""))
.Cache();
var orders = new List<OrderInfo>
{
new OrderInfo
{
OrderId = 1,
Buyer = "yueluo",
TotalPrice = 99999M
},
new OrderInfo
{
OrderId = 2,
Buyer = "newbe36524",
TotalPrice = 36524M
},
new OrderInfo
{
OrderId = 3,
Buyer = "traceless",
TotalPrice = 123456M
}
};
foreach (var order in orders)
{
rowWriter.Run(order, csvWriter);
csvWriter.FinishRow();
}
Console.WriteLine(sb.ToString());
这样就会得到如下的内容:
1,yueluo,99999
2,newbe36524,36524
3,traceless,123456
这正是我们期望的表中的行数据。
创建 CsvExtensions
现在,我们将以上的表头和表行的相关逻辑进行整合,将他们全部都添加到一个 CsvExtensions 的类型中。并且增加对于IEnumerable<T>
的扩展方法,这样在进行调用时就会更加简单。这将仿照先前FormatToStringExtensions
中的做法。
public static class CsvExtensions
{
public static string ToCsv<T>(this IEnumerable<T> items)
where T : new()
{
var re = CsvFilerHelper<T>.Instance.ToCsv(items);
return re;
}
private static class CsvFilerHelper<T>
where T : new()
{
internal static readonly ICsvHelper Instance = new CsvHelper();
public interface ICsvHelper
{
string ToCsv(IEnumerable<T> items);
}
private class CsvHelper : ICsvHelper
{
private readonly ICachedObjectVisitor<T, CsvWriter> _headerWriter;
private readonly ICachedObjectVisitor<T, CsvWriter> _bodyWriter;
public CsvHelper()
{
_headerWriter = default(T)
.V()
.WithExtendObject<T, CsvWriter>()
.ForEach((name, value, w) => w.WriteHeader(name))
.Cache();
_bodyWriter = default(T)
.V()
.WithExtendObject<T, CsvWriter>()
.ForEach((name, value, w) => w.WriteCell(value != null ? value.ToString() : ""))
.Cache();
}
public string ToCsv(IEnumerable<T> items)
{
var sb = new StringBuilder();
var csvWriter = new CsvWriter(new StringWriter(sb));
_headerWriter.Run(new T(), csvWriter);
csvWriter.FinishHead();
foreach (var item in items)
{
_bodyWriter.Run(item, csvWriter);
csvWriter.FinishRow();
}
var re = sb.ToString();
return re;
}
}
}
}
与先前的FormatToStringExtensions
一样,此处采用的是泛型静态类配合扩展方法的形式来创建帮助方法。不同的是,在这个示例中存在两个 object visitor。因此多考虑抽象了一个ICsvHelper
和实现类来实现复杂逻辑的聚合。
输出时跳过特定的列
我们希望在输出 CSV 的时候跳过一些特定的列,这就需要对属性进行过滤。
我们增加一个新的 Attribute:
public class IgnoreAttribute : Attribute
{
}
然后将这个 Attribute 标记在不希望输出的属性上:
public class OrderInfo
{
public int OrderId { get; set; }
[Ignore]
public string Buyer { get; set; }
public decimal TotalPrice { get; set; }
}
然后,我们只要在 object visitor 中忽略这些被标记为 Ingore 的属性即可。
其中核心的修改如下:
static bool Filter(PropertyInfo p) => p.GetCustomAttribute<IgnoreAttribute>() == null;
_headerWriter = default(T)
.V()
.WithExtendObject<T, CsvWriter>()
.FilterProperty(Filter)
.ForEach((name, value, w) => w.WriteHeader(name))
.Cache();
_bodyWriter = default(T)
.V()
.WithExtendObject<T, CsvWriter>()
.FilterProperty(Filter)
.ForEach((name, value, w) => w.WriteCell(value != null ? value.ToString() : ""))
.Cache();
这样我们在输出 CSV 时也就不存在这一列了:
OrderId,TotalPrice
1,99999
2,36524
3,123456
总结
我们通过一个简单的生产实例来理解 object visitor 的用法。实际生产问题会比这个更加复杂。开发者可以在生产实际中进行尝试,强化理解。