In RavenDB, we had this piece of code:
internal T[] LoadInternal<T>(string[] ids, string[] includes)
{
if(ids.Length == 0)
return new T[0];
IncrementRequestCount();
Debug.WriteLine(string.Format("Bulk loading ids [{0}] from {1}", string.Join(", ", ids), StoreIdentifier));
MultiLoadResult multiLoadResult;
JsonDocument[] includeResults;
JsonDocument[] results;
#if !SILVERLIGHT
var sp = Stopwatch.StartNew();
#else
var startTime = DateTime.Now;
#endif
bool firstRequest = true;
do
{
IDisposable disposable = null;
if (firstRequest == false) // if this is a repeated request, we mustn't use the cached result, but have to re-query the server
disposable = DatabaseCommands.DisableAllCaching();
using (disposable)
multiLoadResult = DatabaseCommands.Get(ids, includes);
firstRequest = false;
includeResults = SerializationHelper.RavenJObjectsToJsonDocuments(multiLoadResult.Includes).ToArray();
results = SerializationHelper.RavenJObjectsToJsonDocuments(multiLoadResult.Results).ToArray();
} while (
AllowNonAuthoritiveInformation == false &&
results.Any(x => x.NonAuthoritiveInformation ?? false) &&
#if !SILVERLIGHT
sp.Elapsed < NonAuthoritiveInformationTimeout
#else
(DateTime.Now - startTime) < NonAuthoritiveInformationTimeout
#endif
);
foreach (var include in includeResults)
{
TrackEntity<object>(include);
}
return results
.Select(TrackEntity<T>)
.ToArray();
}
And we needed to take this same piece of code and execute it in:
- Async fashion
- As part of a batch of queries (sending multiple requests to RavenDB in a single HTTP call).
Everything else is the same, but in each case the marked line is completely different.
I chose to address this by doing a Method Object refactoring. I create a new class, and moved all the local variables to fields, and moved each part of the method to its own method. I also explicitly gave up control on executing, deferring that to whoever it calling us. We ended up with this:
public class MultiLoadOperation
{
private static readonly Logger log = LogManager.GetCurrentClassLogger();
private readonly InMemoryDocumentSessionOperations sessionOperations;
private readonly Func<IDisposable> disableAllCaching;
private string[] ids;
private string[] includes;
bool firstRequest = true;
IDisposable disposable = null;
JsonDocument[] results;
JsonDocument[] includeResults;
#if !SILVERLIGHT
private Stopwatch sp;
#else
private DateTime startTime;
#endif
public MultiLoadOperation(InMemoryDocumentSessionOperations sessionOperations,
Func<IDisposable> disableAllCaching,
string[] ids, string[] includes)
{
this.sessionOperations = sessionOperations;
this.disableAllCaching = disableAllCaching;
this.ids = ids;
this.includes = includes;
sessionOperations.IncrementRequestCount();
log.Debug("Bulk loading ids [{0}] from {1}", string.Join(", ", ids), sessionOperations.StoreIdentifier);
#if !SILVERLIGHT
sp = Stopwatch.StartNew();
#else
startTime = DateTime.Now;
#endif
}
public IDisposable EnterMultiLoadContext()
{
if (firstRequest == false) // if this is a repeated request, we mustn't use the cached result, but have to re-query the server
disposable = disableAllCaching();
return disposable;
}
public bool SetResult(MultiLoadResult multiLoadResult)
{
firstRequest = false;
includeResults = SerializationHelper.RavenJObjectsToJsonDocuments(multiLoadResult.Includes).ToArray();
results = SerializationHelper.RavenJObjectsToJsonDocuments(multiLoadResult.Results).ToArray();
return sessionOperations.AllowNonAuthoritiveInformation == false &&
results.Any(x => x.NonAuthoritiveInformation ?? false) &&
#if !SILVERLIGHT
sp.Elapsed < sessionOperations.NonAuthoritiveInformationTimeout
#else
(DateTime.Now - startTime) < sessionOperations.NonAuthoritiveInformationTimeout
#endif
;
}
public T[] Complete<T>()
{
foreach (var include in includeResults)
{
sessionOperations.TrackEntity<object>(include);
}
return results
.Select(sessionOperations.TrackEntity<T>)
.ToArray();
}
}
Note that this class doesn’t contain two very important things:
- The actual call to the database, we gave up control on that.
- The execution order for the methods, we don’t control that either.
That was ugly, and I decided that since I have to write another implementation as well, I might as well do the right thing and have a shared implementation. The key was to extract everything away except for the call to get the actual value. So I did just that, and we got a new class, that does all of the functionality above, except control where the actual call to the server is made and how.
Now, for the sync version, we have this code:
internal T[] LoadInternal<T>(string[] ids, string[] includes)
{
if(ids.Length == 0)
return new T[0];
var multiLoadOperation = new MultiLoadOperation(this, DatabaseCommands.DisableAllCaching, ids, includes);
MultiLoadResult multiLoadResult;
do
{
using(multiLoadOperation.EnterMultiLoadContext())
{
multiLoadResult = DatabaseCommands.Get(ids, includes);
}
} while (multiLoadOperation.SetResult(multiLoadResult));
return multiLoadOperation.Complete<T>();
}
This isn’t the most trivial of methods, I’ll admit, but it is ever so much better than the alternative, especially since now the async version looks like:
/// <summary>
/// Begins the async multi load operation
/// </summary>
public Task<T[]> LoadAsyncInternal<T>(string[] ids, string[] includes)
{
var multiLoadOperation = new MultiLoadOperation(this,AsyncDatabaseCommands.DisableAllCaching, ids, includes);
return LoadAsyncInternal<T>(ids, includes, multiLoadOperation);
}
private Task<T[]> LoadAsyncInternal<T>(string[] ids, string[] includes, MultiLoadOperation multiLoadOperation)
{
using (multiLoadOperation.EnterMultiLoadContext())
{
return AsyncDatabaseCommands.MultiGetAsync(ids, includes)
.ContinueWith(t =>
{
if (multiLoadOperation.SetResult(t.Result) == false)
return Task.Factory.StartNew(() => multiLoadOperation.Complete<T>());
return LoadAsyncInternal<T>(ids, includes, multiLoadOperation);
})
.Unwrap();
}
}
Again, it isn’t trivial, but at least the core stuff, the actual logic that isn’t related to how we execute the code is shared.