I18N with EF Code First – Part II

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.
Advertisements

Internationalization with Entity Framework Code First – Part I

Recently, I needed to include I18N contents on a web application that uses EF Code First. When deciding the data model to use I came across this post, which describes 4 possible approaches. We needed the model to easily support adding new languages, so the first option (having one column for each translation of a given property/column) was no good. The first attempt was to use a single common translation table. The entities would be something like this:

    class Entity
    {
        // ...
        public LocalizableString LocalizableProp { get; set; }
    }

    class LocalizableString
    {
        public long Id { get; set; }
        [Required]
        public virtual ICollection<LocalizableStringEntry> Entries { get; set; }
    }

    class Language
    {
        public int Id { get; set; }
        [StringLength(8)]
        public string Code { get; set; } // Unique
        public string Name { get; set; }
    }

    class LocalizableStringEntry
    {
        public long Id { get; set; }
        [Required]
        public string Value { get; set; }
        [Required]
        public virtual Language Language { get; set; }
    }

However, we dropped this approach for 3 main reasons: it’s not easy (tidy) to ensure that all the properties are translated for a given language; multiple delete cascade paths are not supported by SQL Server (which would be useful to have all the translations deleted automatically); and it’s not very straightforward to select a complete translation for a specific entity. In addition, it isn’t very natural to have translations as top-level entities; even if you have identical properties on different entities you won’t share the LocalizableString instances. Bottom line: we ended up switching to a translation-table-per-entity approach (the last option on the previously mentioned post).

The goal is to have something like:

    public class SomeEntity
    {
        // ...
        public string NonLocalizableProp { get; set; }
        [Required]
        public virtual ICollection<SomeEntityTranslation> Translations { get; set; }
    }

    public class SomeEntityTranslation
    {
        [Required]
        public virtual Language Language { get; set; }
        [Required]
        public string LocalizableProp1 { get; set; }
        [Required]
        public string LocalizableProp2 { get; set; }
    }

This is easy to query, keeps column names and makes it possible to easily validate the set of required translated properties. Since we’ll be needing translations on multiple entities it is appropriate to define some strategy that eases the implementation of a translatable entity. I came up with two different possibilities: one that needs fewer model configurations but is a bit ugly; another that is tidier but requires some explicit model configurations. I’ll present the two approaches on the next posts of this series.