Skip to content

Data Source Extensions

axunonb edited this page Jul 2, 2024 · 19 revisions

About Source Extensions

Source extensions translate complex types into objects, which can be represented as a string. Each source extensions handles one object or interface type (e.g. an IList).

Register Data Sources

Required ISource extensions must be registered with the SmartFormatter.

Data sources are added by calling

  • SmartFormatter.AddExtensions(...)
  • SmartFormatter.InsertExtension(...)

With AddExtensions(...) all WellKnownExtensionTypes.Sources and WellKnownExtensionTypes.Formatters are automatically inserted to the extension list at the place where they usually should be.

InsertExtension(...) lets you insert an extension to the desired position in the list.

From a performance perspective, only register source extensions that are actually needed:

// Add needed source extensions
var smart = new SmartFormatter()
    .AddExtensions(new ReflectionSource(), new DefaultSource());

// Add all default source extensions (and formatters)
smart = Smart.CreateDefaultFormatter();

How do Data Sources Work

The SmartFormatter evaluates the selectors in a Placeholder one by one. Fore each selector, the registered ISources are invoked. As soon as an ISource can detect a matching variable from the data arguments, it returns the value.

That's why the order in the list of registered data sources is important.

If no matching data can be found for a Placeholder across all ISources, a FormattingException will throw.

After the selector was successfully evaluated, IFormatter(s) will be invoked.

For SmartSettings.CaseSensitivity == CaseSensitivityType.CaseInsensitive, and multiple members share the same name but differ in case, the first encountered member will be selected.

Overview

All data source extensions are included in the core NuGet package SmartFormat unless otherwise noted.

DefaultSource

Criteria Details
Data type Any object, including anonymous types
Looks for arguments by index
Example Smart.Format("{0} {1} {2}", 123, "456", default(object))

Like with string.Format several parameters to the formatter are allowed. Indexed and named selectors can be combined like this:

var data1 = new KeyValuePair<string, object>("Name", "John");
var data2 = new KeyValuePair<string, object>("City", "Zurich");

// 1st notation:
Smart.Format("1st notation: {0:{Name}} from {1:{City}}", data1, data2);
// Outputs: "1st notation: John from Zurich"

// alternative notation:
Smart.Format("2nd notation: {0.Name} from {1.City}", data1, data2);
// Outputs: "2nd notation: John from Zurich"

ListFormatter

Criteria Details
Data type IList
Looks for elements in the IList
Remarks See more under Formatter Extensions

Example:

Smart.Format("{0:list:{}|, |, and }", new List<string> { "one", "two", "three" });
// Outputs: "one, two, and three"

ReflectionSource

Criteria Details
Data type Any object, including anonymous types
Looks for Property names, field names, and names of paramterless methods
Example Smart.Format("{Item}", new { Item = 999 })
Remarks Uses caching for accessing object members for best performance.

The cache is static and shared among all instances of ReflectionSource.

By default, the cache is enabled. To disable the cache, set ReflectionSource.IsTypeCacheEnabled to false. The size of the cache can be set with ReflectionSource.MaxCacheSize; defaults to ReflectionSource.DefaultCacheSize. When the cache exceeds the maximum size limit, it removes the oldest entry first, adhering to a First-In-First-Out (FIFO) strategy.

DictionarySource

Criteria Details
Data types IDictionary, IDictionary<string, object>, IReadOnlyDictionary, dynamic
Looks for Dictionary key names, dynamic property names
Example Smart.Format("{Key}", new Dictionary<string, object>(){ { "Key", 999 } } )
Smart.Format("{Key}", (dynamic)(new { Key = 999 }))

Dictionaries may be nested.

For classes that only implement the generic IReadOnlyDictionary interface, property DictionarySource.IsIReadOnlyDictionarySupported must be set to true. Although caching (for the current instance) is used, this is still slower than the other types.

KeyValuePairSource

The KeyValuePairSource as a simple, cheap and performant way to create named placeholders.

Criteria Details
Data type KeyValuePair<string, object?>
Looks for Dictionary key names, dynamic property names
Example Smart.Format("{placeholder}", new KeyValuePair<string, object?>("placeholder", "some value")

Important: The type arguments for KeyValuePair must be <string, object?>.
KeyValuePairs may be nested.

StringSource

Criteria Details
Data type string
Looks for strings, built-in parameterless methods
Example Smart.Format("{0.ToUpper}", "lower")

Built-in methods:

Method Input Output
Length "A name" 6
ToUpper "dış" "DIŞ" Turkish DIŞ or dış (outside)
ToUpperInvariant "dış" "DıŞ" Turkish dış (outside) becomes DıŞ (tooth)
ToLower "DIŞ" "dış" Turkish DIŞ or dış (outside)
ToLowerInvariant "DIŞ" "diş" Turkish DIŞ (outside) becomes diş (tooth)
TrimStart " abc" "abc"
TrimEnd "abc " "abc"
Trim " abc " "abc"
Capitalize "word" "Word"
CapitalizeWords "john DOE" "John Doe"
ToBase64 "xyz" "eHl6"
FromBase64 "eHl6" "xyz"
ToCharArray "abc" "abc".ToCharArray()

NewtonsoftJsonSource

Criteria Details
Data type JObject, JValue
Looks for child elements by their name
Example Smart.Format("{Name}", JObject.Parse("{ 'Name':'John'}"))

Included in NuGet package SmartFormat.Extensions.Newtonsoft.Json

SystemTextJsonSource

Criteria Details
Data type JElement
Looks for child elements by their name
Example Smart.Format("{Name}", JsonDocument.Parse("{ \"Name\":\"John\"}").RootElement)

Included in NuGet package SmartFormat.Extensions.System.Text.Json

JSON Sources in General

JSON also comes in handy when processing data in a web API application where the argument submitted from the browser to the controller is JSON.

Another scenario is working with queries from SQL Server:

SELECT 'John' AS [FirstName], 'Doe' AS [LastName], 32 AS [Age]
FOR JSON PATH, ROOT('Father')

You can parse the query result into a JObject (Newtonsoft.Json) or JsonElement (System.Text.Json) and give it to SmartFormat as an argument. JObject or JElement may contain arrays.

XmlSource

Criteria Details
Data type XElement
Looks for child elements by their name
Example Smart.Format("{Name}", XElement.Parse("<root><Name>Joe</Name></root>");)

Included in NuGet package SmartFormat.Extensions.Xml

ValueTupleSource

The ValueTupleSource is a special source, because it acts merely as a container for data sources.

Example:

Assume, we have 3 objects we need for formatting a string:

var data1 = new { ItemInt = 123 };
var data2 = new { ItemBool = true };
var data3 = new { ItemString = "an item" };

An obvious option to format is:

Smart.Format("{0.ItemInt} * {1.ItemBool} * {2.ItemString}", data1, data2, data3 );
// Outputs: "123 * True * an item"

If all data objects are stored in a ValueTuple argument to the formatter, you can omit the indexed arguments:

Smart.Format("{ItemInt} * {ItemBool} * {ItemString}", (data1, data2, data3));
Smart.Format("{ItemInt} * {ItemBool} * {ItemString}", (data3, data1, data2));
// Both output: "123 * True * an item"

There is no need to care about the index and the sequence of arguments to the formatter.

This is very handy if you need the output of a variable depending on values of other variables. Let's output ItemString only if ItemInt == 123 and ItemBool == true. This keeps the business logic out of the format:

Smart.Format("{Show:cond:{ItemString}|Don't show}", 
    (data3, new { Show = data1.ItemInt == 123 && data2.ItemBool }));
// Output: "an item"

A ValueTuple containing other ValueTuples will be flattened.

PersistentVariableSource, GlobalVariableSource

Both provide global variables that are stored in VariablesGroup containers to the SmartFormatter. These variables are not passed in as arguments when formatting a string. Instead, they are taken from these registered ISources.

VariablesGroups may contain Variable<T>s or other VariablesGroups. The depth of such a tree is unlimited.

a) GlobalVariableSource variables are static and are shared with all SmartFormatter instances.

b) PersistentVariableSource variables are stored per instance.

PersistentVariableSource and GlobalVariableSource must be configured and registered as ISource extensions as shown in the example below.

Example

PersistentVariablesSource 
or GlobalVariablesSource (Containers for Variable / VariablesGroup children)
|
+---- VariablesGroup "group"
|     |
|     +---- StringVariable "groupString", Value: "groupStringValue"
|     |
|     +---- Variable<DateTime> "groupDateTime", Value: 2024-12-31
|	  
+---- StringVariable "topInteger", Value: 12345
|
+---- StringVariable "topString", Value: "topStringValue"

Here, we use the PersistentVariablesSource:

// The top container
// It gets its name later, when being added to the PersistentVariablesSource
var varGroup = new VariablesGroup();

// Add a (nested) VariablesGroup named 'group' to the top container
varGroup.Add("group", new VariablesGroup
{
    // Add variables to the group
    { "groupString", new StringVariable("groupStringValue") },
    { "groupDateTime", new Variable<DateTime>(new DateTime(2024, 12, 31)) }
});
// Add more variables to the container
varGroup.Add("topInteger", new IntVariable(12345));
varGroup.Add("topString", new StringVariable("topStringValue"));

// The formatter for persistent variables requires only 2 extensions
var smart = new SmartFormatter();
smart.FormatterExtensions.Add(new DefaultFormatter());
var pvs = new PersistentVariablesSource
{
    // Here, the top container gets its name
    { "global", varGroup }
};
// Best to put it to the top of source extensions
smart.AddExtensions(0, pvs);

// Note: We don't need args to the formatter for PersistentVariablesSource variables
_ = smart.Format(CultureInfo.InvariantCulture,
    "{global.group.groupString} {global.group.groupDateTime:'groupDateTime='yyyy-MM-dd}");
// result: "groupStringValue groupDateTime=2024-12-31"

_ = smart.Format("{global.topInteger}");
// result: "12345"

_ = smart.Format("{global.topString}");
// result: "topStringValue"
Clone this wiki locally