LINQ translator extension
As I promised earlier, we were going to make LINQ translator extendable and finally we’ve made this. The extension mechanism is called “Custom LINQ query preprocessors” and is already included into DataObjects.Net 4.1 code base.
Custom LINQ preprocessors must implement public interface IQueryPreProcessor which is defined in Xtensive.Storage assembly. Here it is:
public interface IQueryPreProcessor { Expression Apply(Expression query); }
As you might see, the contract is quite simple and straightforward: your preprocessor receives the whole query, modifies it in the way you need, and returns the modified one. All preprocessors are called before the query is processed by internal LINQ translator, so it is the right time and place to apply necessary modifications.
Connecting preprocessors to translator (IoC)
After you have written you preprocessors, it is time to plug-in them to DataObjects.Net. This is done with the help of Inversion of Control concept. In order to follow it, you need to take the following steps:
1. Add reference to Microsoft.Practices.ServiceLocation.dll assembly. It is shipped with DataObjects.Net 4 and can be found in %DataObjects.Net Directory%\Lib\CommonServiceLocator directory.
2. Configure IoC container through application configuration file.
Add this line to configSections part:
<section name="Services" type="Xtensive.Core.IoC.Configuration.ConfigurationSection, Xtensive.Core"/>
Add the corresponding configuration section:
<Services> <containers> <container name="domain"> <types> <type type="Xtensive.Storage.IQueryPreProcessor, Xtensive.Storage" mapTo="Xtensive.Storage.Samples.Localization.QueryPreProcessor, Xtensive.Storage.Samples.Localization" singleton="true" /> </types> </container> </containers> </Services>
Note the usage of named service container (“domain”), the IQueryPreProcessor type as an interface of a service and how it is mapped to the concrete implementation.
3. The last step is to configure Domain object & the above-mentioned service container.
// Building domain domain = Domain.Build(DomainConfiguration.Load("Default")); // Configuring domain-level services var configurationSection = (ConfigurationSection)ConfigurationManager.GetSection("Services"); var container = new ServiceContainer(); container.Configure(configurationSection.Containers["domain"]); domain.Services.SetLocatorProvider(() => new ServiceLocatorAdapter(container));
LINQ preprocessor in action
Having these actions done, we get the capability of using non-persistent localizable properties (Domain model can be found here) in LINQ queries:
using (var ts = Transaction.Open()) { Console.WriteLine("Implicit join through preprocessor"); var pages = from p in Storage.Query.All<Page>() where p.Title=="Welcome!" select p; Console.WriteLine(pages.ToList().Count); ts.Complete(); }
Pay attention that neither PageLocalization type nor its members participate in the query, original p.Title expression in Where clause is used instead. As we know, Page.Title is not a persistent property and regular LINQ translator doesn’t know how to translate this expression. But having the initial query preprocessed with Xtensive.Storage.Samples.Localization.QueryPreProcessor makes such kind of expressions possible to use. The only thing the preprocessor makes is the replacement of p.Title expression to something like this:
p.Localizations.Where(localization => localization.CultureName==LocalizationContext.Current.CultureName) .Select(localization => (string)localization[“Title”]) .FirstOrDefault();
That’s it.
The source code is available in our public repository in Xtensive.Storage.Samples.Localization folder.
Happy preprocessing! =)