Dynamics 365 Dynamic Text Parser – Supercharged | Mage Series
Nearly all business solutions require sending a notification, one way or another. Dynamics easily provides this functionality for emails: ‘send email’ step or message in code. Going a step further, when the business requires more dynamic text to be filled out (e.g. a customer’s name in the email greeting), the user can insert placeholders into the text to be filled by the platform’s dynamic text parser before sending the email.
The out-of-the-box functionality is great for simpler scenarios; however, when asked to add conditions to the text, simplify localisation, retrieve an updated value from a web service, retrieve a value in a deeply nested table, or simply format a date in a way that fits the context more, it becomes a bit harder to achieve, that might require a complex solution for such small ask.
Dynamic Text Parser
To resolve this, I created a parser that overcomes many challenges in the context of Dynamics 365 dynamic text.
I will go through the ‘constructs’ that are supported by this parser, after some terminologies and giving an overview of how it works.
Improvement requests and suggestions are more than welcome, of course. Please open a ticket on GitHub and let’s start a discussion.
Basic Structure
The main structure or placeholder that is placed in the to-be-parsed text takes the form: {_`<description>`_<key>(<parameters>)|%<preprocessors>%<block>@<post-processors>@|<key>}
.
Anything between <
and >
should be manually replaced with what it describes when used.
Quick Sample
{_`Retrieve related accounts budget, sum them, and format the result as currency.`_.(this!ys_accounts_project_accountid)|%distinct(accountid)%{_`Retrieve the budget column value.`_c|ys_budget|c}@sum@format(`$#.#`)@|.}
Features
- Unique construct that guarantees it won’t occur naturally in any text, which increases the solution’s robustness
- Supports parsing expressions; e.g., x>1&&y<3, which can also be used with column values
- Constructs:
- Column: retrieve a column value from CRM
- Preload: performance improvement by caching from CRM
- Reference: loads related rows from CRM, or executes a FetchXML or Action
- Info: displays information related to the current row
- Template: defines a block of text for reuse to be referenced later in the text
- Discard: performs an operation and then discards the result
- Replace: replace a pattern in the text
- Dictionary: retrieve a value from the key-value table from CRM
- Common config: retrieve a column value from the Common Configuration table
- Preprocessors:
- Store: stores the value so far in the pipeline in memory
- Read: restores the value from memory
- Distinct: keeps only unique rows retrieved by the reference-construct
- Order: orders the rows retrieved by the reference-construct
- Post-processors:
- Store and Read
- String ops
- Length, Index, Substring, Trim, Pad, Truncate, Upper, Lower, Sentence case, Title case, Extract text, Replace, Split
- Format:
- Date and Number
- Collection:
- Count, First, Last, Nth, Top, Distinct, Order, Where, Filter
Installation and Usage
The link below contains the full manual of the solution; however, I will just list some of the notable features here as a showcase.
The YS Common solution is required for the configuration entities. It can be skipped if the dictionary
or configuration
constructs are not needed.
Install either Yagasoft.Libraries.Common
(DLL installed) or Yagasoft.Libraries.Common.File
(the parser class itself is embedded in the project itself) NuGet package, and then reference the CrmParser
class.
https://github.com/yagasoft/Dynamics365-YsCommonSolution
https://www.nuget.org/packages/Yagasoft.Libraries.Common/
https://www.nuget.org/packages/Yagasoft.Libraries.Common.File/
https://github.com/yagasoft/Dynamics365-CrmTextParser (manual)
Testing Tool
All of the functionality of this parser can be tested using an XrmToolBox plugin:
https://www.xrmtoolbox.com/plugins/D365-CrmTextParser-Tester-Plugin
Core Algorithm
Depends mainly on the construct. In general, the basic logic is as follows:
- The context is retrieved from the global state.
- Preprocessors are executed, in order, taking into account the block content.
- The result is a single text to replace the block in the construct.
- Construct logic is executed using the block as input.
- The result is a single text per record in the context.
- Post-processors are executed, in order, on each entry resulting from the
construct
logic.
By default, all entries are merged into a single output string, unless an ‘aggregate’ post-processor is used.
Expressions
The parser supports evaluating expressions. Below are the rules in order of precedence. The higher entries in the table are evaluated first, unless wrapped in parenthesis.
Expressions can be used in a block anywhere, except between ticks (`
).
Pattern | Description | ||||||||||||
*,/ | Multiply and divide. | ||||||||||||
+,- | Add and subtract.
Can be a number or date. If date, it must be in the following form: The value is a number. The unit is one of the following:
|
||||||||||||
<,>,<=,>= | Greater and less than. | ||||||||||||
==,!= | Equality. | ||||||||||||
&& | And. | ||||||||||||
|| | Or. | ||||||||||||
?? | If the left side is empty, take the right side. | ||||||||||||
?: | Ternary conditional: <predicate>?<true-clause>:<false-clause> .
E.g., |
Constructs
Column
The most important of all constructs. Returns a column value. Supports traversing using dot and bang operators.
Key: c
{c(name)|ownerid|c}
In this example, we are getting the record of the owner and then his name.
Reference
The most interesting of all constructs. Either reference a query, action, or a relation. The reference is then set as the context for all nested constructs. The block is executed for each record in the context.
Key: .
{.(this.owner)|Owner’s name: {c|fullname|c}|.}
In this example, we are setting the context of execution to the owner record set in the current record. Effectively, we traversed into the owner lookup. The full name will be retrieved from the owner
record. The output could be Owner’s name: Test User
.
Fetch
Stores the FetchXML given in memory for later execution when referenced (reference-construct).
Key: f
{f(anyAccount)|`<fetch no-lock='true' top='1'>
<entity name='account' >
<attribute name='name' />
</entity>
</fetch>`|f}
In this example, we are defining a query that will retrieve any account from CRM upon execution.
Preprocessors
Distinct
Used with the reference-construct to retrieve only unique records.
Key: distinct
{.(this!accounts)|%distinct(accounttype,industry)%{c|ys_taxpercent|c}|.}
In this example, we are retrieving related accounts and then making them distinct over the account type and industry (combined), then selecting the tax percentage value.
Order
Used with the reference-construct to sort records.
Key: order
{.(this!accounts)|%order(accounttype,!industry)%{c|ys_taxpercent|c}|.}
In this example, we are retrieving related accounts, ordering them by account type first, and then by industry in descending order (notice the bang sign !
before industry
), then selecting the tax percentage value.
Post-Processors
Truncate
Shortens the output to the specified length, and then append the given replacement.
Key: truncate
{c|ys_description@truncate(6,`...`)@|c}
In this example, given description
is This is a test.
, the output is This i...
.
Title
Converts the text to title case. Supports regex groups (check terms section).
Key: title
{c|ys_description@title@|c}
In this example, given description
is this is a test.
, the output is This is a Test
.
Sum
Returns the sum of all items in the output.
Key: sum
{.(this!accounts)|{c|ys_taxpercent|c}@sum@|.}
In this example, if the system has 3 accounts stored with the tax percentages: 15, 10, and 20; the output will be 45
.
Conclusion
There are a lot more constructs and processors. Please refer to the manual linked above for the full guide.