Utilities

The CrispyWaffle.Utilities namespace collects small, self-contained building blocks: a chainable query string builder, a lazy value that can be reset and reports load statistics, a dynamic object that serializes to XML or JSON, a phone number value type, and a few contracts for API responses and partial updates.

None of these types need registration. Create them where you need them.

Package CrispyWaffle
Namespace CrispyWaffle.Utilities

Getting started

using System;
using CrispyWaffle.Utilities;

// Build a query string
var query = new QueryStringBuilder()
    .Add("id", 179)
    .Add("q", "crispy waffle")
    .ToString();                       // "?id=179&q=crispy+waffle"

// Cache an expensive value and reload it on demand
var settings = new LazyResettable<DateTime>(() => DateTime.UtcNow);
var first = settings.Value;            // runs the factory
settings.Reset();
var second = settings.Value;           // runs the factory again

Reference

QueryStringBuilder

public class QueryStringBuilder : NameValueCollection — a chainable builder and parser for URL query strings. Marked [Serializable].

Values are URL-encoded with HttpUtility.UrlEncode when added and decoded by the indexers. Adding a second value to an existing key stores both, comma-separated, and ToString() writes the key once per value.

Member Description
QueryStringBuilder() Creates an empty builder.
QueryStringBuilder(string queryString) Parses a query string or a full URL (everything after the first ?). Pairs are split on & and =; values are stored as given, without re-encoding. A pair with no =, or with more than one =, gets an empty value.
this[string name] / this[int index] Returns the URL-decoded value. Hides the base indexers and is read-only.
Add(string name, string value, bool isUnique = false) URL-encodes value and adds it. If the key already has a value, isUnique: true replaces it; otherwise the new value is appended after a comma. Adding an empty value to a key that has no value (without isUnique) stores a bare ,, so ToString() writes the key twice with empty values. Throws ArgumentNullException when name or value is null. Returns the builder.
Add(string name, object value, bool isUnique = false) Calls value.ToString() and forwards to the string overload.
AddRange(Dictionary<string, string> items, bool isUnique = false) Calls Add for each entry. Returns the builder.
AddFromType<T>(T instance, bool convertCamelCaseToUnderscore = true) Adds one entry per public property of T (the static type). Properties with [QueryStringBuilderIgnore] are skipped, and [QueryStringBuilderKeyName] overrides the key. With convertCamelCaseToUnderscore, every uppercase letter after the first character is lowercased and prefixed with _ (FirstName becomes First_name; the first character keeps its case). A key that starts with the type name (case-insensitive) becomes typename[rest]. Null property values are added as empty strings. Returns the builder.
Remove(string name) Removes the key if it has a non-empty value. Returns the builder.
Reset() Clears all entries. Returns the builder.
Contains(string name) Returns true when the key exists with a non-whitespace value.
ToString() Returns the query string with a leading ?, for example ?a=1&a=2&b=3. Returns string.Empty when there are no entries. Entries with a null or whitespace key are skipped.
ToString(bool includeNullFields) Same, but includes entries whose key is null or whitespace when includeNullFields is true. The flag looks at keys, not values.

LazyResettable<T>

public class LazyResettable<T> : ILazyResettable — a lazily created value, like Lazy<T>, that can be discarded with Reset() and recreated on the next access. It counts loads, hits and resets and measures the time spent in the factory. Based on a well-known Stack Overflow answer.

The mode argument controls concurrency:

  • PublicationOnly (default): the factory runs outside the lock. Concurrent callers may each run it; the first result stored wins. Every run counts as a load.
  • ExecutionAndPublication: the factory runs inside a lock, so only one caller runs it.
  • None: no locking. Use it only from a single thread.

Hits counts accesses that find a value already created. The access that creates the value is a load, not a hit.

Member Description
LazyResettable(Func<T> valueFactory, LazyThreadSafetyMode mode = LazyThreadSafetyMode.PublicationOnly, Type declaringType = null) Constructor. Throws ArgumentNullException when valueFactory is null. declaringType defaults to the type that declares the factory method.
Value Returns the value, running the factory if there is no current value.
IsValueCreated true when a value is currently held.
Load() Reads Value, forcing creation.
Reset() Discards the current value, increments Resets and raises OnReset. The next access runs the factory again.
OnReset event EventHandler raised after every Reset().
DeclaringType The type passed to the constructor, or the factory's declaring type.
Stats() Returns a new ResetLazyStats snapshot with Type set to typeof(T).
Loads, Hits, Resets Public int fields with the running counters.
SumLoadTime Public TimeSpan field with the total time spent in the factory. It is not updated atomically.

DynamicSerialization

public class DynamicSerialization : DynamicObject, ISerializable, IXmlSerializable, IEquatable<DynamicSerialization> — a dictionary-backed dynamic object that you fill at run time and serialize as XML or through ISerializable.

The class is marked [Serializable] and [Serializer] (XML format by default), so the serialization helpers can handle it. The key filter (DynamicSerializationOption) changes key names only on output (GetObjectData and WriteXml). Stored keys and GetDictionary() keep the original names.

Member Description
DynamicSerialization() Creates an instance with DynamicSerializationOption.None.
DynamicSerialization(DynamicSerializationOption keyFilter) Creates an instance that transforms keys on output.
DynamicSerialization(SerializationInfo info, StreamingContext context) Deserialization constructor. Copies every entry into the dictionary and uses None.
SetMember(string key, object value) Adds an entry. Throws DynamicSerializationException when the key is null or already exists.
TryGetMember(GetMemberBinder binder, out object result) Dynamic property read. Returns null for unknown members instead of failing.
TrySetMember(SetMemberBinder binder, object value) Dynamic property write. Adds the entry; assigning the same member twice throws ArgumentException.
GetDictionary() Returns the underlying Dictionary<string, object> (not a copy).
GetObjectData(SerializationInfo info, StreamingContext context) virtual. Writes each entry with its filtered key.
GetSchema() Returns null, as required by IXmlSerializable.
ReadXml(XmlReader reader) Reads each child element of the current element into the dictionary as a string value keyed by its local name.
WriteXml(XmlWriter writer) Writes each entry as an element named by its filtered key, using XmlWriter.WriteValue.
Equals(DynamicSerialization other) / Equals(object obj) / == / != Equal when both are the same runtime type, the dictionaries contain the same entries in the same order, and the key filters match.
GetHashCode() Combines the dictionary reference hash with the filter. Two equal instances can have different hash codes, so do not use instances as dictionary keys.
Dictionary protected readonly Dictionary<string, object> holding the entries.
SerializationKeyFilter protected readonly DynamicSerializationOption set by the constructor.

PhoneNumber

public struct PhoneNumber — a phone number split into country code, region (area) code and local number.

Use ParseBrazilianPhoneNumber from the pt-BR package to build one from text.

Member Description
PhoneNumber(int countryCode, int regionCode, long number) Constructor.
CountryCode Country code, for example 55.
RegionCode Region or area code, for example 11.
Number Local number without country or region codes.
IsNinthDigit true when CountryCode is 55 and Number has nine or more digits (greater than 99999999), as with Brazilian mobile numbers.
ToString() Concatenates the three parts with no separators, for example 5511123456789.

ILazyResettable

public interface ILazyResettable — the non-generic contract implemented by LazyResettable<T>, useful for keeping a list of resettable values of different types.

Member Description
Reset() Discards the current value.
Load() Forces the value to be created.
DeclaringType The type that owns the lazy value.
Stats() Returns a ResetLazyStats snapshot.

IJsonResponse

public interface IJsonResponse — a minimal shape for API responses.

Member Description
Code int status code, typically an HTTP status.
ErrorMessage string error description.

IUpdateable

public interface IUpdateable — an empty marker interface. Implement it to enable UpdateValues<T> from TypeExtensions, which merges non-default property values from one instance into another.

ErrorResponse

public class ErrorResponse : IJsonResponse — a standard error payload.

Member Description
ErrorList Dictionary<string, IEnumerable<string>> of messages keyed by field or context. Not initialized by default.
Code int error code.
ErrorMessage string general error message.

ResetLazyStats

public class ResetLazyStats — a snapshot of the counters of a LazyResettable<T>.

Member Description
ResetLazyStats(Type type) Constructor. Sets Type; counters start at zero.
Type Public field. The value type being tracked.
Loads Public int field. Number of factory runs.
Hits Public int field. Number of reads that found a value already created.
Resets Public int field. Number of Reset() calls.
SumLoadTime Public TimeSpan field. Total time spent in the factory.

DynamicSerializationOption

public enum DynamicSerializationOption — how DynamicSerialization transforms keys on output.

Value Description
None Keys are written unchanged.
Lowercase Keys are lowercased with ToLowerInvariant.
Uppercase Keys are uppercased with ToUpperInvariant.
Camelcase Keys are passed through StringExtensions.ToCamelCase, which actually produces title case: "userName" becomes "Username" and "user name" becomes "User Name".

DynamicSerializationException

public class DynamicSerializationException : Exception — thrown by DynamicSerialization.SetMember when an entry cannot be added (null or duplicate key). Marked [Serializable].

Member Description
DynamicSerializationException(string key, object value, Exception innerException) Message: Could not add key {key} with value {value}. The original exception is kept as InnerException.
DynamicSerializationException(SerializationInfo info, StreamingContext context) protected deserialization constructor.

Examples

Build a query string from an object

using CrispyWaffle.Attributes;
using CrispyWaffle.Utilities;

var filter = new SearchFilter { Term = "waffle", PageSize = 20, SortBy = "name", Secret = "x" };

var query = new QueryStringBuilder()
    .AddFromType(filter)
    .ToString();                      // "?Term=waffle&Page_size=20&sort=name" (Secret is skipped)

public class SearchFilter
{
    public string Term { get; set; }

    public int PageSize { get; set; }

    [QueryStringBuilderKeyName("sort")]
    public string SortBy { get; set; }

    [QueryStringBuilderIgnore]
    public string Secret { get; set; }
}

Avoid null or empty property values: they go through the empty-value path of Add described in the reference table.

Parse and modify an existing query string

using CrispyWaffle.Utilities;

var builder = new QueryStringBuilder("https://example.com/search?page=1&tag=a");

builder.Add("page", "2", isUnique: true)   // replaces page=1
       .Add("tag", "b");                   // keeps tag=a and adds tag=b

var page = builder["page"];                // "2"
var query = builder.ToString();            // "?page=2&tag=a&tag=b"

Track a resettable cache

using System;
using System.Threading;
using CrispyWaffle.Utilities;

var rates = new LazyResettable<decimal>(LoadRate, LazyThreadSafetyMode.ExecutionAndPublication);
rates.OnReset += (_, _) => Console.WriteLine("Rate cache cleared");

var a = rates.Value;   // load
var b = rates.Value;   // hit
rates.Reset();
var c = rates.Value;   // load

var stats = rates.Stats();
Console.WriteLine($"{stats.Loads} loads, {stats.Hits} hits, {stats.Resets} resets"); // 2 loads, 1 hits, 1 resets

static decimal LoadRate() => 5.25m;

Build a dynamic payload

using CrispyWaffle.Utilities;

dynamic payload = new DynamicSerialization(DynamicSerializationOption.Lowercase);
payload.OrderId = 42;
payload.Status = "Shipped";

var dictionary = ((DynamicSerialization)payload).GetDictionary(); // keys "OrderId", "Status"

When payload is serialized, the keys are written as orderid and status.

See also