Skip to content

Data Proxy

Aaron Hanusa edited this page May 16, 2016 · 32 revisions

The Data Proxy is the actor within the peasy-js framework that is responsible for data storage and retrieval, and serves as an abstraction layer for data stores that encompass (but not limited to) the following:

Possible implementations

  • Relational Databases - SQLite, MySQL, Oracle, SQL Server, etc.
  • Document (NoSQL) Databases - MongoDB, VelocityDB, etc.
  • Services - HTTP, SOAP, etc.
  • Cache Stores - Redis, Azure, etc.
  • Queues - RabbitMQ, MSMQ, etc.
  • File System
  • In-memory data stores for testing

Abstracting the data store layer allows you to swap data proxy implementations within your service classes to deliver solutions that are scalable and testable, and to support a multitude of deployment scenarios, all while being able to have them subjected to the command execution pipeline.

Public methods

Asynchronously returns all values from a data source in an array and is especially useful for lookup data.

Accepts the id of the entity that you want to query and asynchronously returns an object literal.

Accepts an object literal and asynchronously inserts it into the data store. This method should return a new object literal instance with updated state.

Accepts an object literal and asynchronously updates it in the data store. This method should return a new object literal instance with updated state.

Accepts the id of the entity that you want deleted and asynchronously deletes it from the data store.

Creating a Data Proxy

Creating a data proxy is simple. Create an object literal with the functions which you choose to support. These functions will then be consumed and invoked by the business service based on the successful result of rule executions.

Below is an example of an in-memory data proxy implementation:

var PersonDataProxy = function() {

  var store = [];
  
  return {
    getById: getById,
    getAll: getAll,
    insert: insert,
    update: update,
    remove: remove
  };

  function getById(id, done) {
    var person = findBy(id);
    done(Object.assign({}, person));
  }

  function getAll(done) {
    var all = store.map(function(item) {
      return Object.assign({}, item);
    });
    done(all);
  }

  function insert(data, done) {
    data.id = store.length + 1;
    store.push(Object.assign({}, data));
    done(data);
  }

  function update(data, done) {
    var person = findBy(data.id);
    person.name = data.name;
    done(data);
  }

  function remove(id, done) {
    var person = findBy(id);
    var index = store.indexOf(person);
    store.splice(index, 1);
    done();
  }
  
  function findBy(id) {
    var person = store.filter((function(p) {
      return p.id === id;
    }))[0];
    return person;
  }
};

Extending an Angular Resource or React Data Store

Sample implementation using Entity Framework

The following code sample shows what an implementation of IDataProxy serving as a customer data store might look like using Entity Framework 6.0 or higher. Please note that this code might be written more efficiently and has been scaled back for brevity and serves only as an example.

Also note that because this example uses Entity Framework 6.0, we take advantage of the asynchronous support in the async methods. Versions prior to 6.0 do not provide async support, therefore when using any prior version of Entity Framework, you might not want to provide async functionality.

Lastly, this example uses Automapper(Mapper.Map) to perform mapping logic against the customer DTOs and Entity Framework Models.

public class CustomerRepository : IDataProxy<Customer, int> 
{
    public IEnumerable<Customer> GetAll()
    {
        using (var context = new EFContext())
        {
            var data = context.tCustomers
                              .Select(Mapper.Map<tCustomer, Customer>)
                              .ToArray();
            return data;
        }
    }

    public Customer GetByID(int id)
    {
        using (var context = new EFContext())
        {
            var data = context.tCustomers
                              .Where(b => b.CustomerID == id)
                              .ToList()
                              .Select(Mapper.Map<tCustomer, Customer>)
                              .FirstOrDefault();
            return data;
        }
    }

    public Customer Insert(Customer entity)
    {
        using (var context = new EFContext())
        {
            var data = Mapper.Map(entity, new tCustomer());
            context.Set<tCustomer>().Add(data);
            context.SaveChanges();
            entity.ID = data.CustomerID;
            return entity;
        }
    }

    public Customer Update(Customer entity)
    {
        using (var context = new EFContext())
        {
            var data = Mapper.Map(entity, new tCustomer());
            context.Set<tCustomer>().Attach(data);
            context.Entry<tCustomer>(data).State = EntityState.Modified;
            context.SaveChanges();
            entity = Mapper.Map(data, entity);
            return entity;
        }
    }

    public void Delete(int id)
    {
        using (var context = new EFContext())
        {
            var Customer = new tCustomer() { CustomerID = id };
            context.Set<tCustomer>().Attach(Customer);
            context.Set<tCustomer>().Remove(Customer);
            context.SaveChanges();
        }
    }

    public async Task<IEnumerable<Customer>> GetAllAsync()
    {
        using (var context = new EFContext())
        {
            var data = await context.tCustomers.ToListAsync();
            return data.Select(Mapper.Map<tCustomer, Customer>).ToArray();
        }
    }

    public async Task<Customer> GetByIDAsync(int id)
    {
        using (var context = new EFContext())
        {
            var data = await context.tCustomers
                                    .Where(b => b.CustomerID == id)
                                    .ToListAsync();

            return data.Select(Mapper.Map<tCustomer, Customer>).FirstOrDefault();
        }
    }

    public async Task<Customer> InsertAsync(Customer entity)
    {
        using (var context = new EFContext())
        {
            var data = Mapper.Map(entity, new tCustomer());
            context.Set<tCustomer>().Add(data);
            await context.SaveChangesAsync();
            entity.ID = data.CustomerID;
            return entity;
        }
    }

    public async Task<Customer> UpdateAsync(Customer entity)
    {
        using (var context = new EFContext())
        {
            var data = Mapper.Map(entity, new tCustomer());
            context.Set<tCustomer>().Attach(data);
            context.Entry<tCustomer>(data).State = EntityState.Modified;
            await context.SaveChangesAsync();
            entity = Mapper.Map(data, entity);
            return entity;
        }
    }

    public async Task DeleteAsync(int id)
    {
        using (var context = new EFContext())
        {
            var Customer = new tCustomer() { CustomerID = id };
            context.Set<tCustomer>().Attach(Customer);
            context.Set<tCustomer>().Remove(Customer);
            await context.SaveChangesAsync();
        }
    }
}

Swappable Data Proxies

Because service classes have a dependency upon the IDataProxy abstraction, this means that data proxies can be swapped out and replaced with different implementations. The ability to swap data proxies provides the following benefits:

In many production systems, in-process client applications such as WPF and Windows Forms are often configured to communicate directly with a database. This configuration can lead to bottlenecks and poor performance over time as more clients are added to the environment. Peasy allows you to easily remedy this type of situation by swapping a data proxy that communicates directly with a database with one that scales more efficiently.

For example, instead of injecting data proxies that communicate directly with the database, you could inject data proxies that communicate with a cache or queue. For retrieval, the cache might return cached data, and for storage, you might update the cache or a queue and have a windows service monitoring it for changes. The windows service would then be responsible for data storage manipulation against a persistent data store (database, file system, etc).

Another possible solution would be to expose CRUD operations via HTTP services, and inject a data proxy capable of performing CRUD actions against your services into your service class. A side benefit of having clients use HTTP services is that you could gain the benefits of HTTP Caching, almost for free, which can provide scalability gains.

Because service classes rely on IDataProxy for data store abstraction, introducing solutions that offer scalability gains becomes almost trivial.

Multiple deployment scenarios

Because data proxies are swappable, you are able to reconfigure data storage without having to refactor your code. Let's take a scenario where an organization has deployed a WPF application that directly communicates with a database. As time passes, the organization receives third party pressure to expose the database to the outside world via web services (HTTP, SOAP, etc.).

After the web services are created, it is decided that the WPF application should dogfood the web services and no longer communicate directly with the database. Consuming service classes that abstract data stores via IDataProxy would easily allow swapping out the database data proxies with proxies that communicate with the web services.

Testability

Unit testing should be fast. Really fast. Databases are slow and come with a slew of issues when testing your code via automated unit tests. This is another scenario where swappable data proxies provide great benefit. By creating mock data proxies, you can easily inject them into your service classes and focus on testing initialization logic, validation and business rule logic, and command logic.

Available concrete implementations

The following libraries contain base class implementations that abstract communications with varying data stores.

Peasy.DataProxy.EF6 - An abstract implementation of Peasy.IDataProxy that communicates with databases via Entity Framework 6.0

Peasy.DataProxy.Http - An abstract implementation of Peasy.IDataProxy that communicates with HTTP services

Peasy.DataProxy.InMemory - An in-memory implementation of Peasy.IDataProxy

Clone this wiki locally