EF 6 Alpha: Custom Conventions

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(typeof(IConfigurationConventionContracts<,>))]
    public interface IConfigurationConvention<TMemberInfo, TConfiguration> : IConvention
        where TMemberInfo : MemberInfo
        where TConfiguration : ConfigurationBase
    {
        void 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:

     public class DateTimeIsMappedToSqlServerDateTime2
        :IConfigurationConvention<PropertyInfo,DateTimePropertyConfiguration>
    {
        public void 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.

 

    public class DateTimeIsMappedToSqlServerDateTime2
        :IConfigurationConvention<PropertyInfo,DateTimePropertyConfiguration>
    {
        public void Apply(PropertyInfo memberInfo, Func<DateTimePropertyConfiguration> configuration)
        {
            //get the current datetimepropertyconfiguration
            var dateTimeConfiguration = configuration();
            if (dateTimeConfiguration.ColumnType == null)
                dateTimeConfiguration.ColumnType = "datetime2";
        }
    }

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

 

    public class IdPropertyIsPrimaryKeyConvention
       : IConfigurationConvention<Type, EntityTypeConfiguration>
    {

        public void Apply(Type memberInfo, Func<EntityTypeConfiguration> configuration)
        {
            var entityTypeConfiguration = configuration();

            var idProperty = memberInfo.GetProperty("Id", BindingFlags.Public | BindingFlags.Instance);

            if (idProperty != null)
                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.

    public class EntityConventionConfiguration
    {
        private readonly List<Func<Type, bool>> _predicates = new List<Func<Type, bool>>();
        private Action<LightweightEntityConfiguration> _configurationAction;
        private PropertyConventionConfiguration _propertyConfiguration;

        internal EntityConventionConfiguration()
        {
        }

        internal Action<LightweightEntityConfiguration> ConfigurationAction
        {
            get { return _configurationAction; }
        }

        internal List<Func<Type, bool>> Predicates
        {
            get { return _predicates; }
        }

        internal PropertyConventionConfiguration PropertyConfiguration
        {
            get { return _propertyConfiguration; }
        }

        /// <summary>
        /// Filters the entity types that this convention applies to based on a
        /// predicate.
        /// </summary>
        /// <param name="predicate">A function to test each entity type for a condition.</param>
        /// <returns>
        /// The same EntityConventionConfiguration instance so that multiple calls can
        /// be chained.
        /// </returns>
        public EntityConventionConfiguration Where(Func<Type, bool> predicate)
        {
            Contract.Requires(predicate != null);

            _predicates.Add(predicate);

            return this;
        }

        /// <summary>
        /// Allows configuration of the entity types that this convention applies to.
        /// </summary>
        /// <param name="entityConfigurationAction">
        /// An action that performs configuration against a <see cref="LightweightEntityConfiguration" />.
        /// </param>
        public void Configure(Action<LightweightEntityConfiguration> entityConfigurationAction)
        {
            Contract.Requires(entityConfigurationAction != null);

            _configurationAction = entityConfigurationAction;
        }

        /// <summary>
        /// Allows further configuration of the convention based on the properties of
        /// the entity types that this convention applies to.
        /// </summary>
        /// <returns>
        /// A configuration object that can be used to configure this convention based
        /// on properties.
        /// </returns>
        public PropertyConventionConfiguration Properties()
        {
            var propertyConfiguration = new PropertyConventionConfiguration();
            _propertyConfiguration = propertyConfiguration;

            return propertyConfiguration;
        }
    }

Utilizando esta clase, por lo tanto, podríamos crear una convención tal y como sigue:

 

   public class BlogUnitOfWork
        :DbContext
    {
        public IDbSet<Blog> Blogs { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Add(entityConventionConfiguration =>
            {
                entityConventionConfiguration.Properties()
                                            .Where(p => p.Name == "Id")
                                            .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 = false)]
    public class 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…

 

    class NonUnicodeAttributeConvention :
        AttributeConfigurationConvention<PropertyInfo, StringPropertyConfiguration, NonUnicodeAttribute>
    {

        public override void Apply(

            PropertyInfo propertyInfo,

            StringPropertyConfiguration configuration,

            NonUnicodeAttribute attribute)
        {

            if (configuration.IsUnicode == null)
            {
                configuration.IsUnicode = false;
            }
        }
    }

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

Published 3/11/2012 12:57 por Unai
Archivado en: ,,
Comparte este post:
http://geeks.ms/blogs/unai/archive/2012/11/03/ef-6-alpha-custom-conventions.aspx

Comentarios

# re: EF 6 Alpha: Custom Conventions

Hola Unai,

Muy interesante, tiene buena pinta esta versión de EF.

Lo único que no hubiera metido es la parte de atributos, pero eso ya es cuestión de gustos.

Un saludo,

Juanma.

Monday, November 05, 2012 5:19 PM por Juanma

# re: EF 6 Alpha: Custom Conventions

Bueno, yo soy de la misma opinion, no los uso y no me gustan, pero para algo 'RAD' es una forma sencilla de hacer ciertas cosas, o sea que creo que también tienen su scenario..

Unai

Tuesday, November 06, 2012 9:53 AM por Unai