SkillAgentSearch skills...

WebSerializer

Convert Object into QueryString/FormUrlEncodedContent for C# HttpClient REST Request.

Install / Use

/learn @Cysharp/WebSerializer
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

WebSerializer

GitHub Actions Releases

WebSerializer converts object into HTTP/1 QueryString/FormUrlEncodedContent for C# HttpClient REST Request. For response, HttpClient has ReadFromJsonAsync so can map to object directly, however for request, you should build request data(query-string or form-urlencoded-content) manually. refit solves that problem, but WebSerializer provides a more lightweight way that focuses on building only the request parameters.

// Request-object to query-string
var q = WebSerializer.ToQueryString(req); // foo=aaa&bar=100&baz=zzz

// Method argument to query-string
var q = WebSerializer.ToQueryString(new { foo, bar, baz }); // foo=aaa&bar=100&baz=zzz

// With the url-base
var url = WebSerializer.ToQueryString("https://foo/search", req); // https://foo/search?foo=aaa&bar=100&baz=zzz

// For Post, create form-url-encoded HttpContent
var content = WebSerializer.ToHttpContent(req);

Also, the allocations are very low and the performance is very good. It is designed to be on the same level as MessagePack for C#, a fast binary serializer by the same author.

Getting Started

Supporting platform is .NET Standard 2.0, 2.1, .NET 5, .NET 6.

PM> Install-Package WebSerializer

You can use WebSerializer.ToQueryString or WebSerializer.ToHttpContent to build the request parameter.

using Cysharp.Web; // namespace

var req = new Request(sortBy: "id", direction: SortDirection.Desc, currentPage: 3);

// sortBy=id&direction=Desc&currentPage=3
var q = WebSerializer.ToQueryString(req);

await httpClient.GetAsync("/sort?"+ q);

// data...
public record Request(string? sortBy, SortDirection direction, int currentPage);

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

If you want to build parameter from method argument, use anonymous type is the best way for it.

// If value is null, omitted from build parameter.
// For exampe, (null, SortDirection.Asc, 1) => direction=Asc&currentPage=1
public string BuildSearchRequest(string? sortBy, SortDirection direction, int currentPage)
{
    // Pass url in first argument, build url string with `?`
    const string UrlBase = "https://foo/bar/search";
    return WebSerializer.ToQueryString(UrlBase, new { sortBy, direction, currentPage });
}

For the Post method, you can use WebSerializer.ToHttpContent to build form-url-encoded HttpContent.

async Task PostMessage(string name, string email, string message)
{
    var content = WebSerializer.ToHttpContent(new { name, email, message });
    await httpClient.PostAsync("/postmsg", content);
}

IF you want to build parameters dynamic in application, use Dictionary<string, object>(also allows <TKey, TValue>) or IEnumerable<KeyValuePair<string, object>>(also allows <TKey, TValue>).

var req = new Dictionary<string, object>();
req.Add("sortBy", "id");
req.Add("direction", SortDirection.Asc);
req.Add("currentPage", 10);

// sortBy=id&direction=Asc&currentPage=10
var q = WebSerializer.ToQueryString(req);

// also allows IEnumerable<KeyValuePair<TKey, TValue>>

var parameters = new KeyValuePair<string, string>[]
{
    new ("id", "1"),
    new ("name", "tanaka"),
    new ("email", "test@example.com")
};

// https://example.com/user?id=1&name=tanaka&email=test%40example.com
var url = WebSerializer.ToQueryString("https://example.com/user", parameters);

WebSerializerProvider and IWebSerialize<T>

To customize behaviour of serialization, you can create IWebSerializer<T> and set to IWebSerializerProvider.

public interface IWebSerializer<T> : IWebSerializer
{
    void Serialize(ref WebSerializerWriter writer, T value, WebSerializerOptions options);
}

For example, create custom serializer that convert true/false to 0/1 string.

public class BoolZeroOneSerializer : IWebSerializer<bool>
{
    public void Serialize(ref WebSerializerWriter writer, bool value, WebSerializerOptions options)
    {
        // true => 0, false => 1
        writer.AppendPrimitive(value ? 0 : 1);
    }
}

WebSerializerProvider.Create to composite custom providers.

var customProvider = WebSerializerProvider.Create(
    new[] { new BoolZeroOneSerializer() },
    new[] { WebSerializerProvider.Default });

Finally, set it to WebSerializerOptions and use it for serializing.

// use C# 9.0 with expression for build WebSerializerOptions
var customOptions = WebSerializerOptions.Default with
{
    Provider = customProvider
};

var q = WebSerializer.ToQueryString(request, customOptions);

Advanced note, in default, WebSerializerProvider.Default is composited there internal providers in this order.

IWebSerializerProvider[] providers = new[]
{
    PrimitiveWebSerializerProvider.Instance, // int, double, etc...
    BuiltinWebSerializerProvider.Instance, // string, DateTime, Uri, etc...
    AttributeWebSerializerProvider.Instance, // [WebSerializer] custom serializer
    GenericsWebSerializerProvider.Instance, // Nullable<T>, Enum, Tuple, ValueTuple
    CollectionWebSerializerProvider.Instance, // T[], IEnumerable<T>, IDictionary<TKey, TValue>
    ObjectFallbackWebSerializerProvider.Instance, // object -> <T> serializer
    ObjectGraphWebSerializerProvider.Instance // T
};

Configure serialized name

Configure serialized name, you can use DataMember/EnumMember attributes to customize it.

var req = new MyRequest { Page = 10, Direction = SortDirection.Asc, SortBy = "id" };
// page=10&direction=asc&sortby=id
var q = WebSerializer.ToQueryString(req);

public class MyRequest
{
    [DataMember(Name = "page", Order = 0)]
    public int Page { get; set; }
    [DataMember(Name = "direction", Order = 1)]
    public SortDirection Direction { get; set; }
    [DataMember(Name = "sortby", Order = 2)]
    public string? SortBy { get; set; }
}

public enum SortDirection
{
    [EnumMember(Value = "default")]
    Default,
    [EnumMember(Value = "asc")]
    Asc,
    [EnumMember(Value = "desc")]
    Desc
}

Order is optional, default is ordered by name.

Nested type and NamePrefix

Currently WebSerializer does not flatten value when type is nested. You should use ToQueryString(StringBuilder) to append multiple times on your own. And if you want to add name-prefix to there type, you can use [DataContract(Namespace = )] to set it.


var tweet = new Tweet { Message = "foo", PostTime = DateTimeOffset.Now.ToUnixTimeSeconds() };
var user = new User { Id = 1999, UserName = "baz" };

var sb = new StringBuilder();
WebSerializer.ToQueryString(sb, tweet); // serialize to stringbuilder
sb.Append("&");
WebSerializer.ToQueryString(sb, user);

// tweet.created=1641816420&tweet.msg=foo&user.id=1999&user.name=baz
Console.WriteLine(sb.ToString());

// ----

[DataContract(Namespace = "tweet.")]
public class Tweet
{
    [DataMember(Name = "msg")]
    public string? Message { get; set; }
    [DataMember(Name = "created")]
    public long PostTime { get; set; }
}

[DataContract(Namespace = "user.")]
public class User
{
    [DataMember(Name = "id")]
    public long Id { get; set; }
    [DataMember(Name = "name")]
    public string? UserName { get; set; }
}

If type has not DataContract(Namespace) or add different namespaces per request type, use WebSerializerWriter to configure it.

// use writer instead of StringBuilder
var writer = new WebSerializerWriter();

writer.NamePrefix = "tweet."; // set prefix by writer.
WebSerializer.ToQueryString(writer, tweet); // serialize to writer

writer.AppendConcatenate(); // Append '&'

writer.NamePrefix = "user.";
WebSerializer.ToQueryString(writer, user);

var q = writer.GetStringBuilder().ToString(); // get inner stringbuilder.

// tweet.created=1641816933&tweet.msg=foo&user.id=1999&user.name=baz
Console.WriteLine(q);

// ----

public record Tweet(string? msg, long created);
public record User(long id, string? name);

For Post method, you can use WebSerializerFormUrlEncodedContent(WebSerializerWriter) to create HttpContent.

var content = new WebSerializerFormUrlEncodedContent(writer);

WebSerializerAttribute

Register to WebSerializerProvider affects all type. If you want to configure per member or create own custom serializer to type, use WebSerializerAttribute.

public class MyRequest
{
    // Timestamp is serialized by UnixSecondsSerializer
    [WebSerializer(typeof(UnixSecondsSerializer))]
    public DateTime Timestamp { get; set; }

    public string? Name { get; set; }
}

public class UnixSecondsSerializer : IWebSerializer<DateTime>
{
    public void Serialize(ref WebSerializerWriter writer, DateTime value, WebSerializerOptions options)
    {
        writer.AppendPrimitive(((DateTimeOffset)(value)).ToUnixTimeSeconds());
    }
}

Add attribute to class/struct/enum declaration, use custom serializer instead of default behaviour. options.GetRequiredSerializer<T> is useful to get IWebSerialzier<T>.

[WebSerializer(typeof(CustomRequestSerialzier))]
public class Request
{
    public int MyProperty1 { get; set; }
    public int MyProperty2 { get; set; }
}

public class CustomRequestSerialzier : IWebSerializer<Request>
{
    public void Serialize(ref WebSerializerWriter writer, Request value, WebSerializerOptions options)
    {
        // begin
        writer.EnterAndValidate(options); // recommend to use Enter to detect circular reference.
        writer.AppendNamePrefix(); // recom
View on GitHub
GitHub Stars78
CategoryContent
Updated1mo ago
Forks6

Languages

C#

Security Score

95/100

Audited on Feb 25, 2026

No findings