Home

Awesome

Stringly.Typed

Quick, painless conversion of .NET Types <> Strings.

The Problem

String is everywhere - often being a top-level input to most pieces of code.

We then take this string, and "pass it on" to service layers, which then do their own type-checking/validation, but (for some reason) end up passing the string on to other systems. The "validation" logic is duplicated, refactoring is hard, bugs and "WTF's" ensue.

This is referred to as being "Stringly Typed":

A riff on strongly typed. Used to describe an implementation that needlessly relies on strings when programmer & refactor friendly options are available.

For example:

For example:

public Data GetData(string recordId) {
    int result = -1;
    if (!int.TryParse(recordId, out result))
        throw new ArgumentException(nameof(recordId));

    // Now we can get to code the business cares about...
}

Our lives would have been so much easier if we had just written:

public Data GetData(int recordId) {
    // Living the good life! :)
}

We see this ALL the time, particularly with things like int, Guid, MailAddress, Postal/ZIP Codes, anything-that-is-a-thing-that-can't-be-empty, and so on.

It's really just a specific form of "Primitive Obsession".

You're losing the power (and safety) of a typed system. Validation logic is duplicated, and you get FUGLY code.

Enter Stringly.Typed.


The Solution

If we can easily define what a "valid" string for our type, we can really clean up our codebase.

Stringly.Typed made of a few parts:

  1. Support for Stringly<T>, working with anything that has a TryParse method (e.g. primitives, Guid, DateTime, IPAddress - knock yourself out).
  2. Support for Stringly<Uri>, parsing absolute Uri's (e.g. from configuration files).
  3. A generic type (Stringly<T>) that implements implicit operators so you can seamlessly move to/from String.
  4. A base class that enables quick Regex matching.
  5. A non-generic Stringly base class to make it easy to define "just a string that conforms to a specific format".

Getting Started

Note: You can see NUnit tests demonstrating features in the "Samples".

Simple Conversion via TryParse

Let's say we have a service method that actually wants a Int32:

public void Method(Stringly<int> id) {
    // No validation/guard clauses required.
    // Repository expects (and gets) a Int32.
    var data = respository.Get(id);
}

// Meanwhile...
service.Method("123"); // Works
service.Method(123); // Works
service.Method("im-not-a-int"); // throws ArgumentOutOfRangeException

(Absolute) URI Parsing

public void OutputHost(Stringly<Uri> uri) {
    Console.WriteLine(uri.Result.Host); // 'Result' is a Uri
}

// Both of these naturally work - outputting 'nyan.cat'
OutputHost("http://nyan.cat");
OutputHost(new Uri("http://nyan.cat"));

OutputHost("nyan"); // ArgumentOutOfRangeException - Not an absolute URI.

Simple Regular Expression Matching

Your company has customer identifiers in the format "AA1234"? No problem!

Inherit from the Stringly base class and provide our regular expression. When a string is passed in, they will automagically get validated for you!

public class CustomerIdentifier : Stringly
{
    protected override Regex Regex => new Regex(@"^[a-zA-Z]{2}\d{4}$");
}

public void Method(Stringly<CustomerIdentifier> Id){ /* ... */ }

x.Method("AA1234"); // Works
x.Method(new Stringly<CustomerIdentifier>("AA1234")) // Works
x.Method("1234"); // ArgumentOutOfRangeException

Complex Types

What if our database uses a composite key (such as Table Storage)? Rather than litter our codebase with string partitionKey, string rowKey (I've seen this!) - we can quickly define a complex type matched by regular expression:

public class TableStorageKey : StringlyPattern<TableStorageKey>
{
    public string PartitionKey { get; protected set; }
    public string RowKey { get; protected set; }

    protected override Regex Regex => new Regex(@"^(?<partitionKey>\w+):(?<rowKey>\w+)$");

    protected override TableStorageKey ParseFromRegex(Match match)
    {
        // Regular expression has already been matched for us.
        return new TableStorageKey
        {
            PartitionKey = match.Groups["partitionKey"].Value,
            RowKey = match.Groups["rowKey"].Value
        };
    }

    // Enable implicit conversion
    public static implicit operator TableStorageKey(string value)
    {
        // Helper method from the StringlyPattern base class.
        return Parse(value);
    }
}

// My caller (e.g. Controller, Worker Role etc) now doesn't have to work hard to parse strings.
public void Method(TableStorageKey key) {
    // Do stuff with the key..
    DoALookup(key.PartitionKey, key.RowKey);
}

// No problem! I can do that!
x.Method("pk:rk"); // Works
x.Method("usa:totalSearches"); // Works
x.Method("not-a-key"); // ArgumentOutOfRangeException

There you have it! Now do the right thing! Define custom types and only use the concrete types in future, you've no excuse! :smile: