fd Blog

Daniel Hilgarth on software development

A Fluent Repository: Immutability

It has been pointed out that the implementation of the Fluent Repository base class I showed in Part 2 has a big flaw:
You need a new instance of the repository for each query, because the base class is mutable and thus it appends the query conditions to the internally stored IQueryable<T>. That’s not obvious and in fact counter-intuitive and because of that, it is bad.

Mutable state inside the Fluent Repository - What’s the problem?

The problem is the following:

Assume we acquire an instance of an implementation of IQueryEmployees some way or another, potentially in a constructor:

public Foo(IQueryEmployees employeesRepository)
{
    _employeesRepository = employeesRepository;
}

Further, assume two methods like this:

public void RaiseSalaryOfTeamLeaders()
{
    foreach(var teamLeader in _employeesRepository.TeamLeaders)
        teamLeader.Salary *= 110.Percent();
}

public void GiveOneTimeBonusToAllEmployees()
{
    foreach(var employee in _employeesRepository)
        employee.GiveOneTimeBonus(500.Euro());
}

Now, if you would call it like this, it works as expected:

foo.GiveOneTimeBonusToAllEmployees();
foo.RaiseSalaryOfTeamLeaders();

However, if you would call it the other way around, only the team leaders would get the one time bonus:

foo.RaiseSalaryOfTeamLeaders();
foo.GiveOneTimeBonusToAllEmployees();

Why is that?
Because the IQueryEmployees implementation mutates its own state when calling any of the query methods / properties.
In other words: After the call to IQueryEmployees.TeamLeaders it is pre-filtered to only return team leaders. Even when enumerating _employeesRepository directly.

OK, mutable state inside the Fluent Repository is a bad idea - So how do we change it?

We want to have as much of the repetitive code in the base class as possible.
Because of that, UpdateQuery should actually create a new instance of our concrete query implementation, making that change unfortunately not constrained to the base class.

One possible approach adds an abstract Factory Method to the base class that takes the new query as a parameter. Each concrete query implementation would need to implement that factory method and additionally provide a constructor that accepts an IQueryable<T>.

The code for this could look like the following for the base class:

public class NHibernateIQueryableQueryBase<...> : ...
{
    private readonly IQueryable<TEntity> _query; // Note _query is now readonly!
    
    protected NHibernateIQueryableQueryBase(ISession session) : this(session.Query<TEntity>())
    {
    }

    protected NHibernateIQueryableQueryBase(IQueryable<TEntity> query)
    {
        if (query == null)
            throw new ArgumentNullException("query");
        _query = query;
    }
    
    // all members except UpdateQuery remain unchanged
    
    protected abstract TQueryInterface CreateNewQuery(IQueryable<TEntity> newQuery);
    
    protected TQueryInterface ExtendQuery(Func<IQueryable<TEntity>, IQueryable<TEntity>> queryExtender)
    {
        if (queryExtender == null)
            throw new ArgumentNullException("queryExtender");

        return CreateNewQuery(queryExtender(_query));
    }
}

And the Employees implementation could look like this:

public class Employees : NHibernateIQueryableQueryBase<Employee, Employees, IQueryEmployees>,
                         IQueryEmployees
{
    public Employees(ISession session) : base(session)
    {
    }
    
    public Employees(IQueryable<Employee> query) : base(query)
    {
    }
    
    public IQueryEmployees TeamLeaders
    {
        get { return ExtendQuery(q => q.Where(x => x.IsTeamLeader)); }
    }
    
    public IQueryEmployees ByGender(Gender gender)
    {
        return ExtendQuery(q => q.Where(x => x.Gender == gender));
    }
    
    // more primary operations
    
    protected override IQueryEmployees CreateNewQuery(IQueryable<Employee> newQuery)
    {
        return new Employees(newQuery);
    }
}

Conclusion

With these changes you can re-use instances of the Fluent Repository.

Comments