迁移到 .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。

迁移前准备

  1. 创建一个分支并对现有应用程序进行可部署的备份。
  2. 记录核心和插件包的精确版本。
  3. 列出所有 new Viewer(...) 调用、InitCache() 调用、加载许可证的调用、UseDoconutWebFarm 映射、转换控制器、复制的 Doconut 脚本、自定义工具栏操作、viewer 回调以及文档打开端点。
  4. 将当前的 .lic 文件和部署机密保存在源码控制之外。
  5. 获取一套具有代表性的 PDF、Office、图像、CAD、电子邮件、DICOM、可搜索、受密码保护以及带注释的文档。
  6. 记录现有的会话超时、安全行为、字体和平台设置。

在更改生产环境之前,先迁移一个环境。当前的集成会改变服务生命周期、请求路由、会话所有权、客户端资源交付,并且——如果您运行的是集群——页面到达浏览器的方式也会改变。

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。默认命令会安装最新的稳定版:

bash
dotnet add package Doconut.NETStandard

为了实现可复现的迁移到本指南审计的发布版本,请将版本作为单独的选项传递:

bash
dotnet add package Doconut.NETStandard --version 26.8.0

保持所有 Doconut 插件与核心包版本一致 — Doconut.NETStandard.Converter 和 Doconut.NETStandard.Dicom。当前的集成在 AddDoconut() 时一次性加载许可证,使用以下优先级:

text
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 是在每个请求中构造的,选项通过中间件传递:

csharp
// 先前的集成 — 仅作对比;不要在当前 SDK 上编译此代码。
var viewer = new Viewer(_cache, _accessor);
// …
appBranch.UseDoconut(new DoconutOptions { UnSafeMode = false, ShowDoconutInfo = false });

当前的集成只注册一次 Doconut,并通过依赖注入获取 Viewer:

csharp
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 分支的形状本身无需更改 —— 只需更改其中的中间件:

csharp
app.UseDoconutResources();   // 在分支之前

app.MapWhen(
    ctx => ctx.Request.Path.Value?.EndsWith("DocImage.axd", StringComparison.OrdinalIgnoreCase) == true,
    branch => branch.UseDoconut());

在当前管道中:

  1. 在启用会话安全时,在 Doconut 之前调用 UseSession();
  2. 在 UseDoconut() 之前调用 UseDoconutResources();
  3. 保持 ResourcesPath、生成的资源 URL 与客户端 ResPath 对齐;
  4. 保持映射的分支与客户端 BasePath 对齐。

MiddlewarePath 是已验证的配置;它本身不会创建 ASP.NET Core 分支。

5. 查看器的构建与生命周期

移除应用拥有的 Viewer 对象缓存。Viewer 中没有任何值得保留的内容:它不携带文档状态,会话缓存拥有所有超出请求生命周期的资源。将其注入到端点、Razor 页面、控制器或作用域的应用服务中:

csharp
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(...):

csharp
// 当前集成:Viewer 通过 DI 获取,文档打开为异步方式。
var token = await viewer.OpenDocumentAsync(path, new PdfConfig { AllowSearch = true });

当前的重载接受文件路径或流、可选的格式配置、可选的 DocOptions,以及取消令牌。当浏览器不再需要时,需要显式关闭服务器会话:

csharp
viewer.CloseDocument(token);

删除所有 InitCache() 调用以及其保护标志——没有需要手动预热的内容。切换后不要复用之前库生成的 token:请通过当前 API 重新打开每个文档。

7. Web farm → 分布式:最大的变化

UseDoconutWebFarm 和 WebFarmOptions 已不再存在。 如果你的应用调用了它们,本节即为升级指南;如果没有,直接跳到第 8 节。

以前只有一个中间件完成所有工作:它从共享文件夹提供预渲染的图像,并在另一个节点仍在写入时 轮询。

csharp
// 之前的集成 —— 单次调用,通过文件系统进行隐式协调。
appBranch.UseDoconutWebFarm(new WebFarmOptions
{
    Path                = Path.Combine(env.WebRootPath, "webfarm"),
    PageWaitTimeSeconds = 180,
    StartWaitFromPage   = 5,
});

现在同样的工作被拆分为两个明确的部分。写入端——打开文档的节点将完整的制品集(页面、文档信息、搜索索引、元数据)发布到共享位置:

csharp
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);

读取端——任何节点都从共享位置提供服务:

csharp
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.PathFileShareConfig.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 外观:

csharp
builder.Services.AddDoconut(o => o.AddPlugin<ConverterPlugin>());
csharp
// 之前的集成
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 值切换:

csharp
// 之前的集成
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.UnSafeModeUnsafeMode
DocOptions.IsWebfarmIsWebFarm
DocOptions.WebfarmPathWebFarmPath
DocOptions.ImageResolution仍然存在但 已过时 —— 改为在格式配置上设置分辨率

DocOptions 还新增了 IsSecured。

当前 API 也更严格地划分了职责,请确保每个设置位于现在拥有它的对象上:

关注点当前类型
中间件路径、授权、插件注册DoconutOptions
密码、超时、安全性、水印DocOptions
格式渲染和 DPIPdfConfig、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 对象中显式传入回调,不再寻找全局函数:

javascript
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 必须指向资源前缀

javascript
ResPath: '',                      // 之前 — 资源来自应用程序根目录
ResPath: 'doconut-res/images',    // 现在 — UseDoconutResources() 挂载的前缀

如果您通过 DoconutOptions.ResourcesPath 更改了前缀,请改用该值。

12. 查看器工具栏、搜索和注释

不要逐个迁移旧脚本。当前的参考应用程序会组合成一个完整的页面包:

  1. 使用 ReferenceCss 输出 Viewer CSS 和授权的搜索/注释 CSS;
  2. 渲染由应用程序拥有的 Viewer 工具栏;
  3. 渲染 searchBarMount、annBarMount 以及所需的 Viewer 挂载点;
  4. 使用 ReferenceScripts 输出 Viewer 和授权模块脚本;
  5. 加载应用程序自己的工具栏辅助脚本;
  6. 初始化一个 objViewer;
  7. 初始化授权的搜索和注释功能区;
  8. 在每个功能区上调用 attach(objViewer);
  9. 打开文档并调用 objViewer.View(token)。

搜索和注释是附加到同一 Viewer 的模块,而不是独立的工具栏。主工具栏属于宿主应用程序;搜索和注释功能区是嵌入式、基于能力的资源。

仅在当前页面能够使用 ReferenceCss 和 ReferenceScripts 输出的资源后,才删除手动复制的文件,例如 documentLinks.js 和 docViewer.UI.js。

13. 插件注册

静态插件许可证方法不会注册当前插件。请显式安装并注册每个已发布的包:

csharp
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 会话:

csharp
builder.Services.AddDistributedMemoryCache(); // 会话状态需要缓存(MVC/Razor Pages 已经添加了一个)
builder.Services.AddSession();
app.UseSession();

除非经过审查的设计另有要求,否则保持 DocOptions.IsSecured = true。切勿在单节点上将 UnsafeMode = true 用作迁移捷径。请测试以下请求:没有令牌、令牌格式错误、令牌已过期以及来自不同浏览器会话的令牌。

在集群环境中,页面请求可能落在从未收到打开请求的节点上,从而导致每会话检查被拒绝。这是唯一可以使用 UnsafeMode = true 的情况——但必须配合通过 AddDoconutDistributedDocumentSecurity 的签名访问票据使用,不能单独使用。参见 分布式部署。

命名说明

在所有框架中,该类为 Viewer。如果在旧代码片段或第三方文章中看到 DocumentViewer,该类型在 SDK 中从未存在。

15. 建议的工作顺序

  1. 确认宿主框架能够使用 netstandard2.1。如果不能,停止操作。
  2. 将包升级到 26.8.0 并构建。处理编译错误——大多数是 §1 中的 using 行。
  3. 将启动代码迁移到 AddDoconut(),并按新顺序调用中间件(§3、§4)。
  4. 在构造 Viewer 的位置注入它,将 OpenDocument 改为异步,并删除 InitCache()(§5、§6)。
  5. 如果使用 Web 集群,执行 §7 —— 这是唯一需要重新设计而非重命名的部分。
  6. 如果进行文档转换,执行 §8。
  7. 执行 §11 —— 客户端更改。 这些更改不会自行提示。
  8. 在浏览器中打开真实文档,并观察加载指示器。 绿色构建并不能证明任何问题:授权、插件注册、资源路径以及 Viewer 回调都只会在运行时失败。对于集群,请验证由一个节点发布的文档能由另一个节点提供服务。

测试迁移

至少,验证:

  • 应用启动使用生产许可证和所有已注册插件;
  • Viewer CSS/脚本以及所选路径下的所有页面图像请求;
  • 文档打开、导航、缩放、缩略图、打印以及显式关闭;
  • 在包含文本的文档上搜索,以及仅图像文件的不可搜索状态;
  • 注释加载、保存、导出以及功能门控;
  • 转换器目标发现、输出、下载以及水印状态;
  • DICOM 页面、帧和动画;技术元数据在此包中不可用;
  • 受密码保护的文档、自定义字体、非拉丁文字以及配置的超时;
  • 跨会话令牌拒绝和会话过期行为;
  • 对于集群:在一个节点发布,在另一个节点请求延迟页面,并确认查看器重试 202 响应而不是显示损坏的瓦片;
  • 移动端、暗色模式以及生产环境的反向代理路径。

回滚计划

保留之前的部署制品、匹配的包、许可证文件以及复制的浏览器资源。安全的回滚会切换整个应用程序的代际;它不会将上一代服务器与当前脚本混合,也不会将当前服务器与 new Viewer(...) 调用点混合。

在切换之前,记录:

  • 用于回滚的部署槽或制品;
  • 共享存储的影响(如果有)——由 26.8.0 发布的制品不会被之前的 Web 农场中间件使用;
  • 活动文档会话将如何失效;
  • 用于决定回滚的健康检查和冒烟文档;
  • 谁可以恢复之前的包集合和配置。

旧版文档

已翻译的以前手册仍可在 旧版 .NET Standard 设置 查看。 先前的集成网关 解释相同的识别信号并链接回本迁移指南。

在 netstandard2.0 安装仍然存在期间,请在书签和支持工单中保留历史 URL。它记录了不同的代际,并未重定向到当前 API。

此页面对您有帮助吗?