在 .NET 项目里做 Word 导出,很多团队最先遇到的不是“能不能生成”,而是模板是否好维护、服务端是否依赖 Office、Linux 部署后还能不能稳定运行。MiniWord 这类模板库的价值,就在于把常见的文档生成需求压缩成少量代码,同时保留 Word 模板本身的排版能力。本文按上手、标签能力和 Web 导出三个层面展开,你可以据此判断它是否适合自己的报表、审批单或业务通知生成场景。
MiniWord 是什么,适合解决哪些问题
MiniWord 是一款简单而高效的 .NET Word 模板库,核心思路是:先在 .docx 模板中放置标签,再由程序把数据填充进去生成最终文档。它的优点比较明确:
- 不依赖 Microsoft Word 安装环境;
- 不需要 COM+ 或 Office 互操作组件;
- 支持 Windows、Linux 和 Mac;
- 模板遵循“所见即所得”,标签所在位置的样式可以随模板一起保留。
如果你的需求是合同、申请单、对账单、会议纪要、通知书这类“固定版式 + 动态数据”的文档输出,MiniWord 这类方案通常比手写 OpenXML 更省成本,也比依赖桌面 Office 的方式更适合服务端部署。
它并不是完整的可视化文档编辑器,而是偏向“模板填充”的工具库。因此,判断它是否合适,关键要看你的文档结构是不是相对稳定,以及字段是否能通过标签、循环和条件表达出来。
如何快速上手:安装、最小示例与输入输出方式
安装 NuGet 包
项目包地址如下:

最小可用示例
MiniWord 最容易理解的地方在于,它几乎就是“模板路径 + 数据字典 + 输出文件”的组合。最小示例如下:

var value = new Dictionary<string, object>(){["title"] = "Hello MiniWord"};
MiniSoftware.MiniWord.SaveAsByTemplate(outputPath, templatePath, value);
这里的 title 会去匹配模板中的同名标签,生成后的文档会保留模板原本的样式。对于需要让产品、运营或行政同事直接维护 Word 模板的团队,这种方式比较直观。
支持哪些输入和输出
在集成到真实系统时,另一个实用点是它不只支持文件路径,也支持字节数组和流。这样既能读本地模板,也能从数据库、对象存储或缓存中取模板内容。
输入可使用模板路径或 Byte[],输出支持文件路径、Byte[]、Stream。原始接口如下:
SaveAsByTemplate(string path, string templatePath, Dictionary<string, object> value)
SaveAsByTemplate(string path, byte[] templateBytes, Dictionary<string, object> value)
SaveAsByTemplate(this Stream stream, string templatePath, Dictionary<string, object> value)
SaveAsByTemplate(this Stream stream, byte[] templateBytes, Dictionary<string, object> value)
这几种重载基本覆盖了常见用法:
- 本地批量生成文档时,直接写文件路径;
- 接口下载场景里,更常见的是输出到
MemoryStream; - 模板需要缓存时,可以预先加载为
byte[],减少重复磁盘读取。
模板标签能做什么:文本、表格、嵌套列表与条件判断
MiniWord 的核心不是复杂 API,而是模板语法本身。只要理解它如何匹配标签,就能覆盖大部分文档生成需求。
文本替换:最基础也最常用
普通文本标签使用类似 Vue、React 模板字符串的写法:{{tag}}。需要注意的是,tag 必须与传入数据字典中的 key 完全一致,并且区分大小写。
{{tag}}
示例代码如下:
var value = new Dictionary<string, object>()
{
["Name"] = "Jack",
["Department"] = "IT Department",
["Purpose"] = "Shanghai site needs a new system to control HR system.",
["StartDate"] = DateTime.Parse("2022-09-07 08:30:00"),
["EndDate"] = DateTime.Parse("2022-09-15 15:30:00"),
["Approved"] = true,
["Total_Amount"] = 123456,
};
MiniWord.SaveAsByTemplate(path, templatePath, value);
这类方式适合封面标题、申请人、部门、日期、金额等单值字段。因为样式由 Word 模板控制,所以开发代码不需要额外关心字体、字号和段落格式。
表格填充:按行输出列表数据
当标签值是 IEnumerable<Dictionary<string,object>> 时,可以用于表格类数据输出。这是业务里非常高频的用法,比如差旅明细、商品清单、人员名单、会议安排等。
示例代码如下:
var value = new Dictionary<string, object>()
{
["TripHs"] = new List<Dictionary<string, object>>
{
new Dictionary<string, object>
{
{ "sDate",DateTime.Parse("2022-09-08 08:30:00")},
{ "eDate",DateTime.Parse("2022-09-08 15:00:00")},
{ "How","Discussion requirement part1"},
{ "Photo",new MiniWordPicture() { Path = PathHelper.GetFile("DemoExpenseMeeting02.png"), Width = 160, Height = 90 }},
},
new Dictionary<string, object>
{
{ "sDate",DateTime.Parse("2022-09-09 08:30:00")},
{ "eDate",DateTime.Parse("2022-09-09 17:00:00")},
{ "How","Discussion requirement part2 and development"},
{ "Photo",new MiniWordPicture() { Path = PathHelper.GetFile("DemoExpenseMeeting01.png"), Width = 160, Height = 90 }},
},
}
};
MiniWord.SaveAsByTemplate(path, templatePath, value);
从这个示例可以看出,表格行里不仅能放文本和日期,也能直接插图,图片通过 MiniWordPicture 指定路径、宽度和高度。这意味着你可以把“图文混排表格”也交给模板完成,而不必手动拼装文档结构。
二级列表:处理嵌套信息
如果一行数据里还要继续展开子项,MiniWord 也提供了二级列表能力。对应的 Tag 类型是 IEnumerable<MiniWordForeach>,模板中使用 {{foreach 和 endforeach}}。
示例代码如下:
var value = new Dictionary<string, object>()
{
["TripHs"] = new List<Dictionary<string, object>>
{
new Dictionary<string, object>
{
{ "sDate", DateTime.Parse("2022-09-08 08:30:00") },
{ "eDate", DateTime.Parse("2022-09-08 15:00:00") },
{ "How", "Discussion requirement part1" },
{
"Details", new List<MiniWordForeach>()
{
new MiniWordForeach()
{
Value = new Dictionary<string, object>()
{
{"Text", "Air"},
{"Value", "Airplane"}
},
Separator = " | "
},
new MiniWordForeach()
{
Value = new Dictionary<string, object>()
{
{"Text", "Parking"},
{"Value", "Car"}
},
Separator = " / "
}
}
}
}
}
};
MiniWord.SaveAsByTemplate(path, templatePath, value);
这个能力比较适合“主记录 + 明细项”的组合,比如一条出差记录下的交通方式、多个费用分类、多个补充字段等。Separator 还可以控制每个子项之间的分隔符,适合在一段文本内拼接展示。
条件判断:控制内容是否显示
模板里并不总是所有字段都需要输出。对于会员标识、审批通过状态、积分条件、不同类型文案切换等场景,可以使用 @if 和 @endif 标签。
示例代码如下:
var value = new Dictionary<string, object>()
{
["Name"] = new List<MiniWordHyperLink>(){
new MiniWordHyperLink(){
Url = "https://google.com",
Text = "測試連結22!!"
},
new MiniWordHyperLink(){
Url = "https://google1.com",
Text = "測試連結11!!"
}
},
["Company_Name"] = "MiniSofteware",
["CreateDate"] = new DateTime(2021, 01, 01),
["VIP"] = true,
["Points"] = 123,
["APP"] = "Demo APP",
};
MiniWord.SaveAsByTemplate(path, templatePath, value);
虽然示例里同时演示了超链接数据结构,但更关键的信息是:模板可以根据布尔值或其他条件决定某段内容是否出现。这样一来,同一份模板就能兼容不同业务分支,而不是为每种情况维护一套独立文档。
多彩字体:让模板保留更丰富的表现形式
除了填充值本身,MiniWord 还支持彩色文本对象,适合在文档中强调品牌名、状态、时间或特定字段。
var value = new
{
Company_Name = new MiniWordColorText { Text = "MiniSofteware", FontColor = "#eb70AB", },
Name = new[] {
new MiniWordColorText { Text = "Ja", HighlightColor = "#eb70AB" },
new MiniWordColorText { Text = "ck", HighlightColor = "#a56abe" }
},
CreateDate = new MiniWordColorText
{
Text = new DateTime(2021, 01, 01).ToString(),
HighlightColor = "#eb70AB",
FontColor = "#ffffff",
},
VIP = true,
Points = 123,
APP = "Demo APP",
};
MiniWord.SaveAsByTemplate(path, templatePath, value);
这部分对“规范排版”的业务很有帮助。比如合同中的重点条款、营销通知中的等级标识、内部单据里的状态提示,都可以通过模板与数据对象组合完成,而不是导出后再人工编辑。
放进 ASP.NET Core 后怎么用:在线导出 Word 的实现方式
如果你更关心它能否真正落到接口服务里,原文给出了一套 ASP.NET Core 3.1 API Export 示例。这个示例的价值不在于框架本身,而在于它展示了两种最典型的服务端导出方式:从模板路径读取,以及从缓存字节数组读取。
程序入口与基础配置如下:
public class Program
{
public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
}
public class Startup
{
public void ConfigureServices(IServiceCollection services) => services.AddMvc();
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=api}/{action=Index}/{id?}");
});
}
}
控制器里先准备一个默认数据集,包含标题、管理者列表和员工列表:
static Dictionary<string, object> defaultValue = new Dictionary<string, object>()
{
["title"] = "FooCompany",
["managers"] = new List<Dictionary<string, object>> {
new Dictionary<string, object>{{"name","Jack"},{ "department", "HR" } },
new Dictionary<string, object> {{ "name", "Loan"},{ "department", "IT" } }
},
["employees"] = new List<Dictionary<string, object>> {
new Dictionary<string, object>{{ "name", "Wade" },{ "department", "HR" } },
new Dictionary<string, object> {{ "name", "Felix" },{ "department", "HR" } },
new Dictionary<string, object>{{ "name", "Eric" },{ "department", "IT" } },
new Dictionary<string, object> {{ "name", "Keaton" },{ "department", "IT" } }
}
};
方式一:直接读取模板路径生成文件流
这是最直观的用法,适合模板文件固定放在应用目录的场景:
public IActionResult DownloadWordFromTemplatePath()
{
string templatePath = "TestTemplateComplex.docx";
Dictionary<string, object> value = defaultValue;
MemoryStream memoryStream = new MemoryStream();
MiniWord.SaveAsByTemplate(memoryStream, templatePath, value);
memoryStream.Seek(0, SeekOrigin.Begin);
return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
{
FileDownloadName = "demo.docx"
};
}
它的实现逻辑很清楚:把模板和数据写入 MemoryStream,再把流包装为下载响应。对于中低频导出接口,这种方式通常已经足够。
方式二:预加载模板字节,减少重复读取
如果模板会被频繁下载,或者你希望把模板缓存到内存中,可以提前把模板读成 byte[]:
private static Dictionary<string, Byte[]> TemplateBytesCache = new Dictionary<string, byte[]>();
static ApiController()
{
string templatePath = "TestTemplateComplex.docx";
byte[] bytes = System.IO.File.ReadAllBytes(templatePath);
TemplateBytesCache.Add(templatePath, bytes);
}
public IActionResult DownloadWordFromTemplateBytes()
{
byte[] bytes = TemplateBytesCache["TestTemplateComplex.docx"];
Dictionary<string, object> value = defaultValue;
MemoryStream memoryStream = new MemoryStream();
MiniWord.SaveAsByTemplate(memoryStream, bytes, value);
memoryStream.Seek(0, SeekOrigin.Begin);
return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
{
FileDownloadName = "demo.docx"
};
}
这类方式更适合高频、固定模板的导出接口,能减少磁盘 I/O,也便于后续接入分布式缓存或对象存储。
从工程角度看,这个示例还顺带说明了一点:MiniWord 并不绑定桌面程序,而是天然可以放进 Web API、后台任务或微服务中使用。
选型时重点看什么:MiniWord 的优势与适用边界
结合前面的能力,MiniWord 的优势主要体现在以下几个方面:
- 上手成本低:核心 API 很少,普通变量替换只需一行代码;
- 模板友好:Word 本身就是模板编辑器,业务人员更容易参与维护;
- 跨平台部署方便:不依赖 Office,适合 Linux 服务器环境;
- 覆盖常见业务导出:文本、列表、表格、图片、条件显示、彩色文本都已考虑到。
当然,它更适合“基于模板填充”的场景。如果你的需求是高度动态的复杂排版、接近在线编辑器级别的文档拼装,或者要精确控制每一个底层 OpenXML 节点,那就需要评估它是否足够灵活。
对大多数企业应用来说,真正常见的并不是无限自由排版,而是固定版式文档的大量生成。从这个角度看,MiniWord 的定位其实很准确:用尽可能轻的方式,解决 Word 模板导出这件高频但容易做重的事。
项目地址:













