There is a lot to unpack in that title. Let’s start with the Try-Parse pattern. This pattern is defined in Microsoft’s “Exceptions and Performance” article (which itself comes from Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition) and is likely one you are very familiar with. Fundamentally the return type of a Try-Parse method should take the value to parse, an out parameter that matches the type expected to be the result of the parse and it should return a bool to indicate the success or failure of the parse.

var stringValueToParse = "1";
if(int.TryParse(stringValueToParse, out var result))
    Console.WriteLine($"I parsed {stringValueToParse} to {result}");
else
    Console.WriteLine($"I was unable to parse {nameof(stringValueToParse)} with the value of {stringValueToParse} to an int.");

The above example should be pretty clear, given the value of “1”, if we can parse it, print success, else print failure.

Okay, now let’s get a bit complicated. Where I work we have what we call entities. These represent the main business objects. This is things like, assets, users, sites, jobs, tickets, orders, etc… While each of these entities do have properties that define them (like a Tractor would have a VIN), our customers often want to add custom details to each of these entities. For instance, an asset might have a color, or a site might have a external name. These are properties that are not common across customers and thus must be dynamic.

Each of these entities implements the following interface:

public interface IDetailsEntity
{
    List<Detail> Details { get; }
}

And a detail is simply:

public class Detail
{
    public int Id { get; set; }
    public string Key { get; set; }
    public string Value { get; set; }
}

So an asset might look like:

public class Asset : IDetailsEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Detail> Details { get; set; } = new();
}

I suppose it would be easy to argue that this isn’t a great paradigm, if simply because we rely on the value of details to be a string. I would say that theoretically you would be right. But more importantly operations and maintainability would suffer greatly if we use a different method. Dynamic details are defined for each customer by entity type as JSON. Both the front-end WPF client, the front-end ASP.NET client, the front-end Android client and the back-end server know how to handle details like this.

So how do we handle this? Below is a sample JSON:

{
  "details": [
    {
      "key": "asset|odometer",
      "friendlyName": "Odometer",
      "type": "double"
    },
    {
      "key": "asset|yardId",
      "friendlyName": "Yard",
      "type": "customer"
    }
  ]
}

It is super easy for support to add new details to the JSON through a tool we have so all a customer has to do is call and ask for a new detail to be added. No extra code to write, no new properties to add to the entity, no new columns in the database. It’s important to remember that these are fields specifically requested by the customer.

In order to help we have defined a number of extension methods that work with try-parse to parse these values to the needed corresponding type.

These look something like:

public static double? ToDetailDouble(this IDetailsEntity source, string key)
{
    var detail = GetDetail(source, key);

    if (detail?.Value == null) return null;

    return double.TryParse(detail.Value, out var value) ? value : null;
}

public static int? ToDetailInt(this IDetailsEntity source, string key)
{
    var detail = GetDetail(source, key);

    if (detail?.Value == null) return null;

    return int.TryParse(detail.Value, out var value) ? value : null;
}

private static Detail? GetDetail(IDetailsEntity source, string key)
{
    if (source == null || string.IsNullOrEmpty(key)) return null;
    return source.Details.FirstOrDefault(x => x.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
}

This gets us a lot of the way there in handling these details. But there still ends up being a lot of boilerplate around handling these values. But we’re at the root of the problem here. For the most part, we need to do something if a value exists. Because of the fluid nature of these details we usually don’t care if they don’t exist. That is for the customer to manage, adding and editing the values. But we need to handle when there is a value. This is often setting a value in a view model, or even doing look-ups, like in the case of the customer id for Yard above.

And it would be really nice if we just had it in one method, not split out by type like the extension methods above.

And that leads us to TryParseValue:

public static class Converters 
{
    public static bool TryParseValue<T>(Func<string, T?> tryParseKeyToValue, string key, out T result) where T : struct
    {
        var foundValue = tryParseKeyToValue(key);
        if (!foundValue.HasValue)
        {
            result = default;
            return false;
        }
        result = foundValue.Value;
        return true;
    }
}

Let’s start with that “where” at the end. It’s saying that we can only accept types that are structs, aka value types. This is very important because only a value type has the properties HasValue and Value. This is important because we need to know if the value found was parsed.

The next interesting bit is the out T result parameter. It’s saying that whatever you expect back is the value as a nullable, we’re going to expect back from the Func. And that sentence is pretty dense. So Func is a way to take a delegate, where the first type is the delegate’s parameter and the second type is the return value.

We’re saying, give me a method that takes a string and returns a nullable T. Well, as it would happen we have a bunch of extension methods that do exactly that.

So how does this all work in action?

var newTruck = new Asset { Id = 1, Name = "newTruck" };
newTruck.Details.Add(new Detail { Id = 1, Key = "asset|odometer", Value = "10045.23" });
newTruck.Details.Add(new Detail { Id = 2, Key = "asset|yardId", Value = "21" });

if (Converters.TryParseValue(newTruck.ToDetailDouble, "asset|odometer", out double dblResult))
    //handle the odometer
    Console.WriteLine($"Found odometer for {newTruck.Name} are {dblResult}.");

if (Converters.TryParseValue(newTruck.ToDetailInt, "asset|yardId", out int intResult))
    //look up the customer
    Console.WriteLine($"Found yard id {intResult} for truck {newTruck.Name}.");

Console.ReadLine();

You can see on the if statement, the last parameter passed in is “out double dblResult”. This is important because it sets a implied type for T, that of double. The compiler knows the type for this method is implicitly double. That means for the delegate to be passed in, the delegate must take a string and return a nullable double, which our extension method does.

Is this particularly revolutionary? Probably not. But it cleans up a lot of code, removes a bunch of boilerplate and makes the code easier to read. This in turn makes it more easily maintained and makes our jobs easier.

Thanks,
Brian

One thought on “Parsing a nullable primitive to a primitive with generics and the Try-Parse pattern

Leave a Reply

Your email address will not be published. Required fields are marked *

FormatException

928 East Plymouth Drive Asbury Park, NJ 07712