Skip to content
Aaron Hanusa edited this page Nov 25, 2015 · 82 revisions

peasy

###An easy to use middle tier framework for .net

There are few middle tier .NET frameworks in existence, and the ones that exist cost money and/or come with a HUGE learning curve. Peasy is a middle tier framework that offers an extremely easy to use and flexible rules engine and was designed to address common challenges such as concurrency handling, transactional support, fault tolerance, threading, scalability, async and multiple client support, and easy testability, all without the huge learning curve!

Peasy.Core is the core framework that offers a minimalist set of base class functionality. You can use this framework to develop your own opinionated functionality.

Peasy extends Peasy.Core and offers additional opinionated functionality, such as concurrency handling, transactional support, fault tolerance, and optional field updating support. It was designed to save you from countless hours of design and development time.

Here are the concerns that Peasy was designed to address:

A full implementation

A full implementation of a middle tier built with peasy and sample consumer clients (WPF, Web API, etc.) can be found in the Github repo here. You can clone the repo or download the entire project as a zip.

Once downloaded, open Orders.com.sln with Visual Studio, set the WPF project and Web Api projects as the startup projects and run.

-### The simplest possible example

Start by creating a solution with a class library project. Then install the peasy nuget package or clone the repo.

Next create a domain object (DTO) that implements IDomainObject<T>:

public class Person : Peasy.Core.IDomainObject<int>
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string City { get; set; }
}

Then create a data proxy (aka repository) that implements IDataProxy<T, TKey> (most method implementations left out for brevity):

public class PersonMockDataProxy : Peasy.Core.IDataProxy<Person, int>
{
    public IEnumerable<Person> GetAll()
    {
        return new[]
        {
            new Person() { ID = 1, Name = "Jimi Hendrix" },
            new Person() { ID = 2, Name = "James Page" },
            new Person() { ID = 3, Name = "David Gilmour" }
        };
    }

    public async Task<IEnumerable<Person>> GetAllAsync()
    {
        return GetAll();
    }
        
    public Person Insert(Person entity)
    {
        return new Person() { ID = new Random(300).Next(), Name = entity.Name };
    }

    public async Task<Person> InsertAsync(Person entity)
    {
        return Insert(entity);
    }
        
    public void Delete(int id)
    {
        throw new NotImplementedException();
    }

    public Task DeleteAsync(int id)
    {
        throw new NotImplementedException();
    }
        
    public Person GetByID(int id)
    {
        throw new NotImplementedException();
    }

    public Task<Person> GetByIDAsync(int id)
    {
        throw new NotImplementedException();
    }

    public Person Update(Person entity)
    {
        throw new NotImplementedException();
    }

    public Task<Person> UpdateAsync(Person entity)
    {
        throw new NotImplementedException();
    }
}

Finally, create a service class, which exposes CRUD commands responsible for subjecting IDataProxy invocations to business rules before execution:

public class PersonService : Peasy.Core.ServiceBase<Person, int>
{
    public PersonService(Peasy.Core.IDataProxy<Person, int> dataProxy) : base(dataProxy)
    {
    }
}

Now let's consume our PersonService synchronously:

var service = new PersonService(new PersonMockDataProxy());
var getResult = service.GetAllCommand().Execute();
if (getResult.Success)
{
    foreach (var person in getResult.Value)
        Debug.WriteLine(person.Name);  // prints each person's name retrieved from PersonMockDataProxy.GetAll
}

var newPerson = new Person() { Name = "Freed Jones", City = "Madison" };
var insertResult = service.InsertCommand(newPerson).Execute();
if (insertResult.Success)
{
    Debug.WriteLine(insertResult.Value.ID.ToString()); // prints the id value assigned via PersonMockDataProxy.Insert
}

Let's create a business rule whose execution must be successful before the call to IDataProxy.Insert is invoked

public class PersonNameRule : Peasy.Core.RuleBase
{
    private string _name;

    public PersonNameRule(string name)
    {
        _name = name;
    }

    protected override void OnValidate()
    {
        if (_name == "Fred Jones")
        {
            Invalidate("Name cannot be fred jones");
        }
    }
}

And wire it up in our PersonService to ensure that it gets fired before inserts:

using Peasy.Core;

public class PersonService : Peasy.Core.ServiceBase<Person, int>
{
    public PersonService(IDataProxy<Person, int> dataProxy) : base(dataProxy)
    {
    }

    protected override IEnumerable<IRule> GetBusinessRulesForInsert(Person entity, ExecutionContext<Person> context)
    {
        yield return new PersonNameRule(entity.Name);
    }
}

Testing it out (being sure to add a reference to System.ComponentModel.DataAnnotations)...

var service = new PersonService(new PersonMockDataProxy());
var newPerson = new Person() { Name = "Fred Jones", City = "Madison" };
var insertResult = service.InsertCommand(newPerson).Execute();
if (insertResult.Success)
{
    Debug.WriteLine(insertResult.Value.ID.ToString());
}
else
{
    // This line will execute and print 'Name cannot be fred jones' 
    // Note that insertResult.Value will be NULL as PersonMockDataProxy.Insert did not execute due to failed rule
    Debug.WriteLine(insertResult.Errors.First()); 
}

Let's create one more rule, just for fun:

public class ValidCityRule : Peasy.Core.RuleBase
{
    private string _city;

    public ValidCityRule(string city)
    {
        _city = city;
    }

    protected override void OnValidate()
    {
        if (_city == "Nowhere")
        {
            Invalidate("Nowhere is not a city");
        }
    }
}

We'll associate this one with inserts too:

public class PersonService : Peasy.Core.ServiceBase<Person, int>
{
    public PersonService(IDataProxy<Person, int> dataProxy) : base(dataProxy)
    {
    }

    protected override IEnumerable<IRule> GetBusinessRulesForInsert(Person entity, ExecutionContext<Person> context)
    {
        yield return new PersonNameRule(entity.Name);
        yield return new ValidCityRule(entity.City);
    }
}

And test it out (being sure to add a reference to System.ComponentModel.DataAnnotations)...

var service = new PersonService(new PersonMockDataProxy());
var newPerson = new Person() { Name = "Fred Jones", City = "Nowhere" };
var insertResult = service.InsertCommand(newPerson).Execute();
if (insertResult.Success)
{
    Debug.WriteLine(insertResult.Value.ID.ToString());
}
else
{
    // This line will execute and print 'Name cannot be fred jones' and 'Nowhere is not a city'
    // Note that insertResult.Value will be NULL as PersonMockDataProxy.Insert did not execute due to failed rule
    foreach (var error in insertResult.Errors)
        Debug.WriteLine(error);
}

Finally, let's pass in valid data and watch it be a success

var service = new PersonService(new PersonMockDataProxy());
var newPerson = new Person() { Name = "Freida Jones", City = "Madison" };
var insertResult = service.InsertCommand(newPerson).Execute();
if (insertResult.Success)
{
    Debug.WriteLine(insertResult.Value.ID.ToString()); // prints the id value assigned via PersonMockDataProxy.Insert
}
else
{
    foreach (var error in insertResult.Errors)
        Debug.WriteLine(error);
}

Where's the async support??

Async support exists, we just need to wire up a few things to ensure that our code is subjected to the asynchronous pipeline.

Drawing from the PersonService example, let's wire up the business rules by overriding GetBusinessRulesForInsertAsync:

public class PersonService : Peasy.Core.ServiceBase<Person, int>
{
    public PersonService(IDataProxy<Person, int> dataProxy) : base(dataProxy)
    {
    }

    protected override IEnumerable<IRule> GetBusinessRulesForInsert(Person entity, ExecutionContext<Person> context)
    {
        yield return new PersonNameRule(entity.Name);
        yield return new ValidCityRule(entity.City);
    }

    protected override async Task<IEnumerable<IRule>> GetBusinessRulesForInsertAsync(Person entity, ExecutionContext<Person> context)
    {
        return GetBusinessRulesForInsert(entity, context);
    }
}

Notice that we simply marked the GetBusinessRulesForInsertAsync override with the async keyword and marshalled the call to GetBusinessRulesForInsert. Sometimes you might want to asynchronously acquire data that can be shared among rules and it is within the GetBusinessRulesForInsertAsync override where this can be done.

One final step - let's add async support to the PersonNameRule (skipping async support for the city rule for the sake of brevity):

public class PersonNameRule : Peasy.Core.RuleBase
{
    private string _name;

    public PersonNameRule(string name)
    {
        _name = name;
    }

    protected override void OnValidate()
    {
        if (_name == "Fred Jones")
        {
            Invalidate("Name cannot be fred jones");
        }
    }

    protected async override Task OnValidateAsync()
    {
        OnValidate();
    }
}

Again, we simply marked OnValidateAsync with the async keyword and marshalled the call to the synchronous OnValidate method. At times you will need to pass a data proxy to a rule and execute it asynchronously for data validation.

As for our data proxy implementation, we can use our existing mock data proxy implementation as defined earlier in this tutorial. However, it should be noted that we cheated in PersonMockDataProxy.GetAllAsync by simply marking the method async and marshalling the call to GetAll. Normally, you would invoke an asynchronous Entity Framework, RavenDB, etc. method call (example here) or make an out-of-band async call to an HTTP/SOAP service, etc.

Testing it out ...

public async Task SaveDataAsync()
{
    var service = new PersonService(new PersonMockDataProxy());
    var newPerson = new Person() { Name = "Freed Jones", City = "Madison" };
    var insertResult = await service.InsertCommand(newPerson).ExecuteAsync();
    if (insertResult.Success)
    {
        Debug.WriteLine(insertResult.Value.ID.ToString()); // prints the id value assigned via PersonMockDataProxy.Insert
    }
}
Clone this wiki locally