Visually Located

XAML and GIS

Using the Repository Pattern with ArcObjects

The Repository Pattern is well known to everyone that needs to access a database. That is unless you access a database through ArcObjects. A repository is easy to use, easy to inject, and easy to unit test. Doing anything with ArcObjects is very complicated. Especially unit test. Wrapping ArcObjects in a layer of abstraction usually helps simplify things. By wrapping a few simple operations within a repository, your code no longer needs to start or stop an edit operation, no longer needs to worry about creating a QueryFilter (something that cannot be done within unit tests), and no longer needs to worry about all of the little details of accessing ArcObjects.

Let’s start with a simple repository using ArcObjects. We’ll need the normal CRUD operations.

interface ITableRepository {
    IRow GetById(int id);
    IEnumerable<IRow> GetAll();
    IEnumerable<IRow> GetByWhere(string whereClause);
    void Delete(IRow entity);
    int Save(IRow entity);
    void BeginTransaction();
    void EndTransaction(string name);
    void RollbackTransaction();
}

If this interface is injected into your object, you can now easily unit test all of the basic CRUD operations, something that you cannot do if you rely on ArcObjects.

The basic implementation of this interface will need to do all of the dirty ArcObjects access that your code used to do. It will need an instance of the Editor object so that it can manipulate the operations. It will have to create the QueryFilters for doing queries.. You get the idea.

class TableRepository : ITableRepository {
    private readonly IName _tableName;
    private readonly IEditor _editor;

    public TableRepository(ITable table)
    {
        if (table == null) throw new ArgumentNullException("table");

        // Store the IName, it's cheaper than olding onto the actual table 
        IDataset dataset = (IDataset)table;
        _tableName = dataset.FullName;

        _editor = GetEditor();
    }

    public ITable Table { get { return _tableName.Open() as ITable; } }

    #region ITableRepository Members

    public IRow GetById(int id)
    {
        ITable table = Table;

        return table.GetRow(id);
    }

    public IEnumerable<IRow> GetAll()
    {
        return GetByWhere(null);
    }

    public IEnumerable<IRow> GetByWhere(string whereClause)
    {
        ITable table = Table;

        // Some great magic happens here, throw this code into reflector and see 
        // what comes out 
        using (var releaser = new ComReleaser())
        {
            IQueryFilter filter = new QueryFilterClass();
            filter.WhereClause = whereClause;

            // non-recycling cursor, we are holding on to the row returned 
           ICursor cursor = table.Search(filter, false);
            releaser.ManageLifetime(cursor);

            IRow row = cursor.NextRow();
            while (row != null)
            {
                yield return row;

                row = cursor.NextRow();
            }
        }
    }

    public void Delete(IRow entity)
    {
        if (entity == null) throw new ArgumentNullException("entity");

        entity.Delete();
    }

    public int Save(IRow entity)
    {
        if (entity == null) throw new ArgumentNullException("entity");

        int id = entity.OID;
        if (entity.HasOID && (entity.OID < 0))
        {
            // It's a new row, we need to add it to the table 
            ICursor cursor = Table.Insert(true);
            id = Convert.ToInt32(cursor.InsertRow(entity));
            cursor.Flush();
        }
        else {
            // existing row 
            entity.Store();
        }
        return id;
    }

    public void BeginTransaction()
    {
        if (_editor.EditState == esriEditState.esriStateEditing)
        {
            _editor.StartOperation();
        }
    }

    public void EndTransaction(string name)
    {
        if (_editor.EditState == esriEditState.esriStateEditing)
        {
            _editor.StopOperation(name);
        }
    }

    public void RollbackTransaction()
    {
        if (_editor.EditState == esriEditState.esriStateEditing)
        {
            _editor.AbortOperation();
        }
    }

    #endregion private IEditor GetEditor()
    {
        IApplication app = GetApplication();
        UID editorUid = new UIDClass();
        editorUid.Value = "esriEditor.Editor";
        return app.FindExtensionByCLSID(editorUid) as IEditor;
    }

    private IApplication GetApplication()
    {
        Type t = Type.GetTypeFromProgID("esriFramework.AppRef");
        System.Object obj = Activator.CreateInstance(t);
        return obj as IApplication;
    }
}

If you’re developing with ArcFM, you can make the Transaction methods a little cleaner and you no longer  need to hold onto an IEditor reference.

public void BeginTransaction()
{
    if(Editor.EditState == EditState.StateEditing)
    {
        Editor.StartOperation();                
    }
}

public void EndTransaction(string name)
{
    if(Editor.IsOperationInProgress())
    {
        Editor.StopOperation(name);
    }
}

public void RollbackTransaction()
{
    if (Editor.IsOperationInProgress())
    {
        Editor.AbortOperation();
    }
}

If you like to keep things more generic, remove the ITable and IRow from the methods and properties and make them generic type parameters. This allows you the ability to implement the interface and get back your own custom objects.

interface ITableRepository<TEntity>