Skip to content

Upload files with multipart requests

Run the complete page example.

An upload often includes a file and a few details about it, such as a title or description. A multipart request sends them together as separate named parts. Refit lets you supply each part as a method argument and choose the file names and content types the service expects.

The first example sends a file with some text. Later sections cover multiple files, streams and custom parts, including which streams your app must keep open.

Send a file and text together

1. Put [Multipart] on the HTTP method. It selects multipart/form-data content. The boundary is the text that separates the parts in the HTTP body. The default is ----MyGreatBoundary. [Multipart("report-boundary")] supplies your own. The boundary must meet the HTTP content parser's rules; the attribute does not validate it.

2. Choose a part wrapper. StreamPart uses a stream you own. ByteArrayPart uses bytes already in memory. FileInfoPart opens a local file when the content is created. Each constructor takes (value, fileName, contentType = null, name = null). The file name is the name sent to the service; it need not be a local path.

3. Declare the method. UploadAsync sends a file and a title. The [Query] parameter stays in the URL rather than becoming another body part.

internal interface IMultipartApi
{
    [Multipart]
    [Post("/upload")]
    Task<HttpResponseMessage> UploadAsync([AliasAs("file")] StreamPart file, string title, [Query] string mode);
}

4. Make the call. Create the client with your httpClient and settings, then pass the parts.

IMultipartApi api = RestService.ForGenerated<IMultipartApi>(httpClient, settings);

await using MemoryStream stream = new("Quarterly totals"u8.ToArray());
StreamPart file = new(stream, "report.txt", "text/plain");
using HttpResponseMessage reply = await api.UploadAsync(file, "Annual report", "preview");
// POST /upload?mode=preview
// part "file": file name "report.txt", text/plain, "Quarterly totals"
// part "title": "Annual report"

The generator builds multipart methods inline, so they need no runtime reflection. Refit leaves stream open. The await using declaration disposes it.

Field names, file names and content types

The form field name and transmitted file name are separate values.

InputForm field nameFile name sent
A part wrapper with Name setIts Name, overriding [AliasAs]Its nonempty FileName
A wrapper with Name = null[AliasAs], otherwise parameter nameIts nonempty FileName
A wrapper with empty FileNameThe same field-name rulesThe parameter's aliased or declared name
Raw Stream or byte[]Aliased or declared parameter nameThe same name
Raw FileInfoAliased or declared parameter nameFileInfo.Name
Raw HttpContentIts existing content-disposition metadataIts existing metadata
A string, formatted value or serialized modelAliased or declared parameter nameNone

An empty wrapper Name does not trigger fallback; only null does. Use null when you want the parameter's name. Empty names can be rejected while the multipart body is built. For a raw HttpContent, Refit calls Add(content) and preserves its headers. Set its content disposition yourself when the service expects a named field.

MultipartItem.FileName, ContentType and Name are read-only metadata. StreamPart.Value, ByteArrayPart.Value and FileInfoPart.Value expose the original supplied object. Byte arrays are not copied. Null values or a null file name throw ArgumentNullException. Constructing FileInfoPart does not open the file; ToContent() does.

ToContent() creates HTTP content and applies a nonempty ContentType as its media type. Use a media type such as application/pdf, without a charset parameter. An invalid media type can throw FormatException during content creation. Null or empty ContentType keeps the content's own type. StreamPart, ByteArrayPart and FileInfoPart content has no Content-Type header of its own. The method does not assign content disposition; that happens when the content is added to a multipart body.

ByteArrayPart notes = new("Meeting notes"u8.ToArray(), "notes.txt", "text/plain", "attachment");
using HttpContent content = notes.ToContent();
string body = await content.ReadAsStringAsync(cancellationToken); // "Meeting notes"
// content.Headers.ContentType?.MediaType == "text/plain"
// content.Headers.ContentDisposition == null

Stream ownership and repeated files

Disposing content made from StreamPart or a raw caller stream leaves that stream open. Refit sends from its current position and does not rewind it. You must position the stream before sending and dispose it when finished. A retry must provide readable data again, for example by rewinding a seekable stream before the next call.

FileInfoPart and raw FileInfo open streams that Refit owns. Disposing their content closes those streams, so the file is free again once the call ends. Every ToContent() call creates new content; avoid sharing one content instance across sends.

A collection of part wrappers produces one part per entry. Entries without a Name override share the parameter's field name. A null collection or null single parameter contributes no part. Do not place null file-wrapper entries inside a collection: the generated loop does not skip them.

[Multipart]
[Post("/files")]
Task<HttpResponseMessage> UploadFilesAsync(ByteArrayPart bytes, FileInfoPart file, IEnumerable<ByteArrayPart> attachments);
FileInfo reportFile = new("report.txt");
ByteArrayPart summary = new("Quarterly totals"u8.ToArray(), "summary.txt", "text/plain");
FileInfoPart report = new(reportFile, "q3-report.txt", "text/plain", "document");
ByteArrayPart[] attachments = [new("chart"u8.ToArray(), "chart.png"), new("table"u8.ToArray(), "table.csv")];
using HttpResponseMessage reply = await api.UploadFilesAsync(summary, report, attachments);
// part "bytes": file name "summary.txt"
// part "document": file name "q3-report.txt" (Name overrides the parameter name "file")
// part "attachments": file name "chart.png"
// part "attachments": file name "table.csv"

You can also pass raw values without a wrapper. Each one takes its names from the table above.

[Multipart]
[Post("/raw")]
Task<HttpResponseMessage> UploadRawAsync(HttpContent content, Stream raw, byte[] bytes, FileInfo file);
using StringContent note = new("Reviewed by finance");
note.Headers.ContentDisposition = new("form-data") { Name = "note" };
await using MemoryStream raw = new("Quarterly totals"u8.ToArray());
using HttpResponseMessage reply = await api.UploadRawAsync(note, raw, "Chart data"u8.ToArray(), reportFile);
// part "note": no file name, the content exactly as you built it
// part "raw": file name "raw"
// part "bytes": file name "bytes"
// part "file": file name "report.txt" (reportFile.Name)

Send a model as one JSON part

A model parameter without [FormObject] is serialized as one part, named after its parameter. Declare it as a concrete class or struct. Refit writes the value as that declared type. A collection of such classes sends one part per item. Strings become UTF-8 text/plain parts. Guid and date/time values use the form formatter and plain text. Numbers, booleans, enums and other serialized models use the content serializer.

For standalone use, repeat the generated JSON setup below. JsonSerializerContext holds metadata describing how to read and write a model. [JsonSerializable] registers a type. RefitSettings.ForJsonContext builds settings that read the metadata. Register collection and closed generic model types separately when you add them. Raw file bytes and text do not need JSON metadata.

internal sealed record UploadMetadata(string Title);
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(UploadMetadata))]
internal sealed partial class MultipartJsonContext : JsonSerializerContext;
RefitSettings settings = RefitSettings.ForJsonContext(MultipartJsonContext.Default);
[Multipart]
[Post("/metadata")]
Task<HttpResponseMessage> UploadMetadataAsync(UploadMetadata metadata, Guid token);
Guid token = Guid.Parse("3f2504e0-4f89-11d3-9a0c-0305e82c3301");
using HttpResponseMessage reply = await api.UploadMetadataAsync(new("Annual report"), token);
// part "metadata": application/json, {"title":"Annual report"}
// part "token": text/plain, 3f2504e0-4f89-11d3-9a0c-0305e82c3301

This works with generated request code and runs in a Native AOT app. Refit generates the request for the part wrappers, the raw values above, and any concrete class or struct. A part declared as object, an interface or a method type parameter has no shape that Refit can see when it builds. The same goes for a collection of value types, such as int[]. Such a method falls back to the reflection request builder, and the build reports RF006 with the reason UnsupportedMultipartPart. See find methods that fall back to reflection and JSON configuration.

Extend MultipartItem

Derive from MultipartItem when the built-in wrappers do not supply the content you need. Its protected constructors accept (fileName, contentType) or (fileName, contentType, name). Override protected CreateContent() to return fresh content. Call inherited public ToContent() to create that content and apply the configured media type. The two-argument constructor leaves Name null.

internal sealed class TextPart : MultipartItem
{
    private readonly string _text;

    internal TextPart(string text, string fileName)
        : base(fileName, null) => _text = text;

    internal TextPart(string text, string fileName, string? contentType, string? name)
        : base(fileName, contentType, name) => _text = text;

    protected override HttpContent CreateContent() => new StringContent(_text);
}

Declare the parameter as MultipartItem, or as your own type.

[Multipart]
[Post("/notes")]
Task<HttpResponseMessage> UploadNoteAsync([AliasAs("attachment")] MultipartItem note);

The first part below keeps the text/plain type that StringContent sets. The second overrides the field name and media type. Its empty file name falls back to the parameter's alias.

TextPart plain = new("Reviewed by finance", "notes.txt");
using HttpContent content = plain.ToContent(); // content.Headers.ContentType?.MediaType == "text/plain"

TextPart named = new("Reviewed by finance", string.Empty, "text/markdown", "note");
using HttpResponseMessage reply = await api.UploadNoteAsync(named);
// part "note": file name "attachment", text/markdown, "Reviewed by finance"

Flatten a form object: reflection-only path

[FormObject] writes a complex object's public properties as separate text parts. It does not turn file-valued properties into file attachments; pass files as separate parameters. Aliases take precedence, then serializer field names, then the URL key formatter. Values use FormUrlEncodedParameterFormatter. Collection formats, nested parent.child names, depth limits and reference-cycle guards follow form-body flattening. Null fields are omitted unless their query configuration requests null serialization. An emitted null value becomes empty text; unnamed or whitespace-only fields are skipped.

Only the reflection request builder splits an object into one part per property. With the default generated request mode, a [FormObject] method gets analyzer warning RF006 on that parameter, with the reason FormObjectMultipartPart. Refit sends the method to the reflection request builder. That builder is in the Refit.Reflection package. Create the client with RestService.For. To keep the request generated, declare each form field as its own parameter. Or remove [FormObject] to send the object as one serialized part. Reflection flattening reads properties at runtime, so it is not trim or Native AOT safe. A generated JSON context does not change that. The separate JIT-only project runs this example. It sets RefitGeneratedRequestBuilding=false, which selects reflection request construction for every method.

internal sealed class FormFields
{
    [AliasAs("caption")]
    public string Title { get; init; } = "Annual report";

    [Query(CollectionFormat.Multi)]
    public string[] Tags { get; init; } = ["math", "code"];

    [Query(SerializeNull = true)]
    public string? Note { get; init; }
}
internal interface IFormUploadApi
{
    [Multipart]
    [Post("/form")]
    Task<HttpResponseMessage> UploadAsync([FormObject] FormFields fields, ByteArrayPart recipe);
}
IFormUploadApi api = RestService.For<IFormUploadApi>(httpClient, settings);
using HttpResponseMessage reply = await api.UploadAsync(new FormFields(), new("Mix flour and water"u8.ToArray(), "recipe.txt"));
// part "caption": "Annual report"
// part "Tags": "math"
// part "Tags": "code"
// part "Note": "" (SerializeNull sends the null as empty text)
// part "recipe": file name "recipe.txt"

Obsolete attachment naming

AttachmentNameAttribute(name) stores its supplied string in read-only Name. On a supported parameter, the legacy builder uses it as the file-name override; it does not replace the field's parameter name. Wrapper metadata still supplies a nonempty wrapper file name and its explicit Name. Although the attribute can target properties, multipart attachment routing reads parameter attributes.

The type is obsolete. Using it produces compiler warning CS0618:

[Multipart]
[Post("/form")]
Task UploadAsync([AttachmentName("sent.bin")] byte[] attachment); // warning CS0618

Use StreamPart, ByteArrayPart, FileInfoPart or a MultipartItem extension to choose names in new code.

API reference

The table covers the public types, constructors, methods and properties used by multipart requests. The CreateContent methods are protected implementation points for derived types. The constructors for MultipartItem are also protected, so derive from it when you need a custom part.

APIDescriptionParameters or valueReturns and behavior
AttachmentNameAttribute(string name) (obsolete)Stores the legacy attachment file-name override. Use a part wrapper for new code.name: string to expose through NameCreates the obsolete attribute; using it produces compiler warning CS0618.
AttachmentNameAttribute.Name (obsolete)Gets the legacy file-name override.Read-only stringReturns the constructor's name.
ByteArrayPart(byte[] value, string fileName, string? contentType = null, string? name = null)Creates a multipart item backed by a byte array.value: byte[]; fileName: string; contentType: optional media type, default null; name: optional form field name, default nullStores the same byte array reference. Throws ArgumentNullException when value is null.
ByteArrayPart.ValueGets the bytes supplied to the constructor.Read-only byte[]Returns the original array.
ByteArrayPart.CreateContent() (protected override)Builds content for the byte-array part.NoneReturns ByteArrayContent over Value.
FileInfoPart(FileInfo value, string fileName, string? contentType = null, string? name = null)Creates a multipart item backed by a local file.value: FileInfo; fileName: string; contentType: optional media type, default null; name: optional form field name, default nullStores the file information. Throws ArgumentNullException when value is null; it opens the file only when content is created.
FileInfoPart.ValueGets the source file information.Read-only FileInfoReturns the original FileInfo.
FileInfoPart.CreateContent() (protected override)Opens the source file and builds content for the part.NoneReturns StreamContent over a newly opened read stream.
FormObjectAttribute()Marks a complex multipart parameter for property flattening.NoneCauses each public property to become a text part on the reflection request-builder path.
MultipartAttribute(string boundaryText = "----MyGreatBoundary")Marks an HTTP method as multipart and chooses its boundary.boundaryText: string, default "----MyGreatBoundary"Stores the boundary used to separate parts.
MultipartAttribute.BoundaryTextGets the boundary configured for the method.Read-only stringReturns the supplied boundary text.
MultipartItem(string fileName, string? contentType) (protected)Initializes a custom multipart item without an explicit form field name.fileName: string; contentType: optional media typeStores the file name and content type, with Name set to null. Throws ArgumentNullException for a null file name.
MultipartItem(string fileName, string? contentType, string? name) (protected)Initializes a custom multipart item with optional form field metadata.fileName: string; contentType: optional media type; name: optional form field nameStores all three values. A null file name throws ArgumentNullException.
MultipartItem.NameGets the explicit form field name for the item.Read-only string?Returns null when the constructor did not receive a name.
MultipartItem.ContentTypeGets the optional media type for the item content.Read-only string?Returns the configured content type, or null.
MultipartItem.FileNameGets the file name sent in the multipart disposition.Read-only stringReturns the required file name.
MultipartItem.ToContent()Creates this item's content and applies its nonempty ContentType.NoneReturns HttpContent. The caller disposes the returned content.
MultipartItem.CreateContent() (protected abstract)Defines how a derived item creates fresh underlying content.NoneReturns HttpContent; ToContent() applies the configured media type afterward.
StreamPart(Stream value, string fileName, string? contentType = null, string? name = null)Creates a multipart item backed by a caller-owned stream.value: Stream; fileName: string; contentType: optional media type, default null; name: optional form field name, default nullStores the stream without copying it. Throws ArgumentNullException when value is null; disposing its content leaves the caller's stream open.
StreamPart.ValueGets the caller-owned stream.Read-only StreamReturns the original stream.
StreamPart.CreateContent() (protected override)Wraps the stream without taking ownership of it.NoneReturns HttpContent that reads from Value.
AttachmentNameAttribute (obsolete)Legacy attribute for naming an attachment.NoneAttribute type; prefer the part wrapper types.
ByteArrayPartRepresents byte-array content with multipart metadata.NoneMultipart item type derived from MultipartItem.
FileInfoPartRepresents file content with multipart metadata.NoneMultipart item type derived from MultipartItem.
FormObjectAttributeMarks a complex parameter for multipart property flattening.NoneParameter attribute type.
MultipartAttributeMarks a method whose body contains named multipart parts.NoneMethod attribute type.
MultipartItemBase class for parts that carry a file name and optional content metadata.NoneAbstract type for custom multipart items.
StreamPartRepresents caller-owned stream content with multipart metadata.NoneMultipart item type derived from MultipartItem.