Quantcast
Channel: : software-engineering
Viewing all articles
Browse latest Browse all 19

Entity Framework 5: Adding Support for Indexes via Data Annotations

$
0
0

I’m in the middle of developing my first serious project using Entity Framework 5.0. I’m using ‘code first’ or, more precisely, ‘UML first’. We author our entities using Visual Studio’s UML editor within our modelling project. Here’s a partial view of my domain objects:

image

We use code generation to render these classes into C#, directly into a project containing our domain model. What we get is a class that looks like this:

//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool
// Changes to this file will be lost if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System.Collections.Generic;

namespace Tigra.eStore.DomainModel
{
publicpartialclass UserAccount
{
publicvirtualstring UserName { get; set; }

publicvirtualbool IsAnonymousTrackingToken { get; set; }

publicvirtual ICollection<Order> Orders { get; set; }

publicvirtual ICollection<CartLineItem> CartLineItems { get; set; }

publicvirtual Address BillingAddress { get; set; }

publicvirtual Address DeliveryAddress { get; set; }

publicvirtual PersonName Name { get; set; }
}
}

Now there are a few things missing from this class.

  1. We want all of our entities to have an ‘Id’ field that represents the row unique ID in the database, but we don’t want to add this to our design diagrams because we don’t consider it to be part of our domain model. We will not use the Id in code, but Entity Framework needs it to correctly implement foreign keys to link our tables together; it is an implementation detail that is really specific to the workings of the database engine.
  2. The Address and PersonName classes are shown as compositions in the UML diagram, implying that the lifetime of those objects completely depends on the parent object; they cannot exist outside of the lifetime of the parent. In Entity Framework, we want those compositions to map to Complex Types.
  3. It turns out that in this particular entity, we do a lot of lookups on the UserName so it would be really handy if that had an index.

We could edit the class to add these things, but the comment at the top hopefully explains why we don’t want to do that. We want to be able to regenerate our model in response to design changes. Luckily, the UML editor lets us add the ‘IsPartial’ attribute so that the code generator makes a partial class. We can add a counterpart partial class and add our Id field there, plus any Data Annotation attributes that we need. So here’s what one of our partial classes looks like:

using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;

namespace Tigra.eStore.DomainModel
{
[MetadataType(typeof(UserAccountAnnotations))]
publicpartialclass UserAccount : IDomainEntity
{
[Key]
publicint Id { get; set; } // Unique ID
sealedclass UserAccountAnnotations
{
[Index("IX_UserName", unique: true)]
publicstring UserName { get; set; }
}

/// <summary>
/// Initializes a new instance of the <see cref="UserAccount" /> class.
/// Initializes the collection members with empty collections.
/// </summary>
public UserAccount()
{
BillingAddress = new Address();
DeliveryAddress = new Address();
Orders = new Collection<Order>();
CartLineItems = new Collection<CartLineItem>();
}
}
}
As you can see, we have added the Id property and marked it as the primary key; We’ve added a constructor to initialize the complex types (which are not nullable in EF5); and we’ve declared an index on the UserName property. Note that we’ve used an associated metadata class to do this, linked to the actual entity class via the MetadataType attribute on the entity class.
 
So far so good – none of this should be new, except for the [Index()] attribute. That is not part of the standard data annotations. We found ways to add indexes using the Fluent API, but we wanted to keep everything in our data annotations. It’s just the way we prefer things. Unfortunately EF5 doesn’t provide that feature. Stack Overflow to the rescue. I found a question where someone had posted a code fragment that implemented the IndexAttribute class. The solution had a problem though – it wasn’t aware of the associated metadata class, so it didn’t pick up my [Index()] attribute. A little bit of tinkering soon had that feature added to the code. Below is our finished solution, which defines two main things:
  1. The IndexAttribute class, that defines out new Data Annotation attribute.
  2. A class implementing IDabatabseInitializer<T> that knows how to detect the attribute and do the right thing when the database is created. This also works with Data Migrations.
 
   1:using System;
   2:using System.Collections.Generic;
   3:using System.ComponentModel.DataAnnotations;
   4:using System.ComponentModel.DataAnnotations.Schema;
   5:using System.Data.Entity;
   6:using System.Linq;
   7:using System.Reflection;
   8:  
   9:namespace Tigra.eStore.DomainModel
  10:     {
  11:     [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
  12:publicclass IndexAttribute : Attribute
  13:         {
  14:/// <summary>
  15:///   Initializes a new instance of the <see cref="IndexAttribute" /> class.
  16:///   The index data annotation indicates that the database should contain an index
  17:///   on the associated property, either with or without a uniqueness constraint.
  18:/// </summary>
  19:/// <param name="name">The index name, usually IX_{property}.</param>
  20:/// <param name="unique">
  21:///   if set to <c>true</c> indicates that the index should have a uniqueness constraint.
  22:/// </param>
  23:public IndexAttribute(string name, bool unique = false)
  24:             {
  25:             Name = name;
  26:             IsUnique = unique;
  27:             }
  28:  
  29:publicstring Name { get; private set; }
  30:  
  31:publicbool IsUnique { get; private set; }
  32:         }
  33:  
  34:/// <summary>
  35:///   Class IndexInitializer - a database initialization strategy that extends the default implementation by
  36:///   allowing index attributes to be applied as data annotations.
  37:/// </summary>
  38:/// <typeparam name="T">The DbContext derived type being initialized.</typeparam>
  39:/// <remarks>
  40:///   copied from
  41:///   http://stackoverflow.com/questions/8262590/entity-framework-code-first-fluent-api-adding-indexes-to-columns/13144786
  42:/// </remarks>
  43:publicclass IndexInitializer<T> : IDatabaseInitializer<T>
  44:where T : DbContext
  45:         {
  46:conststring CreateIndexQueryTemplate = "CREATE {unique} INDEX {indexName} ON {tableName} ({columnName});";
  47:  
  48:publicvoid InitializeDatabase(T context)
  49:             {
  50:const BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance;
  51:             var indexes = new Dictionary<IndexAttribute, List<string>>();
  52:string query = string.Empty;
  53:  
  54:foreach (PropertyInfo dataSetProperty in
  55:typeof(T).GetProperties(PublicInstance).Where(p => p.PropertyType.Name == typeof(DbSet<>).Name))
  56:                 {
  57:                 Type entityType = dataSetProperty.PropertyType.GetGenericArguments().Single();
  58:                 var tableAttributes = (TableAttribute[])entityType.GetCustomAttributes(typeof(TableAttribute), false);
  59:  
  60:                 indexes.Clear();
  61:string tableName = tableAttributes.Length != 0 ? tableAttributes[0].Name : dataSetProperty.Name;
  62:  
  63:foreach (PropertyInfo property in entityType.GetProperties(PublicInstance))
  64:                     {
  65://var indexAttributes = (IndexAttribute[])property.GetCustomAttributes(typeof(IndexAttribute), false);
  66:                     IEnumerable<IndexAttribute> indexAttributes = GetIndexAttributes(property);
  67:                     var notMappedAttributes =
  68:                         (NotMappedAttribute[])property.GetCustomAttributes(typeof(NotMappedAttribute), false);
  69:if (indexAttributes.Count() > 0 && notMappedAttributes.Length == 0)
  70:                         {
  71:                         var columnAttributes =
  72:                             (ColumnAttribute[])property.GetCustomAttributes(typeof(ColumnAttribute), false);
  73:  
  74:foreach (IndexAttribute indexAttribute in indexAttributes)
  75:                             {
  76:if (!indexes.ContainsKey(indexAttribute))
  77:                                 indexes.Add(indexAttribute, new List<string>());
  78:  
  79:if (property.PropertyType.IsValueType || property.PropertyType == typeof(string))
  80:                                 {
  81:string columnName = columnAttributes.Length != 0
  82:                                                         ? columnAttributes[0].Name : property.Name;
  83:                                 indexes[indexAttribute].Add(columnName);
  84:                                 }
  85:else
  86:                                 indexes[indexAttribute].Add(
  87:                                     property.PropertyType.Name + "_" + GetKeyName(property.PropertyType));
  88:                             }
  89:                         }
  90:                     }
  91:  
  92:foreach (IndexAttribute indexAttribute in indexes.Keys)
  93:                     {
  94:                     query +=
  95:                         CreateIndexQueryTemplate.Replace("{indexName}", indexAttribute.Name).Replace(
  96:"{tableName}", tableName).Replace(
  97:"{columnName}", string.Join(", ", indexes[indexAttribute].ToArray())).Replace(
  98:"{unique}", indexAttribute.IsUnique ? "UNIQUE" : string.Empty);
  99:                     }
 100:                 }
 101:  
 102:if (context.Database.CreateIfNotExists())
 103:                 context.Database.ExecuteSqlCommand(query);
 104:             }
 105:  
 106:/// <summary>
 107:///   Gets the index attributes on the specified property and the same property on any associated metadata type.
 108:/// </summary>
 109:/// <param name="property">The property.</param>
 110:/// <returns>IEnumerable{IndexAttribute}.</returns>
 111:         IEnumerable<IndexAttribute> GetIndexAttributes(PropertyInfo property)
 112:             {
 113:             Type entityType = property.DeclaringType;
 114:             var indexAttributes = (IndexAttribute[])property.GetCustomAttributes(typeof(IndexAttribute), false);
 115:             var metadataAttribute =
 116:                 entityType.GetCustomAttribute(typeof(MetadataTypeAttribute)) as MetadataTypeAttribute;
 117:if (metadataAttribute == null)
 118:return indexAttributes; // No metadata type
 119:  
 120:             Type associatedMetadataType = metadataAttribute.MetadataClassType;
 121:             PropertyInfo associatedProperty = associatedMetadataType.GetProperty(property.Name);
 122:if (associatedProperty == null)
 123:return indexAttributes; // No metadata on the property
 124:  
 125:             var associatedIndexAttributes =
 126:                 (IndexAttribute[])associatedProperty.GetCustomAttributes(typeof(IndexAttribute), false);
 127:return indexAttributes.Union(associatedIndexAttributes);
 128:             }
 129:  
 130:string GetKeyName(Type type)
 131:             {
 132:             PropertyInfo[] propertyInfos =
 133:                 type.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public);
 134:foreach (PropertyInfo propertyInfo in propertyInfos)
 135:                 {
 136:if (propertyInfo.GetCustomAttribute(typeof(KeyAttribute), true) != null)
 137:return propertyInfo.Name;
 138:                 }
 139:thrownew Exception("No property was found with the attribute Key");
 140:             }
 141:         }
 142:     }
Our added code starts on line 111. One further thing is necessary – the data context (the class that inherits from DbContext) needs to override a method, as follows:
   1: protected override void OnModelCreating(DbModelBuilder modelBuilder)
   2:     {
   3:Database.SetInitializer(new IndexInitializer<EntityFrameworkStoreContext>());
   4:     base.OnModelCreating(modelBuilder);
   5:     }
 
At that point, one can create data migrations and regenerate the database and everything is picked up as expected. We use Glimpse to see what’s going on within our web application and sure enough, it shows the index being created:
   1:CREATEUNIQUEINDEX IX_UserName ON UserAccounts (UserName);
Job done!

Viewing all articles
Browse latest Browse all 19

Trending Articles