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:

VersionTargetAPI
26.7.0 and belownetstandard2.0the previous API
26.8.0 and abovenetstandard2.1the 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(...) / WebFarmOptionsPrevious
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.jsPrevious
builder.Services.AddDoconut(...)Current
app.UseDoconutResources() plus app.UseDoconut() with no argumentsCurrent
Viewer supplied by dependency injectionCurrent
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:

HostCan consume 26.8.0
.NET Core 3.0 or later (including .NET 5/6/7/8)Yes
.NET Framework, any versionNo

.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

  1. Create a branch and a deployable backup of the existing application.
  2. Record the exact core and plugin package versions.
  3. Inventory every new Viewer(...) call, InitCache() call, license-loading call, UseDoconutWebFarm mapping, conversion controller, copied Doconut script, custom toolbar action, viewer callback, and document-open endpoint.
  4. Preserve the current .lic files and deployment secrets outside source control.
  5. Capture a representative set of PDF, Office, image, CAD, email, DICOM, searchable, password-protected, and annotated documents.
  6. 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.

BeforeNow
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:

bash
dotnet add package Doconut.NETStandard

For a reproducible migration to the release audited by this guide, pass the version as a separate option:

bash
dotnet add package Doconut.NETStandard --version 26.8.0

Keep 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:

text
LicenseStream > LicenseContent > LicensePath > automatic discovery

Automatic 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:

csharp
// 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:

csharp
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. Inject Doconut.Viewer into your controller, or use @inject Doconut.Viewer Viewer in a Razor view.
  • UseDoconutResources() replaces hand-wiring app.UseMiddleware<EmbeddedResourceMiddleware>(), and must run before the document branch.
  • UnSafeModeUnsafeMode. 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

BeforeNow
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:

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

  1. call UseSession() before Doconut while session security is enabled;
  2. call UseDoconutResources() before UseDoconut();
  3. keep ResourcesPath, the generated resource URLs, and client ResPath aligned;
  4. keep the branch you map and the client BasePath aligned.

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:

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

csharp
// 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:

csharp
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.

csharp
// 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:

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

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 = /* the SAME FileShareConfig as above */;
    }, "/"));
BeforeNow
WebFarmOptions.PathFileShareConfig.RootPath
PageWaitTimeSeconds / StartWaitFromPagegone — 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 / WebfarmPathstill 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 RootPath or UsePerTokenSubfolders differ 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:

csharp
builder.Services.AddDoconut(o => o.AddPlugin<ConverterPlugin>());
csharp
// 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 *ConfigConverter types (WordConfigConverter, ExcelConfigConverter, …), each with a nested EnTargetFormat enum — gone. There is now one ConversionTarget enum for every family.
  • viewer.Converter.GetConverter() — gone. Inject Doconut.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:

csharp
// 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.CloudDoconut.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

BeforeNow
DoconutOptions.UnSafeModeUnsafeMode
DocOptions.IsWebfarmIsWebFarm
DocOptions.WebfarmPathWebFarmPath
DocOptions.ImageResolutionstill 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:

ConcernCurrent type
Middleware paths, licensing, plugin registrationDoconutOptions
Password, timeout, security, watermarkDocOptions
Format rendering and DPIPdfConfig, WordConfig, ExcelConfig, and other BaseConfig types
Browser widget defaultsViewerConfig or the equivalent JavaScript options
Generated CSS and scriptsCssConfig and ScriptConfig
Shared storage and cloud providersFileShareConfig 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:

javascript
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

javascript
ResPath: '',                      // BEFORE — resources came from the application root
ResPath: 'doconut-res/images',    // NOW    — the prefix UseDoconutResources() mounts

If 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:

  1. emit Viewer CSS and licensed Search/Annotation CSS with ReferenceCss;
  2. render the application-owned Viewer toolbar;
  3. render searchBarMount, annBarMount, and the required Viewer mount;
  4. emit Viewer and licensed module scripts with ReferenceScripts;
  5. load the application's own toolbar helper script;
  6. initialize one objViewer;
  7. initialize the licensed Search and Annotation Ribbons;
  8. call attach(objViewer) on each Ribbon;
  9. 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:

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

csharp
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

  1. Confirm the host framework can consume netstandard2.1. If it cannot, stop.
  2. Bump the package to 26.8.0 and build. Work through the compile errors — most are the using lines in §1.
  3. Move startup to AddDoconut() and the middleware calls to their new order (§3, §4).
  4. Inject Viewer where you were constructing it, make OpenDocument async, and delete InitCache() (§5, §6).
  5. If you use the web farm, do §7 — the only part that is a redesign rather than a rename.
  6. If you convert documents, do §8.
  7. Do §11 — the client-side changes. They are the ones that will not announce themselves.
  8. 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 202 responses 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.

หน้านี้เป็นประโยชน์หรือไม่?