An idea about errors and user groups

When an application fails then the fix for the error is usually found among these three groups of people:

  • The users of the application
  • The infrastructure team
  • The developer team

The users can fix problems like “the credit card number is not correct”, “can’t place order the purchase limit is reached”, “username already taken”. This is typically errors that the developers have thought about when developing the application and is in the category of input validation and business rules errors.

The infrastructure team is the technical people in the IT department that does not use a compiler. Examples are the network, database and server team. They fix problems like “server cannot be reached”, “not enough disk space available”, and “database timeout”.

The developer team can fix errors related to bugs in the code.

Direct the error message toward correct user group

The three groups of people need different information to fix the error. The developers would very much like a stack trace. But a stack trace for the users and infrastructure team is not acceptable.

The developers seems to be the catch-all end point for all errors that the other user groups can’t understand. So if your developer team do not like the disturbance (they usually do not) then improve the error messages so that the users or infrastructure team can fix the error them self.

Example

The example shows a simple Entity Framework example:

public class PersonService
{
    public void WorkWithPerson()
    {
        using (var databaseContext = new DatabaseContext())
        {
            //Create 
            var person = new Person { Name = "John" };
            databaseContext.Persons.Add(person);
            databaseContext.SaveChanges();

            //Find
            var personFromDatabase = databaseContext.Persons
                    .SingleOrDefault(c => c.Name == "John");

            //Remove
            databaseContext.Persons.Remove(personFromDatabase);
            databaseContext.SaveChanges();
        }

    }
}

But what happens if the database cannot be reached? You get a SQLException with this message: “A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: TCP Provider, error: 0 - No such host is known.)”

Notice that the database server name is not included in the error message. Do you think that your infrastructure team can solve this problem without contacting the developer team (or at least some documentation)? We need to add some extra details:

public class PersonService
{
    public void WorkWithPerson()
    {
        using (var databaseContext = new Context())
        {
            try
            {
                //Create 
                var person = new Person {Name = "John"};

                databaseContext.Persons.Add(person);

                databaseContext.SaveChanges();

                //Find
                var personFromDatabase = databaseContext.Persons
                    .SingleOrDefault(c => c.Name == "John");

                //Remove
                databaseContext.Persons.Remove(personFromDatabase);
                databaseContext.SaveChanges();
            }
            catch (SqlException ex) when (ex.Number == 11001)
            {
                throw new DatabaseConnectionException(
                    ex.Message + " Database server: "
                    + databaseContext.Database.Connection.DataSource + ".", ex);
            }
            catch(SqlException ex) when (ex.Number == 18456)
            {
                throw new DatabaseCredentialException(
                    ex.Message + " Database server: " 
                    + Database.Connection.DataSource + ".", ex);
            }
        }
    }
}

public class DatabaseConnectionException : InfrastructureException
{
    public DatabaseException(string message, Exception innerException) 
                   : base(message, innerException)
    {
    }
}

public class DatabaseCredentialException : InfrastructureException
{
    public DatabaseCredentialException(string message, Exception innerException) : base(message, innerException)
    {
    }
}

public abstract class InfrastructureException : Exception
{
    public InfrastructureException(string message) : base(message)
    {
    }
}

 

Now the error message is: A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: TCP Provider, error: 0 - No such host is known.) Database server: databaseclustor1.myfirm.com. You might add other details along the way to help the infrastructure team.

Notice that I do not suggest that you add a try catch every time you access the database. This code should be part of the application infrastructure because of the DRY principle, because your developers will hate you if not and the developers will forget to add it most of the time. So add it to your database access abstraction, repository or in the unhandled exception handler.

Presentation

The errors also need to be presented differently according the user groups.

User errors needs it presented in the user interface of cause. If the users are affected by something that they can’t fix, then it is often best just to say “Something unexpected happened in the system. Call support for help”. It is a security risk if the users see detailed information about the inner workings of an application.

The infrastructure team do often not use your application so they need to another way to be informed. They often have a system that presents errors from many systems. Have a chat with them about how to send the errors to their system. E-mails could also be a solution but consider to limit the number of e-mails you send.

Developers needs as much information as possible so logging is very important.

Given the design above with the InfrastructureException base class, your general exception manager can do different things to InfrastructureException than it do to other unhandled exceptions. Informing the infrastructure team for example.

Improve the use of Entity Framework

It is normal to see this kind of code in Entity Framework examples. But this code it not especially testable. I would like do something about that.

public class PersonService
{
    public void WorkWithPerson()
    {
        using (var databaseContext = new DatabaseContext())
        {
            //Create 
            var person = new Person { Name = "John" };
            databaseContext.Persons.Add(person);
            databaseContext.SaveChanges();

            //Find
            var personFromDatabase = databaseContext.Persons
                    .SingleOrDefault(c => c.Name == "John");

            //Remove
            databaseContext.Persons.Remove(personFromDatabase);
            databaseContext.SaveChanges();
        }

    }
}

public class DatabaseContext : DbContext
{
    public DbSet<Person> Persons { get; set; }
}


public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
}

The first thing to do is to add an interface to the DatabaseContext and so that constructor injection can be used to set the context.

public class PersonService
{
    private readonly IContext _context;

    public PersonService(IContext context)
    {
        _context = context;
    }

    public void WorkWithPerson()
    {
        //Create 
        var person = new Person {Name = "John"};
        _context.Persons.Add(person);
        _context.SaveChanges();

        //Find
        var personFromDatabase = _context.Persons
            .SingleOrDefault(c => c.Name == "John");

        //Remove
        _context.Persons.Remove(personFromDatabase);
        _context.SaveChanges();
    }
}

public interface IContext : IDisposable
{
    DbSet<Person> Persons { get; set; }
    int SaveChanges();
}

public class DatabaseContext : DbContext, IContext
{
    public DbSet<Person> Persons { get; set; }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Now the code can be tested using a mocking framework or a fake. But every time I add a new entity, I need to change the interface and the DatabaseContext class. It violates the open-close principle. I can’t do anything about the class because that is how Entity Framework knows that the entity but I can do something about the interface. Another problem is that the DbSet exposed by the Persons property is an Entity Framework concept. If we later would change the technology it would require a lot work to change. So let’s change the interface a bit.

public class PersonService
{
    private readonly IContext _context;

    public PersonService(IContext context)
    {
        _context = context;
    }

    public void WorkWithPerson()
    {
        //Create 
        var person = new Person {Name = "John"};
        _context.Add(person);
        _context.SaveChanges();

        //Find
        var personFromDatabase = _context.Query<Person>()
            .SingleOrDefault(c => c.Name == "John");

        //Remove
        _context.Remove(personFromDatabase);
        _context.SaveChanges();
    }
}

public interface IContext : IDisposable
{
    IQueryable<TEntity> Query<TEntity>() where TEntity : class;
    void Add<TEntity>(TEntity entity) where TEntity : class;
    void Remove<TEntity>(TEntity entity) where TEntity : class;
    int SaveChanges();
}

public class DatabaseContext : DbContext, IContext
{
    public DbSet<Person> Persons { get; set; }

    public IQueryable<TEntity> Query<TEntity>() where TEntity : class
    {
        return Set<TEntity>().AsQueryable();
    }

    public void Add<TEntity>(TEntity entity) where TEntity : class
    {
        Set<TEntity>().Add(entity);
    }

    public void Remove<TEntity>(TEntity entity) where TEntity : class
    {
        Set<TEntity>().Remove(entity);
    }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
}

This interface can be implemented by most ORM tools that supports LINQ.

To ease the testing an in-memory version of the IContext can be great to have (as an alternative a mock or a real database). The TestDbSet<T> is found msdn.

[TestClass]
public class PersonServiceTests
{
    [TestMethod]
    public void CanWorkWithPeople()
    {
        using (var context = new InMemoryContext())
        {
            //Arrange 
            var personService = new PersonService(context);

            //Act
            personService.WorkWithPerson();

            //Assert
            Assert.AreEqual(2, context.SaveChangesCount);
            Assert.AreEqual(1, context.AddCount);
            Assert.AreEqual(1, context.RemoveCount);
        }
    }
}

public class InMemoryContext : IContext
{
    private readonly Dictionary<Type, object> _dictionary 
            = new Dictionary<Type, object>();

    public int SaveChangesCount { get; private set; }
    public int AddCount { get; private set; }
    public int RemoveCount { get; private set; }

    public IQueryable<TEntity> Query<TEntity>() where TEntity : class
    {
        return GetDbSet<TEntity>();
    }

    public void Add<TEntity>(TEntity entity) where TEntity : class
    {
        AddCount++;
        GetDbSet<TEntity>().Add(entity);
    }

    public void Remove<TEntity>(TEntity entity) where TEntity : class
    {
        RemoveCount++;
        GetDbSet<TEntity>().Remove(entity);
    }

    private TestDbSet<TEntity> GetDbSet<TEntity>() where TEntity : class
    {
        TestDbSet<TEntity> dbSet;
        var type = typeof (TEntity);
        if (!_dictionary.ContainsKey(type))
        {
            dbSet = new TestDbSet<TEntity>();
            _dictionary.Add(type, dbSet);
        }
        else
        {
            dbSet = (TestDbSet<TEntity>) _dictionary[type];
        }
        return dbSet;
    }

    public int SaveChanges()
    {
        SaveChangesCount++;
        return 1;
    }

    public void Dispose()
    {
    }
}

public class TestDbSet<TEntity> : 
        DbSet<TEntity>, IQueryable, IEnumerable<TEntity>, IDbAsyncEnumerable<TEntity>
    where TEntity : class
{
    ObservableCollection<TEntity> _data;
    IQueryable _query;

    public TestDbSet()
    {
        _data = new ObservableCollection<TEntity>();
        _query = _data.AsQueryable();
    }

    public override TEntity Add(TEntity item)
    {
        _data.Add(item);
        return item;
    }

    public override TEntity Remove(TEntity item)
    {
        _data.Remove(item);
        return item;
    }

    public override TEntity Attach(TEntity item)
    {
        _data.Add(item);
        return item;
    }

    public override TEntity Create()
    {
        return Activator.CreateInstance<TEntity>();
    }

    public override TDerivedEntity Create<TDerivedEntity>()
    {
        return Activator.CreateInstance<TDerivedEntity>();
    }

    public override ObservableCollection<TEntity> Local
    {
        get { return _data; }
    }

    Type IQueryable.ElementType
    {
        get { return _query.ElementType; }
    }

    Expression IQueryable.Expression
    {
        get { return _query.Expression; }
    }

    IQueryProvider IQueryable.Provider
    {
        get { return new TestDbAsyncQueryProvider<TEntity>(_query.Provider); }
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _data.GetEnumerator();
    }

    IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
    {
        return _data.GetEnumerator();
    }

    IDbAsyncEnumerator<TEntity> IDbAsyncEnumerable<TEntity>.GetAsyncEnumerator()
    {
        return new TestDbAsyncEnumerator<TEntity>(_data.GetEnumerator());
    }
}

internal class TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
{
    private readonly IQueryProvider _inner;

    internal TestDbAsyncQueryProvider(IQueryProvider inner)
    {
        _inner = inner;
    }

    public IQueryable CreateQuery(Expression expression)
    {
        return new TestDbAsyncEnumerable<TEntity>(expression);
    }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        return new TestDbAsyncEnumerable<TElement>(expression);
    }

    public object Execute(Expression expression)
    {
        return _inner.Execute(expression);
    }

    public TResult Execute<TResult>(Expression expression)
    {
        return _inner.Execute<TResult>(expression);
    }

    public Task<object> ExecuteAsync(
        Expression expression, CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute(expression));
    }

    public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute<TResult>(expression));
    }
}

internal class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
{
    public TestDbAsyncEnumerable(IEnumerable<T> enumerable)
        : base(enumerable)
    { }

    public TestDbAsyncEnumerable(Expression expression)
        : base(expression)
    { }

    public IDbAsyncEnumerator<T> GetAsyncEnumerator()
    {
        return new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
    }

    IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
    {
        return GetAsyncEnumerator();
    }

    IQueryProvider IQueryable.Provider
    {
        get { return new TestDbAsyncQueryProvider<T>(this); }
    }
}

internal class TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
{
    private readonly IEnumerator<T> _inner;

    public TestDbAsyncEnumerator(IEnumerator<T> inner)
    {
        _inner = inner;
    }

    public void Dispose()
    {
        _inner.Dispose();
    }

    public Task<bool> MoveNextAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult(_inner.MoveNext());
    }

    public T Current
    {
        get { return _inner.Current; }
    }

    object IDbAsyncEnumerator.Current
    {
        get { return Current; }
    }
}

Conclusion
I showed how to make a program testable even though it used Entity Framework.