After I received some feedback on my post on filtering with NHibernate, I starting thinking on how to leverage the filter capabilities of NHibernate to create a system for creating localized applications. While the previous post already considered using filters for multi-language apps, there was some criticism.
When I want to localize a pizza I had to create a new record in the database. One of the first drawbacks was the fact, that the ID of the pizza is not unique anymore, since each pizza exists for each language. Well, this is actually just a matter of “adjusting” the data model a little bit. You could just create some internal ID for each pizza as the primary key or you create a composite key based on the ID and the locale-code.
A bigger problem is the fact, that you will have to duplicate all the data, which isn’t actually changing for each pizza, such as the price.
The new model
A better data model could look like this:

Some explanation: a pizza has always a list of toppings. Pizzas as well as toppings have name, which should be localized. This name can exists in various languages for a pizza. The class LocalizableBase adds a list of strings, which contains all localized strings.
This way we don’t have to store language invariant data redundant in the database.
The usage
OK, since we have our revised model, the actual usage is straight-forward.
// create toppings
var pilze = new Entities.Topping { InternalName = "Pilze" };
pilze.AddString("de","Pilze");
pilze.AddString("en","Mushrooms");
_session.Save(pilze);
var salami = new Entities.Topping() { InternalName = "Salami" };
salami.AddString("de","Salami");
salami.AddString("en","Pepperoni");
_session.Save(salami);
var kaese = new Entities.Topping() {InternalName = "Käse"};
kaese.AddString("de","Käse");
kaese.AddString("en","Cheese");
// create pizza
var funghi = new Entities.Pizza()
{
Price = 10,Costs = 5,Toppings = {pilze}
};
funghi.AddString("de", "Funghi");
funghi.AddString("en", "Mushroom Heaven");
_session.Save(funghi);
var peperoni = new Entities.Pizza()
{
Price = 11,
Costs = 6.5,
Toppings = { salami,kaese }
};
peperoni.AddString("de", "Salami++");
peperoni.AddString("en", "Peperoni++");
_session.Save(peperoni);
At first we create some toppings. Each topping has an internal, language-invariant name. Using AddString (from LocalizeableBase) we can add strings for a certain language. The same applies for pizza.
In order to get a pizza, we just create a simple query.
[Test]
public void CanGetLocalizedPizza()
{
_session.EnableFilter("PizzaLocaleFilter").SetParameter("myLocale", "en");
var pizzaList = _session.CreateCriteria<Pizza.Entities.Pizza>()
.List<Entities.Pizza>();
Assert.That(pizzaList,Has.Count.EqualTo(2),"number of pizza");
Assert.That(pizzaList[0].Name, Is.EqualTo("Mushroom Heaven"));
Assert.That(pizzaList[1].Name, Is.EqualTo("Peperoni++"));
Assert.That(pizzaList[0].Toppings, Has.Count.EqualTo(1),"number of toppings");
Assert.That(pizzaList[0].Toppings[0].Name,Is.EqualTo("Mushrooms"),"name of topping");
}
As the above sample shows, we just have to define our filter once (!). In the actual query we don’t have to deal with the filtering anymore, this is all being handled by NHibernate.
The implementation
Finally I would like who this is all accomplished. At first we store all localizable strings in a separate table. This table contains strings for toppings as well as for pizzas. This table is being mapped to a class, which is used by LocalizableBase.
<class name="LocalicedString" table="LocalicedStrings">
<id name="Id">
<generator class="identity"/>
</id>
<property name="ObjectType"/>
<property name="Locale"/>
<property name="Text"/>
</class>
The property ObjectType stores to which entity this string actually belongs to. The value for this property is being determined in the method AddString in LocalizableBase. This way the pizza entity doesn’t have to take care about setting the property to the correct value. Besides the object-type, we also store the id of the actual object. Since this id is not directly being used, we don’t need to map it to the database (at least not at this point; we’ll see that later).
/// <summary>
/// base class to supply localizablity to entities
/// </summary>
public abstract class LocalizableBase
{
/// <summary>
/// Initializes a new instance of the <see cref="LocalizableBase"/> class.
/// </summary>
public LocalizableBase()
{
Strings = new List<LocalicedString>();
}
/// <summary>
/// a list of all localized strings
/// </summary>
public virtual IList<LocalicedStrings> Strings { get; private set; }
/// <summary>
/// adds a new string to the list of <see cref="Strings"/>
/// </summary>
/// <remarks>along with the text and the locale, the object to which
/// this text is associated is also being stored</remarks>
/// <param name="locale">the locale of the text</param>
/// <param name="text">the text to store</param>
public virtual void AddString(string locale, string text)
{
Strings.Add(new LocalicedString
{
Locale = locale,
Text = text,
ObjectType = GetType().ToString()
});
}
}
OK, now we need the entities for pizza and topping as well. This is actually nothing earth-rocking. The property Name returns always the first localized string that was found for the entity. If no localized string could be found, we just return an empty string.
/// <summary>
/// Business Entity representing a pizza
/// </summary>
public class Pizza : LocalizableBase
{
/// <summary>
/// Initializes a new instance of the <see cref="Pizza_old"/> class
/// </summary>
public Pizza()
{
Toppings = new List<Topping>();
}
/// <summary>
/// gets the internal, unique id of this pizza
/// </summary>
/// <value>The id.</value>
public virtual int Id { get; private set; }
/// <summary>
/// gets the localized name of the pizza
/// </summary>
public virtual string Name
{
get { return Strings.Count >= 1 ? Strings[0].Text : string.Empty; }
}
/// <summary>
/// gets or sets the toppings, which are being used by the pizza
/// </summary>
public virtual IList<Topping> Toppings { get; private set; }
/// <summary>
/// selling price for this pizza
/// </summary>
public virtual double Price { get; set; }
/// <summary>
/// gets or sets the costs to produce this pizza
/// </summary>
public virtual double Costs { get; set; }
}
/// <summary>
/// Business Entity representing a pizza-topping
/// </summary>
public class Topping : LocalizableBase
{
/// <summary>
/// gets the internal, unique id of this topping
/// </summary>
/// <value>The id.</value>
public virtual int Id { get; private set; }
/// <summary>
/// gets or sets internal name of this topping - this is language invariant!
/// </summary>
public virtual string InternalName { get; set; }
/// <summary>
/// gets the localized name of the topping
/// </summary>
/// <value>The name.</value>
public virtual string Name
{
get { return Strings.Count >= 1 ? Strings[0].Text : string.Empty; }
}
/// <summary>
/// supplier, from where this topping is being bought
/// </summary>
public virtual string Supplier { get; set; }
}
The clue to this solution is again the usage of NHibernate filters, in order to get the strings for the desired language. For that purpose we filter for the desired locale. In this mapping we also find the ItemId property, which was kinda missing in the LocalizedStrings mapping. The ItemId refers to the Id of the item, to which this string actually belongs. In combination with the ObjectType this should be unique.
<class name="Pizza" table="Pizzas">
<id name="Id">
<generator class="identity" />
</id>
<property name="Price"/>
<property name="Costs"/>
<bag name="Strings" table="LocalicedStrings"
cascade="all" fetch="join"
where="ObjectType='Pizza.Entities.Pizza'">
<key column="ItemId" />
<one-to-many class="LocalicedString"/>
<filter name="PizzaLocaleFilter" condition=":myLocale = Locale"/>
</bag>
<bag name="Toppings" table="PizzaToppings" cascade="all">
<key column="PizzaId" />
<many-to-many column="ToppingId" class="Topping" />
</bag>
</class>
Besides the filtering to a certain language, we also only want to get strings for the current ObjectType. This is done, by specifying a where-clause in the bag-definition. This way we only get strings for pizza.
While this removes the actual need to deal with the ObjectType in the query, we have to specify it in the mapping – well nothing’s perfect. If you already have an application with a lot of existing queries, this might be a better approach.
Just for the hack of it: this is the SQL being send to the database for the little UnitTest query:
SELECT this_.Id as Id4_1_, this_.Price as Price4_1_, this_.Costs as Costs4_1_, strings2_.ItemId as ItemId3_, strings2_.Id as Id3_, strings2_.Id as Id7_0_, strings2_.ObjectType as ObjectType7_0_, strings2_.Locale as Locale7_0_, strings2_.Text as Text7_0_
FROM Pizzas this_ left outer join
LocalicedStrings strings2_ on
this_.Id=strings2_.ItemId and
@p0 = strings2_.Locale and
(strings2_.ObjectType='Pizza.Entities.Pizza')
;@p0 = 'en'
SELECT toppings0_.PizzaId as PizzaId1_, toppings0_.ToppingId as ToppingId1_, topping1_.Id as Id6_0_, topping1_.InternalName as Internal2_6_0_
FROM PizzaToppings toppings0_ left outer join
Toppings topping1_ on
toppings0_.ToppingId=topping1_.Id
WHERE toppings0_.PizzaId=@p0
;@p0 = 1
SELECT strings0_.ItemId as ItemId1_, strings0_.Id as Id1_, strings0_.Id as Id7_0_, strings0_.ObjectType as ObjectType7_0_, strings0_.Locale as Locale7_0_, strings0_.Text as Text7_0_
FROM LocalicedStrings strings0_
WHERE @p0 = strings0_.Locale and
(strings0_.ObjectType='Pizza.Entities.Topping') and
strings0_.ItemId=@p1
;@p0 = 'en', @p1 = 1