El tema de las convenciones personalizadas para el modelo de Code First sin duda ha sido una de las cosas que más echamos de menos en EF, de hecho, durante alguna de las betas estas posibilidades estaban incluidas como en su día vimos en algún post. De entre las distintas novedades de esta versión preliminar de EF 6 podemos destacar la vuelta al ruedo de estas features, aunque ahora con una implementación, en mi opinión, mucho más acertada. A lo largo de la siguiente entrada intentaremos darle un vistazo a estas nuevas posibilidades, si lo desea, también puede leer el walkthough que el equipo de ADO.NET tiene sobre este tema.
Introducción
Todas las convenciones de EF, están basadas en la siguiente interface, IConfigurationConvention:
|
[ContractClass(<span class="kwrd">typeof</span>(IConfigurationConventionContracts<,>))] <span class="kwrd">public</span> <span class="kwrd">interface</span> IConfigurationConvention<TMemberInfo, TConfiguration> : IConvention <span class="kwrd">where</span> TMemberInfo : MemberInfo <span class="kwrd">where</span> TConfiguration : ConfigurationBase { <span class="kwrd">void</span> Apply(TMemberInfo memberInfo, Func<TConfiguration> configuration); } |
Esta interface, como observará tiene dos parámetros genéricos TMemberInfo y TConfiguration, que nos permite establecer, por un lado con TMemberInfo si la configuración es para un tipo o para una propiedad y en ultimo lugar a que elemento en concreto se aplica, para el caso de las propiedades ConfigurationBase tiene un jerarquía similar a esta:
ConfigurationBase
-> Property Configuration
->NavigationPropertyConfiguration
->PrimitivePropertyConfiguraiton
-> BinaryPropertyConfiguraiton
-> DateTimePropertyConfiguration
-> ….
Dicho esto, para crear por ejemplo una convención que afecte a propiedades de tipo DateTime tendríamos que implementar una clase como la siguiente:
|
<span class="kwrd">public</span> <span class="kwrd">class</span> DateTimeIsMappedToSqlServerDateTime2 :IConfigurationConvention<PropertyInfo,DateTimePropertyConfiguration> { <span class="kwrd">public</span> <span class="kwrd">void</span> Apply(PropertyInfo memberInfo, Func<DateTimePropertyConfiguration> configuration) { } } |
Por supuesto, dentro del método Apply tendríamos que establecer que queremos en la convención, si se fija, el delegado configuration nos permite obtener la configuración actual de esa propiedad, con lo cual, podríamos ver/modificar la misma. En este caso optaremos por hacer lo mismo que el el walkthoug, puesto que me parece muy ‘real’ el hecho de adaptar el tipo de datos en Sql Server a datetime2 para estas propiedades.
|
<span class="kwrd">public</span> <span class="kwrd">class</span> DateTimeIsMappedToSqlServerDateTime2 :IConfigurationConvention<PropertyInfo,DateTimePropertyConfiguration> { <span class="kwrd">public</span> <span class="kwrd">void</span> Apply(PropertyInfo memberInfo, Func<DateTimePropertyConfiguration> configuration) { <span class="rem">//get the current datetimepropertyconfiguration</span> var dateTimeConfiguration = configuration(); |
|
<span class="kwrd">if</span> (dateTimeConfiguration.ColumnType == <span class="kwrd">null</span>) dateTimeConfiguration.ColumnType = <span class="str">"datetime2"</span>; } } |
Por supuesto, usted puede revisar el miembro memberInfo para decidir a que propiedades se aplica, por ejemplo, le podría interesar solo aplicar esta convención a las propiedades cuyo nombre por ejemplo acaben con un postfijo determinado, aunque esto, en mi humilde opinión no es algo que me guste para nada. Las convenciones a nivel de entidad, son prácticamente iguales, con la salvedad de que lógicamente configuran aspectos de la entidad y no de las propiedades, por ejemplo, la convención de que la propiedad Id sea la clave primaria de una entidad podría haberse escrito así.
|
<span class="kwrd">public</span> <span class="kwrd">class</span> IdPropertyIsPrimaryKeyConvention : IConfigurationConvention<Type, EntityTypeConfiguration> { <span class="kwrd">public</span> <span class="kwrd">void</span> Apply(Type memberInfo, Func<EntityTypeConfiguration> configuration) { var entityTypeConfiguration = configuration(); var idProperty = memberInfo.GetProperty(<span class="str">"Id"</span>, BindingFlags.Public | BindingFlags.Instance); <span class="kwrd">if</span> (idProperty != <span class="kwrd">null</span>) entityTypeConfiguration.Key(idProperty); } } |
Bien, una vez que tenemos definidas nuestras convenciones, para agregarla usaremos la propiedad Conventions, que ya es conocida de nuestros DbContext, con la diferencia que ahora dispone de métodos para incluir convenciones y no solamente para eliminarlas. De hecho, incluso me permite establecer el orden de ejecución de las mismas.
|
modelBuilder.Conventions.Add<IdPropertyIsPrimaryKeyConvention>(); |
Con el fin de simplificarnos la vida, el equipo de ADO.NET nos ha creado una pequeña clase LightweigtConvention con la cual podremos establecer convenciones sin necesidad de escribir una nueva clase para las mismas. Esta clase no es más que una implementación de IConventionConfiguration para tipos y propiedades que hace uso de una clase, EntityConventionConfiguration para agregar convenciones mediante un API fluent.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
|
<span class="kwrd">public</span> <span class="kwrd">class</span> EntityConventionConfiguration { <span class="kwrd">private</span> <span class="kwrd">readonly</span> List<Func<Type, <span class="kwrd">bool</span>>> _predicates = <span class="kwrd">new</span> List<Func<Type, <span class="kwrd">bool</span>>>(); <span class="kwrd">private</span> Action<LightweightEntityConfiguration> _configurationAction; <span class="kwrd">private</span> PropertyConventionConfiguration _propertyConfiguration; <span class="kwrd">internal</span> EntityConventionConfiguration() { } <span class="kwrd">internal</span> Action<LightweightEntityConfiguration> ConfigurationAction { get { <span class="kwrd">return</span> _configurationAction; } } <span class="kwrd">internal</span> List<Func<Type, <span class="kwrd">bool</span>>> Predicates { get { <span class="kwrd">return</span> _predicates; } } <span class="kwrd">internal</span> PropertyConventionConfiguration PropertyConfiguration { get { <span class="kwrd">return</span> _propertyConfiguration; } } <span class="rem">/// <summary></span> <span class="rem">/// Filters the entity types that this convention applies to based on a</span> <span class="rem">/// predicate.</span> <span class="rem">/// </summary></span> <span class="rem">/// <param name="predicate">A function to test each entity type for a condition.</param></span> <span class="rem">/// <returns></span> <span class="rem">/// The same EntityConventionConfiguration instance so that multiple calls can</span> <span class="rem">/// be chained.</span> <span class="rem">/// </returns></span> <span class="kwrd">public</span> EntityConventionConfiguration Where(Func<Type, <span class="kwrd">bool</span>> predicate) { Contract.Requires(predicate != <span class="kwrd">null</span>); _predicates.Add(predicate); <span class="kwrd">return</span> <span class="kwrd">this</span>; } <span class="rem">/// <summary></span> <span class="rem">/// Allows configuration of the entity types that this convention applies to.</span> <span class="rem">/// </summary></span> <span class="rem">/// <param name="entityConfigurationAction"></span> <span class="rem">/// An action that performs configuration against a <see cref="LightweightEntityConfiguration" />.</span> <span class="rem">/// </param></span> <span class="kwrd">public</span> <span class="kwrd">void</span> Configure(Action<LightweightEntityConfiguration> entityConfigurationAction) { Contract.Requires(entityConfigurationAction != <span class="kwrd">null</span>); _configurationAction = entityConfigurationAction; } <span class="rem">/// <summary></span> <span class="rem">/// Allows further configuration of the convention based on the properties of</span> <span class="rem">/// the entity types that this convention applies to.</span> <span class="rem">/// </summary></span> <span class="rem">/// <returns></span> <span class="rem">/// A configuration object that can be used to configure this convention based</span> <span class="rem">/// on properties.</span> <span class="rem">/// </returns></span> <span class="kwrd">public</span> PropertyConventionConfiguration Properties() { var propertyConfiguration = <span class="kwrd">new</span> PropertyConventionConfiguration(); _propertyConfiguration = propertyConfiguration; <span class="kwrd">return</span> propertyConfiguration; } } |
Utilizando esta clase, por lo tanto, podríamos crear una convención tal y como sigue:
|
<span class="kwrd">public</span> <span class="kwrd">class</span> BlogUnitOfWork :DbContext { <span class="kwrd">public</span> IDbSet<Blog> Blogs { get; set; } <span class="kwrd">protected</span> <span class="kwrd">override</span> <span class="kwrd">void</span> OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Add(entityConventionConfiguration => { entityConventionConfiguration.Properties() .Where(p => p.Name == <span class="str">"Id"</span>) .Configure(lpc => lpc.IsKey()); }); } } |
Puede observar como el trabajo a realizar depende de si configura propiedad o entidad y este está representado por las clases LightweighPropertyConfiguration o LightweighEntityConfiguration.
Bien, pero ¿y si quiero hacer las convenciones con atributos? Pues sencillo, en vez de implementar IConfigurationConvention trabajaremos con AttributeConfigurationConvention, el cual, nos permite indicar que atributo es el que será el atributo marcador, es decir aquel atributo que EF revisará para saber que tiene que aplicar una convención. Para verlo, utilizaremos también el del walkthoug, puesto que al igual que con el DateTime es también bastante habitual. En este ejemplo el objetivo es poder disponer de un atributo, por ejemplo NonUnicodeAttribute que nos permita decorar a las propiedades como no unicode. Pues bien, este atributo será un atributo marcador, es decir, no necesita código de trabajo con EF, por ejemplo igual que así:
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = <span class="kwrd">false</span>)] <span class="kwrd">public</span> <span class="kwrd">class</span> NonUnicodeAttribute : Attribute { } |
Bien, ahora que tenemos creado el atributo marcador, solamente tenemos que aplicarle la convención ,tal cual podríamos hacer antes con nuestras PrimitivePropertyConfiguration…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
<span class="kwrd">class</span> NonUnicodeAttributeConvention : AttributeConfigurationConvention<PropertyInfo, StringPropertyConfiguration, NonUnicodeAttribute> { <span class="kwrd">public</span> <span class="kwrd">override</span> <span class="kwrd">void</span> Apply( PropertyInfo propertyInfo, StringPropertyConfiguration configuration, NonUnicodeAttribute attribute) { <span class="kwrd">if</span> (configuration.IsUnicode == <span class="kwrd">null</span>) { configuration.IsUnicode = <span class="kwrd">false</span>; } } } |
Sencillo ¿verdad?…Bueno, espero poder publicar alguna entrada más sobre EF 6, de hecho hay un par de novedades que me parecen super interesantes.. y me gustaría también sacar algo de tiempo para colaborar un poco con el código, a ver si lo logro…
Saludos
Unai