Query object pattern

I introduktionen till den här miniserien om Query object pattern och Command pattern presenterades en önskan om en plattare struktur på koden jämfört med "traditionell" n-lager-struktur med services, managers, repositories etc. Med hjälp av en ASP.NET MVC-webbapplikation börjar vi med att presentera Query object pattern och hur man kan implementera och använda det mönstret.

Introduktion

Vill man läsa så finns det en uppsjö artiklar i ämnet och man kan även se lite olika sätt att implementera mönstret. Från Google bara, hen vet! Det som presenteras här är ett av alla dessa sätt.

Den kompletta koden finns här https://github.com/HeadlightAB/QueryObjectAndCommandPattern/tree/master/QueryObjectPattern.

Den absolut viktigaste regeln i mönstret är att:

  • En query ska ALDRIG modifiera data

Med detta i bakhuvudet går vi vidare och tittar på kod!

Grundstruktur

Som namnet antyder så borde det finnas objekt som representerar en fråga mot någon datakälla. Frågan borde även kunna exekveras och ett resultat borde då returneras. Grunderna i mönstret består alltså av frågor och datakällor.

Notera selector-parametern i Query-metoden i IDbDataAccess. Tanken med den är att "så fort som möjligt" lämna datalagret, i form av entiteter, och projicera över resultatet till objekt i domänen.

public interface IQuery<out TDomainModel, in TDataSource> where TDataSource : IDataAccess  
{
    Task<TDomainModel> Execute(TDataSource dataSource);
}

public interface IDataAccess  
{}

public interface IApiDataAccess : IDataAccess  
{
    Task<HttpResponseMessage> Request(HttpRequestMessage request);
}

public interface IDbDataAccess : IDataAccess  
{
    Task<TDomainModel[]> Query<TDomainModel, TEntity>(
        Expression<Func<TEntity, bool>> filter,
        Expression<Func<TEntity, TDomainModel>> selector) where TEntity : class where TDomainModel : class;
}

I koden ovan ser vi först interfacet IQuery som är gränssnittet för själva frågeobjektet. Den enda metoden i gränssnittet är Execute som tar in ett beroende till en datakälla. I det här fallet har källan gjorts teknikagnostisk genom att inparametern är av typen TDataSource som måste vara av typen IDataAccess. IDataAccess är ett markeringsinterface men ärvs av två olika typer av dataaccesser, en IApiDataAccess och en IDbDataAccess. Om man i sitt system bara har en typ av datalager, t.ex. en databas, så kan man helt plocka bort IDataAccess-interfacet och låta IQeury ta in en IDbDataAccess som parameter i sin Execute-metoden.

Implementation

Låt oss titta på ett par implementationer av IQuery, så blir det nog mönstrets elegans tydligare. Vi börjar med frågeobjekten, två IQuery-implementationer.

Query-objekten

namespace Domain.Queries  
{
    public class CarByRegNo : IQuery<Domain.Models.Car, IDbDataAccess>
    {
        private readonly string _regNo;

        public CarByRegNo(string regNo)
        {
            _regNo = regNo;
        }

        public async Task<Domain.Models.Car> Execute(IDbDataAccess dataSource)
        {
            var result = await dataSource.Query<Models.Car, DataAccess.Entities.Car>(
                entity => entity.RegNo == _regNo, 
                entity => new Domain.Models.Car(entity.RegNo, entity.Brand, entity.Model, entity.Year));

            return result.SingleOrDefault();
        }
    }
}

Frågeobjektet ovan använder sig av en IDbDataAccess-källa för att filtrera entiteterna och sedan projicera träffarna till det önskade domänobjektstypen. I det här fallet väljer man ut bil(arna) med givet registreringsnummer och sedan projiceras träffen(arna) till domänobjekt av typen Domain.Models.Car.

namespace Domain.Queries  
{
    public class TaxByRegNo : IQuery<float, IApiDataAccess>
    {
        private readonly string _regNo;

        public TaxByRegNo(string regNo)
        {
            _regNo = regNo;
        }

        public async Task<float> Execute(IApiDataAccess dataSource)
        {
            // Url not valid, but for the purpose of the example it is sort of suitable
            var response =
                await dataSource.Request(
                    new HttpRequestMessage(HttpMethod.Get, $"https://www.google.com/{_regNo}"));

            if (response.IsSuccessStatusCode)
            {
                var content = await response.Content.ReadAsStringAsync();
                if (float.TryParse(content, out var result))
                {
                    return result;
                }
            }

            return -1;
        }
    }
}

I det här exemplet använder frågan en IApiDataAccess för att få ut det önskade datat. Med registreringsnumret som inparameter frågar man ett webapi om skatten för fordonet.

Datakällorna

I frågeobjekten ovan användes två olika typer av datakällor. Som vän av ordning presenteras exempel på implementationer av dessa nedan:

namespace DataAccess.DataSources  
{
    public class ApiDataAccess : IApiDataAccess
    {
        private readonly IHttpClientFactory _httpClientFactory;

        public ApiDataAccess(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }

        public async Task<HttpResponseMessage> Request(HttpRequestMessage request)
        {
            return await _httpClientFactory.CreateClient().SendAsync(request);
        }
    }

    public class DbDataAccess : IDbDataAccess
    {
        private readonly DbContext _dbContext;

        public DbDataAccess(DbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public async Task<TDomainModel[]> Query<TDomainModel, TEntity>(
            Func<TEntity, bool> filter,
            Func<TEntity, TDomainModel> selector) where TDomainModel : class where TEntity : class
        {
            return await _dbContext.Set<TEntity>()
                .Where(entity => filter(entity))
                .Select(entity => selector(entity)).ToArrayAsync();
        }
    }
}

Användning

Nu borde det vara ett bra läge att faktiskt använda Query-objekten. I källkoden på GitHub finns det tester som nyttjar dom, men det finns också ett minimalt ASP.NET MVC Core-projekt. En controller med ett par actions skulle kunna se ut så här:

[ApiController]
[Route("api/[controller]")]
public class CarsController : ControllerBase  
{
    private readonly IDbDataAccess _dbSource;
    private readonly IApiDataAccess _apiSource;

    public CarsController(IDbDataAccess dbSource, IApiDataAccess apiSource)
    {
        _dbSource = dbSource;
        _apiSource = apiSource;
    }

    [HttpGet("{regNo}")]
    public async Task<ActionResult<Domain.Models.Car>> Get(string regNo)
    {
        var query = new CarByRegNo(regNo);
        var car = await query.Execute(_dbSource);

        return Ok(car);
    }

    [HttpGet("{regNo}/taxrate")]
    public async Task<ActionResult<float>> GetTaxRate(string regNo)
    {
        var query = new TaxByRegNo(regNo);
        var rate = await query.Execute(_apiSource);

        return Ok(rate);
    }
}

Värt att notera här är dom två beroendena som controllern tar in i konstruktorn. IDbDataAccess fungerar inte i den här lösningen eftersom det inte finns någon databas, migrations eller annat som gör att EntityFramework Core ska fungera. Däremot håller lösningen för att visa konceptet, där grunden i användandet är:

  • Skapa en query, specifik för informationen den ska hämta
  • Anropa Execute-metoden på queryn och ge den datakällan som parameter

Givetvis kan beroendet till datakällan tas in som parameter i konstruktorn, men lösningen med att "hålla datakällan nära" anropet till Execute, till och med som inparameter, känns för mig tilltalande. Det är även väldigt tilltalande att hålla query-objektet väldigt specifik för uppgiften, dvs en query ska innehålla så lite logik som möjligt och se till att önskat data lämnar datalagret så snabbt som möjlig. Projektionen från datalager till domänlager/objekt är alltså något bör göras så tidigt som möjligt. Det brukar förenkla systemets beroende till typerna i datalagret och göra det enklare att skriva testfall.

Fortsättning

I nästa post i den här serien ska vi titta på Command pattern, som på något sätt liknar Query object pattern. Länk kommer senare, posten är just nu under framtagande.