A common thing in any IT solution is to present text to a customer. The text is typical written by a non-tech person, and usually maintained in such way that it can be changed dynamically. The challenge here is that within the text there is reference to data specific information like personal information, different kind of numbers and so on.
So a common solution to this is to define macros, that will be substituted with values before the text is presented.
Hi {{user.name.first}}
Aliquam tristique neque nec turpis viverra rhoncus.
Nulla facilisi.
Vivamus euismod fermentum fermentum.
Nulla lacinia nec lacus et lobortis.
Maecenas facilisis augue erat, non rhoncus sapien vestibulum id.
Pellentesque arcu ante, placerat nec turpis nec, interdum suscipit orci.
Vivamus at nisi nec ante eleifend tristique facilisis dignissim elit.
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
Integer sed faucibus lorem, non ultrices velit. Proin lacinia aliquet libero, vitae dapibus orci lobortis sed.
Nam consectetur elit ut vestibulum pretium. Aliquam posuere egestas justo et semper.
In euismod ligula non imperdiet porta. In hac habitasse platea dictumst.
Sed tempus ex id gravida blandit. Nulla facilisi.
Regards {{advisor.name}}
The macro pattern is configurable, since the text can be of many different kinds. By default the macro starts with {{
and ends with }}
.
This can be changed with TextMacroResolverOptions
that have two properties; MacroNamePrefix
and MacroNamePostfix
.
Since a macro is resolved using the same format pattern, it is possible within the macro definition to define an explicit text format.
So if you have a macro {{user.account.amount}}
that is resolved normally using text format N0
, you can within the macro override it by doing this {{user.account.amount:N2}}
.
Also :
can be replaced in the macro pattern. In TextMacroResolverOptions
with the property MacroNameTextFormatSeperator
.
Each macro must have a resolver, so the macro string can be replaced with the resolved value.
[Macro("user.name.first", Description: "", Synchronize: true)]
public class UserNameFirstMacroValueResolver : MacroValueResolver
{
private UserDbContext _userDbContext;
private MacroContext _MacroValueContext;
public UserNameFirstMacroValueResolver( MacroValueContext macroContext, UserDbContext userDbContext )
{
_macroContext = macroContext;
_userDbContext = userDbContext;
}
public override async Task<MacroValueResult> Resolve(string originalMacroName, string textFormat)
{
var user = await _dbContext.Users.FindAsync(_macroContext.UserId);
return new MacroValueResult(originalMacroName, user.GetFirstName(_macroContext.CultureInfo), textFormat));
}
}
You macro value resolver must either inherit abstract class MacroValueResolver
, or interface IMacroValueResolver
.
Also the attribute [Macro(.....)]
must be set on the class with a name.
Each MacroValueResolver
will be resolved using Dependency Injection, and therefor they can individually get what is required to resolve the value through the constructor.
All MacroValueResolver
must be registered at Scope
level, and will be done if services.AddTextMacroResolver(....)
is used in your startup.
Since many MacroValueResolver
must resolve data that depends on a context, or application state. It is possible to register a specialized MacroValueContext
type.
You can specialize (inherit) MacroValueContext
with your own properties, or make a complete different type. The context type, also registered at Scope
level in the DI, can then have its properties set just before the actual macro value resolving.
The TextMacroResolver
is resolving macros in a task async manner, meaning that they will be executed in parallel.
This is good for performance, if there is a lot of resource depended macros, but also bad since some resources do not allow parallel work - like Entity Framework Core.
To solve this non-parallel-execution there is a Synchronize
flag. Setting Synchronize
to true, means it will be executed one at time with all others synchronized MacroValueResolver
.
Now lets see how a Startup
could look like.
.AddTextMacroResolver<MyMacroValueContext>( options =>
{
options.AssembliesWithMacroValueResolverTypes = new[] {Assembly.GetExecutingAssembly()};
})
one or more assemblies can be specified, and they will be scanned and the found MacroValueResolver
types will be registered for DI usage.
or
.AddTextMacroResolver<MyMacroValueContext>( options =>
{
options.MacroValueResolverTypes = new[] {typeof(UserNameFirstMacroValueResolver),.... };
})
where one or more types can be specified, and the will be registered for DI usage.
the TextMacroResolverOptions
is registered as Singleton.
With all MacroValueResolver
registered, it is time to do some macro stuff:
var list = textMacroResolver.GetResolverMacroNames()
gives a list of all registered MacroValueResolver
macro names.
this can be used to call:
var result = await textMacroResolver.ResolveMacro( macroName );
which returns a result with the value, or an exception if result.IsResolved == false
.
doing and complete text replacement, call:
// Set context related info.
var context = serviceProvider.GetRequiredService<MyMacroValueContext>();
context.UserId = 42;
// Replace macros
var result = await textMacroResolver.ResolveText( textWithMacros );
if result.IsResolved == true
then all macros are replaced successfully.
var list = textMacroResolver.ExtractMacros( textWithMacros );
also look at the UnitTest project, where you can see it in action.
PM> Install-Package MicroKnights.TextMacroResolver