Modernize to .NET 8 LTS, add fluent API, DI package, and samples#78
Open
Modernize to .NET 8 LTS, add fluent API, DI package, and samples#78
Conversation
…saries, and style rules
Adds a LINQ-style fluent layer on top of the existing ITranslator / IWriter /
IGlossaryManager / IStyleRuleManager surfaces. Builders are directly awaitable;
every fluent method is an extension over the existing interfaces, so the new
API is non-breaking and works with any ITranslator/IWriter implementation
(including mocks).
Examples:
await translator.Translate("Hello").From("en").To("de")
.WithFormality(Formality.More)
.WithGlossary(glossary);
await client.CreateGlossary("My glossary")
.WithDictionary("en", "de", entries);
await translator.TranslateDocument(input)
.To("de")
.WithFormality(Formality.More)
.SaveTo(output);
Includes 73 unit tests (via NSubstitute) covering argument forwarding,
option-configuration helpers, validation, and both single/batch shapes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops dead runtimes and brings dependencies onto the .NET 8 LTS line. Library: TFMs: net5.0;netstandard2.0 -> netstandard2.0;net8.0 LangVersion: 8 -> 12 Microsoft.Extensions.Http.Polly: 5.0.1 -> 8.0.26 System.Text.Json: 5.0.2 -> 8.0.6 System.Net.Http.Json: (new) -> 8.0.1 Tests: TFMs: net5.0;netcoreapp3.1;net462 -> net8.0;net462 LangVersion: 8 -> 12 Microsoft.NET.Test.Sdk: 16.9.4 -> 17.11.1 xunit: 2.4.1 -> 2.9.2 xunit.runner.visualstudio: 2.4.3 -> 2.8.2 NSubstitute: 4.3.0 -> 5.3.0 coverlet.collector: 3.0.2 -> 6.0.2 JunitXml.TestLogger: 3.0.98 -> 5.0.0 Motivation: - net5.0 reached end of support in May 2022; netcoreapp3.1 in Dec 2022. The 5.x dependencies have since-patched CVEs that only ship in 8.0+. - netstandard2.0 is retained as the floor so the package stays usable from .NET Framework 4.7.2+, Mono, Unity, etc. - LangVersion 12 is purely additive on ns2.0 (no new BCL surface required). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Uses modern BCL surface on net8.0 while keeping the existing code as the
netstandard2.0 fallback, guarded by #if NET8_0_OR_GREATER / NET5_0_OR_GREATER.
Changes:
- JsonUtils: use JsonNamingPolicy.SnakeCaseLower (net8 built-in) and
HttpContent.ReadFromJsonAsync; fall back to the custom snake-case policy
+ stream reader on ns2.0.
- DeepLHttpClient:
* HttpMethod.Patch factored to a static field (net5+ uses the built-in
HttpMethod.Patch, ns2.0 uses the string constructor once at init).
* Extracted CreateJsonContent/CreateFormContent helpers; on net8 JSON
bodies flow through JsonContent.Create (streams instead of string).
* Inner handler on net8 is SocketsHttpHandler with PooledConnectionLifetime
and PooledConnectionIdleTimeout set, so long-lived HttpClient instances
pick up DNS changes correctly.
* HTTP/2 preferred (RequestVersionOrHigher) on net8 for proper multiplexing
on batch translation.
- LargeFormUrlEncodedContent: the .NET 5 fix for the pre-.NET 5 size limit
(dotnet/corefx#41686) means the built-in FormUrlEncodedContent now works
correctly. The custom type is gated behind !NET5_0_OR_GREATER so it only
compiles into the ns2.0 asset.
No public API change. netstandard2.0 consumers see identical behavior;
net8.0 consumers get the modern BCL paths and per-call allocation wins.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New NuGet package (TFMs: netstandard2.0 + net8.0) providing
Microsoft.Extensions.DependencyInjection integration for DeepL.net.
Surface:
services.AddDeepLClient(o => o.AuthKey = "...");
services.AddDeepLClient(builder.Configuration);
services.AddDeepLClient(builder.Configuration.GetSection("..."));
Behavior:
- Registers DeepLClient as a singleton (documented thread-safe).
- Forwards every surface interface (ITranslator, IWriter, IGlossaryManager,
IStyleRuleManager, IVoiceManager) to the same singleton.
- Routes the underlying HttpClient through IHttpClientFactory with the named
client "DeepL", so consumers can layer their own handlers / resilience /
logging on top without re-implementing the DeepL client.
- Validates AuthKey via IValidateOptions — missing key surfaces on first
resolve, not on first API call.
- AddDeepLClient is idempotent (TryAdd semantics).
Why a separate package (rather than adding DI to DeepL.net itself):
- The main DeepL.net package stays dependency-free for consumers who use
Autofac/DryIoc/SimpleInjector or construct DeepLClient manually.
- Matches the established .NET ecosystem pattern
(MediatR.Extensions.Microsoft.DependencyInjection, Polly.Extensions.Http,
Serilog.Extensions.Hosting, OpenTelemetry.Extensions.Hosting).
- DI-integration shape can evolve independently without forcing main-library
version bumps.
Versioning: lockstep with DeepL.net for simplicity until integration surface
diverges. Strong-named with the shared sgKey.snk.
Includes 15 DI-container tests covering registration, singleton lifetime,
interface forwarding, auth-key validation, HttpClientFactory integration,
ServerUrl propagation (verified with a capturing HttpMessageHandler),
idempotency, and both configure-delegate + IConfiguration overloads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two runnable sample console apps demonstrating idiomatic use of the new
fluent API and the DeepL.Extensions.DependencyInjection package.
Layout:
samples/
DeepL.Samples.slnx (standalone solution, not in main CI scope)
Directory.Build.props (shared net8.0 / LangVersion 12)
README.md (usage + ASP.NET Core adaptation)
FluentApi/ (every fluent entry point)
DependencyInjection/ (generic host + AddDeepLClient + IHostedService consumer)
The samples solution is deliberately separate from DeepL.net.sln so the
library's CI build scope, NuGet pack, and signing behavior are unaffected.
They reference the library via ProjectReference, so local changes to the
library are picked up on each build.
Both samples require DEEPL_AUTH_KEY to run, but compile without it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts the changes introduced by 5330a21 ("Draft for a DeepL Voice implementation") so this branch no longer carries an unrelated WIP. Voice remains tracked on branch tc/add-voice and can be landed independently. Removed: - DeepL/IVoiceManager.cs, IVoiceSession.cs, VoiceSession.cs, VoiceSessionOptions.cs, VoiceMessageFormat.cs, TargetMediaVoice.cs, SourceMediaContentType.cs, SourceLanguageMode.cs - DeepL/Model/TargetMediaChunk.cs, TranscriptSegment.cs, TranscriptUpdate.cs, VoiceSessionInfo.cs, VoiceStreamError.cs - DeepLTests/VoiceSessionTest.cs - DeepL/DeepLClient.cs: using System.Net.WebSockets; IVoiceManager interface on DeepLClient; CreateVoiceSessionAsync method - DeepL/DeepL.csproj: System.Net.WebSockets.Client package reference Follow-up edits to keep this branch internally consistent: - DeepL.Extensions.DependencyInjection: drop the IVoiceManager forwarder; the DI package now forwards only ITranslator / IWriter / IGlossaryManager / IStyleRuleManager to the singleton. When Voice lands, re-adding the line is a one-line change. - DI package README + samples README: remove IVoiceManager from the interface list. - DI tests: drop the IVoiceManager assertions from the two "register-all" / "all-resolve-to-singleton" tests. Verified green: main solution builds on net8.0 + netstandard2.0 + net462, samples solution builds, Fluent tests 73/73 pass, DI tests 15/15 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nslation
Two ergonomic additions to the fluent document-translation layer that address
the async-poll nature of DeepL's document API.
IProgress<DocumentStatus>:
- New DocumentTranslationBuilder.WithProgress(IProgress<DocumentStatus>) method.
Reports each status tick during the wait phase (between upload and download),
useful for UI progress indicators, structured logging, or webhook emissions.
- New DocumentRef.WaitUntilDoneAsync(IProgress<DocumentStatus>, CancellationToken)
overload so the split upload/poll/download flow can also use progress.
- When progress is configured the builder runs upload → poll-with-callbacks →
download in the fluent layer rather than delegating to the library's one-shot
TranslateDocumentAsync (which has no progress hook). Without progress the
existing delegation path is preserved, so DocumentTranslationException
wrapping and minification semantics stay identical to the non-fluent API.
DocumentTranslationJob:
- SaveTo(FileInfo) and SaveTo(Stream) now return DocumentTranslationJob, a
lightweight wrapper around Task that also exposes Cancel(). The job is
directly awaitable (GetAwaiter) and implicitly converts to Task, so every
existing call site (await builder.SaveTo(...)) compiles and behaves
unchanged.
- Internally each SaveTo call creates a CancellationTokenSource linked to any
token provided via WithCancellation, so job.Cancel() propagates cancellation
into the library even when the caller supplied their own token. The linked
CTS is disposed in a continuation once the job completes.
- Lets callers keep the fluent style end-to-end without pre-building a
CancellationTokenSource just to have a cancel handle:
var job = translator.TranslateDocument(file).To("de").SaveTo(out);
// ...later...
job.Cancel();
await job; // throws OperationCanceledException
Tests (7 new, 80 total Fluent tests passing on net8.0 + net462):
- SaveTo_ReturnsAwaitableJob — job is awaitable + implicitly convertible to Task
- Job_Cancel_PropagatesThroughLinkedToken — cancel propagates into library call
- Job_Cancel_AfterCompletion_IsNoOp — safe post-completion
- WithCancellation_ExternalCancelPropagatesThroughLinkedToken — external cts
cancellation also propagates via the linked token
- WithProgress_ReportsStatusDuringPolling — each poll tick surfaces to progress
- WithProgress_ErrorStatus_ThrowsDeepLException — error-status path
- WithProgress_NullProgress_Throws — null guard
- DocumentRef_WaitUntilDoneAsync_WithProgress_ReportsTicks — split-flow progress
Samples:
- samples/FluentApi/Program.cs demonstrates both new features end-to-end:
WithProgress callback printing each status tick, and SaveTo-returns-Job with
a mid-flight job.Cancel() triggered from a background task.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Modernizes the DeepL .NET SDK to current TFMs/dependencies while adding a fluent API layer, a companion Microsoft DI integration package, and runnable sample apps to demonstrate both.
Changes:
- Add fluent, awaitable builder extensions for translation/rephrase/document/glossary/style-rule flows plus unit tests.
- Upgrade library/tests to
net8.0(keepingnetstandard2.0for the library) and adopt newer BCL HTTP/JSON APIs with conditional fallbacks. - Introduce
DeepL.Extensions.DependencyInjection+ tests and add/samplessolution demonstrating fluent + DI usage.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| samples/README.md | Documents how to build/run the sample apps. |
| samples/FluentApi/Program.cs | End-to-end console sample showcasing the fluent API surface. |
| samples/FluentApi/FluentApi.csproj | Sample project referencing the library via ProjectReference. |
| samples/Directory.Build.props | Shared build settings for sample projects (net8, nullable, no pack/sign). |
| samples/DependencyInjection/TranslationService.cs | Hosted service sample consuming DeepL interfaces from DI. |
| samples/DependencyInjection/Program.cs | Generic-host sample wiring up AddDeepLClient. |
| samples/DependencyInjection/DependencyInjection.csproj | Sample project referencing DI package + Hosting. |
| samples/DeepL.Samples.slnx | Standalone samples solution file. |
| DeepLTests/FluentTranslationTest.cs | Unit tests for fluent text translation + rephrase builders. |
| DeepLTests/FluentStyleRuleTest.cs | Unit tests for fluent style-rule management builders/refs. |
| DeepLTests/FluentGlossaryTest.cs | Unit tests for fluent glossary builders/refs. |
| DeepLTests/FluentDocumentTranslationTest.cs | Unit tests for fluent document translation builders/refs including progress + cancellation. |
| DeepLTests/DeepLTests.csproj | Update test TFMs/packages to net8/net462 and newer test deps. |
| DeepL/Internal/LargeFormUrlEncodedContent.cs | Compile out workaround on modern TFMs (keep for pre-.NET5). |
| DeepL/Internal/JsonUtils.cs | Use JsonNamingPolicy.SnakeCaseLower + ReadFromJsonAsync on net8 with fallback. |
| DeepL/Internal/DeepLHttpClient.cs | Modernize HTTP internals (PATCH static, JSON/form helpers, SocketsHttpHandler, HTTP/2 defaults). |
| DeepL/FluentTranslation.cs | New fluent translation/rephrase API with awaitable builders and option helpers. |
| DeepL/FluentStyleRule.cs | New fluent API for style-rule management (refs + create builder). |
| DeepL/FluentGlossary.cs | New fluent API for multilingual glossary management (refs + create builder). |
| DeepL/FluentDocumentTranslation.cs | New fluent API for document translation (one-shot, split flow, progress + cancelable job). |
| DeepL/DeepL.csproj | Update TFMs/lang version and bump core dependencies. |
| DeepL.net.sln | Add DI projects to the main solution and expand configs. |
| DeepL.Extensions.DependencyInjection/README.md | Usage docs for the DI companion package. |
| DeepL.Extensions.DependencyInjection/DeepLServiceCollectionExtensions.cs | Implements AddDeepLClient registration and interface forwarding. |
| DeepL.Extensions.DependencyInjection/DeepLOptions.cs | Options contract for DI registration (AuthKey, ServerUrl, client name). |
| DeepL.Extensions.DependencyInjection/DeepL.Extensions.DependencyInjection.csproj | New package project targeting netstandard2.0/net8.0 with signing/pack settings. |
| DeepL.Extensions.DependencyInjection.Tests/DeepLServiceCollectionExtensionsTest.cs | Container-level tests for registration/validation/httpclient usage. |
| DeepL.Extensions.DependencyInjection.Tests/DeepL.Extensions.DependencyInjection.Tests.csproj | New test project for DI package (net8, updated test deps). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… builder Agent-Logs-Url: https://github.com/DeepLcom/deepl-dotnet/sessions/ff72e677-f6ec-41d2-89db-47ce9d9494e6 Co-authored-by: DeeJayTC <4077759+DeeJayTC@users.noreply.github.com>
…; guard unsupported minification paths in FluentDocumentTranslation Agent-Logs-Url: https://github.com/DeepLcom/deepl-dotnet/sessions/7bbc1d60-4844-45bd-9a53-7bcbcbe8e1de Co-authored-by: DeeJayTC <4077759+DeeJayTC@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A bundled modernization of the library that fixes a pile of latent issues and pays the "eventually we should do this" tax in one coordinated change.
Runtime and dependency support has rotted
net5.0reached end of support on 2022-05-10.netcoreapp3.1ended on 2022-12-13. Both have been unsupported for multiple years. The library's test project still targeted both.Microsoft.Extensions.Http.Polly 5.0.1,System.Text.Json 5.0.2) have known security advisories that only ship fixes on 6.x+ / 8.x lines. We were shipping consumers onto a patched-CVE surface by default.Microsoft.Extensions.Http.Polly 5.x) is effectively frozen — active development moved to Polly v8'sResiliencePipelineAPI and Microsoft's replacement packageMicrosoft.Extensions.Http.Resilience. Sitting on the 5.x-of-the-wrapper permanently blocks us from that ecosystem.xunit 2.4.1/NSubstitute 4.3.0/Microsoft.NET.Test.Sdk 16.9.4are all 2021-era and miss several analyzer fixes, async-safety guards, and bug fixes that ship in 2.9 / 5.3 / 17.11.LargeFormUrlEncodedContentfor a size-limit bug,LowerSnakeCaseNamingPolicyfor missing BCL API) was no longer necessary: the first was fixed in .NET 5 (Remove unnecessary length restriction on Uri.Escape{Data/Uri}String dotnet/corefx#41686, as our own source comment noted), the second is built into .NET 8 asJsonNamingPolicy.SnakeCaseLower.Consumer ergonomics gaps
TextTranslateOptions/DocumentTranslateOptionsetc. and passing it through positional arguments. Verbose for the common case of "translate this with formality=more".IHttpClientFactorythemselves, handle options validation themselves. Every major .NET client library ships a companion*.Extensions.DependencyInjectionpackage; we didn't.What this PR delivers
Logically separated commits. Each can be reviewed independently.
1.
feat: Add fluent API layer(220a5d8)LINQ-style extensions on top of
ITranslator/IWriter/IGlossaryManager/IStyleRuleManager. Builders are directly awaitable (GetAwaiter) and have implicitTask<T>conversion so they drop intoTask.WhenAlletc. Non-breaking: every fluent method is an extension over an existing interface, so the new API works with anyITranslator/IWriterimplementation (including mocks) and doesn't alter existing call sites.Includes 73 unit tests (NSubstitute-based, no network) covering argument forwarding, every option helper, validation, single + batch shapes, cancellation propagation, and the implicit
Task<T>conversion.2.
build: Modernize target frameworks to net8.0 + netstandard2.0(53b10b1)net5.0;netstandard2.0netstandard2.0;net8.0net5.0;netcoreapp3.1;net462net8.0;net462Dependency bumps:
Microsoft.Extensions.Http.PollySystem.Text.JsonSystem.Net.Http.JsonMicrosoft.NET.Test.Sdkxunitxunit.runner.visualstudioNSubstitutecoverlet.collectorJunitXml.TestLoggernetstandard2.0retained as the floor — the package stays usable from .NET Framework 4.7.2+, Mono, Unity, etc.net462retained in the test project to validate that consumer story.3.
refactor: Adopt .NET 8 BCL APIs in HTTP/JSON internals(4a0465f)Modern BCL on
net8.0, existing code preserved asnetstandard2.0fallback behind#if NET8_0_OR_GREATER/NET5_0_OR_GREATER:JsonNamingPolicy.SnakeCaseLower+HttpContent.ReadFromJsonAsyncreplace the custom snake-case policy and stream reader.HttpMethod.Patchreplaces thenew HttpMethod("PATCH")allocation (factored to a static field in both cases).JsonContent.Createreplacesnew StringContent(JsonSerializer.Serialize(...))— streams instead of an intermediate string allocation.SocketsHttpHandlerwithPooledConnectionLifetime = 5minandPooledConnectionIdleTimeout = 2minreplacesHttpClientHandler. Long-livedHttpClientinstances now pick up DNS changes correctly.HttpVersion.Version20+RequestVersionOrHigherpolicy: DeepL's API supports HTTP/2, enabling proper multiplexing for batch translation.FormUrlEncodedContent(built-in) replacesLargeFormUrlEncodedContentonnet5+. The underlying size-limit bug was fixed in .NET 5 — our custom workaround is now compiled out of the modern asset and only survives for ns2.0.No public API change; ns2.0 consumers see identical behavior, net8 consumers get the modern paths.
4.
feat: Add DeepL.Extensions.DependencyInjection companion package(d9bba3a)New NuGet package (TFMs:
netstandard2.0+net8.0) providingMicrosoft.Extensions.DependencyInjectionintegration:DeepLClientas a singleton (documented thread-safe) and forwards every surface interface (ITranslator,IWriter,IGlossaryManager,IStyleRuleManager) to the same instance.HttpClientthroughIHttpClientFactorywith the named client"DeepL", so consumers can layer their own handlers / resilience policies on top:AuthKeyviaIValidateOptions<>— missing key surfaces on first resolve, not on first API call.Why a separate package rather than adding DI to
DeepL.netitself:DeepL.netstays dependency-free for consumers using Autofac / DryIoc / SimpleInjector or constructingDeepLClientmanually.MediatR.Extensions.Microsoft.DependencyInjection,Polly.Extensions.Http,Serilog.Extensions.Hosting,OpenTelemetry.Extensions.Hosting).Versioning: lockstep with
DeepL.netfor simplicity. Strong-named with the sharedsgKey.snk. Includes 15 DI-container tests covering registration, singleton lifetime, interface forwarding, auth-key validation,HttpClientFactoryintegration,ServerUrlpropagation (verified end-to-end via a capturingHttpMessageHandler), idempotency, and both the configure-delegate +IConfigurationoverloads.5.
docs: Add /samples with FluentApi and DependencyInjection examples(4b88dc8)Two runnable sample console apps:
samples/FluentApi— exhaustive demo of every fluent entry point (text translation, rephrase, document translation one-shot + split, glossary CRUD, style rule CRUD).samples/DependencyInjection— generic host +AddDeepLClient+ anIHostedServiceconsumer pullingITranslator/IWriter/IGlossaryManagerfrom DI.Samples live in their own solution (
samples/DeepL.Samples.slnx) deliberately — out of the main CI scope so they don't affect the library's build or NuGet pack. They useProjectReferenceagainst the sibling library so local changes are picked up on each build.6.
chore: Drop in-progress Voice API draft from this branch(d772750)The branch had picked up an unrelated WIP commit for the Voice API. That work is separately tracked on
tc/add-voiceand will land in its own PR; it was stripped from this branch to keep scope clean. When Voice lands, the DI package just needs one line re-added to forwardIVoiceManagerto the same singleton.Verification
All projects build clean on all TFMs. Tests pass:
The existing integration test suite (
DeepLTests/*Test.cs, requiresDEEPL_AUTH_KEYor a mock server) is unchanged and untouched by this PR — needs a run against staging / mock server before merge to confirm no behavioral regression from the HTTP/JSON refactor.Notes for reviewers
netstandard2.0. Consumer apps on .NET Framework 4.7.2+ see identical behavior; only thenet8.0asset swaps to the new BCL paths.ArgumentNullException.ThrowIfNullconversion). ~40-file mechanical diff; belongs in its own PR.Microsoft.Extensions.Http.Resiliencemigration. Rewrites the retry stack; separate PR.Test plan
dotnet build DeepL.net.sln— clean on all TFMsdotnet test DeepLTests/DeepLTests.csproj -f net8.0 --filter FullyQualifiedName~Fluent— 73/73dotnet test DeepL.Extensions.DependencyInjection.Tests— 15/15dotnet test DeepLTests/DeepLTests.csproj -f net8.0withDEEPL_AUTH_KEYor mock-server — existing suite passes (verifies HTTP/JSON refactor is behavior-preserving)dotnet test DeepLTests/DeepLTests.csproj -f net462with auth key — verifies ns2.0 consumer story still worksdotnet build samples/DeepL.Samples.slnx— samples build cleandotnet run --project samples/FluentApiwith a realDEEPL_AUTH_KEY— end-to-end smokedotnet run --project samples/DependencyInjection— DI end-to-end smoke🤖 Generated with Claude Code