Raw SQL Query en EF Core

Cuando, en 2016, empecé a trastear con Entity Framework, tuve que elegir entre la versión clásica o la nueva versión «Core». En la documentación te indicaba que, para proyectos nuevos, lo mejor era elegir EF Core, que era lo que se iba a desarrollar a futuro. Pero, ¡ay!, la versión entonces disponible no soportaba vistas y yo las necesitaba como el comer. No me quedó otra que empezar con EF clásico y dejar EF Core para más adelante.

El año pasado, aprovechando que ya teníamos una versión para .Net Standard de EF clásico, modifiqué mis proyectos con EF para pasarlos del tipo .Net Framework al tipo actual .Net Core, en donde se puede tener varios frameworks de destino. Así, los compilé para .Net Framework y .Net Core.

Este otoño, preparando mi salto a .Net Core del año que viene (posiblemente con Blazor), se me ha ocurrido también compilar mis proyectos de EF tanto para la versión clásica como para la nueva EF 7.0, recién salida del horno. En parte también porque en la documentación decía más o menos que los desarrolladores de EF clásico ya no teníamos excusa. Bien, me dije, veamos si es así. La madre del cordero es el mapeo parcial de la base de datos de SAP Business One, que es sólo relacional en parte. Más de 150 entidades (hasta la fecha), entre tablas y vistas.

Lo primero fue revisar qué usaba en EF clásico y cómo se hacía en EF Core, así que monté un prototipo y mapeé un par de tablas y vistas, con sus relaciones. Sin problemas.

Luego llegó el turno de las consultas SQL a pelo y ahí, ¡ah, amigo!, ahí se complicó todo.

En EF clásico teníamos una opción maravillosa que era:

public DbRawSqlQuery<TElement> SqlQuery<TElement>(string sql, params object[] parameters)

Con la que podíamos hacer algo como esto:

Dim oConsulta = Me.Database.SqlQuery(Of EstadoProduccion)("dbo.YNECAM_InformeEstadoProduccion @IdProduccion, @IdTipoConsulta",
                                                       New SqlParameter("@IdProduccion", IdProduccion),
                                                       New SqlParameter("@IdTipoConsulta", TipoConsulta)).ToList

Donde, al ejecutarse la consulta con el ToList, nos devuelve una colección del tipo pasado, casando propiedad del tipo con el campo homónimo de la consulta.

En EF Core eso no existe. En el mundo de las bases de datos ideales, se ve que no es necesario.

En la versión 7, se ha recuperado un método parecido, pero que devuelve un IQueryable y sólo admite tipos primitivos de la base de datos. Por ejemplo, una colección de fechas o de valores numéricos. Además, en la primera versión tiene limitaciones, pues no puedes invocar la consulta con un Single o First (por ejemplo, una llamada a una función escalar) porque explota.

Buscando en la documentación y por internet, encuentro que la opción que nos queda es obtener un objeto DbCommand (sí, el básico de ADO.Net) del objeto Database del Context. Luego, pasarle la consulta, ejecutar el DbCommand para que nos devuelva un DataReader y recorrerlo. Han borrado los datasets tipados de la faz de la Tierra (bueno, de la última versión de .Net Core), pero las bases de ADO.Net siguen estando en las tripas del más moderno EF Core y tan básico ahora como cuando empezamos con .Net Framework hace 20 años.

Se me ocurrió probar a hacer un método de extensión que me simplificara la operación e hiciera algo como

oResultado.Edad = CInt(miReader("Edad"))

para cada propiedad de la clase que le pasara. En Stack Overflow encontré una propuesta que consistía en pasarle un delegado que se encargase del mapeo:

public static List<T> RawSqlQuery<T>(string query, Func<DbDataReader, T> map)

Pero lo sigo viendo un engorro. Como opción, me puse a darle vueltas al uso de reflexión. Hice una primera prueba satisfactoria con un T limitado a clase con constructor sin parámetros y luego lo amplié para controlar también tipos básicos (de una forma un poco grosera, seguro que hay alguna forma mejor). Con todo esto, he llegado a:

<System.Runtime.CompilerServices.Extension>
Public Function SqlQuery(Of TResult)(database As Infrastructure.DatabaseFacade,
                                     query As String,
                                     ParamArray parameters As Object()) As IEnumerable(Of TResult)
    Dim mType = GetType(TResult)
    Dim mReturn As New List(Of TResult)
    Dim mValue As TResult
    Dim mProperties = mType.GetProperties().Where(Function(x) x.CanWrite).ToList
    Using Command = database.GetDbConnection.CreateCommand
        Command.CommandText = query
        For Each parameter In parameters
            Command.Parameters.Add(parameter)
        Next
        database.OpenConnection
        Using consulta = Command.ExecuteReader
            While consulta.Read
                If mType.IsPrimitive OrElse
                    mType Is GetType(DateTime) OrElse
                    mType Is GetType(String) OrElse
                    mType Is GetType(Decimal) Then
                    mValue = Convert.ChangeType(consulta(0), mType)
                Else
                    mValue = Activator.CreateInstance(mType)
                    For Each mProperty In mProperties
                        mProperty.SetMethod.Invoke(mValue,
                              {Convert.ChangeType(consulta(mProperty.Name), mProperty.PropertyType)})
                    Next
                End If
                mReturn.Add(mValue)
            End While
        End Using
    End Using
    Return mReturn
End Function

Cuyo uso sería similar al de EF clásico.

Vale, no tiene la ejecución diferida marca de EF (no necesito de un ToList, Single, For Each… para ejecutar la consulta), pero, francamente, lo veo un precio pequeño a pagar. Ahora me toca probarlo, usarlo y mejorarlo.

Un último apunte: el método, tal y como lo he hecho, coincide casi con la que trae de serie EF Core. Hay otras opciones, como, por ejemplo, método de extensión del DbContext. En mi caso he elegido esta opción porque la firma es similar a la de EF clásico y si paso código de .Net Framework/EF clásico a .Net Core/EF Core, me aseguro de que esa parte funcione igual.

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.