In the previous post of this series I introduced some possible approaches to I18N using EF code first, and ended up with a translation-table-per-entity approach. In this post I’ll cover the first of the two solutions I found to implement it.
Since we’re translating some entity, our entity types need a collection of translations, which will contain a translation instance for each language/culture:
public class SomeEntity
{
public int Id { get; set; }
public string NonLocalizableProp { get; set; }
[Required]
public virtual ICollection<SomeEntityTranslation> Translations { get; set; }
}
That’s pretty straightforward. What I’m really concerned about is the definition of the translation entity. It would be nice if we got two features:
- Guarantee that each entity contains at most one translation for each language/culture;
- Cascade delete, to automatically remove all the translations of an entity being deleted.
The first can be achieved by using some kind of weak entities (aka parent/children) relationship between the translations and the entity being translated. To that end, the key of the translation is composed by the id (key) of the entity being translated plus the language’s id (key). The second feature is accomplished by defining a one-to-many relationship with a required multiplicity on the one edge (“every translation must be associated to a translated entity“). When processing this type of associations, EF’s default behavior is to define on cascade delete on the translation’s foreign key to the translated entity table.
This gets clearer with some code! Following the principles above, we can define a generic base class for our translations:
- TEntityKey is the type of the key of the entity being translated (usually some int or long id);
- TEntity is the type of the entity being translated.
public class EntityTranslationBase<TEntityKey, TEntity>
{
[Key, Column(Order = 0), ForeignKey("Entity")]
public TEntityKey EntityKey { get; set; }
[Key, Column(Order = 1), ForeignKey("Language")]
public int LanguageId { get; set; }
[Required]
public virtual TEntity Entity { get; set; }
[Required]
public virtual Language Language { get; set; }
}
The Key and Column attributes on the EntityKey and LanguageId properties are used to define the composite key. But we still need to tell EF that those should be foreign keys to the entity being translated and to the language, respectively. To that end, two additional properties (Entity and Language) are defined, which tell EF that the translation has associations with those two entities. Furthermore, we use the ForeignKey attribute to identify the columns that should be used as foreign keys on those associations. This way we have a composite key and appropriate foreign keys. The last step is to mark the Entity property as Required, to cause the default on cascade delete.
Going back to our sample some entity, the translation would be:
public class SomeEntityTranslation : EntityTranslationBase<int, SomeEntity>
{
[Required]
public string LocalizableProp1 { get; set; }
[Required]
public string LocalizableProp2 { get; set; }
}
And that’s it! No additional configuration needed: all those properties and generic arguments provide EF with the needed information to establish the associations. In the next post I’ll present an alternative, which is cleaner on the types definition but will required some code when configuring the model.