Dev: A Dapper Wrapper, deferred async
As a company based around a software solution, not only do we want to contribute ideas around social services, but we want to contribute to the world of software. This is part of a series of posts about some things we are doing without code.
There is a good chance one of the following two things is true:
- There is a better way to do this, if so (please add to the conversation)
- Dapper may already do this, and I am just oblivious.
One thing, however, is true; I learned a lot in trying to do this.
If you are used to Microsoft’s Entity Framework and Async Await, you will recognize this pattern:
var myList = await dbContext.Entity.Where(x => ...).Select(x => ...).ToListAsync();
I appreciate this syntax because I can do my await last, I also get to defer execution of the database query till I am ready to process the data.
With Dapper, I found myself doing this a lot:
var myList = (await conn.QueryAsync<T>(statement, parameter)).ToList();
While not terrible, it wasn’t as fluid to write as the EF statement above.
So, I decided to write an IDapper wrapper that lets me do this:
var myList = await conn.DapperQuery<T>(statement, parameter).ToListAsync();
Additionally, if you wanted to chain another select, you can as follows:
var myList = await conn.DapperQuery<T>(statement, parameter).Select(x => ...).ToListAsync();
A quick note: I used extension methods to accomplish this. I have a love/hate relationship with extension methods. I think they are cool and awesome to use, but I do worry about the effect they have on code dependency discovery and testing. But that’s a rant for another day.
To accomplish the above I wrote the following interface:
public interface IDapper<T> : IEnumerable<T>
{
Task<List<T>> ToListAsync(CancellationToken cancellationToken = default(CancellationToken)); List<T> ToList(); IDapper<TOut> Select<TOut>(Func<T, TOut> func);}
My extension method looked something like this:
public static IDapper<T> DapperQuery<T>(this DbConnection conn, string statement, DynamicParameters parameters)
{
return Dapper<T>.Create(
() => conn.Query<T>(statement, parameters),
async cancellationToken => await conn.QueryAsync<T>(statement, parameters));}
The function create takes two Func arguments, one that returns an IEnumerable (for the non-async path) and Task<IEnumerable> for the async path. These Func variables are stored on the IDapper object.
So, how do we chain in a select and still defer execution till .ToList() or .ToListAsync()? I wrote an implementation of Select that creates a new IDapper object chaining a select onto the invoked Func from above. At this point no Funcs have been invoked, as we just create a new Func here.
public IDapper<TOut> Select<TOut>(Func<T, TOut> func)
{
return Dapper<TOut>.Create(() => _normalFunc.Invoke().Select(func),
async (cancellationToken) => (await _asyncFunc.Invoke(cancellationToken)).Select(func));}
So we now have a Func with a call to our initial Func inside it. While dapper doesn’t have the option to pass a cancellation token, I allowed for one to be passed in. One, I wanted to prove to myself I could do it, two, maybe it will be beneficial in the future, three, I may remove it because its confusing to expect cancellation but not actually get it from Dapper.
My implementation of ToList and ToListAsync:
public async Task<List<T>> ToListAsync(CancellationToken cancellationToken = default(CancellationToken))
{
return (await _asyncFunc.Invoke(cancellationToken)).ToList();
}
public List<T> ToList()
{
return _normalFunc.Invoke().ToList();}
In our example above, with a select, calling ToList or more importantly ToListAsync finally invokes our Func (this is the one we got from the Select), which in turn invokes the one from the original IDapper, executing the query against the database.
I also implemented the IEnumerable interface, I haven’t played much with that, but figured it would be nice to have.
Any feedback? I don’t necessarily think this is the one and only way to do this, or possibly the right way (please contribute to the conversation if there are better ways of doing this). But I do like the way it cleaned up my calling code and moved the await to a more convenient place of code.