About this tutorial

You're developing great software at Nocciola, an authentic Italian gelato producer and restaurant supplier, and you're in charge of creating Wafer the software for your Sales department to create and track in the web the yummi gelato orders from all around the world. KEYSTONE will help you to make it fast and high quality!

This tutorial is designed for you to follow the steps on it, so you can progressively get familiar with the vast KEYSTONE component arrangements and technologies.

If you prefer to keep your fingers relaxed, we've prepared and published for you all the finished files we'll create during the tutorial. Please feel free to clone or download these files from Github, and to repurpose it as you may wish.

Prerequisites

Software

KEYSTONE is pretty much a self contained implementation written entirely in C# and compiled to run in the .NET Platform.

The following software must be already installed on your Windows computer in order to develop KEYSTONE based business applications

For the purpose of this tutorial the following software is recommended but not mandatory to develop KEYSTONE based business applications:

Packages

To make the installation and update of KEYSTONE very easy, safe and reliable, its Foundation Components including Polaris, Almanac, Carbonite, Clockwork, Mystere and Quantum, are distributed through the Nuget Package Manager platform.

Installation

When creating your Visual Studio projects, be sure to set it to use .NET Framework 4.6 or higher! You can configure this property at any time in the Edit ❯ Property Pages of the project.

Once created your Visual Studio projects, you can install the Foundation Components by searching for the keystone keyword in the Nuget Package Manager. The Nuget Package Manager can be shown by right-clicking the References icon in the Solution Explorer and selecting the Manage Nuget Packages menu.

As an alternate installation method, you can use the command line interface called Package Manager Console located in the Tools ❯ Nuget Package Manager ❯ Package Manager Console menu. From that console write the following command to install the KEYSTONE Foundation Components, just replace the [foundation component] placeholder with the desired component name.

Install-Package [foundation component]

How to update

Thanks to the Nuget Package Manager platform, updating the KEYSTONE Foundation Components is a task as simple as opening the Nuget Package Manager, clicking the Updates tab, checking the components you want to update and clicking the Update button. That's all, you're up to date!

Alternatively, you can use the Package Manager Console. From that console write the following command to update the KEYSTONE Foundation Components, just replace the [foundation component] placeholder with the desired component name.

Update-Package [foundation component] -DependencyVersion Highest

Create your solution and projects

KEYSTONE is a development framework designed to assist you in the creation of great applications at lightning speed and without compromising architectural value. Following its architectural prescriptions we can use Visual Studio to create an application solution that you can use as a baseline template for future projects.

First we'll create a new Blank Solution by selecting the File ❯ New ❯ Project menu and selecting the Templates ❯ Other Project Types ❯ Visual Studio Solutions ❯ Blank Solution template. Name the newly created solution Nocciola.Wafer.

Architectural prescription

Our next step is to create the Data, Business and Presentation layer projects. KEYSTONE prescription is to adopt the multilayer architectural style as the dominant/general architecture of your applications. For example, you can use the three layer pattern, Data, Business and Presentation, to create UI applications or use a four layer pattern, Data, Business, Services and Presentation, to build cloud-based backends for mobile apps.

The best thing of all is that each layer can have its own particular architecture. For example you can use ASP.NET MVC to implement your Presentation layer and still use the KEYSTONE architectural prescriptions to build the Data and Business layers.

Data, Business and Presentation layer projects

To create our Data layer project, right-click in the Nocciola.Wafer solution icon in the Solution Explorer and select the Add ❯ New Project menu. Then select the Templates ❯ Visual C# ❯ Class Library template and name Nocciola.Wafer.Entities our project. Be sure to create a .NET Framework 4.6 project by selecting it in the upper middle part of the Add New Project dialog box.

Repeat the previous steps to create the Business layer project, and name it Nocciola.Wafer.Processes.

We'll use a command based application to illustrate a simple Presentation layer. To create the corresponding project repeat the previous steps but select the Templates ❯ Visual C# ❯ Console Application project template instead of the Class Library template. Name the project Nocciola.Wafer.Presentation. Finally, right-click in the recently create project icon in the Solution Explorer and select the Set as StartUp Project menu.

Wrapping all together

When compiled, each project will produce an specific binary file. DLLs are created by the compiler for the Data and Business layes and an EXE is created for the Presentation layer. These are called application components.

As a last step we will link our application components together to form the whole solution. The Presentation layer depends on the Business and Data layers, to link it to those projects right-click on its References icon in the Solution Explorer, then select the Add Reference menu and check the Nocciola.Wafer.Processes and Nocciola.Wafer.Entities items located in the Projects ❯ Solution section.

The Business layer depends on the Data layer, to link it to those projects right-click on its References icon, select the Add Reference menu and check the Nocciola.Wafer.Entities.

Develop your Data layer with Carbonite

Carbonite is a data technology that includes Diamant, a new lightweight ORM capable of mapping your objects to your SQL Server, Oracle, MySQL, and more databases by convention, configuration or code. We'll use this ORM in the tutorial.

Carbonite also conceals other popular ORMs into the same programming model so you never will need to deal again with hard-to-mantain heterogeneous data layers implementation models.

Since we'll use SQL Server in this tutorial, we need to reference the Carbonite core and ORM, and its SQL Server extensions. The easiest way to accomplish this is to install the Keystone.Carbonite.Diamant.Sql package by running the following Nuget Package Console command over the Nocciola.Wafer.Entities project:

Install-Package Keystone.Carbonite.Diamant.Sql

Nuget will install the requested package and all its dependencies including Keystone.Carbonite which is the Carbonite core package. For more details on installing the KEYSTONE packages please jump to the Packages section of this tutorial.

Database creation

We'll use an oversimplified SQL Server database to keep the record of Users of our application and its actions within it. To do this we need three database tables, person, action and person_action. To simplifly this tutorial run the following SQL Script to build this database and its tables.

CREATE DATABASE nocciola_wafer
GO
USE nocciola_wafer

/****** Object:  Table dbo.gelato ******/
CREATE TABLE dbo.gelato(
    id varchar(50) NOT NULL PRIMARY KEY,
    description varchar(200) NULL,
    price_per_gallon money NOT NULL)

/****** Object:  Table dbo.restaurant ******/
CREATE TABLE dbo.restaurant(
    id varchar(50) NOT NULL PRIMARY KEY,
        address varchar(250) NULL)

/****** Object:  Table dbo.purchase_order ******/
CREATE TABLE dbo.purchase_order(
    id uniqueidentifier NOT NULL PRIMARY KEY,
    restaurant_id varchar(50) NOT NULL,
    gelato_id varchar(50) NOT NULL,
    created datetime NOT NULL,
    gallons int NOT NULL,
    total_price money NOT NULL,
        shipping_address varchar(250) NOT NULL,
    status varchar(50) NOT NULL,
        FOREIGN KEY (restaurant_id) REFERENCES restaurant(id),
        FOREIGN KEY (gelato_id) REFERENCES gelato(id))

INSERT dbo.gelato (id, description, price_per_gallon) VALUES ('Cioccolato', 'Good old chocolate. Sometimes this comes in two versions, cioccolato and cioccolato fondente (extra dark chocolate).', 80.0000)
INSERT dbo.gelato (id, description, price_per_gallon) VALUES ('Caffè', 'For lovers of espresso and cappuccino here is the dessert version.', 80.0000)
INSERT dbo.gelato (id, description, price_per_gallon) VALUES ('Nocciola', 'Hazlenut – my favourite!', 85.0000)

INSERT dbo.restaurant (id, address) VALUES ('Gelato Lovers', '3738 Scott Street, New York, NY')
INSERT dbo.restaurant (id, address) VALUES ('The italian touch', '1082 Indian Ridge, Kashiagamiut, Nebraska')
INSERT dbo.restaurant (id, address) VALUES ('Caffè e gelati', '6325 Blue Falls, Niotaze, Ohio')

/* Special thanks to Pocket Cultures for providing us with great gelati names and descriptions. */

Entities

Our database tables will be represented as objects inside the application. These objects are called Application domain entities and are created after a class that models its data properties. To create your entities add four new files Gelato.cs, Restaurant.cs, PurchaseOrder.cs and PurchaseOrderSummary.cs, to the Nocciola.Wafer.Entities project root.

using Keystone.Carbonite;

namespace Nocciola.Wafer.Entities
{
    public class Gelato : StrongEntity<string>
    {
        public string Description { get; set; }
        public decimal PricePerGallon { get; set; }
    }
}

using Keystone.Carbonite;

namespace Nocciola.Wafer.Entities
{
    public class Restaurant : StrongEntity<string>
    {
        public string Address { get; set; }
    }
}

using System;
using Keystone.Carbonite;

namespace Nocciola.Wafer.Entities
{
    public class PurchaseOrder : StrongEntity<Guid>
    {
        public string RestaurantId { get; set; }
        public string GelatoId { get; set; }
        public DateTime Created { get; set; }
        public int Gallons { get; set; }
        public decimal TotalPrice { get; set; }
        public string ShippingAddress { get; set; }
        public string Status { get; set; }
    }
}

using System;
using Keystone.Carbonite;

namespace Nocciola.Wafer.Entities
{
    public class PurchaseOrderSummary : StrongEntity<Guid>
    {
        public string RestaurantName { get; set; }
        public string GelatoName { get; set; }
        public decimal Amount { get; set; }
        public string CurrentStatus { get; set; }
    }
}

Carbonite DAOs and Controller

Entities do not hold their own persistence. Depending on the application needs some entities are persistent while others can be transient.

In order to store and retrieve your entities from the database, you need to write a Carbonite DAO for each persistent entity, and a Carbonite Controller that represents your database. Lets start by creating our Carbonite Controller class, WaferCarboniteController.cs. Don't forget to adjust the Connection string specified in the base("...") constructor call, to point to your SQL Server and the database we create.

using Keystone.Carbonite.Diamant.Sql;

namespace Nocciola.Wafer.Entities
{
    public class WaferCarboniteController : SqlCarboniteController
    {
        public WaferCarboniteController() : base("Data source=(local); Initial catalog=nocciola_wafer; Integrated security=SSPI") { }
    }
}

Next we'll add four new files to the same folder, GelatoDao.cs, RestaurantDao.cs, PurchaseOrderDao.cs and PurchaseOrderSummaryDao.cs, with the following code each.

using Keystone.Carbonite.Diamant.Sql;

namespace Nocciola.Wafer.Entities
{
    public class GelatoDao : ConventionFirstSqlDao<Gelato> { }
}

using Keystone.Carbonite.Diamant.Sql;

namespace Nocciola.Wafer.Entities
{
    public class RestaurantDao : ConventionFirstSqlDao<Restaurant> { }
}

using System;
using Keystone.Carbonite.Diamant.Sql;

namespace Nocciola.Wafer.Entities
{
    public class PurchaseOrderDao : ConventionFirstSqlDao<PurchaseOrder> { }
}

using Keystone.Carbonite.Diamant;
using Keystone.Carbonite.Diamant.Sql;

namespace Nocciola.Wafer.Entities
{
    public class PurchaseOrderSummaryDao : ConfigurationFirstSqlDao<PurchaseOrderSummary>
    {
        protected override TableMappings OnGetMappings() => TableMappings.ForTable("purchase_order").Map(
                Column(Name: "id", With: entity => entity.Id, IsPrimaryKey: true),
                Column(Name: "restaurant_id", With: entity => entity.RestaurantName),
                Column(Name: "gelato_id", With: entity => entity.GelatoName),
                Column(Name: "total_price", With: entity => entity.Amount),
                Column(Name: "status", With: entity => entity.CurrentStatus));
    }
}

Notice that GelatoDao, RestaurantDao and PurchaseOrderDao are Convention First DAOs, which means that Carbonite will automatically map your database tables and columns against your entity classes and properties by using naming convention transformations, in this case it will transform entity names and properties written in PascalCasing, into database tables and columns written in snake_casing, just be sure that its corresponding names match. No extra mapping nor SQL code is required!

Also notice that PurchaseOrderSummaryDao is a Configuration First DAO. Because our purchase_order table don't match by convention to our PurchaseOrderSummary class, Carbonite will need your help to map it. For this purpose we overrode the OnGetMappings method to provide the required database to application equivalencies.

In KEYSTONE many methods with the naming pattern OnXxx are designed to be overriden by subclasses, but the base class always has the execution control, this is a special form of inheritance based Inversion of Control that lets KEYSTONE factorize as much code as possible and yet keep flexible enough to cover a wide range of business requirements.

That's all we need for now. Our Data layer is fully implemented. It was easy, wasn't it?

Business layer

Clockwork is a complete programming paradigm that let you model and execute complex business process.

In Clockwork you write Gears that encapsulate specific business steps. Then you write code to assemble Gears together to compose complete business processes called Mechanisms. A Mechanism is by definition a Gear, so you can create new Mechanisms by mixing and matching existing Mechanisms and Gears you have already in your library.

Given our three layer architecture, we'll need to consume our Data layer from within this Business layer. To do this we need to reference the Clockwork core and its Carbonite binding to consume our SQL Server based Data layer. Naturally, the easiest way to add those references is to install the Keystone.Clockwork.To.Carbonite.Diamant.Sql package by running the following Nuget Package Console command over the Nocciola.Wafer.Process project:

Install-Package Keystone.Clockwork.To.Carbonite.Diamant.Sql

Nuget will install the requested package and all its dependencies including Keystone.Clockwork which is the Clockwork core package. For more details on installing the KEYSTONE packages please jump to the Packages section of this tutorial.

How to consume our Data layer

Another important KEYSTONE prescription suggests that all our business processes should be represented and consumed in the form of Mechanisms.

In order to make this very easy and fast, the Clockwork to Carbonite binding already defines many ready to use Mechanisms that we can subclass or call directly to consume our Data layer operations and entities. Common operations like adding, updating, removing and selecting our entities are supported out of the box. We only need to add a few configuration code.

Lets create a simple file, GetPurchaseOrdersSummary.cs, at the root of our Business layer project.

using Keystone.Carbonite.Diamant;
using Keystone.Clockwork.To.Carbonite.Diamant.Sql;
using Nocciola.Wafer.Entities;

namespace Nocciola.Wafer.Processes
{
    public class GetPurchaseOrdersSummary : GetMaster<PurchaseOrderSummary, PurchaseOrderSummaryDao, WaferCarboniteController>
    {
        public GetPurchaseOrdersSummary() { Order = new OrderBy("Created DESC"); }
    }
}

GetPurchaseOrdersSummary is a subclass of GetMaster, a KEYSTONE Mechanism that enables paged selection to any entity whose persistence is controlled by a Convention or Configuration first DAO. Its Order property is a Mechanism input that receives the entity property or properties, and its ordering direction to sort the selected entities just before the paging operation takes place.

How do we implement our business processes

Every business application is composed of many tenths to hundreds of rules defined by our business users according to their particular needs and to the business own characteristics.

We'll implement each rule with a Gear, and we'll compose a whose business process by assembling gears together into a Mechanism. For the purpose of our tutorial we'll be automating the following business processes and rules:

Purchase order creation (POC)
  • POC1: A purchase order can be taken by specifying the requesting Restaurant and the requested Gelato and Gallons (at least 1 gallon). The rest of the purchase order information must be filled by the application.
  • POC2: A purchase order must born with the Created status.
  • POC3: A newly created purchase order must have all of its information correctly filled before it can be stored in the application.

To implement the previous rules we'll create three files in our Business layer project, NewPurchaseOrder.cs, PurchaseOrderCreationValidations.cs and CreatePurchaseOrder.cs, with the following code.

using System;
using Keystone.Carbonite.Diamant;
using Keystone.Clockwork;
using Keystone.Clockwork.To.Carbonite.Diamant.Sql;
using Nocciola.Wafer.Entities;

namespace Nocciola.Wafer.Processes
{
    public class NewPurchaseOrder : Gear<PurchaseOrder>
    {
        [Required]
        public Input<string> Restaurant;

        [Required]
        public Input<string> Gelato;

        [Required]
        public Input<int> Gallons;

        protected override PurchaseOrder OnRun() {
            var restaurant = new GetDetail<Restaurant, RestaurantDao, WaferCarboniteController> { Filter = new Where("Id = @Id", new WhereParameter("@Id", Restaurant.Value)) }.Run();
            var gelato = new GetDetail<Gelato, GelatoDao, WaferCarboniteController> { Filter = new Where("Id = @Id", new WhereParameter("@Id", Gelato.Value)) }.Run();

            return new PurchaseOrder
            {
                Id = Guid.NewGuid(),
                RestaurantId = restaurant.Id,
                GelatoId = gelato.Id,
                Created = DateTime.Now,
                Gallons = Gallons.Value,
                ShippingAddress = restaurant.Address,
                TotalPrice = Gallons.Value * gelato.PricePerGallon
            };
        }
    }
}

We've created our first Gear, it fulfills our POC1 business rule. NewPurchaseOrder is a simple code that receives three inputs, the Restaurant, the Gelato name and the number of Gallons to order. These inputs are used in the OnRun Inversion of Control method to get complementary information from the database and instantiate a new PuchaseOrder object with its required properties automatically filled.

Validation for Business processes input integrity assurance is another prescription suggested by KEYSTONE, so each input is validated using a form of tag known as attribute in .NET. Attribute based validations are called Declaration First by KEYSTONE. These validations run before any logic in the Gear or Mechanism. Every failed validation will be collected into an AggregatedValidationException, which is thrown preventing the Gear or Mechanism logic to run.

KEYSTONE includes a wide arrange of validator known as Validation Gears. Required, Optional, Email and LengthBetween are a few examples of these Validation Gears, and you can also create your own! Along with the Declaration First validations, KEYSTONE supports Configuration First and Code First validation techniques, so you can choose what is better and faster to cover your validation needs.

using Keystone;
using Keystone.Clockwork;
using Keystone.Clockwork.To.Carbonite.Diamant.Sql;
using Nocciola.Wafer.Entities;

namespace Nocciola.Wafer.Processes
{
    public class CreatePurchaseOrder : Mechanism<Null>
    {
        [Required]
        public Input<string> Restaurant;

        [Required]
        public Input<string> Gelato;

        [Required]
        public Input<int> Gallons;

        protected override Gear<Null> OnAssemble() {
            var newPurchaseOrder = new NewPurchaseOrder
            {
                Restaurant = Restaurant,
                Gelato = Gelato,
                Gallons = Gallons
            };

            var setPurchaseOrderStatus = new SetPropertyOf<PurchaseOrder, string>
            {
                Object = newPurchaseOrder,
                PropertyName = "Status",
                NewValue = "Created"
            };

            var addPurchaseOrder = new AddDetail<PurchaseOrder, PurchaseOrderCreationValidations, PurchaseOrderDao, WaferCarboniteController>
            {
                Entity = setPurchaseOrderStatus
            };

            return addPurchaseOrder;
        }
    }
}

Now we've created our first Mechanism, it return Null, an equivalent of void for methods. In the OnAssemble Inversion of Control method of CreatePurchaseOrder, we put together the NewPurchaseOrder, SetPropertyOf (this supports business rule POC2) and AddDetail gears. Pay attention to the form we plugged those gears into a dataflow sequence: a gear returns a given value, so it can be assigned to provide another gear's input value if both share the same datatype. For example, the newPurchaseOrder gear is assigned to the setPurchaseOrderStatus.Object input. The same happens between the setPurchaseOrderStatus and the addPurchaseOrderStatus.Entity input.

addPurchaseOrder is the root gear and the one that will be called at the end of the sequence. That's why we return it as the OnAssemble method result.

Flint, the gear execution engine in Clockwork, will traverse in a depth-first fashion the gear sequence we assemble. It will start asking the root gear for its running result. addPurchaseOrder will tell Flint that it needs its Entity input that provides setPurchaseOrderStatus, in order to deliver its result. Flint will ask setPurchaseOrderStatus for its result and it will tell that its Object input depends on newPurchaseOrder. Again, Flint will move to ask newPurchaseOrder for its result and at this point no more providers are plugged so newPurchaseOrder will deliver its result. Flint will move backwards (backtracking) feeding setPurchaseOrderStatus that in turns feeds addPurchaseOrder, that provides the final mechanism result.

Notice that the addPurchaseOrder gear requires a PurchaseOrderCreationValidations class in order to work. This class contains the related valitions to the fulfill business rule POC3. Lets create the file PurchaseOrderCreationValidations.cs at the root of our Business layer project.

using System;
using Keystone.Clockwork;

namespace Nocciola.Wafer.Processes
{
    public class PurchaseOrderCreationValidations
    {
        [Required]
        [NotEqualTo("00000000-0000-0000-0000-000000000000")]
        public Guid Id { get; set; }

        [Required]
        public string RestaurantId { get; set; }

        [Required]
        public string GelatoId { get; set; }

        [Required]
        public DateTime Created { get; set; }

        [Required]
        [GreaterOrEqualTo(0)]
        public int Gallons { get; set; }

        [Required]
        [GreaterOrEqualTo("0")]
        public decimal TotalPrice { get; set; }

        [Required]
        public string ShippingAddress { get; set; }

        [Optional(DefaultsTo = "Created")]
        [EqualTo("Created")]
        public string Status { get; set; }
    }
}

In this case the PurchaseOrder database insertion or update operation will only run if the PurchaseOrderCreationValidations are completely met.

Purchase order status change (POSC)
  • POSC1: A purchase order status can be changed to the status Preparing, On its way or Delivered.
  • POSC2: An updated purchase order must have all of its information correctly filled before it can be stored in the application.

We'll create two files in our Business layer project to support the previous business rules, ChangePurchaseOrderStatus.cs and PurchaseOrderChangeValidations.cs, with the following code each.

using System;
using Keystone;
using Keystone.Carbonite.Diamant;
using Keystone.Clockwork;
using Keystone.Clockwork.To.Carbonite.Diamant.Sql;
using Nocciola.Wafer.Entities;

namespace Nocciola.Wafer.Processes
{
    public class ChangePurchaseOrderStatus : Mechanism<Null>
    {
        [Required]
        [NotEqualTo("00000000-0000-0000-0000-000000000000")]
        public Input<Guid> PurchaseOrderId;

        [Required]
        public Input<string> PurchaseOrderStatus;

        protected override Gear<Null> OnAssemble() {
            var getPurchaseOrder = new GetDetail<PurchaseOrder, PurchaseOrderDao, WaferCarboniteController>
            {
                Filter = new Where("Id = @Id", new WhereParameter("@Id", PurchaseOrderId.Value))
            };

            var setPurchaseOrderStatus = new SetPropertyOf<PurchaseOrder, string>
            {
                Object = getPurchaseOrder,
                PropertyName = "Status",
                NewValue = PurchaseOrderStatus
            };

            var updatePurchaseOrder = new UpdateDetail<PurchaseOrder, PurchaseOrderChangeValidations, PurchaseOrderDao, WaferCarboniteController>
            {
                Entity = setPurchaseOrderStatus
            };

            return updatePurchaseOrder;
        }
    }
}

using System;
using Keystone.Clockwork;

namespace Nocciola.Wafer.Processes
{
    public class PurchaseOrderChangeValidations
    {
        [Required]
        [NotEqualTo("00000000-0000-0000-0000-000000000000")]
        public Guid Id { get; set; }

        [Required]
        public string RestaurantId { get; set; }

        [Required]
        public string GelatoId { get; set; }

        [Required]
        public DateTime Created { get; set; }

        [Required]
        [GreaterOrEqualTo(0)]
        public int Gallons { get; set; }

        [Required]
        [GreaterOrEqualTo("0")]
        public decimal TotalPrice { get; set; }

        [Required]
        public string ShippingAddress { get; set; }

        [Required]
        [EqualToAny("Preparing", "On its way", "Delivered")]
        public string Status { get; set; }
    }
}

PurchaseOrderChangeValidations adds support to POSC1 and POSC2 business rules.

Purchase order erasure (POE)
  • POE1: All purchase orders can be erased at any time.

Lets create one last file in our Business layer project to support the previous business rule, EraseAllPurchaseOrders.cs.

using Keystone;
using Keystone.Clockwork;
using Keystone.Clockwork.To.Carbonite.Diamant.Sql;
using Nocciola.Wafer.Entities;

namespace Nocciola.Wafer.Processes
{
    public class EraseAllPurchaseOrders : Mechanism<Null>
    {
        protected override Gear<Null> OnAssemble() {
            var removePurchaseOrders = new RemoveAll<PurchaseOrder, PurchaseOrderDao> { WithCarbonite = new WaferCarboniteController() };

            return removePurchaseOrders;
        }
    }
}

RemoveAll gear inside the EraseAllPurchaseOrders mechanism is the implementation of POE1 business rule.

Presentation layer

KEYSTONE is a behind the scene software development framework. It is designed to create application cores that are activation agnostic.

Once you create your KEYSTONE based core, it can be consumed and reused in any presentation framework like ASP.NET MVC, ASP.NET Web Forms, WPF, Windows Forms, Metro based apps and Console Applications.

The same core can be consumed inside non-presentational frameworks like ASP.NET Web API, Windows Services, WCF Services and WF workflows.

How to consume our Business layer

In this tutorial we'll use a very simple Console Application as our presentation scheme. Lets create in our Presentation layer project the two files that build up our views, MenuAction.cs and Program.cs, with the following code.

namespace Nocciola.Wafer.Presentation
{
    public enum MenuAction
    {
        CreateOrders = 1,
        ChangeFirstOrderStatus = 2,
        ShowPagedOrders = 3,
        EraseAllOrders = 4,
        Exit = 5
    }
}

using System;
using System.Linq;
using Keystone.Polaris;
using Keystone.Carbonite.Diamant;
using Nocciola.Wafer.Processes;

namespace Nocciola.Wafer.Presentation
{
    class Program
    {
        static void Main(string[] args) {
            var stopApplication = false;
            do
            {
                switch (ShowMenuAndGetSelection())
                {
                    case MenuAction.CreateOrders: CreateOrders(); break;
                    case MenuAction.ChangeFirstOrderStatus: ChangeFirstOrderStatus("Preparing"); break;
                    case MenuAction.ShowPagedOrders: ShowPagedOrders(); break;
                    case MenuAction.EraseAllOrders: EraseAllPurchaseOrders(); break;
                    case MenuAction.Exit: stopApplication = true; break;
                }
            } while (!stopApplication);
        }

        private static MenuAction ShowMenuAndGetSelection() {
            Console.WriteLine("=== Wafer Main Menu ===\n");
            Console.WriteLine("1. Create some sample orders");
            Console.WriteLine("2. Change the first order status");
            Console.WriteLine("3. Show current orders (paged view)");
            Console.WriteLine("4. Erase all orders");
            Console.WriteLine("5. Exit application\n");
            Console.Write("Write the number corresponding to your choice: ");

            int selection;
            if (!int.TryParse(Console.ReadLine(), out selection) || selection < 0 || selection > 5) { Console.Clear(); return ShowMenuAndGetSelection(); }
            else { Console.Clear(); return (MenuAction)selection; }
        }

        private static void CreateOrders() {
            Console.Write("Creating orders... ");
            new CreatePurchaseOrder { Restaurant = "The italian touch", Gelato = "Cioccolato", Gallons = 10 }.Run();
            new CreatePurchaseOrder { Restaurant = "Caffè e gelati", Gelato = "Caffè", Gallons = 50 }.Run();
            new CreatePurchaseOrder { Restaurant = "Caffè e gelati", Gelato = "Nocciola", Gallons = 24 }.Run();
            ShowDoneMessage();
        }

        private static void ChangeFirstOrderStatus(string newStatus) {
            Console.Write("Change order status...");
            var firstPurchaseOrdersPage = new GetPurchaseOrdersSummary() { Page = new Paging(1) }.Run();
            if (firstPurchaseOrdersPage.IsNotEmpty())
                new ChangePurchaseOrderStatus
                {
                    PurchaseOrderId = firstPurchaseOrdersPage.First().Id,
                    PurchaseOrderStatus = newStatus
                }.Run();
            ShowDoneMessage();
        }

        private static void EraseAllPurchaseOrders() {
            Console.Write("Erasing all orders... ");
            new EraseAllPurchaseOrders().Run();
            ShowDoneMessage();
        }

        private static void ShowPagedOrders() {
            Console.WriteLine(new string('-', 58));
            Console.WriteLine($"{"RESTAURANT",-20}{"GELATO",-15}{"STATUS",-10}{"AMOUNT",13}");
            Console.WriteLine(new string('-', 58));
            var currentViewingPage = 0;
            var stopOrderViewing = false;
            do
            {
                var purchaseOrdersInCurrentPage = new GetPurchaseOrdersSummary { Page = new Paging(5, ++currentViewingPage) }.Run();
                purchaseOrdersInCurrentPage.ForEach(order => Console.WriteLine($"{order.RestaurantName,-20}{order.GelatoName,-15}{order.CurrentStatus,-10}{order.Amount,13:c}"));
                Console.Write(purchaseOrdersInCurrentPage.IsNotEmpty() ? $"-- More --" : "\t\t\n");
                stopOrderViewing = purchaseOrdersInCurrentPage.IsEmpty() || Console.ReadKey().Key == ConsoleKey.Escape;

            } while (!stopOrderViewing);
            ShowDoneMessage();
        }

        private static void ShowDoneMessage() {
            Console.WriteLine("Done.\n\nPress any key to return to menu...\n");
            Console.ReadKey();
            Console.Clear();
        }
    }
}

Since we follow KEYSTONE architectural prescriptions, notice how simple is to consume our Business layer Mechanisms.

For example, look at the CreateOrders method, to create a new purchase order we consume our CreatePurchaseOrder mechanism. We only need to set its Restaurant, Gelato and Gallons inputs, and call its Run method to let Flint to execute the mechanism gear sequence. All Mechanisms and Gears share that same simple consumption pattern!

Make it configurable

At this point we've completed our application, but we've barely scratched the surface of the comprehensive KEYSTONE components and APIs.

One of the pillar components in KEYSTONE is Quantum.

KEYSTONE philosophy favour the creation of loosely coupled components.

Quantum is a configuration provider that feeds each of our components with its own variables, kept on its own configuration media. This way you can be sure the configurations will always follow its owner component no matter if it is used for mobile, web, desktop or console.

We can use Quantum to feed our components with Connection strings, SMTP parameters, File paths and any other application and business specific configuration we need.

Since we'll use JSON configuration files in this tutorial, we need to reference the Quantum core and its JSON extensions. The easiest way to acomplish this is to install the Keystone.Quantum.Json package by running the following Nuget Package Console command over the Nocciola.Wafer.Entities project:

Install-Package Keystone.Quantum.Json

Nuget will install the requested package and all its dependencies including Keystone.Quantum which is the Quantum core package. For more details on installing the KEYSTONE packages please jump to the Packages section of this tutorial.

Enhancing our Data layer

In this tutorial we can for example, enhance our Data layer by removing our connection string hardcode and creating a Nocciola.Wafer.Entities-Development.json file with the following content.

{
    "WaferCarboniteController": {
    "ConnectionString": "Data source=(local); Initial catalog=nocciola_wafer; Integrated security=SSPI"
  }
}

Please note that we must select our newly created file in the Solution Explorer and use the Properties Window to set the Copy to Output Directory property to Copy always. This property will ensure that everytime we compile our code, a copy of our configuration file is created in the bin/Debug or bin/Release folders.

Now we can use Quantum to feed our WaferCarboniteController with its configuration.

using Keystone.Quantum;
using Keystone.Quantum.Json;
using Keystone.Carbonite.Diamant.Sql;
namespace Nocciola.Wafer.Entities
{
    public class WaferCarboniteController : SqlCarboniteController
    {
        public WaferCarboniteController() : base(QuantumController.CreateWith<JsonPersistence>()["WaferCarboniteController", "ConnectionString"]) { }
    }
}

That's it, we don't need to explicitly tell Quantum which file contains the requested configuration variable. Quantum will infer the filename by using the name of our component, in this case Nocciola.Wafer.Entities, and concatenating the -Development.json, -Testing.json, -Staging.json or -Production.json postfixes in that order, this is known as the configuration domain inspection sequence. The first existing file in the folder in which our Nocciola.Wafer.Entities.DLL resides, that matches an inferred name in the sequence, will be used as source of our configurations.

The configuration domain inspection sequence lets you have your development, testing, staging and production configurations in different files and by deleting one or another during your deployment to each environment, you choose which of those files is used by Quantum to feed your configurations. No more variable commenting/uncommenting to express in a single file each environment configuration!

By explicitly instatiating the JsonPersistence class, for example with the code new QuantumController.CreateWith(new JsonPersistence("C:\path\to\WaferConfigurations.json"));, you can control the name and path of the file that Quantum will use to read your configuration variables.

Quantum supports many other features like variable symmetric and asymmetric encryption, configuration sections, and multivalued variables. In addition, the Clockwork to Quantum binding enables you to feed Gear and Mechanism inputs with Quantum managed variable values. Stay tuned for our Quantum tutorial coming soon!

Make it auditable

Once deployed to testing, staging or production environments, our business application will require to keep a record of two main events during its execution: 1. Application errors and warnings, and 2. User actions taken over sesitive information.

Almanac is KEYSTONE's logging component. It manages for you the event recording of your application and user events. Those events are know as Log entries.

Almanac is flexible enough to route log entries by its type to different destinations like email, text, XML files or even to the Windows Event Log.

In this tutorial we'll be using text files to keep our log entries, so we need to reference the Almanac core and its Plain text extensions. The easiest way to acomplish this is to install the Keystone.Clockwork.Persistence.PlainText package by running the following Nuget Package Console command over the Nocciola.Wafer.Processes project:

Install-Package Keystone.Almanac.PlainText

Nuget will install the requested package and all its dependencies including Keystone.Almanac which is the Almanac core package. For more details on installing the KEYSTONE packages please jump to the Packages section of this tutorial.

Enhancing our Business layer

We'll enhance our Business layer by recording with Almanac three events in our NewPurchaseOrder class:

  • A record will be kept when starting a new order.
  • A record will be kept for the new order gear successful activation.
  • A record will be kept for any given failure in the activation of the new order gear.

To do this, lets modify EraseAllPurchaseOrders class as follows.

using System;
using Keystone.Almanac;
using Keystone.Almanac.PlainText;
using Keystone.Carbonite.Diamant;
using Keystone.Clockwork;
using Keystone.Clockwork.To.Carbonite.Diamant.Sql;
using Nocciola.Wafer.Entities;

namespace Nocciola.Wafer.Processes
{
    public class NewPurchaseOrder : Gear<PurchaseOrder>
    {
        [Required]
        public Input<string> Restaurant;

        [Required]
        public Input<string> Gelato;

        [Required]
        public Input<int> Gallons;

        protected override PurchaseOrder OnRun() {
            var almanac = AlmanacController.CreateWith<PlainTextPersistence>("Noccional.Wafer");
            try
            {
                almanac.WriteEntry(new Information("Starting a new purchase order..."));
                var restaurant = new GetDetail<Restaurant, RestaurantDao, WaferCarboniteController> { Filter = new Where("Id = @Id", new WhereParameter("@Id", Restaurant.Value)) }.Run();
                var gelato = new GetDetail<Gelato, GelatoDao, WaferCarboniteController> { Filter = new Where("Id = @Id", new WhereParameter("@Id", Gelato.Value)) }.Run();

                var newPurchaseOrder = new PurchaseOrder
                {
                    Id = Guid.NewGuid(),
                    RestaurantId = restaurant.Id,
                    GelatoId = gelato.Id,
                    Created = DateTime.Now,
                    Gallons = Gallons.Value,
                    ShippingAddress = restaurant.Address,
                    TotalPrice = Gallons.Value * gelato.PricePerGallon
                };
                almanac.WriteEntry(new Information($"New purchase order ready: {newPurchaseOrder}"));

                return newPurchaseOrder;
            }
            catch (Exception ex) { almanac.WriteEntry(new Error("New purchase order failure.", ex)); throw ex; }
        }
    }
}

With the previous code in place, any running of our NewPurchaseOrder gear will write its corresponding log entries into a Nocciola.Wafer-Log.txt file located in the same folder in which our Nocciola.Wafer.Processes.DLL is stored, normally a bin/Debug folder inside our Presentation layer project base folder.

Notice how the log file is named. Almanac uses the application name as file name prefix. The application name, in our case Nocciola.Wafer, is transferred to Almanac as a string parameter during its creation, AlmanacController.CreateWith<PlainTextPersistence>("Noccional.Wafer").

By explicitly instatiating the PlainTextPersistence class, for example with the code var almanac = AlmanacController.CreateWith(new PlainTextPersistence(@"C:\path\to\WaferLog.txt"), "Nocciola.Wafer");, you can control both the application name and the name and path of the file that Almanac will use to write its entries.

Almanac supports many other features like log entry routing based on type and turning off or on any single route or the entire Almanac recording at any time. In addition, the Clockwork to Almanac binding enables you to record any arbitrary gear's lifecycle events with ease. Stay tuned for our Almanac tutorial coming at any time soon!

Make it extensible

One of the most important design principles and prescriptions in KEYSTONE is the strict use of abstraction levels that separate components inside our application. This is known as Separation of concerns. Our goal is not to couple together two concerns that are at different levels of abstraction and therefore are characterized by different rates of change during the application lifecycle inside your organization.

A component that is designed to work at an abstract level tends to change less than another component that solves a more specific concern. These levels of abstraction are complementary, it cannot solve the entire application requirements by itself. It needs to work together.

A more abstract component should be designed against well-known behavioral contracts defined at its same level of abstraction. That contracts are fulfilled at a more concrete level of abstraction by many different providers that respect the contract. In this way we can safely interchange levels of abstraction without polluting abstract levels with unnecesary detail and concerns that change often.

Mystere is KEYSTONE's lightweight dependency injection component. Mystere builds and completes behind the scene your concrete objects, and at the same time it acts as a level of abstraction frontier. It's fast execution is closely related to the fact that it minimizes the use of reflexion and favours configurable build and complete rules written in plain old code.

For the purpose of our tutorial, we need to reference just the Mystere core. The easiest way to acomplish this is to install the Keystone.Mystere package by running the following Nuget Package Console command over the Nocciola.Wafer.Processes project:

Install-Package Keystone.Mystere

Nuget will install Keystone.Mystere which is the Mystere core package. For more details on installing the KEYSTONE packages please jump to the Packages section of this tutorial.

Enhancing even further our Business layer

We'll assume that, after analysing our requirements and our business rules and processes, we come to the conclusion that our NewPurchaseOrder class could change more often than the rest of our gears and mechanisms. So we think that class belongs to a different abstraction level, more specific than the other classes in the Business layer.

First we'll create a contract file NewPurchaseOrderProvider.cs that will represent our NewPurchaseOrder at an abstract level.

using System;
using Keystone.Almanac;
using Keystone.Almanac.PlainText;
using Keystone.Clockwork;
using Nocciola.Wafer.Entities;

namespace Nocciola.Wafer.Processes
{
    public abstract class NewPurchaseOrderProvider : Gear<PurchaseOrder>
    {
        [Required]
        public Input<string> Restaurant;

        [Required]
        public Input<string> Gelato;

        [Required]
        public Input<int> Gallons;

        protected override PurchaseOrder OnRun() {
            var almanac = AlmanacController.CreateWith<PlainTextPersistence>("Noccional.Wafer");
            try
            {
                almanac.WriteEntry(new Information("Starting a new purchase order..."));
                var newPurchaseOrder = OnGetNewPurchaseOrder();
                almanac.WriteEntry(new Information($"New purchase order ready: {newPurchaseOrder}"));

                return newPurchaseOrder;
            }
            catch (Exception ex) { almanac.WriteEntry(new Error("New purchase order failure.", ex)); throw ex; }
        }

        protected abstract PurchaseOrder OnGetNewPurchaseOrder();
    }
}

We choose an abstract class to implement our contract instead of an interface because we want to write the less possible code to inherit the almanac log entry recording code and the input definition and validations. Those bits of code are valid for any concrete class that will implement our contract represented by the OnGetNewPurchaseOrder Inversion of Control abstract method. Now we can modify our NewPurchaseOrder to subclass our newly created file.

using System;
using Keystone.Carbonite.Diamant;
using Keystone.Clockwork.To.Carbonite.Diamant.Sql;
using Nocciola.Wafer.Entities;

namespace Nocciola.Wafer.Processes
{
    public class NewPurchaseOrder : NewPurchaseOrderProvider
    {
        protected override PurchaseOrder OnGetNewPurchaseOrder() {
            var restaurant = new GetDetail<Restaurant, RestaurantDao, WaferCarboniteController> { Filter = new Where("Id = @Id", new WhereParameter("@Id", Restaurant.Value)) }.Run();
            var gelato = new GetDetail<Gelato, GelatoDao, WaferCarboniteController> { Filter = new Where("Id = @Id", new WhereParameter("@Id", Gelato.Value)) }.Run();

            return new PurchaseOrder
            {
                Id = Guid.NewGuid(),
                RestaurantId = restaurant.Id,
                GelatoId = gelato.Id,
                Created = DateTime.Now,
                Gallons = Gallons.Value,
                ShippingAddress = restaurant.Address,
                TotalPrice = Gallons.Value * gelato.PricePerGallon
            };
        }
    }
}

Now our NewPurchaseOrder class is very focused on implementing the OnGetNewPurchaseOrder Inversion of Control abstract method, that solves the concern of how to get a new purchase order.

Next, we'll need to modify NewPurchaseOrder's consumer classes, in this case just the CreatePurchaseOrder mechanism, to get a new purchase order against our abstract class NewPurchaseOrderProvider and allowing Mystere to build and feed us the concrete class that supports that contract. This will keep apart our abstraction levels.

using Keystone;
using Keystone.Clockwork;
using Keystone.Clockwork.To.Carbonite.Diamant.Sql;
using Keystone.Mystere;
using Nocciola.Wafer.Entities;

namespace Nocciola.Wafer.Processes
{
    public class CreatePurchaseOrder : Mechanism<Null>
    {
        [Required]
        public Input<string> Restaurant;

        [Required]
        public Input<string> Gelato;

        [Required]
        public Input<int> Gallons;

        protected override Gear<Null> OnAssemble() {
            var mystere = MystereController.CreateWith<WaferInjectionRules>();

            var newPurchaseOrder = mystere.Build<NewPurchaseOrderProvider>();
            newPurchaseOrder.Restaurant = Restaurant;
            newPurchaseOrder.Gelato = Gelato;
            newPurchaseOrder.Gallons = Gallons;

            var setPurchaseOrderStatus = new SetPropertyOf<PurchaseOrder, string>
            {
                Object = newPurchaseOrder,
                PropertyName = "Status",
                NewValue = "Created"
            };

            var addPurchaseOrder = new AddDetail<PurchaseOrder, PurchaseOrderCreationValidations, PurchaseOrderDao, WaferCarboniteController>
            {
                Entity = setPurchaseOrderStatus
            };

            return addPurchaseOrder;
        }
    }
}

Notice how this modified version of our CreatePurchaseOrder mechanism requires a new class, WaferInjectionRules, in order to allow Mystere to correctly build the concrete contract provider. Lets create that file to finish our enhacement.

using Keystone.Mystere;
namespace Nocciola.Wafer.Processes
{
    public class WaferInjectionRules : ConfigurationFirstInjectionStrategy
    {
        protected override BuildRules OnGetBuildRules() => BuildRules.CreateAsFollows(
                    ToBuild<NewPurchaseOrderProvider>(Do: () => new NewPurchaseOrder()));
    }
}

That's all we need to follow KEYSTONE prescription about separation of concerns!

Mystere supports many other features like object completion and injection rules loading at runtime from another assembly by using .NET reflection. This makes it possible to physically separate the assemblies that hold the abstract contracts from the assemblies that provide the contract's concrete support. In addition, the Clockwork to Mystere binding enables you to feed Gear and Mechanism inputs with Mystere builded and completed objects. Stay tuned to check our Mystere tutorial at any time soon!

Congratulations! You've completed the KEYSTONE tutorial.

Now you're on the way to develop high quality business software at lightning speed.

Don't forget to take a look at our API Reference and remember to stay tuned for our KEYSTONE advanced tutorials that will be available very soon!