迁移到 .NET Standard 2.1
将现有 Doconut.NETStandard 应用程序从 26.7.0 迁移到 26.8.0
Doconut 有两种不同的 .NET Standard 集成。它们使用相同的 Doconut.NETStandard 包名,因此在更改包、启动、许可证或浏览器资源之前,请根据版本号和应用程序中的 API 确认所使用的代际。
版本是分叉点
两个代际都使用 相同的包 ID 发布。只有版本号可以区分它们:
| 版本 | 目标 | API |
|---|---|---|
| 26.7.0 及以下 | netstandard2.0 | 之前的 API |
| 26.8.0 及以上 | netstandard2.1 | 当前的 API |
提升版本号即为升级。 没有单独的包需要安装,也没有兼容性垫片——26.8.0 拥有不同的 API 表面。升级后会出现编译错误;本指南其余部分列出了这些错误。
您正在使用哪种集成?
| 如果项目包含… | 代际 |
|---|---|
new Viewer(_cache, _accessor, ...) | 之前 |
viewer.OpenDocument(...)(同步) | 之前 |
viewer.InitCache() | 之前 |
UseDoconut(new DoconutOptions { ... }) | 之前 |
UseDoconutWebFarm(...) / WebFarmOptions | 之前 |
Viewer.DoconutLicense(...) 或 Viewer.SetLicensePlugin(...) | 之前 |
viewer.Converter.GetConverter() | 之前 |
using Doconut.Configs; / using Doconut.Models; | 之前 |
手动复制的 docViewer.js、documentLinks.js 或 docViewer.UI.js | 之前 |
builder.Services.AddDoconut(...) | 当前 |
app.UseDoconutResources() 加 app.UseDoconut()(无参数) | 当前 |
通过依赖注入提供的 Viewer | 当前 |
await viewer.OpenDocumentAsync(...) | 当前 |
如果同一应用程序中出现两列的情况,则视迁移为未完成。不要通过资源或中间件将一个代际的文档令牌发送给另一个代际。
首先检查宿主框架
这是一道硬性门槛,而非偏好。当前包的目标是 netstandard2.1:
| 宿主 | 是否能使用 26.8.0 |
|---|---|
| .NET Core 3.0 或更高(包括 .NET 5/6/7/8) | 是 |
| .NET Framework,任何版本 | 否 |
.NET Framework 无法引用 netstandard2.1 库。如果应用程序运行在 .NET Framework 上,请在此停止并改用 Doconut.NETFramework 包;从 26.9.0 起它要求 .NET Framework 4.7.2。本次发布的两个参考应用程序的目标都是 net8.0。
迁移前准备
- 创建一个分支并对现有应用程序进行可部署的备份。
- 记录核心和插件包的精确版本。
- 列出所有
new Viewer(...)调用、InitCache()调用、加载许可证的调用、UseDoconutWebFarm映射、转换控制器、复制的 Doconut 脚本、自定义工具栏操作、viewer 回调以及文档打开端点。 - 将当前的
.lic文件和部署机密保存在源码控制之外。 - 获取一套具有代表性的 PDF、Office、图像、CAD、电子邮件、DICOM、可搜索、受密码保护以及带注释的文档。
- 记录现有的会话超时、安全行为、字体和平台设置。
在更改生产环境之前,先迁移一个环境。当前的集成会改变服务生命周期、请求路由、会话所有权、客户端资源交付,并且——如果您运行的是集群——页面到达浏览器的方式也会改变。
1. 命名空间:几乎所有内容都迁移到根命名空间
配置类型原本分布在四个命名空间中。现在它们全部位于 Doconut 中。
| 之前 | 现在 |
|---|---|
using Doconut.Configs.View; | using Doconut; |
using Doconut.Models; | using Doconut; |
using Doconut.Configs; | using Doconut; |
using Doconut.Configs.Cloud; | using Doconut.Clouds; |
using Doconut.Configs.Conversion; | 未更改 |
using Doconut.Middleware; | 未更改 |
类型名称未改变:WordConfig、PdfConfig、ExcelConfig、CssConfig、ScriptConfig、CloudUploadConfig、AzureConfig、CDNConfig、FileShareConfig 等仍然存在。在大多数文件中,整个编辑工作仅是删除两行 using。
Doconut.Configs.Editor(BaseConfigEditor、PdfConfigEditor)已被移除,在此包中没有替代。若您的应用程序引用了这些类型,请单独规划,不要将其视为本次迁移的一部分。
2. 包和许可证兼容性
有意地替换或更新核心包;不要依赖相同的包 ID 来选择新 API。默认命令会安装最新的稳定版:
dotnet add package Doconut.NETStandard为了实现可复现的迁移到本指南审计的发布版本,请将版本作为单独的选项传递:
dotnet add package Doconut.NETStandard --version 26.8.0保持所有 Doconut 插件与核心包版本一致 — Doconut.NETStandard.Converter 和 Doconut.NETStandard.Dicom。当前的集成在 AddDoconut() 时一次性加载许可证,使用以下优先级:
LicenseStream > LicenseContent > LicensePath > automatic discovery自动发现会查找 Doconut.Viewer.lic 以及伴随的 Doconut.Viewer.<Capability>.lic 文件。之前的静态调用 Viewer.DoconutLicense(...) 和 Viewer.SetLicensePlugin(...) 已不再是当前的启动机制,旧的 Viewer 构造函数接受的 List<string> 许可证路径也不再使用。将许可证移动到 DoconutOptions,在使用自动发现时将伴随文件放在一起,修改许可证后重新启动,并通过 IDoconutLicenseService 验证功能。
不要假设旧插件许可证的存在就证明当前插件构建有相应授权。请分别使用已批准的发布制品测试 Viewer、Search、Annotation、Converter 和 DICOM。
3. 启动和依赖注入
之前,viewer 是在每个请求中构造的,选项通过中间件传递:
// 先前的集成 — 仅作对比;不要在当前 SDK 上编译此代码。
var viewer = new Viewer(_cache, _accessor);
// …
appBranch.UseDoconut(new DoconutOptions { UnSafeMode = false, ShowDoconutInfo = false });当前的集成只注册一次 Doconut,并通过依赖注入获取 Viewer:
builder.Services.AddDoconut(options =>
{
options.UnsafeMode = false; // 注意拼写更改:UnSafeMode → UnsafeMode
options.ShowDoconutInfo = false;
options.LicensePath = "Doconut.Viewer.lic";
});
builder.Services.AddDistributedMemoryCache(); // 会话状态需要缓存(MVC/Razor Pages 已经添加了一个)
builder.Services.AddSession();
app.UseSession();
app.UseDoconutResources();
app.UseDoconut();Viewer 是瞬态服务。文档会话管理器及其缓存拥有更长期的文档状态,而不是特定注入的 Viewer 实例。
需要保留的三点:
new Viewer(cache, accessor)已不存在。 将Doconut.Viewer注入到你的控制器中,或在 Razor 视图中使用@inject Doconut.Viewer Viewer。UseDoconutResources()替代手动接线app.UseMiddleware<EmbeddedResourceMiddleware>(),并且必须在文档分支 之前 运行。UnSafeMode→UnsafeMode。 这是一个字符的重命名,编译器会捕获。
当 UnsafeMode 为 false 时,SDK 会将会话令牌与发起请求进行验证,因此 AddSession() / UseSession() 必须仍然在管道中,并且 UseSession() 要在分支之前调用。
不再存在的方法
| 之前 | 现在 |
|---|---|
viewer.OpenDocument(path, config, options) | await viewer.OpenDocumentAsync(path, config, options) |
viewer.InitCache() | 已移除 — 手动没有需要预热的内容;删除该调用以及围绕它的任何 “我已经初始化了吗?” 标志 |
viewer.ExportToPng(sharedPath) | see §7 |
viewer.ExportToCloud(cloudConfig) | see §7 |
using var v = new Viewer(...); v.CloseDocument(t) | viewer.CloseDocument(token) on the injected instance |
4. 中间件和资源路由
UseDoconut() 现在不接受任何参数。MapWhen 分支的形状本身无需更改 —— 只需更改其中的中间件:
app.UseDoconutResources(); // 在分支之前
app.MapWhen(
ctx => ctx.Request.Path.Value?.EndsWith("DocImage.axd", StringComparison.OrdinalIgnoreCase) == true,
branch => branch.UseDoconut());在当前管道中:
- 在启用会话安全时,在 Doconut 之前调用
UseSession(); - 在
UseDoconut()之前调用UseDoconutResources(); - 保持
ResourcesPath、生成的资源 URL 与客户端ResPath对齐; - 保持映射的分支与客户端
BasePath对齐。
MiddlewarePath 是已验证的配置;它本身不会创建 ASP.NET Core 分支。
5. 查看器的构建与生命周期
移除应用拥有的 Viewer 对象缓存。Viewer 中没有任何值得保留的内容:它不携带文档状态,会话缓存拥有所有超出请求生命周期的资源。将其注入到端点、Razor 页面、控制器或作用域的应用服务中:
app.MapPost("/api/open", async (Viewer viewer) =>
{
var token = await viewer.OpenDocumentAsync("wwwroot/files/Sample.pdf");
return Results.Ok(new { token });
});返回的 token 标识一个服务器端文档会话。将其视为持有者凭证:不要记录、持久化或放入分析中。
6. 打开与关闭文档
用 OpenDocumentAsync(...) 替代同步的 OpenDocument(...):
// 当前集成:Viewer 通过 DI 获取,文档打开为异步方式。
var token = await viewer.OpenDocumentAsync(path, new PdfConfig { AllowSearch = true });当前的重载接受文件路径或流、可选的格式配置、可选的 DocOptions,以及取消令牌。当浏览器不再需要时,需要显式关闭服务器会话:
viewer.CloseDocument(token);删除所有 InitCache() 调用以及其保护标志——没有需要手动预热的内容。切换后不要复用之前库生成的 token:请通过当前 API 重新打开每个文档。
7. Web farm → 分布式:最大的变化
UseDoconutWebFarm 和 WebFarmOptions 已不再存在。 如果你的应用调用了它们,本节即为升级指南;如果没有,直接跳到第 8 节。
以前只有一个中间件完成所有工作:它从共享文件夹提供预渲染的图像,并在另一个节点仍在写入时 轮询。
// 之前的集成 —— 单次调用,通过文件系统进行隐式协调。
appBranch.UseDoconutWebFarm(new WebFarmOptions
{
Path = Path.Combine(env.WebRootPath, "webfarm"),
PageWaitTimeSeconds = 180,
StartWaitFromPage = 5,
});现在同样的工作被拆分为两个明确的部分。写入端——打开文档的节点将完整的制品集(页面、文档信息、搜索索引、元数据)发布到共享位置:
var result = await _publisher.PublishAsync( // DistributedDocumentPublisher,来自 DI 注入
pathToFile,
new FileShareConfig
{
Location = CloudLocation.FileShare,
RootPath = @"\\fileserver\doconut", // 与您今天使用的同一文件夹
UsePerTokenSubfolders = true,
},
config,
documentOptions,
new DistributedDocumentPublishOptions
{
CleanUpStagingDirectory = true,
FirstPagePriority = true,
});
return Content(result.Token);读取端——任何节点都从共享位置提供服务:
app.MapWhen(
ctx => ctx.Request.Path.Value?.EndsWith("DocImage.axd", StringComparison.OrdinalIgnoreCase) == true,
branch => branch.UseDoconutCloud<FileShareCloudHandler>(options =>
{
options.Location = CloudLocation.FileShare;
options.CloudUploadConfig = /* 与上面相同的 FileShareConfig */;
}, "/"));| 之前 | 现在 |
|---|---|
WebFarmOptions.Path | FileShareConfig.RootPath |
PageWaitTimeSeconds / StartWaitFromPage | 已移除 —— 由写入端的 FirstPagePriority 以及读取端的 FileShareConfig.ReadReadyPollMs 取代 |
viewer.OpenDocument(...) 随后 viewer.ExportToPng(share) | 单次 PublishAsync(...) 调用 |
DocOptions.IsWebfarm / WebfarmPath | 仍然存在,写作 IsWebFarm / WebFarmPath —— 但在发布路径上已不需要 |
你的拓扑结构保持不变。 将 RootPath 指向相同的 UNC 共享,所有节点仍按当前方式工作。变化在于发布现在是显式调用的操作,而不是渲染的副作用。
两端必须保持一致。 如果
RootPath或UsePerTokenSubfolders在发布者和处理程序之间不一致,另一端会返回 404。请在单一帮助方法中构建配置,并在两端共用。
PageWaitTimeSeconds = 180 的含义是“阻塞最多三分钟,等待页面出现”。现在的做法更明确:使用 FirstPagePriority = true 时,第 1 页可立即提供服务,后续页面在写入完成前会返回 HTTP 202 Accepted。当设置 retryOn409 时,捆绑的查看器会自动重试。未启用重试的客户端将在仍在渲染的页面上显示破碎的瓦片。
完整的方案,包括后台发布和访问票据,详见 分布式部署。
8. 转换是一个插件,且体积更小
注册插件后,注入 DocumentConverter 外观:
builder.Services.AddDoconut(o => o.AddPlugin<ConverterPlugin>());// 之前的集成
var converter = viewer.Converter.GetConverter();
var config = new WordConfigConverter { TargetFormat = WordConfigConverter.EnTargetFormat.PDF };
var result = converter.Convert(stream, SourceFormatConverter.DOCX, config);
// 当前的集成
var result = await _converter.ConvertAsync(stream, ".docx", ConversionTarget.Pdf, null, ct);这取代了:
SourceFormatConverter— 已移除。改为直接传入包含点的文件扩展名字符串。- 每个系列的
*ConfigConverter类型(WordConfigConverter、ExcelConfigConverter…),以及内部的EnTargetFormat枚举 — 已移除。现在所有系列共用一个ConversionTarget枚举。 viewer.Converter.GetConverter()— 已移除。改为注入Doconut.DocumentConverter。
ConvertAsync 返回可寻址的流,因此 return File(result, mime, name) 可以直接使用——不再需要旧代码中对 MemoryStream 的拷贝。在随附的示例中,这删除了一个包含 68 种情况的扩展名 switch 以及十个几乎相同的控制器。
请参阅 转换插件 了解完整接口。
9. 云提供商
UseDoconutCloud 变为泛型。提供程序现在是类型参数,而不是在中间件内部根据 Location 值切换:
// 之前的集成
appBranch.UseDoconutCloud(new CloudOptions { Location = CloudLocation.CDN, ... });
// 当前的集成
branch.UseDoconutCloud<CDNCloudHandler>(options =>
{
options.Location = CloudLocation.CDN;
options.CloudUploadConfig = new CDNConfig { DoconutCdnUrl = "..." };
}, "/");配置类保持不变,仅命名空间从 Doconut.Configs.Cloud → Doconut.Clouds:AzureConfig、AmazonConfig、GoogleCloudConfig、DropBoxConfig、RedisConfig、CDNConfig、FTPConfig,以及新增的 FileShareConfig。处理程序命名为 <Provider>CloudHandler。
CDN 处理程序是 只读 的——它只提供已经在 CDN 上的内容,不会进行上传。
10. 更名的配置对象
| 之前 | 现在 |
|---|---|
DoconutOptions.UnSafeMode | UnsafeMode |
DocOptions.IsWebfarm | IsWebFarm |
DocOptions.WebfarmPath | WebFarmPath |
DocOptions.ImageResolution | 仍然存在但 已过时 —— 改为在格式配置上设置分辨率 |
DocOptions 还新增了 IsSecured。
当前 API 也更严格地划分了职责,请确保每个设置位于现在拥有它的对象上:
| 关注点 | 当前类型 |
|---|---|
| 中间件路径、授权、插件注册 | DoconutOptions |
| 密码、超时、安全性、水印 | DocOptions |
| 格式渲染和 DPI | PdfConfig、WordConfig、ExcelConfig 以及其他 BaseConfig 类型 |
| 浏览器部件默认值 | ViewerConfig 或等价的 JavaScript 选项 |
| 生成的 CSS 与脚本 | CssConfig 与 ScriptConfig |
| 共享存储和云提供商 | FileShareConfig 与其他 Doconut.Clouds 配置 |
不要再使用 DocOptions.ImageResolution 作为渲染控制。它已过时;请在特定格式的配置上设置 BaseConfig.ImageResolution。请检查所有默认值,而不是假设之前的配置行为保持不变。
11. 客户端查看器 — 两个静默失效的改动
这两项改动不会影响服务器的正常工作。文档仍能渲染,页面显示正常,浏览器控制台也没有任何提示。即使你的 C# 编译成功,也请阅读本节。
查看器回调现在是选项,而不是全局函数
之前的查看器会 按名称 调用你的处理函数,从容器 id 推导出全局函数名。只要在页面上声明 function ctlDoc_OnViewerReady() 并且有 <div id="div_ctlDoc"> 即可。当前的查看器改为在 options 对象中显式传入回调,不再寻找全局函数:
objctlDoc = $("#div_ctlDoc").docViewer({
// ... 你已有的选项 ...
onViewerBusy: ctlDoc_OnViewerBusy, // 以前是通过名称查找
onViewerReady: ctlDoc_OnViewerReady, // 以前是通过名称查找
onCopy: ctlDoc_Copy, // 以前是 ctlDoc_Copy(text)
onAutoLoadStatus: ctlDoc_AutoLoadStatus // 以前是 ctlDoc_AutoLoadStatus(page)
});为什么这很糟糕: 旧的调用被包裹在一个空的 catch 中,所有错误都被吞掉。现在你的函数根本不会被执行,也不会报错。常见症状是加载指示器一直转动——因为隐藏指示器的函数 ctlDoc_OnViewerReady 没有运行。文档本身是正常的。
完整的选项列表包括 onPageLoading、onPageLoaded、onThumbnailClicked、onPageClicked、onDoubleClick、onViewerBusy、onViewerReady、onViewerError、onCopy、onAnnSaved、onAnnSaveError、onAnnLoaded、onAnnClosed、onAutoLoadStatus、onThumbsShown 与 onError。没有链接点击的回调:超链接处理已内置,由 showHyperlinks 驱动。
ResPath 必须指向资源前缀
ResPath: '', // 之前 — 资源来自应用程序根目录
ResPath: 'doconut-res/images', // 现在 — UseDoconutResources() 挂载的前缀如果您通过 DoconutOptions.ResourcesPath 更改了前缀,请改用该值。
12. 查看器工具栏、搜索和注释
不要逐个迁移旧脚本。当前的参考应用程序会组合成一个完整的页面包:
- 使用
ReferenceCss输出 Viewer CSS 和授权的搜索/注释 CSS; - 渲染由应用程序拥有的 Viewer 工具栏;
- 渲染
searchBarMount、annBarMount以及所需的 Viewer 挂载点; - 使用
ReferenceScripts输出 Viewer 和授权模块脚本; - 加载应用程序自己的工具栏辅助脚本;
- 初始化一个
objViewer; - 初始化授权的搜索和注释功能区;
- 在每个功能区上调用
attach(objViewer); - 打开文档并调用
objViewer.View(token)。
搜索和注释是附加到同一 Viewer 的模块,而不是独立的工具栏。主工具栏属于宿主应用程序;搜索和注释功能区是嵌入式、基于能力的资源。
仅在当前页面能够使用 ReferenceCss 和 ReferenceScripts 输出的资源后,才删除手动复制的文件,例如 documentLinks.js 和 docViewer.UI.js。
13. 插件注册
静态插件许可证方法不会注册当前插件。请显式安装并注册每个已发布的包:
builder.Services.AddDoconut(options =>
{
options.AddPlugin<Doconut.Plugins.Converter.ConverterPlugin>();
options.AddPlugin<Doconut.Plugins.Dicom.DicomPlugin>();
});AddDoconut() 在启动时验证已注册的插件功能。Converter 和 DICOM 是已发布的 .NET Standard 插件。普通的搜索和注释是内置的授权功能,而不是 AddPlugin<TPlugin>() 包。
14. 会话和文档安全
当前的集成将文档绑定到不透明令牌和缓存的会话。使用默认的 UnsafeMode = false 时,UseDoconut() 会添加文档访问安全性,宿主必须配置 ASP.NET 会话:
builder.Services.AddDistributedMemoryCache(); // 会话状态需要缓存(MVC/Razor Pages 已经添加了一个)
builder.Services.AddSession();
app.UseSession();除非经过审查的设计另有要求,否则保持 DocOptions.IsSecured = true。切勿在单节点上将 UnsafeMode = true 用作迁移捷径。请测试以下请求:没有令牌、令牌格式错误、令牌已过期以及来自不同浏览器会话的令牌。
在集群环境中,页面请求可能落在从未收到打开请求的节点上,从而导致每会话检查被拒绝。这是唯一可以使用 UnsafeMode = true 的情况——但必须配合通过 AddDoconutDistributedDocumentSecurity 的签名访问票据使用,不能单独使用。参见 分布式部署。
命名说明
在所有框架中,该类为 Viewer。如果在旧代码片段或第三方文章中看到 DocumentViewer,该类型在 SDK 中从未存在。
15. 建议的工作顺序
- 确认宿主框架能够使用 netstandard2.1。如果不能,停止操作。
- 将包升级到 26.8.0 并构建。处理编译错误——大多数是 §1 中的
using行。 - 将启动代码迁移到
AddDoconut(),并按新顺序调用中间件(§3、§4)。 - 在构造
Viewer的位置注入它,将OpenDocument改为异步,并删除InitCache()(§5、§6)。 - 如果使用 Web 集群,执行 §7 —— 这是唯一需要重新设计而非重命名的部分。
- 如果进行文档转换,执行 §8。
- 执行 §11 —— 客户端更改。 这些更改不会自行提示。
- 在浏览器中打开真实文档,并观察加载指示器。 绿色构建并不能证明任何问题:授权、插件注册、资源路径以及 Viewer 回调都只会在运行时失败。对于集群,请验证由一个节点发布的文档能由另一个节点提供服务。
测试迁移
至少,验证:
- 应用启动使用生产许可证和所有已注册插件;
- Viewer CSS/脚本以及所选路径下的所有页面图像请求;
- 文档打开、导航、缩放、缩略图、打印以及显式关闭;
- 在包含文本的文档上搜索,以及仅图像文件的不可搜索状态;
- 注释加载、保存、导出以及功能门控;
- 转换器目标发现、输出、下载以及水印状态;
- DICOM 页面、帧和动画;技术元数据在此包中不可用;
- 受密码保护的文档、自定义字体、非拉丁文字以及配置的超时;
- 跨会话令牌拒绝和会话过期行为;
- 对于集群:在一个节点发布,在另一个节点请求延迟页面,并确认查看器重试
202响应而不是显示损坏的瓦片; - 移动端、暗色模式以及生产环境的反向代理路径。
回滚计划
保留之前的部署制品、匹配的包、许可证文件以及复制的浏览器资源。安全的回滚会切换整个应用程序的代际;它不会将上一代服务器与当前脚本混合,也不会将当前服务器与 new Viewer(...) 调用点混合。
在切换之前,记录:
- 用于回滚的部署槽或制品;
- 共享存储的影响(如果有)——由 26.8.0 发布的制品不会被之前的 Web 农场中间件使用;
- 活动文档会话将如何失效;
- 用于决定回滚的健康检查和冒烟文档;
- 谁可以恢复之前的包集合和配置。
旧版文档
已翻译的以前手册仍可在 旧版 .NET Standard 设置 查看。 先前的集成网关 解释相同的识别信号并链接回本迁移指南。
在 netstandard2.0 安装仍然存在期间,请在书签和支持工单中保留历史 URL。它记录了不同的代际,并未重定向到当前 API。
此页面对您有帮助吗?