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