I18N with EF Code First – Part III

In the previous post I presented one approach to entities internationalization that doesn’t require any configuration on the ModelBuilder; however, the types definition was a bit messy. A tidier approach is to have the entities and corresponding translations defined as follows:

    public class SomeOtherEntity: TranslatableEntity<SomeOtherEntityTranslation>
    {
        public int Id { get; set; }
        public string NonLocalizableProp { get; set; }
    }

    public class SomeOtherEntityTranslation : EntityTranslation
    {
        [Required]
        public string LocalizableProp1 { get; set; }
        [Required]
        public string LocalizableProp2 { get; set; }
    }

This is a more concise approach and you just need to focus on defining the needed properties. The base types are defined as follows:

    public class EntityTranslation
    {
        [Key]
        public long Id { get; set; }

        [Required]
        public virtual Language Language { get; set; }
    }
    public class TranslatableEntity<TEntityTranslation>
        where TEntityTranslation : EntityTranslation
    {
        [Required]
        public virtual ICollection<TEntityTranslation> Translations { get; set; }
    }

The drawbacks are that we lost the out-of-the-box cascade on delete for the translations and the assurance of a single translation per entity/language. This is the part where the additional configuration kicks in. I defined two additional extension method for the DbContext class, one that should be invoked from the OnModelCreating method on your DbContext-derived class; and another that should be invoked by a database initializer after creating the database . The final goal is to have the cascade on delete behavior and an additional unique constraint to ensure the single translation per language. The two extensions methods are used as follows:

    public class SampleDbContext: DbContext
    {
        public virtual IDbSet<Language> Languages { get; set; }
        public virtual IDbSet<SomeOtherEntity> SomeEntities { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            this.ConfigureTranslatableEntities(modelBuilder);
        }
    }
    class SampleDbContextInitializer : DropCreateDatabaseAlways<SampleDbContext>
    {
        protected override void Seed(SampleDbContext context)
        {
            context.AddSingleTranslationPerLanguageConstraints();
            // ...
        }
    }

The ConfigureTranslatableEntities method searches for all the IDbSet properties on the context and extracts the ones that correspond to translatable entities. To each of them, the following configuration is executed:

        private static void ConfigureEntity<T, TTrans>(DbModelBuilder modelBuilder)
            where T : TranslatableEntity<TTrans>
            where TTrans : EntityTranslation
        {
            // Ensure that the key from TTrans to T has a known name
            modelBuilder.Entity<T>()
                .HasMany(e => e.Translations)
                .WithRequired()
                .Map(c => c.MapKey("TranslatedEntityId"))
                .WillCascadeOnDelete();

            // Ensure that the key from TTrans to Language has a known name
            // Ensure that the TTrans table has a known name (this could check if TableAttribute was present)
            modelBuilder.Entity<TTrans>()
                .Map(c => c.ToTable(typeof(TTrans).Name))
                .HasRequired(t => t.Language)
                .WithMany()
                .Map(c => c.MapKey("LanguageId"));
        }

The relevant actions undertaken by this method are:

  • Establish the parent/child relationship (with cascade on delete) between an entity and its translations
  • Ensure that the foreign key column from a translation table to the corresponding entity table has a known name
  • Ensure that the foreign key column from a translation table to the language table has a known name (this and the previous names are used later to create the unique restriction)
  • Ensure that the translation tables have a known name (the .NET type’s name is used) so that we know the name of table on which the unique constraint should be placed.
On the other hand, the AddSingleTranslationPerLanguageConstraints method is responsible for defining the unique constraint on the translation tables. The unique constraint is placed over the foreign keys to the translated entity table and the languages table (single entity translation per language). The method searches for translatable entities over the context – just like the previous method – and executes an SQL command for each of them to create the unique constraint:
        public static void AddSingleTranslationPerLanguageConstraints(this DbContext ctx)
        {
            ctx.Database.ExecuteSqlCommand("alter table Language add unique (code)");

            var translatableEntities = GetTranslatableEntities(ctx);
            foreach (var entity in translatableEntities)
            {
                ctx.Database.ExecuteSqlCommand(String.Format("alter table {0} add unique (TranslatedEntityId, LanguageId)", entity.TranslationType.Name));
            }
        }
And that’s it! I’m not totally convinced by any of the solution on the two posts, but I tend to like this last one better. As a final note, I tried to use the MetadataWorkspace API, which allows one to analyse the model’s metadata. The objective was to get the final table and column names on the storage space so that I didn’t have to force them on configuration, but it seems that the names weren’t really the final ones (for instance, the table names weren’t pluralized when leaving the pluralizing convention in place). If someone can get this to work, please tell me!
The source code for both posts can be downloaded here. Hope this helps!
Advertisements

8 thoughts on “I18N with EF Code First – Part III

    • Well pointed. Quick thinking: A possible approach might be to use an ITranslatableEntity interface and change the setup code to look for the interface rather than the current base class. In this case, each “most-derived” entity type would implement the interface for its specific translation.

  1. I have now implemented the solution from your second post. It works quite well! Because we access the entities with a WCF Data Service I added appropriate attribution so that the translations can participate in the data contracts. Your Language and TranslationBase classes are otherwise unchanged.

    Here’s the model initialization code:

    modelBuilder.Entity().HasMany(b => b.Translations);
    modelBuilder.Entity().HasRequired(b => b.Language);
    modelBuilder.Entity().HasRequired(b => b.Entity);
    modelBuilder.Entity().Map(m =>
    {
    m.MapInheritedProperties();
    m.ToTable(“MyEntityTranslations”);
    });

    Note that in my implementation TranslationBase is not, itself, an entity. It serves only as the base class for the translation entities. This simplifies the model: you don’t have deal with the complexity & performance problems of inheritence.

    Thanks for the inspiration!

      • Would mind showing the code if we have to go with a translation base as not an entity. I really like you solution, I want to implement it but I want to make sure I won’t have issue complexity & performance problems of inheritance.
        Thanks

  2. Hi rabah. The code that instructs EF to not generate a table for the base class is above (on Rob’s comment). Adjusting it to my post you’d change the translation entity configurations on the ConfigureEntity helper method as follows:

    modelBuilder.Entity()
    .Map(c =>
    {
    c.MapInheritedProperties();
    c.ToTable(typeof(TTrans).Name);
    })
    .HasRequired(t => t.Language)
    .WithMany()
    .Map(c => c.MapKey(“LanguageId”));

    Since we’re not using polymorfic associations, we’re good (each entity type as a collection of specific translations).

    More info on hierarchy configurations: http://weblogs.asp.net/manavi/archive/2011/01/03/inheritance-mapping-strategies-with-entity-framework-code-first-ctp5-part-3-table-per-concrete-type-tpc-and-choosing-strategy-guidelines.aspx

    • Hi Luis,

      Thanks Luis for the quick and detailed response. I have started reading the linked articles you’ve provided. Good Job you did here and good continuation.

      Rabah

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s