DotGram.Web 0.1.0

dotnet add package DotGram.Web --version 0.1.0
                    
NuGet\Install-Package DotGram.Web -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DotGram.Web" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DotGram.Web" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="DotGram.Web" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DotGram.Web --version 0.1.0
                    
#r "nuget: DotGram.Web, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DotGram.Web@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DotGram.Web&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=DotGram.Web&version=0.1.0
                    
Install as a Cake Tool

DotGram.Web

NuGet build .NET Standard 2.0 License: MIT

Parsers for the formats of the web — JSON, HTTP header fields, URIs, email addresses, timestamps and language tags — each written against its specification and held to the test suite written for it.

The parsers were generated into this assembly by .Gram when it was compiled, so there is no parser runtime behind them. The one dependency is System.Memory, on netstandard2.0 only; on net10.0 there is none.

dotnet add package DotGram.Web

What it reads

Format Read with Specification Held to
JSON
JSON JsonValue.Parse RFC 8259 JSONTestSuite
JSON Pointer JsonPointer.Parse, JsonPointer.ParseFragment RFC 6901 the RFC's examples
JSON Patch JsonPatch.Parse, JsonPatch.Read RFC 6902 json-patch-tests
HTTP
Content-Type MediaType.Parse RFC 9110 §8.3 the IANA media types registry
Accept MediaRange.ParseAccept RFC 9110 §12.5.1 the RFC's examples
Structured Fields StructuredField.ParseItem, ParseList, ParseDictionary RFC 9651 structured-field-tests
Link WebLink.ParseField RFC 8288 the RFC's examples
Content-Disposition ContentDisposition.Parse RFC 6266 tc2231
Set-Cookie, Cookie SetCookie.Parse, CookiePair.ParseField, CookieDate.Parse RFC 6265 the httpstate working group's cases
Forwarded ForwardedElement.ParseField, ForwardedNode.Parse RFC 7239 the RFC's examples
Addresses
URI UriReference.Parse, UriReference.ParseUri RFC 3986 the RFC's examples
URI Template UriTemplate.Parse RFC 6570 uritemplate-test
Email address AddrSpec.Parse, EmailAddress.ParseList RFC 5322 is_email
Time and language
Timestamp Timestamp.Parse, FullDate.Parse, FullTime.Parse RFC 3339 JSON Schema test suite
Language tag LanguageTag.Parse RFC 5646 (BCP 47) the IANA language subtag registry

Every Parse throws FormatException, its message naming where the text stopped fitting; every TryParse answers false instead. A value that parses is a record: equal to another that means the same, and written back by ToString in the form the specification generates.

JSON

using DotGram.Web;

var value = (JsonValue.Object)JsonValue.Parse("""{ "pi": 3.14159265358979323846, "tags": ["a", "b"] }""");

var pi = (JsonValue.Number)value.Members[0].Value;
pi.Text;                  // 3.14159265358979323846
pi.ToDouble();            // 3.141592653589793
value.ToString();         // {"pi":3.14159265358979323846,"tags":["a","b"]}

A number keeps the text it was written with, and ToDouble, TryToDecimal and TryToInt64 read it at the precision wanted. An object keeps its members in order, a name written twice included. Nesting is read as deep as the input goes. The text is characters: decoding bytes, as UTF-8, is the caller's, and a byte order mark is refused rather than skipped.

JSON Pointer

var document = JsonValue.Parse("""{ "foo": ["bar", "baz"], "a/b": 1 }""");

JsonPointer.Parse("/foo/1").Resolve(document);   // "baz"
JsonPointer.Parse("/a~1b").Resolve(document);    // 1
JsonPointer.Parse("/foo/-").Resolve(document);   // null: the element after the last

var pointer = JsonPointer.Parse("/a~1b/0");
pointer.Tokens;                                  // [a/b, 0]
pointer.ToUriFragment();                         // #/a~1b/0
JsonPointer.ParseFragment("#/c%25d").Tokens;     // [c%d]

Escapes are undone in the order the RFC gives, so ~01 is ~1. Resolve answers null where the pointer refers to nothing — a missing member, an index past the end or with a leading zero, -, or a name an object holds twice — which is not JSON's null.

JSON Patch

var patch = JsonPatch.Parse("""
    [
      { "op": "test", "path": "/a/b/c", "value": "foo" },
      { "op": "replace", "path": "/a/b/c", "value": 42 },
      { "op": "copy", "from": "/a/b/c", "path": "/a/b/d" }
    ]
    """);

patch.Apply(JsonValue.Parse("""{ "a": { "b": { "c": "foo" } } }""")).ToString();
// {"a":{"b":{"c":42,"d":42}}}

Applying changes nothing it is given: the result is a new document sharing what the patch did not touch, and a patch that fails throws JsonPatchException — or TryApply answers false — with no half-patched document left behind. test compares numbers by value at any precision and objects whatever the order of their members; JsonPatch.AreEqual is that comparison. Removing the whole document is an error.

HTTP header fields

Content-Type and Accept

var type = MediaType.Parse("Text/HTML; Charset=\"UTF-8\"");

type.Charset;                                        // UTF-8
type == MediaType.Parse("text/html;charset=utf-8");  // true

var accept = MediaRange.ParseAccept("text/*;q=0.3, text/plain;q=0.7, */*;q=0.5");

MediaRange.Quality(accept, MediaType.Parse("text/plain"));   // 0.7
MediaRange.Quality(accept, MediaType.Parse("text/html"));    // 0.3
MediaRange.Quality(accept, MediaType.Parse("image/png"));    // 0.5

A type, a subtype, a parameter name and a charset value compare without case; other values compare as written. Empty parameters and empty list elements are accepted, as RFC 9110 asks of a recipient. A parameter named q is the weight wherever it stands, and has to be a qvalue. Quality takes the weight of the most specific matching range, or 0 where none matches.

Structured Field Values

For the fields HTTP defines this way — Priority, Cache-Status, Proxy-Status, signatures — as the three types RFC 9651 gives them.

var list = StructuredField.ParseList("text/html;q=1.0, (\"a\" \"b\");lvl=5");

var item = (Item)list[0];
item.Value;                    // BareItem.Token { Value = "text/html" }
item.Parameters["q"];          // BareItem.Decimal { Value = 1.0 }

var dictionary = StructuredField.ParseDictionary("u=3, i");
dictionary["i"];               // Item { Value = BareItem.Boolean { Value = true } }

StructuredField.SerializeDictionary(dictionary);   // u=3, i

Parameters and Dictionaries are read by position and by key; a key written twice keeps its first place and its last value. A field in several lines is one value: StructuredField.Combine(lines) joins them. A field that does not parse is refused whole. SerializeItem, SerializeList and SerializeDictionary write the canonical form, and throw ArgumentException for what has none — a key with an uppercase letter, a String outside printable ASCII, an Integer of sixteen digits.

var links = WebLink.ParseField(
    "</TheBook/chapter2>; rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, " +
    "</TheBook/chapter4>; rel=\"next\"; title=\"next chapter\"");

links[0].Target;       // /TheBook/chapter2
links[0].Relations;    // [previous]
links[0].Title;        // letztes Kapitel
links[1].Title;        // next chapter

A link-param's value is the same written as a token or a quoted string. Where RFC 8288 allows a parameter once — rel, title, title*, media, type — the first counts, and Title prefers a title* that decodes, from UTF-8 or ISO-8859-1. A target and an anchor are kept as written: resolving a relative one needs the URL of the response that carried the field.

Content-Disposition

var field = ContentDisposition.Parse(
    "attachment; filename=\"EURO rates\"; filename*=utf-8''%e2%82%ac%20rates");

field.IsAttachment;               // true
field.Filename;                   // € rates
field.Find("filename")!.Value;    // EURO rates

Filename prefers a filename* that decodes to filename. A type other than inline is an attachment. A field the grammar does not make, a parameter named twice included, is refused whole. The filename is what the sender wrote; stripping its path and making it safe to save is the caller's.

Cookies

var cookie = SetCookie.Parse("SID=31d4d96e407aad42; Path=/; Max-Age=3600; Secure; HttpOnly");

cookie.Name;                                // SID
cookie.Path;                                // /
cookie.Secure;                              // true
cookie.ExpiryTime(DateTimeOffset.UtcNow);   // an hour from now

CookieDate.Parse("Sun, 06-Nov-94 08:49:37 GMT");      // 1994-11-06 08:49:37 +00:00
CookiePair.ParseField("SID=31d4d96e407aad42; lang=en-US");   // two pairs

SetCookie.Parse is the user agent's algorithm of RFC 6265 §5.2, which reads nearly anything: it refuses only a field with no = or no name. Every attribute is kept; the properties take the last one that counts, so a Path that is not absolute means the default path, and Max-Age comes before Expires. CookieDate finds a time, a day, a month and a year among the tokens in any order, as browsers do. CookiePair.ParseField reads the Cookie field a server receives. SetCookie.DomainMatches, DefaultPath and PathMatches decide which request gets a cookie; the store itself is the caller's.

Forwarded

var elements = ForwardedElement.ParseField(
    "for=192.0.2.43, for=\"[2001:db8:cafe::17]:4711\";by=_hidden;proto=https;host=example.com");

elements[0].For!.Name;         // 192.0.2.43
elements[1].For!.Kind;         // ForwardedNode.Kinds.IPv6
elements[1].For!.PortNumber;   // 4711
elements[1].By!.Kind;          // ForwardedNode.Kinds.Obfuscated
elements[1].Proto;             // https

A by or for value has to be a node identifier, host a host and port, and proto a URI scheme; a field where one is not, or where a parameter appears twice in an element, is refused. Several Forwarded fields are one list: join them with commas. Nothing in the field can be trusted — any proxy on the way may have written it.

Addresses

URIs

var uri = UriReference.ParseUri("https://user@example.com:8080/a/b?q=1#top");

uri.Scheme;    // https
uri.Host;      // example.com
uri.Port;      // 8080
uri.Path;      // /a/b
uri.Query;     // q=1
uri.Fragment;  // top

UriReference.Parse("../images/logo.png?size=2").Path;   // ../images/logo.png
UriReference.Decode("hello%20world");                    // hello world

Parse reads a URI reference, relative or not; ParseUri asks for a scheme. IPv4, IPv6 and IPvFuture hosts are read as the RFC writes them. Every part comes back undecoded: %2F in a path segment is data, and decoding it while parsing would make it a separator it is not.

URI Templates

var template = UriTemplate.Parse("/users{/id}{?fields,page:3}{&tags*}");

template.Expand(new Dictionary<string, object?>
{
    ["id"]     = "igor",
    ["fields"] = new[] { "name", "email" },
    ["page"]   = "12345",
    ["tags"]   = new[] { "a", "b" },
});
// /users/igor?fields=name,email&page=123&tags=a&tags=b

All four levels of RFC 6570. A value is a string, a list or an associative array; a number is its invariant text, and a missing or null value is undefined. template.Parts says which variables a template asks for. A template using a reserved operator is refused, and a prefix on a list or an associative array throws on expansion.

Email addresses

var list = EmailAddress.ParseList(
    "\"Joe Q. Public\" <john.q.public@example.com>, jdoe@example.org, Undisclosed recipients:;");

var joe = (EmailAddress.Mailbox)list[0];
joe.DisplayName;          // Joe Q. Public
joe.Address.LocalPart;    // john.q.public
joe.Address.Domain;       // example.com

var group = (EmailAddress.Group)list[2];
group.Members.Count;      // 0

AddrSpec.Parse("(comment)\"john smith\"@example.com").LocalPart;   // john smith
AddrSpec.TryParseStrict("john . smith@example.com", out _);        // false: obsolete syntax

A value is what the address means: comments and folding are gone, a quoted local part is its content, and a display name reads with single spaces. Parse and ParseList accept the obsolete syntax RFC 5322 requires a receiver to accept — routes, empty list members, space around dots, control characters in quoted text; the Strict forms accept only what a sender may write. ToString writes the form a sender should. The domain is RFC 5322's; whether mail could be delivered to it is not asked.

Time and language

Timestamps

var timestamp = Timestamp.Parse("1996-12-19T16:39:57-08:00");

timestamp.Date;                // FullDate { Year = 1996, Month = 12, Day = 19 }
timestamp.Time.Offset;         // -08:00:00
timestamp.ToDateTimeOffset();  // 12/19/1996 4:39:57 PM -08:00

FullDate.Parse("2020-02-29");             // a leap year
FullDate.TryParse("2021-02-29", out _);   // false
FullTime.Parse("15:59:60-08:00").Second;  // 60: the leap second, at 23:59:60 UTC

The date and time format of the Internet, the profile of ISO 8601 that HTTP and JSON Schema use. A day has to be in its month and a leap second in the last minute of the UTC day. The fraction of a second is kept as written, however long. -00:00 is UTC with the local offset unknown, and LocalOffsetUnknown tells it from Z. ToDateTimeOffset() throws for a leap second, which DateTimeOffset cannot hold.

Language tags

var tag = LanguageTag.Parse("zh-cmn-Hans-CN-u-ca-chinese");

tag.Language;            // zh
tag.ExtendedLanguages;   // [cmn]
tag.Script;              // Hans
tag.Region;              // CN
tag.Extensions[0];       // Extension { Singleton = u, Subtags = [ca, chinese] }

LanguageTag.Parse("EN-latn-us").ToString();   // en-Latn-US
LanguageTag.Parse("i-klingon").Grandfathered; // i-klingon

A tag compares without case; its parts are kept as written, and ToString() writes the case BCP 47 recommends. A tag read here is well-formed; whether every subtag is in the IANA registry is not asked.

More

The grammars are in the repository, one file per specification, beside the tests that hold them: src/DotGram.Web. How they are written is .Gram's documentation.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 56 9/15/2026

First release: JsonValue (RFC 8259), JsonPointer (RFC 6901) and JsonPatch (RFC 6902); MediaType and MediaRange (RFC 9110), StructuredField (RFC 9651), WebLink (RFC 8288), ContentDisposition (RFC 6266), SetCookie, CookiePair and CookieDate (RFC 6265), ForwardedElement (RFC 7239); UriReference (RFC 3986), UriTemplate (RFC 6570), AddrSpec and EmailAddress (RFC 5322); Timestamp (RFC 3339) and LanguageTag (RFC 5646).