-
Notifications
You must be signed in to change notification settings - Fork 26
Modernize to .NET 8 LTS, add fluent API, DI package, and samples #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
DeeJayTC
wants to merge
10
commits into
main
Choose a base branch
from
deepl-dotnet-2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
5330a21
Draft for a DeepL Voice implementation
DeeJayTC 220a5d8
feat: Add fluent API layer for translation, rephrase, documents, glos…
DeeJayTC 53b10b1
build: Modernize target frameworks to net8.0 + netstandard2.0
DeeJayTC 4a0465f
refactor: Adopt .NET 8 BCL APIs in HTTP/JSON internals
DeeJayTC d9bba3a
feat: Add DeepL.Extensions.DependencyInjection companion package
DeeJayTC 4b88dc8
docs: Add /samples with FluentApi and DependencyInjection examples
DeeJayTC d772750
chore: Drop in-progress Voice API draft from this branch
DeeJayTC a3247f2
feat: Progress reporting + fluent-style cancellation for document tra…
DeeJayTC b63a512
fix: enforce WithStyle/WithTone mutual exclusivity in fluent rephrase…
Copilot a95637a
fix: validate language codes in FluentGlossary.WithDictionary/FromCsv…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
28 changes: 28 additions & 0 deletions
28
DeepL.Extensions.DependencyInjection.Tests/DeepL.Extensions.DependencyInjection.Tests.csproj
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net8.0</TargetFramework> | ||
| <LangVersion>12</LangVersion> | ||
| <TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||
| <WarningsAsErrors>nullable</WarningsAsErrors> | ||
| <Nullable>enable</Nullable> | ||
| <IsPackable>false</IsPackable> | ||
| <RootNamespace>DeepL.Extensions.DependencyInjection.Tests</RootNamespace> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" /> | ||
| <PackageReference Include="xunit" Version="2.9.2" /> | ||
| <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"> | ||
| <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
| <PrivateAssets>all</PrivateAssets> | ||
| </PackageReference> | ||
| <PackageReference Include="coverlet.collector" Version="6.0.2"> | ||
| <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
| <PrivateAssets>all</PrivateAssets> | ||
| </PackageReference> | ||
| <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> | ||
| <ProjectReference Include="..\DeepL.Extensions.DependencyInjection\DeepL.Extensions.DependencyInjection.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
229 changes: 229 additions & 0 deletions
229
DeepL.Extensions.DependencyInjection.Tests/DeepLServiceCollectionExtensionsTest.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,229 @@ | ||
| // Copyright 2026 DeepL SE (https://www.deepl.com) | ||
| // Use of this source code is governed by an MIT | ||
| // license that can be found in the LICENSE file. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Net.Http; | ||
| using DeepL.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Options; | ||
| using Xunit; | ||
|
|
||
| namespace DeepL.Extensions.DependencyInjection.Tests { | ||
| /// <summary> | ||
| /// Tests for <see cref="DeepLServiceCollectionExtensions" />. | ||
| /// These are pure DI-container tests — no DeepL API is called, the configured auth key | ||
| /// is only used to construct the <see cref="DeepLClient" /> (construction is lazy-safe). | ||
| /// </summary> | ||
| public sealed class DeepLServiceCollectionExtensionsTest { | ||
| private const string FakeKey = "00000000-0000-0000-0000-000000000000:fx"; | ||
|
|
||
| private static ServiceProvider BuildProvider(Action<IServiceCollection> configureServices) { | ||
| var services = new ServiceCollection(); | ||
| configureServices(services); | ||
| return services.BuildServiceProvider(); | ||
| } | ||
|
|
||
| // ---------- Configure overload ---------- | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_ConfigureOverload_RegistersClient() { | ||
| using var sp = BuildProvider(s => s.AddDeepLClient(o => o.AuthKey = FakeKey)); | ||
|
|
||
| var client = sp.GetService<DeepLClient>(); | ||
|
|
||
| Assert.NotNull(client); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_RegistersAllSurfaceInterfaces() { | ||
| using var sp = BuildProvider(s => s.AddDeepLClient(o => o.AuthKey = FakeKey)); | ||
|
|
||
| Assert.NotNull(sp.GetService<ITranslator>()); | ||
| Assert.NotNull(sp.GetService<IWriter>()); | ||
| Assert.NotNull(sp.GetService<IGlossaryManager>()); | ||
| Assert.NotNull(sp.GetService<IStyleRuleManager>()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_AllInterfacesResolveToSameSingleton() { | ||
| using var sp = BuildProvider(s => s.AddDeepLClient(o => o.AuthKey = FakeKey)); | ||
|
|
||
| var client = sp.GetRequiredService<DeepLClient>(); | ||
| var translator = sp.GetRequiredService<ITranslator>(); | ||
| var writer = sp.GetRequiredService<IWriter>(); | ||
| var glossary = sp.GetRequiredService<IGlossaryManager>(); | ||
| var styleRule = sp.GetRequiredService<IStyleRuleManager>(); | ||
|
|
||
| Assert.Same(client, translator); | ||
| Assert.Same(client, writer); | ||
| Assert.Same(client, glossary); | ||
| Assert.Same(client, styleRule); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_SingletonLifetime_ReturnsSameInstance() { | ||
| using var sp = BuildProvider(s => s.AddDeepLClient(o => o.AuthKey = FakeKey)); | ||
|
|
||
| var first = sp.GetRequiredService<DeepLClient>(); | ||
| var second = sp.GetRequiredService<DeepLClient>(); | ||
|
|
||
| Assert.Same(first, second); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_MissingAuthKey_ThrowsOnResolve() { | ||
| using var sp = BuildProvider(s => s.AddDeepLClient(o => o.AuthKey = "")); | ||
|
|
||
| var ex = Assert.Throws<OptionsValidationException>(() => sp.GetRequiredService<DeepLClient>()); | ||
| Assert.Contains("AuthKey", ex.Message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_WhitespaceAuthKey_ThrowsOnResolve() { | ||
| using var sp = BuildProvider(s => s.AddDeepLClient(o => o.AuthKey = " ")); | ||
|
|
||
| Assert.Throws<OptionsValidationException>(() => sp.GetRequiredService<DeepLClient>()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_UsesNamedHttpClientFromFactory() { | ||
| using var sp = BuildProvider(s => s.AddDeepLClient(o => o.AuthKey = FakeKey)); | ||
|
|
||
| // Resolving DeepLClient invokes IHttpClientFactory.CreateClient("DeepL") during construction; | ||
| // if the named client wasn't registered, the resolution would throw. | ||
| var factory = sp.GetRequiredService<IHttpClientFactory>(); | ||
| using var namedClient = factory.CreateClient(DeepLOptions.HttpClientName); | ||
|
|
||
| Assert.NotNull(namedClient); | ||
|
|
||
| // And actually resolving DeepLClient (which goes through the factory) must succeed. | ||
| var client = sp.GetRequiredService<DeepLClient>(); | ||
| Assert.NotNull(client); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async System.Threading.Tasks.Task AddDeepLClient_ServerUrl_PropagatesToClient() { | ||
| // Construct with a custom server URL and a captured HttpClient we can interrogate | ||
| // via an ordinary HttpMessageHandler that simply records the request URI. | ||
| var captured = new UriCapturingHandler(); | ||
|
|
||
| var services = new ServiceCollection(); | ||
| services.AddDeepLClient(o => { | ||
| o.AuthKey = FakeKey; | ||
| o.ServerUrl = "https://example.invalid/deepl/"; | ||
| }); | ||
|
|
||
| // Replace the named HttpClient's primary handler with the capturing one. | ||
| services.AddHttpClient(DeepLOptions.HttpClientName) | ||
| .ConfigurePrimaryHttpMessageHandler(() => captured); | ||
|
|
||
| using var sp = services.BuildServiceProvider(); | ||
| var client = sp.GetRequiredService<DeepLClient>(); | ||
|
|
||
| // Fire a request and swallow the resulting exception. We only care that the request | ||
| // hit the configured ServerUrl. | ||
| try { | ||
| await client.GetUsageAsync(); | ||
| } catch { | ||
| /* expected — the fake handler returns an empty response the client can't parse */ | ||
| } | ||
|
|
||
| Assert.NotNull(captured.LastRequestUri); | ||
| Assert.StartsWith("https://example.invalid/deepl/", captured.LastRequestUri!.ToString()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_Idempotent_SecondCallDoesNotDuplicateRegistrations() { | ||
| // Consumers sometimes call AddDeepLClient in library extensions plus app startup. | ||
| // TryAdd* semantics should make the second call a no-op. | ||
| var services = new ServiceCollection(); | ||
| services.AddDeepLClient(o => o.AuthKey = FakeKey); | ||
| services.AddDeepLClient(o => o.AuthKey = FakeKey); | ||
|
|
||
| using var sp = services.BuildServiceProvider(); | ||
| var clients = sp.GetServices<DeepLClient>(); | ||
|
|
||
| Assert.Single(clients); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_NullServices_Throws() { | ||
| IServiceCollection? services = null; | ||
| Assert.Throws<ArgumentNullException>( | ||
| () => services!.AddDeepLClient(o => o.AuthKey = FakeKey)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_NullConfigureDelegate_Throws() { | ||
| var services = new ServiceCollection(); | ||
| Assert.Throws<ArgumentNullException>( | ||
| () => services.AddDeepLClient((Action<DeepLOptions>)null!)); | ||
| } | ||
|
|
||
| // ---------- Configuration overload ---------- | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_ConfigurationOverload_BindsFromDefaultSection() { | ||
| var config = new ConfigurationBuilder() | ||
| .AddInMemoryCollection(new Dictionary<string, string?> { | ||
| ["DeepL:AuthKey"] = FakeKey, | ||
| ["DeepL:ServerUrl"] = "https://api.deepl.com/" | ||
| }) | ||
| .Build(); | ||
|
|
||
| using var sp = BuildProvider(s => s.AddDeepLClient(config)); | ||
|
|
||
| var opts = sp.GetRequiredService<IOptions<DeepLOptions>>().Value; | ||
| Assert.Equal(FakeKey, opts.AuthKey); | ||
| Assert.Equal("https://api.deepl.com/", opts.ServerUrl); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_ConfigurationOverload_AcceptsExplicitSection() { | ||
| var config = new ConfigurationBuilder() | ||
| .AddInMemoryCollection(new Dictionary<string, string?> { | ||
| ["Translation:DeepL:AuthKey"] = FakeKey, | ||
| }) | ||
| .Build(); | ||
|
|
||
| using var sp = BuildProvider(s => s.AddDeepLClient(config.GetSection("Translation:DeepL"))); | ||
|
|
||
| var opts = sp.GetRequiredService<IOptions<DeepLOptions>>().Value; | ||
| Assert.Equal(FakeKey, opts.AuthKey); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_ConfigurationOverload_MissingKey_Throws() { | ||
| var config = new ConfigurationBuilder() | ||
| .AddInMemoryCollection(new Dictionary<string, string?>()) | ||
| .Build(); | ||
|
|
||
| using var sp = BuildProvider(s => s.AddDeepLClient(config)); | ||
|
|
||
| Assert.Throws<OptionsValidationException>(() => sp.GetRequiredService<DeepLClient>()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AddDeepLClient_ConfigurationOverload_NullConfig_Throws() { | ||
| var services = new ServiceCollection(); | ||
| Assert.Throws<ArgumentNullException>( | ||
| () => services.AddDeepLClient((IConfiguration)null!)); | ||
| } | ||
|
|
||
| // ---------- Test helpers ---------- | ||
|
|
||
| private sealed class UriCapturingHandler : HttpMessageHandler { | ||
| public Uri? LastRequestUri { get; private set; } | ||
|
|
||
| protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync( | ||
| HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { | ||
| LastRequestUri = request.RequestUri; | ||
| return System.Threading.Tasks.Task.FromResult( | ||
| new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent("{}") }); | ||
| } | ||
| } | ||
| } | ||
| } |
45 changes: 45 additions & 0 deletions
45
DeepL.Extensions.DependencyInjection/DeepL.Extensions.DependencyInjection.csproj
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <Description>Microsoft.Extensions.DependencyInjection integration for DeepL.net. Adds AddDeepLClient() to register DeepLClient and its interfaces (ITranslator, IWriter, IGlossaryManager, IStyleRuleManager, IVoiceManager) with an IServiceCollection, routing the underlying HttpClient through IHttpClientFactory.</Description> | ||
| <AssemblyTitle>DeepL.Extensions.DependencyInjection</AssemblyTitle> | ||
| <Version>1.20.0</Version> | ||
| <PackageVersion>1.20.0</PackageVersion> | ||
| <FileVersion>1.20.0.0</FileVersion> | ||
| <AssemblyVersion>1.0.0.0</AssemblyVersion> | ||
| <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks> | ||
| <LangVersion>12</LangVersion> | ||
| <Nullable>enable</Nullable> | ||
| <TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||
| <WarningsAsErrors>nullable</WarningsAsErrors> | ||
| <RootNamespace>DeepL.Extensions.DependencyInjection</RootNamespace> | ||
| <Authors>DeepL SE</Authors> | ||
| <Company>DeepL SE</Company> | ||
| <AssemblyName>DeepL.Extensions.DependencyInjection</AssemblyName> | ||
| <PackageId>DeepL.Extensions.DependencyInjection</PackageId> | ||
| <PackageTags>deepl;translation;api;dependency-injection;di;aspnetcore</PackageTags> | ||
| <PackageIcon>icon.png</PackageIcon> | ||
| <PackageProjectUrl>https://www.deepl.com/pro-api</PackageProjectUrl> | ||
| <RepositoryUrl>https://github.com/DeepLcom/deepl-dotnet</RepositoryUrl> | ||
| <PackageLicenseExpression>MIT</PackageLicenseExpression> | ||
| <PackageReadmeFile>README.md</PackageReadmeFile> | ||
| <PackageReleaseNotes>Release notes can be found at https://github.com/DeepLcom/deepl-dotnet/blob/main/CHANGELOG.md</PackageReleaseNotes> | ||
| <IsPackable>true</IsPackable> | ||
| <PublishRepositoryUrl>true</PublishRepositoryUrl> | ||
| <GeneratePackageOnBuild>true</GeneratePackageOnBuild> | ||
| <SignAssembly>true</SignAssembly> | ||
| <AssemblyOriginatorKeyFile>..\DeepL\sgKey.snk</AssemblyOriginatorKeyFile> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\DeepL\DeepL.csproj" /> | ||
| <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" /> | ||
| <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <None Include="README.md" Pack="true" PackagePath=""/> | ||
| <None Include="..\icon.png" Pack="true" PackagePath=""/> | ||
| </ItemGroup> | ||
|
|
||
| </Project> | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // Copyright 2026 DeepL SE (https://www.deepl.com) | ||
| // Use of this source code is governed by an MIT | ||
| // license that can be found in the LICENSE file. | ||
|
|
||
| namespace DeepL.Extensions.DependencyInjection { | ||
| /// <summary> | ||
| /// Configuration contract for | ||
| /// <see cref="DeepLServiceCollectionExtensions.AddDeepLClient(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{DeepLOptions})" /> | ||
| /// (and its overloads). | ||
| /// Typically populated from configuration: | ||
| /// <code> | ||
| /// services.AddDeepLClient(builder.Configuration.GetSection("DeepL")); | ||
| /// </code> | ||
| /// with a matching <c>appsettings.json</c> section: | ||
| /// <code> | ||
| /// "DeepL": { | ||
| /// "AuthKey": "...", | ||
| /// "ServerUrl": "https://api.deepl.com" | ||
| /// } | ||
| /// </code> | ||
| /// </summary> | ||
| public sealed class DeepLOptions { | ||
| /// <summary>Default configuration section name (<c>"DeepL"</c>) used by the <c>IConfiguration</c> overload.</summary> | ||
| public const string DefaultSectionName = "DeepL"; | ||
|
|
||
| /// <summary> | ||
| /// Name used when resolving the underlying <see cref="System.Net.Http.HttpClient" /> via | ||
| /// <see cref="System.Net.Http.IHttpClientFactory" />. Consumers can call | ||
| /// <see cref="Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions" /> | ||
| /// against this name to layer on additional handlers or policies. | ||
| /// </summary> | ||
| public const string HttpClientName = "DeepL"; | ||
|
|
||
| /// <summary>DeepL API auth key. Required.</summary> | ||
| public string AuthKey { get; set; } = string.Empty; | ||
|
|
||
| /// <summary>Optional override for the DeepL API server URL (for testing / proxying).</summary> | ||
| public string? ServerUrl { get; set; } | ||
| } | ||
| } |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.