RavenDB: results transformers
RavenDB has an amazing feature called result transformer that is an hook in the server side document processing pipeline that let us intercept documents before they leave the server and allows us to change the shape of these documents.
Look at the following diagram that summarize what happens at query time:
We can define results transformers that have the opportunity to intercept, transform or simply enrich our data.
First of all: what is a result transformer?
We can define result transformers by inheriting from AbstractTransformerCreationTask class, or from the much more convenient generic version:
public class MyDocument_Transformer : AbstractTransformerCreationTask<MyDocument> { public MyDocument_Transformer() { this.TransformResults = results => from result in results select new { Id = result.Id, FullName = result.FirstName + " " + result.LastName, }; } }
In the above sample we are taking in input a list of documents, of type MyDocument, and we are emitting a new type of document, we have the opportunity to manipulate the shape of the document before it leaves the server.
We can do much more such as load “referenced” documents in order to build complex results on the fly.
We can use a transformer both in queries and in load operations as in the following sample:
using( var session = store.OpenSession() ) { var doc = session.Load<MyDocument_Transformer, MyNewShape>( "your/document/id" ); }
The API expects that the MyNewShape class matches the shape of the returned json object.
One really interesting usage of result transformers is, for example in a CQRS based application, to use them to customize “views” on the fly to satisfy things like globalization requirement of the client. Imagine a scenario where you have documents such as the following:
{ "CreateOn": /* a date that is stored as DateTimeOffset */, "FirstName": "Mario", "LastName": "Rossi" }
Since the transformer can execute arbitrary .net code, quite everything that can be executed inside a linq query, we can imagine to write a transformer such as the following:
from result in results let tzi = TimeZoneInfo.FindSystemTimeZoneById( "time-zone-id" ) let clientCreatedOn = TimeZoneInfo.ConvertTime( result.CreatedOn, tzi ) select new { ClientCreatedOn = ( ( DateTimeOffset )clientCreatedOn ).DateTime, FullName = result.FirstName + " " + result.LastName }
We are asking the server to pre-convert for us the returned document(s) doing some computation, since the document is explicitly thought for the UI we can expect to have it back already formatted, localized and globalized.
One more thing we can do is to pass parameters to the transformer, so we can decide at query time which we’ll be the time zone to convert to:
from result in results let tzi = TimeZoneInfo.FindSystemTimeZoneById( this.Query( "tzi" ).Value<String>() ) let clientCreatedOn = TimeZoneInfo.ConvertTime( result.CreatedOn, tzi ) select new { ClientCreatedOn = ( ( DateTimeOffset )clientCreatedOn ).DateTime, FullName = result.FirstName + " " + result.LastName }
And pass parameters “down” at query time using the following syntax:
var query = session.Query<MyDocument, MyDocument_Index>()
.TransformWith<MyDocument_ClientAdapter, MyClientDocument>()
.AddQueryInput( "tzi", user.TimeZoneId );
Where the MyDocument_ClientAdapter is the C# class that represents the transformer and MyClientDocument is the class that matches the shape projected by the transformer itself and the TimeZoneId property of the “user” class is where we are currently storing user settings.
Thanks to Matt Johnson for some suggestions regarding the TimeZoneInfo and DateTimeOffset usage.
.m