Refactoring 404 handler to use SQL store
The 404 handler used DDS as storage for redirects. While DDS has a simple API and is easy to use, it doesn't perform well. So I decided to refactor the 404 handler to support different stores and implemented a SQL store.
If you want to get started with the new version, head over to Geta's blog for details.
Refactoring steps
The first challenge I faced was that data access was mixed with some logic in a DataStoreHandler
. However, it was not so easy to extract it out. So I started abstracting away the logic behind the DataStoreHandler
and created an interface for it - IRedirectsService
and implemented it in the DataStoreHandler
. I didn't like method names in the DataStoreHandler
. So I used different naming in IRedirectsService
:
public interface IRedirectsService
{
IEnumerable<CustomRedirect> GetAll();
IEnumerable<CustomRedirect> GetSaved();
IEnumerable<CustomRedirect> GetIgnored();
IEnumerable<CustomRedirect> GetDeleted();
IEnumerable<CustomRedirect> Search(string searchText);
void AddOrUpdate(CustomRedirect redirect);
void AddOrUpdate(IEnumerable<CustomRedirect> redirects);
void DeleteByOldUrl(string oldUrl);
int DeleteAll();
int DeleteAllIgnored();
}
This new interface works as a facade for all actions that could be done with a redirect.
The next task was extracting data access. I wanted to separate reads from writes. For writes, I have introduced a generic IRepository
interface. For now, it just supports saving and deleting of requests.
public interface IRepository<TEntity>
where TEntity : class
{
void Save(TEntity entity);
void Delete(TEntity entity);
}
For reads, I have created an interface specific for redirect querying - IRedirectLoader
.
public interface IRedirectLoader
{
CustomRedirect GetByOldUrl(string oldUrl);
IEnumerable<CustomRedirect> GetAll();
IEnumerable<CustomRedirect> GetByState(RedirectState state);
IEnumerable<CustomRedirect> Find(string searchText);
}
I could use a query pattern here, but it would complicate things. I also wanted to stick to Episerver naming convention (for example, IContentLoader
for content querying in Episerver).
With these two interfaces, I could extract out data access. So I have implemented DdsRedirectRepository
by extracting all data access logic from DataStoreHandler
.
Once this was done, it was simple to move the rest of the DataStoreHandler
logic into a separate IRedirectsService
implementation and leave DataStoreHandler
just for backward compatibility (if someone uses it). I have implemented DefaultRedirectsService
which mostly coordinates data access.
SQL store
As I have finished refactoring, I could start implementing SQL store. The first task was creating a new table and implement upgrade steps. The 404 handler has Upgrader
class which already creates a table for suggestions. I have used it as an example for the redirects table. However, before implementation of it, Upgrader
needed a refactoring. I have extracted all upgrade steps in separate methods so that it is easier to add new steps.
Once table creation was implemented in the Upgrader
, I could start working on SqlRedirectRepository
. I used Episerver's IDatabaseExecutor
to run ADO.NET queries. For more straightforward data reading I have used Linq to DataSet.
private IEnumerable<CustomRedirect> ExecuteEnumerableQuery(DbCommand command)
{
var table = ExecuteDataTableQuery(command);
return table
.AsEnumerable()
.Select(ToCustomRedirect);
}
private DataTable ExecuteDataTableQuery(DbCommand command)
{
var adapter = _executor.DbFactory.CreateDataAdapter();
if (adapter == null) throw new Exception("Unable to create DbDataAdapter");
adapter.SelectCommand = command;
var ds = new DataSet();
adapter.Fill(ds);
return ds.Tables[0];
}
private static CustomRedirect ToCustomRedirect(DataRow x)
{
return new CustomRedirect(
x.Field<string>("OldUrl"),
x.Field<string>("NewUrl"),
x.Field<bool>("WildCardSkipAppend"))
{
Id = Identity.NewIdentity(x.Field<Guid>("Id")),
State = x.Field<int>("State")
};
}
The last step was adding data migration from DDS to the SQL store. I have just added an action to the gadget's controller, then used the DdsRedirectRepository
to read old records and the new IRedirectsService
to save records.
Performance
After I have implemented SQL store, I wanted to see how much faster the 404 handler become. I have imported 6000
redirects in DDS and SQL stores and tried to load all redirects. The results were fascinating. Loading of all records from DDS store took ~16000 ms
while from SQL store it took only 26 ms
. It is more than 600 times faster.
Summary
While it didn't seem much work implementing support for multiple storages, it took some time to finish it. Ideally, I would write integration tests to make sure that nothing was broken, but it was too hard to implement those to work with Episerver APIs - DDS and SQL.
# New Year!