Skip to content

Reference Architecture

Jezz Santos edited this page Aug 10, 2020 · 15 revisions

Here we demonstrate what a codebase may look like that makes use of QueryAny.

Context

Imagine that you are building an API (in your favorite Web API framework - ours is ServiceStack).

At some "layer" of that architecture you are going to implement some domain objects - domain entities to model the real world.

Let's say for the sake of this reference, that you are building a new product to help people find and borrow cars, in a "Car Sharing" product.

The main API resource and the main domain object you decide to define is around Cars themselves.

So, you might have an API endpoint that returns a list of Available Cars that are currently available in your area for users to rent.

Now, forgetting the specific Web API framework that you are using. After the incoming HTTP call is authorized and the user is identified you might decide to forward the API call directly to your application layer called CarsApplication to deal with the request. (We are obviously going to hide other details in this call chain too, just for simplicity).

OK, so we will demonstrate how that kind of application and domain code looks, and hint at how you would approach the delicate matter of isolating your domain objects from any web or persistence technologies, properly encapsulating and distributing the knowledge about how the state of these objects is persisted.

With that, you can just focus on applying your domain rules and workflows.

Note: Obviously, you would never cut and paste any of this code into your codebase (you wouldn't do that, right?). Much of the code we are going to show here is necessarily pseudo-code (because who wants all the gory bits to get in the way?). But the patterns are real (taken from real products), and we will point out where you would think it through more in your code.)

Implementation

So, its important to approach a new product by NOT using your past experience of data modeling your domain based upon relational tables and normalized table schema. Instead, you need to focus on what your domain actually does, and how it behaves. Everything else is YAGNI at this point. Your domain is the center of your software, not your database!

Of course you are building a HTTP (REST) API but your domain cares very little about how its called, so we have to abstract away your web API details, and your persistence details. All that is left is the core of your domain, unfettered by databases, JSON and HTTP status codes.

So, now that you are not thinking about your database anymore, and you can now focus on your domain.

We define 3 broad layers an boundaries here (more details):

  1. Infrastructure: which is where your REST API endpoints/operations are and where your database adapters are. This layer changes remarkably frequently as you product grows, and as platforms/libraries/frameworks advance.

  2. Application: where you provide the main interface to the actual app you are building. Be that a desktop application, a web API or a Mobile App. (We are not talking about .NET project types, we are talking about the deployed things that expose parts of your domain to a specific sets of users.) This layer changes as you change what software you deploy and as you come to learn about your customers and markets better.

  3. Domain: the core part of your product, that models some aspect of how the real world works. Ideally centralized somewhere in the cloud. This layer will change very slowly over time (in fact, at the rate you and your team learn about the domain), but most importantly this software is never repeated anywhere else. It has the longest lifespan of all the software you will right. Which means you spend most of your time writing it carefully and properly.

So, In your "Application Layer", you might define the CarsApplication object something like this:

public class CarsApplication : ApplicationObject
{
    public IStorage<CarEntity> Storage { get; set; }
    
    public List<Car> SearchAvailable(SearchOptions searchOptions, GetOptions getOptions)
    {
        ...code to fetch the available cars from persistence storage
    }
    
    ...
}

Where ApplicationObject is just a simple marker class used only in this example, but in reality would be called something else and might provide some base services to your "Application Layer" objects in a real implementation.

And, where SearchOptions provides options for the caller to: limit, sort, order, etc, and GetOptions provides options for the caller to expand the hierarchy of the returned entities in the result set, should the result set return child resources. These are pretty standard things for an API to want to provide control of to its callers. Don't worry about this stuff just yet. It is optional.

Now, IStorage<TEntity> is your repository to access all the things you might want to do with your persistence layer.

Obviously, the real instance of this thing will be injected by your DI container into your production code at runtime (which you would always be using of course, to decouple your domain objects from their adapters to any IO repositories that they need to use - right?).

So, to keep it simple, we define the IStorage<TEntity> interface something like this, which gives you the basics for use by any domain object:

    public interface IStorage<TEntity> where TEntity : IPersisableEntity
    {
        void Add(TEntity entity);

        TEntity Update(TEntity entity);

        void Delete(Identifier id);

        TEntity Get(Identifier id);

        QueryResults<TEntity> Query(QueryClause query);

        long Count();
    }

Now, focus on this method specifically: QueryResults<TEntity> Query<TEntity>(Query query);

This method is the one that you are going to use to retrieve the collection of CarEntity that you want.

And you will need to give this method a configured query to locate the correct collection of cars.

You may also want to: limit, sort, order and control the fields coming back in the list of cars (for efficiency).

OK, but first what does CarEntity look like? A simple DTO/POCO like this:

public class CarEntity : IPersistableEntity
{
    public Identifier Id { get; set; }

    public Manufacturer Manufacturer { get; } // eg. Make, Model, Year, etc

    public DateTime CreatedOnUtc { get; set; }

    public DateTime OccupiedUntilUtc { get; set; }

    ...other attributes
}

Just ignore the obsession with primitives here (we are trying to keep things simple for you for this example).

OK, so now let's implement the SearchAvailable() method.

It might be a simple as this:

    public List<Car> SearchAvailable(SearchOptions searchOptions, GetOptions getOptions)
    {
        // Obviously a simple way of determining availability for this sample!
        var query = Query
            .From<CarEntity>()
            .Where(car => car.OccupiedUntilUtc, ConditionOperator.LessThan, DateTime.UtcNow) 
            .Select<CarEntity>(car => car.Id)
            .WithSearchOptions(searchOptions);

        var cars = Storage.Query(query);

        return cars.Results.ConvertAll(c => c.ConvertTo<Car>());
    }

As you can see, the only knowledge this "Application Layer" code has of your persistence layer is:

  1. Your domain has domain entities called CarEntity that are persisted somewhere.
  2. IStorage<TEntity> is your single gateway to reading that persisted state.
  3. Your domain must know that the CarEntity it wants are defined in a simple data structure. Your domain could access other resources to in the same way.
  4. A carEntity must be converted to a DTO (Car) and returned to the caller (who wont know anything of entities or domains etc.)

What you don't have to know anymore:

  1. How to use any ORM or database.
  2. You no longer assume a relational database design, or any relationships for that matter.
  3. Where the data is actually persisted. eg. which database, or what format.
  4. Constructing queries is done based on reflection of the actual Entities that you have defined, and is fully typed. No guessing, no ugly strings!
  5. Your domain logic has no reference to any persistence technology at all. That will be plugged in at runtime.
  6. Your domain entities have no idea of persistence at all.

What this now means is that your domain logic will never care about where the data comes from anymore.

The domain is totally portable, and decoupled from when you decide to change persistence stores, or more likely when your product grows to the point that you want to split the CarsApi out into its own deployed service, and may decide then to change how its persisted for performance reasons.

See full example

If you want to see the full example implemented in real working code, head over our reference Implementation: https://github.com/jezzsantos/queryany/tree/master/samples/ri

Clone this wiki locally