diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index e51709ec3..dd55f8c7a 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -80,6 +80,10 @@
+
+
+
+
$(AssemblyName).testconfig.json
diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets
index 461c687e1..5ed00154a 100644
--- a/src/Directory.Build.targets
+++ b/src/Directory.Build.targets
@@ -10,4 +10,10 @@
+
+
+
+
+
+
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 765448f97..414dc0196 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -5,7 +5,7 @@
- 5.4.0
+ 5.6.01.56.0
@@ -28,7 +28,6 @@
-
diff --git a/src/Refit.Newtonsoft.Json/NewtonsoftJsonContentSerializer.cs b/src/Refit.Newtonsoft.Json/NewtonsoftJsonContentSerializer.cs
index 2a89a473e..b6bdeeaf7 100644
--- a/src/Refit.Newtonsoft.Json/NewtonsoftJsonContentSerializer.cs
+++ b/src/Refit.Newtonsoft.Json/NewtonsoftJsonContentSerializer.cs
@@ -41,13 +41,11 @@ public NewtonsoftJsonContentSerializer()
"AOT",
"IL3050:Calling members annotated with RequiresDynamicCodeAttribute may break when AOT compiling",
Justification = "Interface method is unannotated on net8.0+ so cannot propagate; Newtonsoft path is documented as unsuitable for trimmed/AOT apps.")]
- public HttpContent ToHttpContent(T item)
- {
- return new StringContent(
+ public HttpContent ToHttpContent(T item) =>
+ new StringContent(
JsonConvert.SerializeObject(item, _jsonSerializerSettings.Value),
Encoding.UTF8,
"application/json");
- }
///
[UnconditionalSuppressMessage(
diff --git a/src/Refit/AnonymousDisposable.cs b/src/Refit/AnonymousDisposable.cs
deleted file mode 100644
index d69d6986c..000000000
--- a/src/Refit/AnonymousDisposable.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
-// ReactiveUI and Contributors licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for full license information.
-namespace Refit;
-
-/// An that runs the supplied action when disposed.
-/// The action to run on disposal.
-internal sealed class AnonymousDisposable(Action block) : IDisposable
-{
- ///
- public void Dispose() => block();
-}
diff --git a/src/Refit/ApiResponse{T}.cs b/src/Refit/ApiResponse{T}.cs
index 10e26de99..f3eb3e449 100644
--- a/src/Refit/ApiResponse{T}.cs
+++ b/src/Refit/ApiResponse{T}.cs
@@ -129,23 +129,19 @@ public ApiResponse(
/// The current
/// Thrown when an unsuccessful response was received from the server.
/// Thrown when the request failed before receiving a response from the server.
- public Task> EnsureSuccessStatusCodeAsync()
- {
- return IsSuccessStatusCode
+ public Task> EnsureSuccessStatusCodeAsync() =>
+ IsSuccessStatusCode
? Task.FromResult(this)
: EnsureSlowAsync();
- }
/// Ensures the request was successful and without any other error by throwing an exception in case of failure.
/// The current
/// Thrown when an unsuccessful response was received from the server.
/// Thrown when the request failed before receiving a response from the server.
- public Task> EnsureSuccessfulAsync()
- {
- return IsSuccessful
+ public Task> EnsureSuccessfulAsync() =>
+ IsSuccessful
? Task.FromResult(this)
: EnsureSlowAsync();
- }
///
[SuppressMessage(
diff --git a/src/Refit/Refit.csproj b/src/Refit/Refit.csproj
index 27725daf8..54e338df3 100644
--- a/src/Refit/Refit.csproj
+++ b/src/Refit/Refit.csproj
@@ -11,6 +11,7 @@
+
diff --git a/src/Refit/RequestBuilderImplementation.Execution.cs b/src/Refit/RequestBuilderImplementation.Execution.cs
index f04e54f65..f82282708 100644
--- a/src/Refit/RequestBuilderImplementation.Execution.cs
+++ b/src/Refit/RequestBuilderImplementation.Execution.cs
@@ -97,9 +97,8 @@ await RequestExecutionHelpers.SendVoidAsync(
Justification = "Type parameter intentionally specified explicitly by callers.")]
[RequiresDynamicCode("Serializing a body by runtime Type requires runtime generic method instantiation.")]
private Func> BuildCancellableTaskFuncForMethod(
- RestMethodInfoInternal restMethod)
- {
- return async (client, ct, paramList) =>
+ RestMethodInfoInternal restMethod) =>
+ async (client, ct, paramList) =>
{
RequestExecutionHelpers.ThrowIfBaseAddressMissing(client);
@@ -120,7 +119,6 @@ await RequestExecutionHelpers.SendVoidAsync(
request?.Dispose();
}
};
- }
/// Processes a response for a reflection-built request using the shared runtime state machine.
/// The result type returned to the caller.
diff --git a/src/Refit/RequestBuilderImplementation.TaskToObservable.cs b/src/Refit/RequestBuilderImplementation.TaskToObservable.cs
deleted file mode 100644
index cc34b32ac..000000000
--- a/src/Refit/RequestBuilderImplementation.TaskToObservable.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
-// ReactiveUI and Contributors licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for full license information.
-namespace Refit;
-
-/// Adapts cancellable task factories into observable sequences for .
-[System.Diagnostics.CodeAnalysis.SuppressMessage(
- "Minor Code Smell",
- "SST1432:Mark type as static",
- Justification = "False positive: this is one part of a partial class whose other parts declare instance members; the type cannot be static.")]
-internal partial class RequestBuilderImplementation
-{
- /// Adapts a cancellable task factory into an observable sequence.
- /// The result type produced by the task.
- private sealed class TaskToObservable : IObservable
- {
- /// The factory that produces the task to observe.
- private readonly Func> _taskFactory;
-
- /// Initializes a new instance of the class.
- /// The factory that produces the task to observe.
- public TaskToObservable(Func> taskFactory) => this._taskFactory = taskFactory;
-
- ///
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD110", Justification = "Subscription is fire-and-forget; the continuation task is intentionally not awaited.")]
- public IDisposable Subscribe(IObserver observer)
- {
- var cts = new CancellationTokenSource();
- _taskFactory(cts.Token)
- .ContinueWith(
- t =>
- {
- try
- {
- if (cts.IsCancellationRequested)
- {
- return;
- }
-
- ToObservableDone(t, observer);
- }
- finally
- {
- cts.Dispose();
- }
- },
- TaskScheduler.Default);
-
- return new AnonymousDisposable(() =>
- {
- try
- {
- cts.Cancel();
- }
- catch (ObjectDisposedException)
- {
- // The token source was already disposed by the completed continuation; nothing to cancel.
- }
- });
- }
-
- /// Forwards the completed task's outcome to the observer.
- /// The result type of the task.
- /// The completed task.
- /// The observer to notify.
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002", Justification = "Task is already completed here, so the result read never blocks.")]
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Sonar", "S4462", Justification = "Task is already completed here, so the result read never blocks.")]
- private static void ToObservableDone(Task task, IObserver subject)
- {
- switch (task.Status)
- {
- case TaskStatus.RanToCompletion:
- {
- subject.OnNext(task.Result);
- subject.OnCompleted();
- break;
- }
-
- case TaskStatus.Faulted:
- {
- subject.OnError(task.Exception!.InnerException!);
- break;
- }
-
- case TaskStatus.Canceled:
- {
- subject.OnError(new TaskCanceledException(task));
- break;
- }
- }
- }
- }
-}
diff --git a/src/Refit/RequestBuilderImplementation.cs b/src/Refit/RequestBuilderImplementation.cs
index 51015feaa..4abab1d05 100644
--- a/src/Refit/RequestBuilderImplementation.cs
+++ b/src/Refit/RequestBuilderImplementation.cs
@@ -5,10 +5,15 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
+using ReactiveUI.Primitives.Advanced;
namespace Refit
{
/// Reflection-based request builder that turns Refit interface calls into HTTP requests.
+ [SuppressMessage(
+ "Minor Code Smell",
+ "SST1432:Mark type as static",
+ Justification = "False positive: this is one part of a partial class whose other parts declare instance members; the type cannot be static.")]
internal partial class RequestBuilderImplementation : IRequestBuilder
{
/// Maximum stack-allocated buffer size, in characters, used when building paths and query strings.
@@ -441,9 +446,8 @@ private RestMethodInfoInternal CloseGenericMethodIfNeeded(
Justification = "Type parameter intentionally specified explicitly by callers.")]
[RequiresDynamicCode("Serializing a body by runtime Type requires runtime generic method instantiation.")]
private Func BuildGeneratedSyncFuncForMethodGeneric(
- RestMethodInfoInternal restMethod)
- {
- return (client, paramList) =>
+ RestMethodInfoInternal restMethod) =>
+ (client, paramList) =>
RunSynchronous(() =>
ExecuteRequestAsync(
client,
@@ -451,7 +455,6 @@ private RestMethodInfoInternal CloseGenericMethodIfNeeded(
paramList,
paramsContainsCancellationToken: false,
CancellationToken.None));
- }
/// Builds an observable invocation delegate for a method.
/// The result type returned to the caller.
@@ -469,7 +472,7 @@ private RestMethodInfoInternal CloseGenericMethodIfNeeded(
var taskFunc = BuildCancellableTaskFuncForMethod(restMethod);
return (client, paramList) =>
- new TaskToObservable(ct =>
+ new FromAsyncSignal(ct =>
{
var methodCt = CancellationToken.None;
if (restMethod.CancellationToken is not null)
@@ -539,9 +542,8 @@ private RestMethodInfoInternal CloseGenericMethodIfNeeded(
/// A delegate that returns a task with no result.
[RequiresDynamicCode("Serializing a body by runtime Type requires runtime generic method instantiation.")]
private Func BuildVoidTaskFuncForMethod(
- RestMethodInfoInternal restMethod)
- {
- return (client, paramList) =>
+ RestMethodInfoInternal restMethod) =>
+ (client, paramList) =>
{
var ct = CancellationToken.None;
@@ -557,6 +559,5 @@ private Func BuildVoidTaskFuncForMethod(
restMethod.CancellationToken is not null,
ct);
};
- }
}
}
diff --git a/src/tests/Refit.GeneratorTests/Fixture.cs b/src/tests/Refit.GeneratorTests/Fixture.cs
index af407c2f0..d41c1289c 100644
--- a/src/tests/Refit.GeneratorTests/Fixture.cs
+++ b/src/tests/Refit.GeneratorTests/Fixture.cs
@@ -32,7 +32,7 @@ public static class Fixture
[
typeof(Binder),
typeof(GetAttribute),
- typeof(System.Reactive.Unit),
+ typeof(ReactiveUI.Primitives.RxVoid),
typeof(Enumerable),
typeof(Newtonsoft.Json.JsonConvert),
typeof(TestAttribute),
diff --git a/src/tests/Refit.GeneratorTests/ParserCoverageTests.cs b/src/tests/Refit.GeneratorTests/ParserCoverageTests.cs
index a7684b53e..47c9d3d02 100644
--- a/src/tests/Refit.GeneratorTests/ParserCoverageTests.cs
+++ b/src/tests/Refit.GeneratorTests/ParserCoverageTests.cs
@@ -18,8 +18,7 @@ public sealed class ParserCoverageTests
/// Verifies parser argument validation.
/// A task representing the asynchronous test.
[Test]
- public async Task GenerateInterfaceStubsRejectsNullCompilation()
- {
+ public async Task GenerateInterfaceStubsRejectsNullCompilation() =>
await Assert.That(
() => Parser.GenerateInterfaceStubs(
null!,
@@ -30,7 +29,6 @@ await Assert.That(
[],
CancellationToken.None))
.ThrowsExactly();
- }
/// Verifies parser diagnostics and namespace normalization when Refit is not referenced.
/// A task representing the asynchronous test.
diff --git a/src/tests/Refit.GeneratorTests/Refit.GeneratorTests.csproj b/src/tests/Refit.GeneratorTests/Refit.GeneratorTests.csproj
index 0e1b75307..8bc3a8032 100644
--- a/src/tests/Refit.GeneratorTests/Refit.GeneratorTests.csproj
+++ b/src/tests/Refit.GeneratorTests/Refit.GeneratorTests.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/src/tests/Refit.Tests/FormValueMultimapTests.cs b/src/tests/Refit.Tests/FormValueMultimapTests.cs
index 743e18490..473fd76f4 100644
--- a/src/tests/Refit.Tests/FormValueMultimapTests.cs
+++ b/src/tests/Refit.Tests/FormValueMultimapTests.cs
@@ -40,11 +40,9 @@ public async Task EmptyIfNullPassedIn()
/// Verifies a null settings instance is rejected before source processing.
/// A task that represents the asynchronous operation.
[Test]
- public async Task RejectsNullSettings()
- {
+ public async Task RejectsNullSettings() =>
await Assert.That(() => new FormValueMultimap(new object(), null!))
.ThrowsExactly();
- }
/// Verifies the multimap loads entries from a dictionary source.
/// A task that represents the asynchronous operation.
diff --git a/src/tests/Refit.Tests/ObservableTestHelpers.cs b/src/tests/Refit.Tests/ObservableTestHelpers.cs
new file mode 100644
index 000000000..e5275663f
--- /dev/null
+++ b/src/tests/Refit.Tests/ObservableTestHelpers.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
+// ReactiveUI and Contributors licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using ReactiveUI.Primitives.Advanced;
+using ReactiveUI.Primitives.Concurrency;
+using ReactiveUI.Primitives.Signals;
+
+namespace Refit.Tests;
+
+/// Test helpers for awaiting observable results through concrete Primitives types.
+internal static class ObservableTestHelpers
+{
+ /// The timeout used by observable integration tests.
+ private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10);
+
+ /// Wraps the source in a timeout signal.
+ /// The observable value type.
+ /// The source observable.
+ /// A timeout-wrapped observable.
+ public static IObservable WithTimeout(IObservable source) =>
+ new ExpireSignal(source, DefaultTimeout, ThreadPoolSequencer.Instance);
+
+ /// Awaits the timeout-wrapped source.
+ /// The observable value type.
+ /// The source observable.
+ /// The final observable value.
+ public static Task AwaitWithTimeout(IObservable source) =>
+ Await(WithTimeout(source));
+
+ /// Awaits the source through a concrete Primitives await signal.
+ /// The observable value type.
+ /// The source observable.
+ /// The final observable value.
+ public static async Task Await(IObservable source)
+ {
+ AsyncSignal signal = new();
+ using var subscription = source.Subscribe(signal);
+ return await signal;
+ }
+}
diff --git a/src/tests/Refit.Tests/Refit.Tests.csproj b/src/tests/Refit.Tests/Refit.Tests.csproj
index 814b673ee..a05b6ccd8 100644
--- a/src/tests/Refit.Tests/Refit.Tests.csproj
+++ b/src/tests/Refit.Tests/Refit.Tests.csproj
@@ -34,8 +34,8 @@
+
-
diff --git a/src/tests/Refit.Tests/RequestBuilderTests.cs b/src/tests/Refit.Tests/RequestBuilderTests.cs
index 820b6fbca..48772393b 100644
--- a/src/tests/Refit.Tests/RequestBuilderTests.cs
+++ b/src/tests/Refit.Tests/RequestBuilderTests.cs
@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for full license information.
using System.Diagnostics.CodeAnalysis;
-using System.Reactive.Linq;
namespace Refit.Tests;
@@ -22,11 +21,9 @@ public partial class RequestBuilderTests
/// Rejects non-interface request-builder targets.
/// A task that represents the asynchronous operation.
[Test]
- public async Task ConstructorRejectsNonInterfaceTargets()
- {
+ public async Task ConstructorRejectsNonInterfaceTargets() =>
await Assert.That(() => new RequestBuilderImplementation(typeof(string)))
.ThrowsExactly();
- }
/// Verifies the public request-builder factory entry points create usable builders.
/// A task that represents the asynchronous operation.
@@ -397,7 +394,7 @@ public async Task ObservableMethodsWithCancellationTokenShouldCancelWhenRequeste
},
["value", cts.Token])!;
- await Assert.That(() => observable.Wait()).ThrowsExactly();
+ await Assert.That(() => (Task)ObservableTestHelpers.Await(observable)).ThrowsExactly();
await Assert.That(testHttpMessageHandler.RequestMessage!.RequestUri!.ToString()).IsEqualTo("http://api/value/value");
await Assert.That(testHttpMessageHandler.CancellationToken.IsCancellationRequested).IsTrue();
}
diff --git a/src/tests/Refit.Tests/RestServiceIntegrationTests.GitHub.cs b/src/tests/Refit.Tests/RestServiceIntegrationTests.GitHub.cs
index ec47901bd..7902cf661 100644
--- a/src/tests/Refit.Tests/RestServiceIntegrationTests.GitHub.cs
+++ b/src/tests/Refit.Tests/RestServiceIntegrationTests.GitHub.cs
@@ -6,7 +6,6 @@
using System.Linq;
using System.Net;
using System.Net.Http;
-using System.Reactive.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -167,9 +166,8 @@ public async Task HitTheGitHubUserApiAsObservableApiResponse()
var fixture = RestService.For("https://api.github.com", settings);
- var result = await fixture
- .GetUserObservableWithMetadata("octocat")
- .Timeout(TimeSpan.FromSeconds(10));
+ var result = await ObservableTestHelpers.AwaitWithTimeout(
+ fixture.GetUserObservableWithMetadata("octocat"));
await Assert.That(result.Headers!.Any()).IsTrue();
await Assert.That(result.IsSuccessStatusCode).IsTrue();
@@ -216,9 +214,8 @@ public async Task HitTheGitHubUserApiAsObservableIApiResponse()
var fixture = RestService.For("https://api.github.com", settings);
- var result = await fixture
- .GetUserIApiResponseObservableWithMetadata("octocat")
- .Timeout(TimeSpan.FromSeconds(10));
+ var result = await ObservableTestHelpers.AwaitWithTimeout(
+ fixture.GetUserIApiResponseObservableWithMetadata("octocat"));
await Assert.That(result.Headers!.Any()).IsTrue();
await Assert.That(result.IsSuccessStatusCode).IsTrue();
@@ -286,7 +283,7 @@ public async Task HitWithCamelCaseParameter()
var fixture = RestService.For("https://api.github.com", settings);
- var result = await fixture.GetUserCamelCase("octocat");
+ var result = await ObservableTestHelpers.AwaitWithTimeout(fixture.GetUserCamelCase("octocat"));
await Assert.That(result.Login).IsEqualTo("octocat");
await Assert.That(string.IsNullOrEmpty(result.AvatarUrl)).IsFalse();
@@ -516,7 +513,7 @@ public async Task HitTheGitHubUserApiAsObservable()
var fixture = RestService.For("https://api.github.com", settings);
- var result = await fixture.GetUserObservable("octocat").Timeout(TimeSpan.FromSeconds(10));
+ var result = await ObservableTestHelpers.AwaitWithTimeout(fixture.GetUserObservable("octocat"));
await Assert.That(result.Login).IsEqualTo("octocat");
await Assert.That(string.IsNullOrEmpty(result.AvatarUrl)).IsFalse();
@@ -547,12 +544,12 @@ public async Task HitTheGitHubUserApiAsObservableAndSubscribeAfterTheFact()
var fixture = RestService.For("https://api.github.com", settings);
- var obs = fixture.GetUserObservable("octocat").Timeout(TimeSpan.FromSeconds(10));
+ var obs = ObservableTestHelpers.WithTimeout(fixture.GetUserObservable("octocat"));
// NB: We're gonna await twice, so that the 2nd await is definitely
// after the result has completed.
- await obs;
- var result2 = await obs;
+ await ObservableTestHelpers.Await(obs);
+ var result2 = await ObservableTestHelpers.Await(obs);
await Assert.That(result2.Login).IsEqualTo("octocat");
await Assert.That(string.IsNullOrEmpty(result2.AvatarUrl)).IsFalse();
}
@@ -573,12 +570,12 @@ public async Task TwoSubscriptionsResultInTwoRequests()
await Assert.That(input.MessagesSent).IsEqualTo(0);
- var obs = fixture.GetIndexObservable().Timeout(TimeSpan.FromSeconds(10));
+ var obs = ObservableTestHelpers.WithTimeout(fixture.GetIndexObservable());
- var result1 = await obs;
+ var result1 = await ObservableTestHelpers.Await(obs);
await Assert.That(input.MessagesSent).IsEqualTo(1);
- var result2 = await obs;
+ var result2 = await ObservableTestHelpers.Await(obs);
await Assert.That(input.MessagesSent).IsEqualTo(2);
// NB: TestHttpMessageHandler returns what we tell it to ('test' by default)