Dictionary
Aurelius dictionary is intended to be used in Aurelius queries. The idea is that instead of using strings to reference entity properties, you use the dictionary directly. For example, a usual Aurelius query is like this:
Results := Manager.Find<TCustomer>
.Where(Linq['Name'].Like('M%'))
.List;
But the above approach is error prone, the 'Name'
string can be wrong, it can be wrongly typed, or the name might be modified. You will only find the errors at runtime. With the dictionary, you can write the query like this:
Results := Manager.Find<TCustomer>
.Where(Dic.Customer.Name.Like('M%'))
.List;
With the code above you have full code completion when you are coding, so you don't have to remember the property names, and you get errors at compile-time.
Warning
Aurelius dictionary should only be used from Delphi XE6 and later versions. Older Delphi versions have RTTI issues that cause the dictionary to not work correctly in some situations, especially the dictionary validator.
Dictionary generation
To be used, the dictionary has to be generated from the existing mapped classes or the database. "Generate" means creating a Delphi unit .pas
file with source code containing the dictionary classes. You can then add such unit to the uses
clause and use the dictionary in queries. There are three ways you can generate the dictionary.
Generate from application
The dictionary can be generated directly from your entity classes. It has to be generated from an existing Delphi application so that Aurelius retrieve information via RTTI and generate the source code unit for you. This is very convenient if you use a code-first approach, when you create your entity classes and ask Aurelius to create the database for you.
To generate the dictionary, you should use the TDictionaryGenerator
class declared in unit Aurelius.Dictionary.Generator
. It can be as simple as this. Use the unit:
uses Aurelius.Dictionary.Generator;
And then use this line of code anywhere in your application:
TDictionaryGenerator.GenerateFile('C:\SomeFolder\MyDictionary.pas');
The above line is enough to generate the unit in the specified file location. TDictionaryGenerator
provides several options you can configure, in case you want to have more control over the generated dictionary. Here is a more advanced use of it:
var
Generator: TDictionaryGenerator;
SourceCode: string;
begin
// Create the dictionary for the entity classes defined in a specific
// mapping explorer. In this case, use model "Accounting"
Generator := TDictionaryGenerator.Create(TMappingExplorer.Get('Accounting'));
try
// The DictionaryId is used to build the name of dictionary interface and class.
// By default the model name is used, so the default interface and class names would be
// IAccountingDictionary and TAccountingDictionary.
// The change below will make them IMyAccountingDictionary and TMyAccountingDictionary
Generator.DictionaryId := 'MyAccounting';
// The name of the global variable holding the dictionary. By default, the name will be Dic.
// With the change below, an eventual dictionary entity
// will be accessed as D.Customer instead of Dic.Customer
Generator.GlobalVarName := 'D';
// The name of the unit to be generated. When you call TDictionaryGenerator.GenerateFile
// this is automatically set based on the file name. In this case we have to set it directly.
// If you do not set this, the default unit name will be Unit1.
Generator.OutputUnitName := 'AccountingDictionary';
// Retrieve the full source code as a string
SourceCode := Generator.GenerateSource;
// Save the source to a file
TFile.WriteAllText('C:\SomeFolder\AccountingDictionary.pas', SourceCode);
finally
Generator.Free;
end;
end;
Generate from database
In a database-first approach, you create your database tables first, and then generate Aurelius classes from such database. You can do that using the TMS Data Modeler tool or using the TAureliusConnection "generate entities" wizard from the IDE.
In both cases, you have an option to also generate the dictionary, in addition to the Aurelius classes. This is straightforward and the dictionary will be automatically created for you.
Generate from command-line tool
You can also generate the dictionary from a command-line tool called AureliusDictionaryGenerator. This can be useful if you want to automate the dictionary generation and for some reason you don't want to do it from your own application.
With the generator tool you need to have a package file (.bpl) compiled with your entities, and then pass the package file to the it. It will extract the classes from the package and generate the source code file. The source code of the AureliusDictionaryGenerator tool is available in Aurelius distribution under the demos
folder.
When you launch the tool without passing parameters, you get the help page:
PS> .\AureliusDictionaryGenerator.exe
TMS Aurelius Dictionary Generator version 0.1
Copyright (c) TMS Software. All rights reserved.
OutputFileName - Required parameter was not provided.
AureliusDictionaryGenerator.exe <OutputFileName> [options]
<OutputFileName> - Output file name to be generated
-pvalue, /package:value - The bpl package file to extract the model from
-ivalue, /id:value - Dictionary id
[-gvalue], [/globalvar:value] - Global variable name, default: Dic
[-mvalue], [/model:value] - Name of the model to generate the dictionary
for, default: Default
Parameters -g
and -m
are optional and if omitted the default values are used. The other parameters are required and their equivalent are described in section Generate from application.
Here is one example that reads Aurelius entity classes from package C:\MyProject\Bpl\Entities.bpl
and generates the dictionary Default
in file C:\MyProject\MyDictionary.pas
:
AureliusDictionaryGenerator.exe -i:Default -p:"C:\MyProject\Bpl\Entities" "C:\MyProject\MyDictionary.pas"
Using the dictionary
Using Aurelius dictionary is very simple. It's intended to be used in Aurelius queries and all you have to do is to use the dictionary properties and query from them. The dictionary properties holds a tree structure representing all the entity classes you have in your model, and the properties and associations for each entity class.
To use the dictionary just add the dictionary unit name (the file you generated) to your unit uses clause, and it's available via the Dic
global variable (unless you explicitly changed the variable name when generating the dictionary).
Simple properties as projections
Just reference the entity and the property and compare it to a value. The property in dictionary is a query projection, which means you can also use all methods available in projections like Sum, Contains and many others.
You can do simple comparisons like these:
Manager.Find<TCustomer>
.Where(Dic.Customer.CountryName = 'Germany')
.Where(Dic.Customer.Name.Contains('Herwig'))
And:
Manager.Find<TInvoice>
.Where(
(Dic.Invoice.IssueDate >= EncodeDate(2020, 10, 10))
and (Dic.Invoice.IssueDate < EncodeDate(2021, 2, 2)))
.OrderBy(Dic.Invoice.Code)
Associations
The dictionary also makes it very easy to query by associated objects. All you need do is just use the associated properties into a deeper level and do the comparison the same way. For example, the following query calculates the sum of totals for invoices which country is Germany. It uses the customer associated with the invoice, and then the country associated with the customer:
Manager.Find<TInvoice>
.Select(Dic.Invoice.Total.Sum)
.Where(Dic.Invoice.Customer.Country.Name = 'Brazil')
To make it easier to read and write the queries, you can also put some associations in specific variables like this:
var
Est: IEstimateDictionary;
begin
Est := Dic.Estimate;
Manager.Find<TEstimate>
.Select(TProjections.ProjectionList
.Add(Est.EstimateNo.Sum)
.Add(Est.Customer.Name.Group))
.Where(Est.IssueDate.IsNotNull)
.Where(
(Est.EstimateNo.Sum >= 6)
and (Est.EstimateNo.Sum <= 14))
.OrderBy(Est.Customer.Name)
Dictionary validation
Since dictionary must be explicitly generated from existing classes, it's possible that it might get outdated. You might add new entity classes, or even add, rename or remove properties in existing entity classes. This might lead, again, to runtime errors. For example, you might have written the expresison `Dic.Customer.Name = 'John'
but later you renamed the Name
property to CompanyName
property. You will get an error at runtime indicating that Name
property does not exist.
To avoid such problems Aurelius provides the dictionary validator. It's just a simple check you add at the beginning of your application (or at any point you want, but obviously before you execute any code that uses the dictionary). The validator will perform a full check of the dictionary and make sure its contents matches the real entity classes.
Using the dictionary is very simple and can be accomplished with a single line of code. You need to first add the Aurelius.Dictionary.Validator
unit to your uses clause:
uses Aurelius.Dictionary.Validator;
And then use Check
method of TDictionaryValidator
class. The method can optionally receive a mapping explorer, this way you can explicitly tell Aurelius the model which the dictionary should be validated for.
// Check if the dictionary is valid for the default model
TDictionaryValidator.Check(Dic);
// Check if another dictionary in a different unit is valid
// for model Accounting
TDictionaryValidator.Check(AccountingDictionary.Dic, TMappingExplorer.Get('Accounting'));
The above code will raise an EDictionaryValidationException
exception with detailed information about the problems (the exception class has an Errors
property you can inspect).
Alternatively, if you prefer a silent validation, you can create a TDictionaryValidator
instance yourself, call the Validate
method (which returns a boolean value indicating if the validation was succesful) and then inspect the Errors
property yourself:
Validator := TDictionaryValidator.Create(TMappingExplorer.Default);
if not Validator.Validate(Dic) then
begin
for ErrorMessage in Validator.Errors do
LogSomewhere(ErrorMessage);