EFCore: Raw Sql Query with custom types
Hi everyone!
It has been too long since I posted on this blog.
Here is a small article about a workaround I had to do using ASP.Net Core and its Entity Framework Core (EFCore).
A very useful method on Entity Framework is the raw sql query with a custom type, which goes pretty much like this:
var myCustomList = await context.Database.SqlQuery<CustomType>("SELECT col1, col2, … FROM MyTable").ToListAsync();
This allow you to use custom types to define quick out-of-the-box entities / query mapping, and unfortunately EFCore does not support this (at least right now).
I could not go without this super-useful method, so I made an extension to support it.
Basics
I chose to use AutoMapper for the mapping and AutoMapper.Data which supports implicit IDataReader
mapping. (Edit: Note that Jeremy Sinclair wrote a custom method to map the entities that I did no see before, the link is down below).
We need a generic class for the logic (CustomTypeSqlQuery<T>
) and an extension to mimic the behavior we like (DatabaseExtensions
).
Please beware that only classes are supported, not basic types like string or int.
DatabaseExtensions
First the extension, in a file named DatabaseExtensions.cs
:
public static class DatabaseExtensions
{
public static CustomTypeSqlQuery<T> SqlQuery<T>(
this DatabaseFacade database,
string sqlQuery) where T : class
{
return new CustomTypeSqlQuery<T>()
{
DatabaseFacade = database,
SQLQuery = sqlQuery
};
}
}
This is an extension to DatabaseFacade
which will allow us to to something like:
context.Database.SqlQuery<CustomType>("SELECT … FROM …")
and returns a CustomTypeSqlQuery<T>
, which holds the logic.
CustomTypeSqlQuery
The full class goes like this:
public class CustomTypeSqlQuery<T> where T : class
{
private IMapper _mapper;
public DatabaseFacade DatabaseFacade { get; set; }
public string SQLQuery { get; set; }
public CustomTypeSqlQuery()
{
_mapper = new MapperConfiguration(cfg => {
cfg.AddDataReaderMapping();
cfg.CreateMap<IDataRecord, T>();
}).CreateMapper();
}
public async Task<IList<T>> ToListAsync()
{
IList<T> results = new List<T>();
var conn = DatabaseFacade.GetDbConnection();
try
{
await conn.OpenAsync();
using (var command = conn.CreateCommand())
{
command.CommandText = SQLQuery;
DbDataReader reader = await command.ExecuteReaderAsync();
if (reader.HasRows)
results = _mapper.Map<IDataReader, IEnumerable<T>>(reader)
.ToList();
reader.Dispose();
}
}
finally
{
conn.Close();
}
return results;
}
public async Task<T> FirstOrDefaultAsync()
{
T result = null;
var conn = DatabaseFacade.GetDbConnection();
try
{
await conn.OpenAsync();
using (var command = conn.CreateCommand())
{
command.CommandText = SQLQuery;
DbDataReader reader = await command.ExecuteReaderAsync();
if (reader.HasRows)
{
var results = _mapper.Map<IDataReader, IEnumerable<T>>(reader)
.ToList();
result = results.FirstOrDefault();
}
reader.Dispose();
}
}
finally
{
conn.Close();
}
return result;
}
}
As you can see, I create the mapping configuration on the go, which allows me not to declare the mapping. cfg.AddDataReaderMapping()
is the method that allows me to map IDataReader
directly.
Then I added two methods : ToListAsync()
and FirstOrDefaultAsync()
. It basically uses a connection and a DbDataReader, which is what Microsoft recommends for raw SQL Queries.
Please note that FirstOrDefaultAsync()
is absolutely barbaric inefficient. A better solution would be to parse the results one by one (await reader.ReadAsync()
) and return as soon as we have one result. I did this is just for the example.
This allows us to use the extension and the class like this:
var myList = await context.Database.SqlQuery<CustomType>("SELECT … FROM …")
.ToListAsync();
« Et voilà ! »
Almost as easy to use as in Entity Framework 6. I am missing support for basic types, it should not be really difficult to do but I do not need it right now. Feel free to contact me if you want your own appendix.
If you want it on github I can do that too.
See you soon!
PS: Jeremy Sinclair did a pretty nice job writing a method to parse from DbDataReader to a class without AutoMapper here