docs/breaking-changes.md
Every breaking change and notable addition, newest first. Each major version links back to the feature documentation in the main README.
Refit 14 finishes the move to generated request building. Interfaces whose methods all generate inline no longer touch the reflection request builder at any point, which is what makes them trim and Native AOT clean — and that shift is visible in three places.
Some hot-path async members now return ValueTask instead of Task (allocation reductions for the breaking
release). ApiResponse<T>.EnsureSuccessStatusCodeAsync()/EnsureSuccessfulAsync() (and the matching
IApiResponse<T> extension methods) and DefaultApiExceptionFactory.CreateAsync now return ValueTask<...>, making
their common success path allocation-free. The RefitSettings async delegates ExceptionFactory,
DeserializationExceptionFactory, and AuthorizationHeaderValueGetter now use ValueTask-returning Func shapes,
removing a per-request Task allocation from the default (synchronously-completing) exception factory. async
lambdas assigned to these delegates need no change; a delegate that returned Task.FromResult(x) should return
new ValueTask<T>(x), and a caller that stored an EnsureSuccess...Async() result as a Task should await it or
call .AsTask(). TransportExceptionFactory is unaffected (it was already synchronous).
The reflection request builder is now an opt-in package. The Refit package no longer carries it. If any of
your interface methods cannot generate inline — the RF006 diagnostic reports exactly which — add a reference to the
Refit.Reflection package and everything works as before, with
no code changes. Without it, RestService.For and AddRefitClient throw a NotSupportedException naming the
package when they reach a method that needs reflection. Applications whose interfaces all generate inline should not
install it: that is what keeps a client trimmable and Native AOT clean.
Interface validation for fully generated interfaces happens when a request is built, not when the client is
created. RestService.For<T> still validates reflection-backed interfaces eagerly, but an interface whose methods
all generate inline never touches the reflection request builder — an invalid route template (for example a
placeholder with no matching parameter) now throws the same ArgumentException on the first call instead of inside
RestService.For.
ApiException stack traces no longer contain the generated method frame for methods that construct requests
inline (they return the runner task directly instead of awaiting inside an async wrapper). Use
ApiException.HttpMethod/ApiException.Uri to identify the failing request.
Query objects are flattened by their declared type, not their runtime type. A complex parameter whose public
properties become query pairs is now flattened at compile time, the same way the System.Text.Json source generator
treats a declared type. The reflection request builder called value.GetType(), so passing a derived instance
through a base-typed parameter also contributed the derived type's extra properties. Generated request building emits
only the properties declared on the parameter's type:
public record BaseRecord(string Value);
public sealed record DerivedRecord(string Name) : BaseRecord("value");
[Get("/search")]
Task<string> Search(BaseRecord query);
Search(new DerivedRecord("ada"))
>>> "/search?Value=value" // V14, generated: declared type only
>>> "/search?Name=ada&Value=value" // V13, reflection: runtime type
Only polymorphic query objects are affected; a parameter whose declared type is the type you actually pass is unchanged. If you rely on the old behavior, declare the parameter as the derived type.
Flattened query-object keys honor the content serializer's property names. A query object's property keys now
resolve with the same precedence form-encoded field names already used: [AliasAs] first, then the configured
content serializer's field name ([JsonPropertyName] for the default System.Text.Json serializer, [JsonProperty]
for Refit.Newtonsoft.Json), then the URL parameter key formatter. Previously query keys consulted only [AliasAs]
and the key formatter, so a [JsonPropertyName] on a flattened property had no effect on the query string.
public sealed class Filter
{
[JsonPropertyName("created_after")]
public DateTimeOffset? CreatedAfter { get; set; }
}
[Get("/search")]
Task<string> Search([Query] Filter filter);
Search(new Filter { CreatedAfter = ... })
>>> "/search?created_after=..." // V14: serializer field name
>>> "/search?CreatedAfter=..." // V13: CLR name
Both request builders apply this. The generated path bakes the [JsonPropertyName] name at compile time (matching
the generated form path); the reflection path resolves it through the configured serializer, so it also picks up
[JsonProperty] when Refit.Newtonsoft.Json is installed. To keep the pre-V14 behavior, set
RefitSettings.HonorContentSerializerPropertyNamesInQuery = false; [AliasAs] still takes precedence in either mode.
Refit no longer disposes a request-body stream you supply. Refit disposes each request message after sending,
which disposes its content and any stream that content holds. Previously that reached a caller-owned stream: a
StreamPart, or a Stream passed as a [Body] or multipart parameter, was closed once the request completed, so a
second call with the same stream failed. Refit now wraps caller-supplied streams so disposal stops at the wrapper and
the stream you passed stays open — you own it and are responsible for disposing it. Streams Refit opens itself are
unchanged: a FileInfoPart (or a FileInfo multipart parameter) still has the file stream Refit opened closed for
you. Both request builders apply this. If you relied on Refit closing your stream, dispose it yourself after the call.
URL-encoded bodies flatten nested objects, dictionaries, and collections instead of emitting the type name. A
[Body(BodySerializationMethod.UrlEncoded)] model whose property is a nested object, an IDictionary, or a plain
collection under the default collection format previously serialized the property's ToString() — the type name for
a complex object or dictionary. Those properties now flatten exactly like a flattened [Query] object: a nested
object contributes parent.child=... pairs, a dictionary contributes parent.key=value pairs, and a collection
joins its elements (comma-separated under the default format). Field naming keeps the existing precedence ([AliasAs],
then the content serializer's property name, then the key formatter), and a nested property's [Query(delimiter, prefix)] composes its keys.
public sealed class SignupForm
{
public string Name { get; set; }
public Address Detail { get; set; }
public Dictionary<string, string> Extra { get; set; }
}
Submit(new SignupForm { Name = "ada", Detail = new() { City = "Wien" }, Extra = new() { ["k"] = "v" } })
>>> "Name=ada&Detail.City=Wien&Extra.k=v" // V14: flattened
>>> "Name=ada&Detail=RefitGeneratorTest.Address&Extra=System..." // pre-V14: ToString
This is a behavior change to previously unusable output, so it is extremely unlikely anyone depended on it. Complex
elements of a collection are still ToString()-ed per element (the same limitation as the query path).
An empty token from AuthorizationHeaderValueGetter now omits the Authorization header instead of sending a
blank one (#1688). Previously a getter that returned null, an empty string, or whitespace produced a blank
Authorization: <scheme> header (and could throw for some scheme/value combinations). Refit now clears the header for
that request, so a single client can mix authenticated and anonymous calls by returning a token or an empty string. If
you relied on a blank header being sent, return a non-empty value.
[PathPrefix] shared route prefix. Put [PathPrefix("/api/v2")] on an interface to prepend a common prefix to
every method's relative path, instead of repeating it in each route or baking it into HttpClient.BaseAddress. The
join uses exactly one slash (leading/trailing slashes are normalized, an empty prefix is a no-op) and runs before the
base-address merge, preserving placeholders and query strings. The prefix declared on the client interface (the T in
RestService.For<T>/AddRefitClient<T>) applies to every method it exposes, including inherited ones; prefixes are
not concatenated across interface inheritance. Both the source generator and the reflection request builder honor it.
See Shared route prefix with [PathPrefix].[Timeout] attribute. Decorate a method with [Timeout(milliseconds)] to give that single call its
own deadline; when it elapses the request is canceled and surfaces as an OperationCanceledException (typically a
TaskCanceledException), the same way a lapsed HttpClient.Timeout reports. It is additive and layers onto the
request's effective cancellation token, so a caller-supplied CancellationToken still works alongside it and the
per-call deadline composes with HttpClient.Timeout and any Polly/DelegatingHandler timeout — whichever fires first
wins. Methods without [Timeout] are unaffected. Both request paths honor it (reflection and source-generated). See
Per-request timeouts.[FormObject] flattens a model into individual multipart fields. In a [Multipart] method, decorating a
complex-object parameter with [FormObject] sends each of its public properties as its own multipart/form-data text
field instead of the default single serialized part, so a server model bound from the form (such as an ASP.NET
[FromForm] model) binds field by field. Field names resolve with the form-encoded precedence ([AliasAs], then the
content serializer's property name, then the key formatter), values render through the FormUrlEncodedParameterFormatter,
collections honor CollectionFormat, and nested objects compose parent.child field names. File-typed members are
left as separate part parameters. The default (no attribute) single-part behavior is unchanged. See
Flattening a model into form fields with [FormObject].[AliasAs], [Query(Format = ...)],
and scalar collections with every CollectionFormat — now generate reflection-free request construction, so the most
common Refit method shapes work with generated-only clients (AddRefitGeneratedClient, RestService.ForGenerated)
and under Native AOT. Implicit body detection (a complex parameter on POST/PUT/PATCH without [Body]) generates
inline too, and value formatting for statically-known types (numbers, dates, GUIDs, enums with [EnumMember]) is
resolved at compile time while still honoring a custom IUrlParameterFormatter when one is configured.[AliasAs], the content serializer's property name ([JsonPropertyName]), [Query]
prefix/delimiter/format/SerializeNull, and the IgnoreDataMember/JsonIgnore attributes. Value-typed properties
are formatted without boxing. A property that is itself a collection of simple elements (for example int[],
List<TEnum>, IReadOnlyList<string>) flattens too, honoring its CollectionFormat; a customized
IUrlParameterFormatter still gets the reflection builder's two formatting passes. A property that is itself a
concrete nested object flattens recursively under a dotted key (Address.City=...), composing each level through the
key formatter and honoring per-level [Query(Prefix)]; a self-referential (cyclic) type keeps using the reflection
builder. See the declared-type and property-name notes above.IDictionary<TKey, TValue> and Dictionary<TKey, TValue> with
simple keys and values expand inline, one query pair per entry, with the same semantics as before: entries with a
null value are omitted, blank keys drop the pair, enum keys render through [EnumMember], and a parameter-level
[Query(Prefix)] is prepended to every key. Dictionaries with object values still use the reflection request
builder, because the value's runtime type decides whether it is recursed into — unless you attach an
[QueryConverter] (below).[QueryConverter] for shapes only known at runtime. Attach [QueryConverter(typeof(MyConverter))] to a query
parameter whose shape the generator cannot flatten from the declared type — an object value, a polymorphic base
type, a Dictionary<string, object>. The converter (IQueryConverter<T>) writes query pairs directly into the
pooled GeneratedQueryStringBuilder, so the parameter generates inline and stays reflection- and allocation-free.
It is a source-generation-only feature (like [QueryName]/[Encoded]): a method that carries it but cannot generate
inline for another reason reports RF007. The reflection request builder is unaffected; it keeps walking the runtime
type. Refit ships SystemTextJsonQueryConverter<T>, which flattens any type registered in your configured
System.Text.Json context by walking its JsonTypeInfo.[QueryName] valueless query flags. The equivalent of Retrofit's @QueryName: the parameter's value becomes a
bare ?flag segment with no =value; collections render one flag per element. See
Valueless query flags.[Encoded] per-parameter encoding opt-out. The equivalent of Retrofit's encoded = true: a query or path value
(including round-tripping {**param} segments) passes through verbatim so pre-encoded values are not double-encoded.
See Unescape Querystring parameters.[QueryName], [Encoded]) on methods that cannot generate inline.EnableTrimAnalyzer and warnings as
errors build cleanly; the compile-time RF006 diagnostic is the signal that a method still needs reflection.CancellationTokenSource when a
token can't cancel, uses HttpHeaders.NonValidated for generated header checks (no value re-parsing), and returns
ValueTask from the internal deserialization chain so empty/204 responses complete without a Task allocation —
alongside the public ValueTask moves noted in the breaking changes above.Task<T> Get<T>(string id) or
Task<TResponse> Post<TRequest, TResponse>([Body] TRequest body)) no longer falls back to the reflection request
builder — the type parameter flows straight through to the generated runner (SendAsync<T, TBody>), so generic
methods are reflection-free and Native AOT clean, with their generic constraints preserved. The narrow exceptions that
still require the reflection builder are the ones an open type parameter genuinely can't generate: a complex query
object (its query pairs are only known per value) and a form-url-encoded [Body] (its [DynamicallyAccessedMembers]
contract can't be satisfied by an open type parameter). RF006 continues to report exactly which methods still fall
back.IReturnTypeAdapter<TReturn, TResult>). Surface any custom return type (for
example IObservable<T> or a Result<T> wrapper) from an interface method by implementing the interface. The source
generator discovers adapters declared in your project at compile time and emits a direct Adapt call — no reflection,
so adapter-backed methods stay trim and Native AOT clean; the reflection request builder resolves adapters registered
in RefitSettings.ReturnTypeAdapters. See Custom return types.RefitSettings.ValidateHeaders). Header values are still added verbatim by default
(TryAddWithoutValidation), so nothing changes unless you ask for it. Set ValidateHeaders = true to have Refit apply
headers with HttpHeaders.Add instead, validating each value against its header parser and throwing a
FormatException at request-build time when a value is malformed. CR/LF stripping still applies in both modes. Both
request builders honor the flag identically. See Validating header values.[Url] for absolute per-call URLs. The equivalent of Retrofit's @Url: mark a string or System.Uri
parameter with [Url] to dispatch the call to that absolute URL — often a different host — bypassing the client's
base address. The route template must be empty ([Url] provides the full URL), the value must be absolute (a relative
or invalid value throws an ArgumentException), and [Query] parameters are still appended to the URL's query string.
It generates inline (reflection-free, Native AOT clean) and behaves identically through the reflection request
builder. See Absolute URLs per call with [Url].text/event-stream response returned to an IAsyncEnumerable<T>
method now streams one deserialized T per SSE data event, live and unbuffered, through the same seam as JSON
arrays and JSON Lines — no generator or reflection change, so both generated and reflection clients inherit it. This
adds the StreamingContentFormat.ServerSentEvents enum member and takes a new System.Net.ServerSentEvents package
reference on the targets that lack it in-box (net4x/netstandard2.0/net8.0/net9.0; it is a framework reference on
net10.0+). Each event's payload is deserialized through the configured System.Text.Json metadata path, so it stays
trim and Native AOT clean. See Consuming Server-Sent Events.Task<HttpRequestMessage> builds the
full request (method, URL, headers, and body/multipart content) and hands it back to the caller instead of dispatching
it — the equivalent of Retrofit's Call.request(), useful for inspection, signing, logging, or manual dispatch. Both
the generated and reflection paths build byte-identical requests. The caller owns the returned request and its content
and is responsible for disposing it; the request is not disposed for you and its content stays readable. A configured
async AuthorizationHeaderValueGetter runs at dispatch time and is therefore not applied to a request obtained this
way. See Obtaining the built request without sending.{name?}). Append ? to a placeholder name (matching ASP.NET routing) to make the
segment optional. When the bound argument is null the segment and its preceding / are dropped, so a route such as
[Get("/push/notifMsg/{deviceId}/{notifMsgId?}")] produces /push/notifMsg/device1 (not a 404-prone
/push/notifMsg/device1/) when notifMsgId is null. This is additive syntax: existing {name} placeholders are
unchanged, a non-null (or empty-string) value formats exactly as before, and the behaviour is identical on the
reflection and source-generated request paths. See API Attributes.RefitSettings.UrlParameterFormatterMap). Register an IUrlParameterFormatter
for a specific CLR type instead of hand-rolling a type switch inside a single custom formatter. When a value is
rendered into a path or query string, its runtime type is looked up in the map first (exact type only, no base-class or
interface walking); a registered formatter wins, and every other type falls back to UrlParameterFormatter. Both the
reflection and source-generated request builders consult the map identically. This is a purely additive, opt-in setting
— an empty map (the default) changes no behavior. See
Formatting URL Parameter Values.RefitSettings.CaptureMethodArguments). Opt in and a DelegatingHandler
can read the call's argument values from HttpRequestMessageOptions.MethodArguments — an object?[] in declared
parameter order (including any CancellationToken), the equivalent of Retrofit's Invocation.arguments. It is off by
default because it boxes and retains the arguments on every request, the same allocation/retention/PII trade-off as
CaptureRequestContent. The values are positional; the generated path supplies the bare array while the reflection
path also exposes RestMethodInfo for parameter names. See
Inspecting the current call's arguments.AddAuthorizationHeaderValueProvider). A new
IHttpClientBuilder extension in Refit.HttpClientFactory resolves the Authorization token from dependency
injection per request. Because IHttpClientFactory pools message handlers, it creates a fresh DI scope for every
request and resolves your delegate (IServiceProvider, HttpRequestMessage, CancellationToken) -> ValueTask<string>
from that scope, disposing it when the request completes — no Microsoft.AspNetCore.* dependency required (#1679). See
Scoped (per-request) authorization tokens with dependency injection.HttpRequestMessageOptions.MethodName (the interface method's name) and
HttpRequestMessageOptions.RelativePathTemplate (the raw route template with its {placeholders}, not the filled
URL). Both are populated identically on the source-generated path and the reflection path, with no runtime
reflection, so a DelegatingHandler can read them for logging, metrics, and tracing. RelativePathTemplate is the
stable, low-cardinality label to use for OpenTelemetry spans and metrics instead of the per-id RequestUri. This is
purely additive and does not change the existing InterfaceType or RestMethodInfo options. See
Target Interface Type and method info.Refit 13 hardens the default security posture from a security audit. The new behavior is safe by default; the changes below only affect code that previously relied on the less-secure defaults.
XmlContentSerializer now forces DtdProcessing.Prohibit and clears the
XmlResolver on every read, blocking XML External Entity (XXE) and entity-expansion ("billion laughs") attacks. If
you were deserializing trusted XML that depends on a DTD or external entities, that content will now throw. We
strongly recommend against re-enabling DTD processing, but if your XML comes from a fully trusted source you can
opt out via the obsolete XmlReaderWriterSettings.AllowDtdProcessing flag (marked [Obsolete] deliberately, so it
surfaces a compiler warning) - you must also configure DtdProcessing/XmlResolver yourself on ReaderSettings.
Prefer pre-processing untrusted documents instead.TypeNameHandling. When you do not pass explicit
JsonSerializerSettings, NewtonsoftJsonContentSerializer now forces TypeNameHandling.None even if
JsonConvert.DefaultSettings configured a different value, closing a known remote-code-execution gadget vector on
response bodies. If you genuinely need polymorphic ($type) deserialization, opt in explicitly by passing your own
JsonSerializerSettings (ideally constrained with a SerializationBinder).The release also adds two opt-in, non-breaking knobs on RefitSettings for hardening exception handling:
RefitSettings.ExceptionRedactor — a hook invoked before an ApiException propagates, so you can scrub the
Authorization header, request/response bodies, and Set-Cookie before they reach logging or telemetry pipelines
that serialize exceptions.RefitSettings.MaxExceptionContentLength — caps how many characters of an error response body are read into
ApiException.Content, bounding memory use against hostile or oversized error responses. Defaults to unbounded.Refit.Testing — a new first-party package for testing Refit clients without a mocking library or a live
server. You describe expected calls as a route table (Route → Reply) and point a real client at it via
StubHttp.CreateClient<T>(...). Route templates mirror your interface attributes, typed replies (Reply.With<T>)
are serialized with the client's own serializer, and the sent request body can be read back as a typed object with
LastRequestBodyAsync<T>(). It also includes NetworkBehavior for seeded latency/fault injection and
StubApiResponse<T> for unit-testing code that consumes IApiResponse<T>. See
Testing your Refit clients.Refit 12.0 is a large release centered on a near-complete rewrite of request building. The source generator now builds eligible HTTP requests inline at compile time instead of going through the reflection request-builder pipeline, with the reflection path kept as a fallback for shapes that cannot be generated inline.
Two breaking changes are called out for migration:
IApiResponse<T> no longer shadows base interface members. The new-shadowed Error, ContentHeaders,
IsSuccessStatusCode, and IsSuccessful members were removed from the generic interface. Source that reads these
members still binds to the inherited base members, but assemblies compiled against v8-v11 should be recompiled.
If you used IsSuccessful to narrow Content to non-null on an IApiResponse<T> value, use HasContent or
IsSuccessfulWithContent instead.
Because the shadow is gone, Error is now typed as ApiExceptionBase?, which exposes only request-side context
(RequestContent, HttpMethod, Uri, RequestMessage). The response body lives on the derived ApiException, so
response.Error.Content no longer compiles. The feature was not removed; the body simply moved to the derived type.
Migrate like this:
// Before (v8-v11)
var content = response.Error.Content;
// After (v12+) - null-safe typed access to the response body
if (response.HasResponseError(out var apiException))
logger.LogError(apiException, apiException.Content);
// Or, as a shorthand cast when you just want the body
var content = (response.Error as ApiException)?.Content;
The default System.Text.Json serializer now reads numbers from JSON strings by setting
JsonNumberHandling.AllowReadingFromString. To opt back out, set NumberHandling = JsonNumberHandling.Strict on
your JsonSerializerOptions.
See the Refit 12.0.0 release notes for the full release details.
Refit 11 introduces ApiRequestException to represent requests that fail before receiving a response from the server.
This exception will now wrap previous exceptions such as HttpRequestException and TaskCanceledException when they
occur during request execution.
IApiResponse and were catching these exceptions directly, you will need to
update your code to catch ApiRequestException instead.IApiResponse, these exceptions will no longer be thrown and will instead be
captured in the IApiResponse.Error property.
You can use the new IApiResponse.HasRequestError(out var apiRequestException) method to safely check and retrieve
the ApiRequestException instance.The IApiResponse.Error property's type has also changed to ApiExceptionBase, which is the new base class for
ApiException and ApiRequestException.
If your code accessed members specific to ApiException (i.e. anything related to the response from the server), you
can use the new IApiResponse.HasResponseError(out var apiException) method to safely check and retrieve the
ApiException instance.
All response-related properties of IApiResponse are now nullable.
The new IApiResponse.IsReceived property can be used to check if a response was received from the server, and will
mark those properties as non-null.
The original IApiResponse.IsSuccessful and IApiResponse.IsSuccessStatusCode properties can still be used to check if
the response was received and is successful.
Fixes for some issues experienced, this led to some breaking changes. See Releases for full details.
Refit 6 requires Visual Studio 16.8 or higher, or the .NET SDK 5.0.100 or higher. It can target any .NET Standard 2.0 platform.
Refit 6 does not support the old packages.config format for NuGet references (as they do not support analyzers/source
generators). You must
migrate to PackageReference to use
Refit v6 and later.
Refit 6
makes System.Text.Json the
default JSON serializer. If you'd like to continue to use Newtonsoft.Json, add the Refit.Newtonsoft.Json NuGet
package and set your ContentSerializer to NewtonsoftJsonContentSerializer on your RefitSettings instance.
System.Text.Json is faster and uses less memory, though not all features are supported.
The migration guide
contains more details.
IContentSerializer was renamed to IHttpContentSerializer to better reflect its purpose. Additionally, two of its
methods were renamed, SerializeAsync<T> -> ToHttpContent<T> and DeserializeAsync<T> -> FromHttpContentAsync<T>.
Any existing implementations of these will need to be updated, though the changes should be minor.
Refit 6.3 splits out the XML serialization via XmlContentSerializer into a separate package, Refit.Xml. This
is to reduce the dependency size when using Refit with Web Assembly (WASM) applications. If you require XML, add a
reference
to Refit.Xml.