Migrate to .NET Standard 2.1
Move an existing Doconut.NETStandard application from 26.7.0 to 26.8.0
Doconut has two distinct .NET Standard integrations. They use the same Doconut.NETStandard
package name, so identify the generation from the version and the APIs in the application
before changing packages, startup, licenses, or browser resources.
The version is the fork in the road
Both generations publish under the same package ID. Only the version tells them apart:
| Version | Target | API |
|---|---|---|
| 26.7.0 and below | netstandard2.0 | the previous API |
| 26.8.0 and above | netstandard2.1 | the current API |
Bumping the version number is the upgrade. There is no separate package to install and no compatibility shim — 26.8.0 is a different API surface. Expect compile errors after the bump; the rest of this guide is the list of them.
Which integration are you using?
| If the project contains… | Generation |
|---|---|
new Viewer(_cache, _accessor, ...) | Previous |
viewer.OpenDocument(...) (synchronous) | Previous |
viewer.InitCache() | Previous |
UseDoconut(new DoconutOptions { ... }) | Previous |
UseDoconutWebFarm(...) / WebFarmOptions | Previous |
Viewer.DoconutLicense(...) or Viewer.SetLicensePlugin(...) | Previous |
viewer.Converter.GetConverter() | Previous |
using Doconut.Configs; / using Doconut.Models; | Previous |
Manually copied docViewer.js, documentLinks.js, or docViewer.UI.js | Previous |
builder.Services.AddDoconut(...) | Current |
app.UseDoconutResources() plus app.UseDoconut() with no arguments | Current |
Viewer supplied by dependency injection | Current |
await viewer.OpenDocumentAsync(...) | Current |
If both columns appear in the same application, treat the migration as incomplete. Do not send one document token through resources or middleware from the other generation.
Check the host framework first
This is a hard gate, not a preference. The current package targets netstandard2.1:
| Host | Can consume 26.8.0 |
|---|---|
| .NET Core 3.0 or later (including .NET 5/6/7/8) | Yes |
| .NET Framework, any version | No |
.NET Framework cannot reference a netstandard2.1 library. If the application runs on .NET
Framework, stop here and use the Doconut.NETFramework package instead: it stays on the
previous architecture and ships in every release, including this one. Both reference
applications for this release target net8.0.
Before you migrate
- Create a branch and a deployable backup of the existing application.
- Record the exact core and plugin package versions.
- Inventory every
new Viewer(...)call,InitCache()call, license-loading call,UseDoconutWebFarmmapping, conversion controller, copied Doconut script, custom toolbar action, viewer callback, and document-open endpoint. - Preserve the current
.licfiles and deployment secrets outside source control. - Capture a representative set of PDF, Office, image, CAD, email, DICOM, searchable, password-protected, and annotated documents.
- Record the existing session timeout, security behavior, fonts, and platform settings.
Migrate one environment before changing production. The current integration changes service lifetime, request routing, session ownership, client resource delivery, and — if you run a farm — how pages reach the browser at all.
1. Namespaces: almost everything moved to the root
The configuration types were spread across four namespaces. They are now all in Doconut.
| Before | Now |
|---|---|
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; | unchanged |
using Doconut.Middleware; | unchanged |
The type names did not change: WordConfig, PdfConfig, ExcelConfig, CssConfig,
ScriptConfig, CloudUploadConfig, AzureConfig, CDNConfig, FileShareConfig and the
rest are all still there. In most files the entire edit is deleting two using lines.
Doconut.Configs.Editor (BaseConfigEditor, PdfConfigEditor) was removed and has no
replacement in this package. If your application referenced those types, plan that part
separately and do not treat it as part of this migration.
2. Package and license compatibility
Replace or update the core package deliberately; do not rely on the identical package ID to select the new API. The default command installs the latest stable release:
dotnet add package Doconut.NETStandardFor a reproducible migration to the release audited by this guide, pass the version as a separate option:
dotnet add package Doconut.NETStandard --version 26.8.0Keep every Doconut plugin at the same version as the core package — Doconut.NETStandard.Converter
and Doconut.NETStandard.Dicom. The current integration loads licenses once during
AddDoconut(), using this precedence:
LicenseStream > LicenseContent > LicensePath > automatic discoveryAutomatic discovery looks for Doconut.Viewer.lic and companion
Doconut.Viewer.<Capability>.lic files. The previous static calls
Viewer.DoconutLicense(...) and Viewer.SetLicensePlugin(...) are not a current startup
mechanism, and neither is the List<string> of license paths that the old Viewer
constructor accepted. Move the license to DoconutOptions, keep companion files together
when using automatic discovery, restart after changing a license, and verify capabilities
through IDoconutLicenseService.
Do not assume that the presence of an old plugin license proves entitlement for a current plugin build. Test Viewer, Search, Annotation, Converter, and DICOM separately with the approved release artifacts.
3. Startup and dependency injection
Previously the viewer was constructed per request and options were passed to the middleware:
// Previous integration — contrast only; do not compile this against the current SDK.
var viewer = new Viewer(_cache, _accessor);
// …
appBranch.UseDoconut(new DoconutOptions { UnSafeMode = false, ShowDoconutInfo = false });The current integration registers Doconut once and receives Viewer from dependency
injection:
builder.Services.AddDoconut(options =>
{
options.UnsafeMode = false; // note the spelling change: UnSafeMode → UnsafeMode
options.ShowDoconutInfo = false;
options.LicensePath = "Doconut.Viewer.lic";
});
builder.Services.AddSession();
app.UseSession();
app.UseDoconutResources();
app.UseDoconut();Viewer is a transient service. The document session manager and its cache own the
longer-lived document state, not the particular injected Viewer instance.
Three things to carry over:
new Viewer(cache, accessor)is gone. InjectDoconut.Viewerinto your controller, or use@inject Doconut.Viewer Viewerin a Razor view.UseDoconutResources()replaces hand-wiringapp.UseMiddleware<EmbeddedResourceMiddleware>(), and must run before the document branch.UnSafeMode→UnsafeMode. A one-character rename the compiler will catch.
When UnsafeMode is false the SDK validates the session token against the originating
request, so AddSession() / UseSession() must still be in the pipeline, UseSession()
before the branch.
Methods that no longer exist
| Before | Now |
|---|---|
viewer.OpenDocument(path, config, options) | await viewer.OpenDocumentAsync(path, config, options) |
viewer.InitCache() | removed — there is nothing to warm up by hand; delete the call and any "did I already init?" flag around it |
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. Middleware and resource routing
UseDoconut() now takes no arguments. The MapWhen branch shape itself does not have to
change — only the middleware inside it:
app.UseDoconutResources(); // BEFORE the branch
app.MapWhen(
ctx => ctx.Request.Path.Value?.EndsWith("DocImage.axd", StringComparison.OrdinalIgnoreCase) == true,
branch => branch.UseDoconut());In the current pipeline:
- call
UseSession()before Doconut while session security is enabled; - call
UseDoconutResources()beforeUseDoconut(); - keep
ResourcesPath, the generated resource URLs, and clientResPathaligned; - keep the branch you map and the client
BasePathaligned.
MiddlewarePath is validated configuration; it does not create an ASP.NET Core branch by
itself.
5. Viewer construction and lifetime
Remove application-owned caches of Viewer objects. There is nothing left in a Viewer
worth holding on to: it carries no document state, and the session cache owns everything
that outlives a request. Inject it into an endpoint, Razor page, controller, or scoped
application service:
app.MapPost("/api/open", async (Viewer viewer) =>
{
var token = await viewer.OpenDocumentAsync("wwwroot/files/Sample.pdf");
return Results.Ok(new { token });
});The returned token identifies a server-side document session. Treat it as a bearer credential: do not log it, persist it, or place it in analytics.
6. Opening and closing documents
Replace synchronous OpenDocument(...) with OpenDocumentAsync(...):
// Current integration: Viewer comes from DI and document opening is asynchronous.
var token = await viewer.OpenDocumentAsync(path, new PdfConfig { AllowSearch = true });Current overloads accept a file path or stream, an optional format config, optional
DocOptions, and a cancellation token. Close the server session explicitly when the
browser no longer needs it:
viewer.CloseDocument(token);Delete any InitCache() call and the flag guarding it — there is nothing to warm up by
hand. Do not reuse a token minted by the previous library after the cutover: open each
document again through the current API.
7. Web farm → distributed: the biggest change
UseDoconutWebFarm and WebFarmOptions do not exist any more. If your app calls them,
this section is your upgrade; if it does not, skip to §8.
Previously one middleware did everything: it served pre-rendered images out of a shared folder, and polled while another node was still writing them.
// Previous integration — one call, implicit coordination through the filesystem.
appBranch.UseDoconutWebFarm(new WebFarmOptions
{
Path = Path.Combine(env.WebRootPath, "webfarm"),
PageWaitTimeSeconds = 180,
StartWaitFromPage = 5,
});The same job is now split into two explicit halves. Write side — the node that opens the document publishes the whole artifact set (pages, document info, search index, metadata) into the share:
var result = await _publisher.PublishAsync( // DistributedDocumentPublisher, injected from DI
pathToFile,
new FileShareConfig
{
Location = CloudLocation.FileShare,
RootPath = @"\\fileserver\doconut", // the same folder you use today
UsePerTokenSubfolders = true,
},
config,
documentOptions,
new DistributedDocumentPublishOptions
{
CleanUpStagingDirectory = true,
FirstPagePriority = true,
});
return Content(result.Token);Read side — any node serves from the share:
app.MapWhen(
ctx => ctx.Request.Path.Value?.EndsWith("DocImage.axd", StringComparison.OrdinalIgnoreCase) == true,
branch => branch.UseDoconutCloud<FileShareCloudHandler>(options =>
{
options.Location = CloudLocation.FileShare;
options.CloudUploadConfig = /* the SAME FileShareConfig as above */;
}, "/"));| Before | Now |
|---|---|
WebFarmOptions.Path | FileShareConfig.RootPath |
PageWaitTimeSeconds / StartWaitFromPage | gone — replaced by FirstPagePriority on the write side plus FileShareConfig.ReadReadyPollMs on the read side |
viewer.OpenDocument(...) then viewer.ExportToPng(share) | one PublishAsync(...) call |
DocOptions.IsWebfarm / WebfarmPath | still there, spelled IsWebFarm / WebFarmPath — but not needed on the publish path |
Your topology does not change. Point RootPath at the same UNC share and every node
keeps working the way it does today. What changes is that publishing is an operation you
call, instead of a side effect of rendering.
Both halves must agree. If
RootPathorUsePerTokenSubfoldersdiffer between the publisher and the handler, the other node answers 404. Build the config in one helper and call it from both sides.
PageWaitTimeSeconds = 180 meant "block up to three minutes hoping the page appears". The
replacement is explicit: with FirstPagePriority = true, page 1 is servable immediately
and later pages answer HTTP 202 Accepted until written. The bundled viewer retries
automatically when retryOn409 is set. A client that does not retry will show broken
tiles for pages still rendering.
The full arrangement, including background publishing and access tickets, is on Distributed Deployments.
8. Conversion is a plugin, and much smaller
Register the plugin, then inject the DocumentConverter facade:
builder.Services.AddDoconut(o => o.AddPlugin<ConverterPlugin>());// Previous integration
var converter = viewer.Converter.GetConverter();
var config = new WordConfigConverter { TargetFormat = WordConfigConverter.EnTargetFormat.PDF };
var result = converter.Convert(stream, SourceFormatConverter.DOCX, config);
// Current integration
var result = await _converter.ConvertAsync(stream, ".docx", ConversionTarget.Pdf, null, ct);What that replaces:
SourceFormatConverter— gone. Pass the file extension as a string, dot included.- The per-family
*ConfigConvertertypes (WordConfigConverter,ExcelConfigConverter, …), each with a nestedEnTargetFormatenum — gone. There is now oneConversionTargetenum for every family. viewer.Converter.GetConverter()— gone. InjectDoconut.DocumentConverter.
ConvertAsync returns a seekable stream, so return File(result, mime, name) works
directly — the MemoryStream copy the old code needed is unnecessary. In the shipped
sample this deleted a 68-case extension switch and ten near-identical controllers.
See Converter Plugin for the full surface.
9. Cloud providers
UseDoconutCloud became generic. The provider is the type argument instead of a
Location value switched on inside the middleware:
// Previous integration
appBranch.UseDoconutCloud(new CloudOptions { Location = CloudLocation.CDN, ... });
// Current integration
branch.UseDoconutCloud<CDNCloudHandler>(options =>
{
options.Location = CloudLocation.CDN;
options.CloudUploadConfig = new CDNConfig { DoconutCdnUrl = "..." };
}, "/");The config classes are unchanged apart from the namespace (Doconut.Configs.Cloud →
Doconut.Clouds): AzureConfig, AmazonConfig, GoogleCloudConfig, DropBoxConfig,
RedisConfig, CDNConfig, FTPConfig, plus the new FileShareConfig. Handlers are named
<Provider>CloudHandler.
The CDN handler is read-only — it serves what is already on the CDN; it does not upload.
10. Configuration objects that changed name
| Before | Now |
|---|---|
DoconutOptions.UnSafeMode | UnsafeMode |
DocOptions.IsWebfarm | IsWebFarm |
DocOptions.WebfarmPath | WebFarmPath |
DocOptions.ImageResolution | still present but obsolete — set resolution on the format config instead |
DocOptions also gains IsSecured.
The current API also separates concerns more strictly than the previous one, so check that each setting is on the object that now owns it:
| Concern | Current type |
|---|---|
| Middleware paths, licensing, plugin registration | DoconutOptions |
| Password, timeout, security, watermark | DocOptions |
| Format rendering and DPI | PdfConfig, WordConfig, ExcelConfig, and other BaseConfig types |
| Browser widget defaults | ViewerConfig or the equivalent JavaScript options |
| Generated CSS and scripts | CssConfig and ScriptConfig |
| Shared storage and cloud providers | FileShareConfig and the other Doconut.Clouds configs |
Do not carry DocOptions.ImageResolution forward as the rendering control. It is
obsolete; set BaseConfig.ImageResolution on the format-specific config. Review all
defaults instead of assuming a previous config has the same behavior.
11. The client-side viewer — two changes that fail SILENTLY
Both of these leave the server working perfectly. Your document renders, the page looks right, and nothing appears in the browser console. Read this section even if your C# compiles first try.
Viewer callbacks are options now, not global functions
The previous viewer called your handlers by name, deriving the global function name
from the container id. Declaring function ctlDoc_OnViewerReady() on a page with
<div id="div_ctlDoc"> was enough. The current viewer takes explicit callbacks in the
options object and no longer looks for those globals:
objctlDoc = $("#div_ctlDoc").docViewer({
// ... your existing options ...
onViewerBusy: ctlDoc_OnViewerBusy, // was found by name
onViewerReady: ctlDoc_OnViewerReady, // was found by name
onCopy: ctlDoc_Copy, // was ctlDoc_Copy(text)
onAutoLoadStatus: ctlDoc_AutoLoadStatus // was ctlDoc_AutoLoadStatus(page)
});Why this is nasty: the old call was wrapped in an empty catch, which swallowed
everything. Now your functions simply never run and nothing reports an error. The usual
symptom is a busy spinner that never stops — because the function that hid it was
ctlDoc_OnViewerReady. The document behind it is fine.
The full option list is onPageLoading, onPageLoaded, onThumbnailClicked,
onPageClicked, onDoubleClick, onViewerBusy, onViewerReady, onViewerError,
onCopy, onAnnSaved, onAnnSaveError, onAnnLoaded, onAnnClosed,
onAutoLoadStatus, onThumbsShown, and onError. There is no link-click callback:
hyperlink handling is built in, driven by showHyperlinks.
ResPath must point at the resource prefix
ResPath: '', // BEFORE — resources came from the application root
ResPath: 'doconut-res/images', // NOW — the prefix UseDoconutResources() mountsIf you changed the prefix via DoconutOptions.ResourcesPath, use that value instead.
12. Viewer toolbar, Search, and Annotation
Do not migrate the old scripts one by one. The current reference applications compose one complete page package:
- emit Viewer CSS and licensed Search/Annotation CSS with
ReferenceCss; - render the application-owned Viewer toolbar;
- render
searchBarMount,annBarMount, and the required Viewer mount; - emit Viewer and licensed module scripts with
ReferenceScripts; - load the application's own toolbar helper script;
- initialize one
objViewer; - initialize the licensed Search and Annotation Ribbons;
- call
attach(objViewer)on each Ribbon; - open the document and call
objViewer.View(token).
Search and Annotation are modules attached to the same Viewer, not independent toolbars. The main toolbar belongs to the host application; the Search and Annotation Ribbons are embedded, capability-gated resources.
Remove manually copied files such as documentLinks.js and docViewer.UI.js only after
the current page works with resources emitted by ReferenceCss and ReferenceScripts.
13. Plugin registration
Static plugin-license methods do not register current plugins. Install and register each released package explicitly:
builder.Services.AddDoconut(options =>
{
options.AddPlugin<Doconut.Plugins.Converter.ConverterPlugin>();
options.AddPlugin<Doconut.Plugins.Dicom.DicomPlugin>();
});AddDoconut() validates registered plugin capabilities at startup. Converter and DICOM
are the released .NET Standard plugins. Normal Search and Annotation are built-in licensed
features, not AddPlugin<TPlugin>() packages.
14. Session and document security
The current integration binds documents to opaque tokens and cached sessions. With the
default UnsafeMode = false, UseDoconut() adds document-access security and the host
must configure ASP.NET session:
builder.Services.AddSession();
app.UseSession();Keep DocOptions.IsSecured = true unless a reviewed design requires otherwise. Never use
UnsafeMode = true as a migration shortcut on a single node. Test requests with no token,
a malformed token, an expired token, and a token from a different browser session.
In a farm the page request can land on a node that never saw the opening request, so the
per-session check would reject it. That is the one case where UnsafeMode = true is
correct — and it must be paired with signed access tickets through
AddDoconutDistributedDocumentSecurity, not shipped on its own. See
Distributed Deployments.
Naming note
In all frameworks the class is Viewer. If you find DocumentViewer in old snippets
or third-party articles, that type never existed in the SDK.
15. Suggested order of work
- Confirm the host framework can consume netstandard2.1. If it cannot, stop.
- Bump the package to 26.8.0 and build. Work through the compile errors — most are
the
usinglines in §1. - Move startup to
AddDoconut()and the middleware calls to their new order (§3, §4). - Inject
Viewerwhere you were constructing it, makeOpenDocumentasync, and deleteInitCache()(§5, §6). - If you use the web farm, do §7 — the only part that is a redesign rather than a rename.
- If you convert documents, do §8.
- Do §11 — the client-side changes. They are the ones that will not announce themselves.
- Open a real document in a browser, and watch the busy spinner. A green build proves nothing here: licensing, plugin registration, the resource path, and the viewer callbacks all fail only at runtime. For a farm, verify that a document published by one node is served by another.
Testing the migration
At minimum, verify:
- application startup with the production license and every registered plugin;
- Viewer CSS/scripts and all page-image requests under the chosen paths;
- document open, navigation, zoom, thumbnails, print, and explicit close;
- Search on a text-bearing document and the non-searchable state of an image-only file;
- Annotation load, save, export, and capability gating;
- Converter target discovery, output, download, and watermark state;
- DICOM pages, frames, and animation; technical metadata is unavailable on this package;
- password-protected documents, custom fonts, non-Latin text, and configured timeouts;
- cross-session token rejection and expired-session behavior;
- for a farm: publish on one node, request a late page from another, and confirm the
viewer retries
202responses instead of showing broken tiles; - mobile, dark mode, and the production reverse-proxy path.
Rollback plan
Keep the previous deployment artifact, matching packages, license files, and copied browser
resources together. A safe rollback switches the entire application generation; it does not
mix a previous-generation server with current scripts, or a current server with
new Viewer(...) call sites.
Before cutover, document:
- the deployment slot or artifact used for rollback;
- the shared-storage impact, if any — artifacts published by 26.8.0 are not consumed by the previous web-farm middleware;
- how active document sessions will be invalidated;
- the health check and smoke document used to decide rollback;
- who can restore the previous package set and configuration.
Legacy documentation
The translated previous manual remains available at Legacy .NET Standard setup. The Previous integration gateway explains the same identification signals and links back to this migration guide.
Keep the historic URL in bookmarks and support tickets while netstandard2.0 installations still exist. It documents a different generation and is not redirected to the current API.
หน้านี้เป็นประโยชน์หรือไม่?