45

I need to create a very long string in a program, and have been using String.Format. The problem I am facing is keeping track of all the numbers when you have more than 8-10 parameters.

Is it possible to create some form of overload that will accept a syntax similar to this?

String.Format("You are {age} years old and your last name is {name} ",
{age = "18", name = "Foo"});
user2864740
  • 60,010
  • 15
  • 145
  • 220
Espo
  • 41,399
  • 21
  • 132
  • 159

6 Answers6

71

How about the following, which works both for anonymous types (the example below), or regular types (domain entities, etc):

static void Main()
{
    string s = Format("You are {age} years old and your last name is {name} ",
        new {age = 18, name = "Foo"});
}

using:

static readonly Regex rePattern = new Regex(
    @"(\{+)([^\}]+)(\}+)", RegexOptions.Compiled);
static string Format(string pattern, object template)
{
    if (template == null) throw new ArgumentNullException();
    Type type = template.GetType();
    var cache = new Dictionary<string, string>();
    return rePattern.Replace(pattern, match =>
    {
        int lCount = match.Groups[1].Value.Length,
            rCount = match.Groups[3].Value.Length;
        if ((lCount % 2) != (rCount % 2)) throw new InvalidOperationException("Unbalanced braces");
        string lBrace = lCount == 1 ? "" : new string('{', lCount / 2),
            rBrace = rCount == 1 ? "" : new string('}', rCount / 2);

        string key = match.Groups[2].Value, value;
        if(lCount % 2 == 0) {
            value = key;
        } else {
            if (!cache.TryGetValue(key, out value))
            {
                var prop = type.GetProperty(key);
                if (prop == null)
                {
                    throw new ArgumentException("Not found: " + key, "pattern");
                }
                value = Convert.ToString(prop.GetValue(template, null));
                cache.Add(key, value);
            }
        }
        return lBrace + value + rBrace;
    });
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • I didn't now you could use anonymous types like this. It's not just .net 4 is it? – Preet Sangha Aug 24 '09 at 12:38
  • 4
    Plus it'll work for domain entities, i.e. `Format("Dear {Title} {Forename},...", person)` – Marc Gravell Aug 24 '09 at 12:38
  • 1
    @Preet - C# 3.0, so VS2008 or the .NET 3.5 compiler, but fine targetting .NET 2.0 – Marc Gravell Aug 24 '09 at 12:39
  • There's security concerns to think about here, if the values contain other `{Format}` blocks. – Jason Aug 24 '09 at 12:45
  • How does that present a security concern? Care to give an example? – Marc Gravell Aug 24 '09 at 12:49
  • Here's one, in a server-side call: Format("/resource/to/post?username={User}&status={Msg}&apikey={Key}", new { Key = "myKeyThatIsSensitiveInformation", User = username, Msg = message }); If message contains {Key}, then you've revealed your API Key to the user/public, or something similar. – Jason Aug 24 '09 at 12:56
  • +1/+1 Is there a way to nominate functionality for the next version of the .Net FCL? If so, count me in, This is a great question and solution! – Paul Sasik Aug 24 '09 at 13:56
  • 1
    @Jason - that is a circular argument; if you have code that chooses to format (and hence output) sensitive data, then it doesn't matter **what** approach you use to do it... – Marc Gravell Aug 24 '09 at 14:03
  • This works like a charm, very good. Did you come up with this just of the top of your head? Maybe you should include in the answer itself that is also works for domain entities. – Espo Aug 25 '09 at 05:19
  • The pattern itself is used quite a bit in things like ASP.NET MVC, and was presumably inspired by the way that jQuery allows you to specify multiple options on an object in javascript. – Marc Gravell Aug 25 '09 at 07:30
  • amazing, i had no idea that was possible (new{var=woot, var2=moreWoot}) –  Oct 07 '09 at 00:01
  • 1
    There's a bug in this code, it doesn't handles escaped braces. Try with this format for instance: "{{age}} = {age}, {{name}} = {name}". I had a question about how to solve that issue: http://stackoverflow.com/questions/1445571/regular-expression-for-string-format-like-utility. I eventually found a solution, but I didn't post it because I wasn't really happy with it... – Thomas Levesque Oct 09 '09 at 09:47
  • Well, I could argue it is an unsupported usage ;-p But it should be possible to do that just by changing the regex? – Marc Gravell Oct 09 '09 at 11:10
  • @Marc, this is beautiful code and it's easily modified to fit my needs. Thank you. – Steven Sudit Nov 22 '10 at 16:02
  • 1
    something like that should be in the .NET framework (and DataBinder.Eval can be used to access property by name) – Guillaume86 Oct 31 '11 at 21:51
4

As of C#6, this kind of string interpolation is now possible using the new string interpolation syntax:

var formatted = $"You are {age} years old and your last name is {name}";
Jordan Wallwork
  • 3,116
  • 2
  • 24
  • 49
2

not quite the same but sort of spoofing it... use an extension method, a dictionary and a little code:

something like this...

  public static class Extensions {

        public static string FormatX(this string format, params KeyValuePair<string, object> []  values) {
            string res = format;
            foreach (KeyValuePair<string, object> kvp in values) {
                res = res.Replace(string.Format("{0}", kvp.Key), kvp.Value.ToString());
            }
            return res;
        }

    }
Preet Sangha
  • 64,563
  • 18
  • 145
  • 216
1

primitive implementation:

public static class StringUtility
{
  public static string Format(string pattern, IDictionary<string, object> args)
  {
    StringBuilder builder = new StringBuilder(pattern);
    foreach (var arg in args)
    {
      builder.Replace("{" + arg.Key + "}", arg.Value.ToString());
    }
    return builder.ToString();
  }
}

Usage:

StringUtility.Format("You are {age} years old and your last name is {name} ",
  new Dictionary<string, object>() {{"age" = 18, "name" = "Foo"}});

You could also use a anonymous class, but this is much slower because of the reflection you'll need.

For a real implementation you should use regular expression to

  • allow escaping the {}
  • check if there are placeholders that where not replaced, which is most probably a programming error.
Stefan Steinegger
  • 63,782
  • 15
  • 129
  • 193
1

What about if age/name is an variable in your application. So you would need a sort syntax to make it almost unique like {age_1}?

If you have trouble with 8-10 parameters: why don't use

"You are " + age + " years old and your last name is " + name + "
RvdK
  • 19,580
  • 4
  • 64
  • 107
  • +1 for the simplicity and willingness to buck the string.Format requirement in the original question. Though i do like Marc Gravell's solution. – Paul Sasik Aug 24 '09 at 13:54
  • In my simple example, you could, but when you are outputting say, HTML, it gets even harder to read. string test = ""; – Espo Aug 26 '09 at 07:21
  • so true, it really depends on the usage. Even String.Format with html is crappy to read – RvdK Aug 26 '09 at 07:49
  • 1
    yeah but there's no requirement so why bother... Making complex things just to make it possible easier later on, it's just wasting time. – RvdK Oct 09 '09 at 10:32
0

Although C# 6.0 can now do this with string interpolation, it's sometimes necessary to do this with dynamic format strings at runtime. I've been unable to use other methods that require DataBinder.Eval due to them not being available in .NET Core, and have been dissatisfied with the performance of Regex solutions.

With that in mind, here's a Regex free, state machine based parser that I've written up. It handles unlimited levels of {{{escaping}}} and throws FormatException when input contains unbalanced braces and/or other errors. Although the main method takes a Dictionary<string, object>, the helper method can also take an object and use its parameters via reflection.

public static class StringExtension {
    /// <summary>
    /// Extension method that replaces keys in a string with the values of matching object properties.
    /// </summary>
    /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
    /// <param name="injectionObject">The object whose properties should be injected in the string</param>
    /// <returns>A version of the formatString string with keys replaced by (formatted) key values.</returns>
    public static string FormatWith(this string formatString, object injectionObject) {
        return formatString.FormatWith(GetPropertiesDictionary(injectionObject));
    }

    /// <summary>
    /// Extension method that replaces keys in a string with the values of matching dictionary entries.
    /// </summary>
    /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
    /// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param>
    /// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns>
    public static string FormatWith(this string formatString, IDictionary<string, object> dictionary) {
        char openBraceChar = '{';
        char closeBraceChar = '}';

        return FormatWith(formatString, dictionary, openBraceChar, closeBraceChar);
    }
        /// <summary>
        /// Extension method that replaces keys in a string with the values of matching dictionary entries.
        /// </summary>
        /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
        /// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param>
        /// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns>
    public static string FormatWith(this string formatString, IDictionary<string, object> dictionary, char openBraceChar, char closeBraceChar) {
        string result = formatString;
        if (dictionary == null || formatString == null)
            return result;

        // start the state machine!

        // ballpark output string as two times the length of the input string for performance (avoids reallocating the buffer as often).
        StringBuilder outputString = new StringBuilder(formatString.Length * 2);
        StringBuilder currentKey = new StringBuilder();

        bool insideBraces = false;

        int index = 0;
        while (index < formatString.Length) {
            if (!insideBraces) {
                // currently not inside a pair of braces in the format string
                if (formatString[index] == openBraceChar) {
                    // check if the brace is escaped
                    if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) {
                        // add a brace to the output string
                        outputString.Append(openBraceChar);
                        // skip over braces
                        index += 2;
                        continue;
                    }
                    else {
                        // not an escaped brace, set state to inside brace
                        insideBraces = true;
                        index++;
                        continue;
                    }
                }
                else if (formatString[index] == closeBraceChar) {
                    // handle case where closing brace is encountered outside braces
                    if (index < formatString.Length - 1 && formatString[index + 1] == closeBraceChar) {
                        // this is an escaped closing brace, this is okay
                        // add a closing brace to the output string
                        outputString.Append(closeBraceChar);
                        // skip over braces
                        index += 2;
                        continue;
                    }
                    else {
                        // this is an unescaped closing brace outside of braces.
                        // throw a format exception
                        throw new FormatException($"Unmatched closing brace at position {index}");
                    }
                }
                else {
                    // the character has no special meaning, add it to the output string
                    outputString.Append(formatString[index]);
                    // move onto next character
                    index++;
                    continue;
                }
            }
            else {
                // currently inside a pair of braces in the format string
                // found an opening brace
                if (formatString[index] == openBraceChar) {
                    // check if the brace is escaped
                    if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) {
                        // there are escaped braces within the key
                        // this is illegal, throw a format exception
                        throw new FormatException($"Illegal escaped opening braces within a parameter - index: {index}");
                    }
                    else {
                        // not an escaped brace, we have an unexpected opening brace within a pair of braces
                        throw new FormatException($"Unexpected opening brace inside a parameter - index: {index}");
                    }
                }
                else if (formatString[index] == closeBraceChar) {
                    // handle case where closing brace is encountered inside braces
                    // don't attempt to check for escaped braces here - always assume the first brace closes the braces
                    // since we cannot have escaped braces within parameters.

                    // set the state to be outside of any braces
                    insideBraces = false;

                    // jump over brace
                    index++;

                    // at this stage, a key is stored in current key that represents the text between the two braces
                    // do a lookup on this key
                    string key = currentKey.ToString();
                    // clear the stringbuilder for the key
                    currentKey.Clear();

                    object outObject;

                    if (!dictionary.TryGetValue(key, out outObject)) {
                        // the key was not found as a possible replacement, throw exception
                        throw new FormatException($"The parameter \"{key}\" was not present in the lookup dictionary");
                    }

                    // we now have the replacement value, add the value to the output string
                    outputString.Append(outObject);

                    // jump to next state
                    continue;
                } // if }
                else {
                    // character has no special meaning, add it to the current key
                    currentKey.Append(formatString[index]);
                    // move onto next character
                    index++;
                    continue;
                } // else
            } // if inside brace
        } // while

        // after the loop, if all braces were balanced, we should be outside all braces
        // if we're not, the input string was misformatted.
        if (insideBraces) {
            throw new FormatException("The format string ended before the parameter was closed.");
        }

        return outputString.ToString();
    }

    /// <summary>
    /// Creates a Dictionary from an objects properties, with the Key being the property's
    /// name and the Value being the properties value (of type object)
    /// </summary>
    /// <param name="properties">An object who's properties will be used</param>
    /// <returns>A <see cref="Dictionary"/> of property values </returns>
    private static Dictionary<string, object> GetPropertiesDictionary(object properties) {
        Dictionary<string, object> values = null;
        if (properties != null) {
            values = new Dictionary<string, object>();
            PropertyDescriptorCollection props = TypeDescriptor.GetProperties(properties);
            foreach (PropertyDescriptor prop in props) {
                values.Add(prop.Name, prop.GetValue(properties));
            }
        }
        return values;
    }
}

Ultimately, all the logic boils down into 10 main states - For when the state machine is outside a bracket and likewise inside a bracket, the next character is either an open brace, an escaped open brace, a closed brace, an escaped closed brace, or an ordinary character. Each of these conditions is handled individually as the loop progresses, adding characters to either an output StringBuffer or a key StringBuffer. When a parameter is closed, the value of the key StringBuffer is used to look up the parameter's value in the dictionary, which then gets pushed into the output StringBuffer.

EDIT:

I've turned this into a full on project at https://github.com/crozone/FormatWith

Ryan
  • 1,670
  • 18
  • 25