Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Line of Business Application, the New Way

4.71/5 (5 votes)
29 Nov 2019CPOL11 min read 9.2K   213  
The new way to develop LOB applications

Introduction

RDO.Net is an open source framework under the MIT license to handle data in .NET platform, and consists of the following libraries and tools:

Image 1

RDO.Net is an end to end solution from database to UI, particularly:

  • Best balanced data and business layer for both programmability and performance. No more object-relational impedance mismatch.
  • Database operation testing as first class citizen.
  • A one-for-all, fully customizable MVP framework to handle presentation logic including layout, data binding and data validation, all consumed in clean C#/VB.NET code (no XAML needed). You don't need complex controls such as ListBox, TreeView, DataGrid any more.

A sample application AdventureWorksLT is provided:

Image 2

You can download it here or check it out at github. A detailed description of the sample application can be found here.

Data and Business Logic, the New Way

Every enterprise application is backed by a persistent data store, typically a relational database. Object-oriented programming (OOP), on the other hand, is the mainstream for enterprise application development. According to Martin Fowler's post, currently there are 3 patterns to develop business logic:

  • Transaction Script and Domain Model: The business logic is placed in-memory code and the database is used pretty much as a storage mechanism.
  • Logic in SQL: Business logic is placed in SQL queries such as stored procedure.

Each pattern has its own pros and cons, basically it's a tradeoff between programmability and performance. Most people go with the in-memory code way for better programmability, which requires an Object-Relational Mapping (ORM, O/RM, and O/R mapping tool), such as Entity Framework. Great efforts have been made to reconcile these two, however it's still The Vietnam of Computer Science, due to the misconceptions of SQL and OOP.

The Misconceptions

SQL is Obsolete

The origins of SQL take us back to the 1970s. Since then, IT world has changed, projects are much more complicated, but SQL stays - more or less - the same. It works, but it's not elegant for nowadays modern application development. Most ORM implementations, like Entity Framework, try to encapsulate the code needed to manipulate the data, so you don't use SQL anymore. Unfortunately, this is wrongheaded and will end up with Leaky Abstraction.

As coined by Joel Spolsky, the Law of Leaky Abstractions states:

All non-trivial abstractions, to some degree, are leaky.

Apparently, RDBMS and SQL, being a fundamental of your application, is far from trivial. You can't expect to abstract it away - you have to live with it. Most ORM implementations provide native SQL execution because of this.

OOP/POCO Obsession

OOP, on the other hand, is modern and the mainstream of application development. It's so widely adopted by developers that many developers subconsciously believe OOP can solve all the problems. Moreover, many framework authors have the religion that any framework, if it does not support POCO, is not a good framework.

In fact, like any technology, OOP has its limitations too. The biggest one, IMO, is: OOP is limited to local process, it's not serialization/deserialization friendly. Each and every object is accessed via its reference (the address pointer), and the reference, together with the type metadata and compiled byte code (further reference to type descriptors, vtable, etc.), is private to local process. It's just too obvious to realize this.

By nature, any serialized data is value type, which means:

  1. To serialize/deserialize an object, a converter for the reference is needed, either implicitly or explicitly. ORM can be considered as the converter between objects and relational data.
  2. As the object complexity grows, the complexity of the converter grows respectively. Particularly, the type metadata and compiled byte code (the behavior of the object, or the logic), are difficult or maybe impossible for the conversion - in the end, you need virtually the whole type runtime. That's why so many applications start with Domain Drive Design, but end up with Anemic Domain Model.
  3. On the other hand, relational data model is very complex by nature, compares to other data format such as JSON. This adds another complexity to the converter. ORM, which is considered as the converter between objects and relational data, will sooner or later hit the wall.

That's the real problem of object-relational impedance mismatch, if you want to map between arbitrary objects (POCO) and relational data. Unfortunately, almost all ORM implementations are following this path, none of them can survive from this.

The New Way

When you're using relational database, implementing your business logic using SQL/stored procedure is the shortest path, therefore can have best performance. The cons lie in the code maintainability of SQL. On the other hand, implementing your business logic as in-memory code, has many advantages in terms of code maintainability, but may have performance issue in some cases, and most importantly, it will end up with object-relational impedance mismatch as described above. How can we get the best of both?

RDO.Data is the answer to this question. You can write your business logic in both ways, as stored procedures alike or in-memory code, using C#/VB.NET, independent of your physical database. To achieve this, we're implementing relational schema and data into a comprehensive yet simple object model:

Image 3

The following data objects are provided with a rich set of properties, methods and events:

  • Model/Model<T>: Defines the meta data of model, and declarative business logic such as data constraints, automatic calculated field and validation, which can be consumed for both database and local in-memory code.
  • DataSet<T>: Stores hierarchical data locally and acts as domain model of your business logic. It can be conveniently exchanged with relational database in set-based operations (CRUD), or external system via JSON.
  • Db: Defines the database session, which contains:
    • DbTable<T>: Permanent database tables for data storage;
    • Instance methods of Db class to implement procedural business logic, using DataSet<T> objects as input/output. The business logic can be simple CRUD operations, or complex operation such as MRP calculation:
      • You can use DbQuery<T> objects to encapsulate data as reusable view, and/or temporary DbTable<T> objects to store intermediate result, to write stored procedure alike, set-based operations (CRUD) business logic.
      • On the other hand, DataSet<T> objects, in addition to be used as input/output of your procedural business logic, can also be used to write in-memory code to implement your business logic locally.
      • Since these objects are database agnostic, you can easily port your business logic into different relational databases.
  • DbMock<T>: Easily mock the database in an isolated, known state for testing.

The following is an example of business layer implementation, to deal with sales orders in AdventureWorksLT sample. Please note the example is just CRUD operations for simplicity, RDO.Data is capable of doing much more than it.

C#
public async Task<dataset<salesorderinfo>> GetSalesOrderInfoAsync(_Int32 salesOrderID,
    CancellationToken ct = default(CancellationToken))
{
    var result = CreateQuery((DbQueryBuilder builder, SalesOrderInfo _) =>
    {
        builder.From(SalesOrderHeader, out var o)
            .LeftJoin(Customer, o.FK_Customer, out var c)
            .LeftJoin(Address, o.FK_ShipToAddress, out var shipTo)
            .LeftJoin(Address, o.FK_BillToAddress, out var billTo)
            .AutoSelect()
            .AutoSelect(c, _.Customer)
            .AutoSelect(shipTo, _.ShipToAddress)
            .AutoSelect(billTo, _.BillToAddress)
            .Where(o.SalesOrderID == salesOrderID);
    });

    await result.CreateChildAsync(_ => _.SalesOrderDetails, (DbQueryBuilder builder,
        SalesOrderInfoDetail _) =>
    {
        builder.From(SalesOrderDetail, out var d)
            .LeftJoin(Product, d.FK_Product, out var p)
            .AutoSelect()
            .AutoSelect(p, _.Product)
            .OrderBy(d.SalesOrderDetailID);
    }, ct);

    return await result.ToDataSetAsync(ct);
}

public async Task<int?> CreateSalesOrderAsync(DataSet<SalesOrderInfo> salesOrders,
    CancellationToken ct)
{
    using (var transaction = BeginTransaction())
    {
        salesOrders._.ResetRowIdentifiers();
        await SalesOrderHeader.InsertAsync(salesOrders, true, ct);
        var salesOrderDetails = salesOrders.GetChild(_ => _.SalesOrderDetails);
        salesOrderDetails._.ResetRowIdentifiers();
        await SalesOrderDetail.InsertAsync(salesOrderDetails, ct);

        await transaction.CommitAsync(ct);
        return salesOrders.Count > 0 ? salesOrders._.SalesOrderID[0] : null;
    }
}

public async Task UpdateSalesOrderAsync(DataSet<SalesOrderInfo> salesOrders,
    CancellationToken ct)
{
    await EnsureConnectionOpenAsync(ct);
    using (var transaction = BeginTransaction())
    {
        salesOrders._.ResetRowIdentifiers();
        await SalesOrderHeader.UpdateAsync(salesOrders, ct);
        await SalesOrderDetail.DeleteAsync
              (salesOrders, (s, _) => s.Match(_.FK_SalesOrderHeader), ct);
        var salesOrderDetails = salesOrders.GetChild(_ => _.SalesOrderDetails);
        salesOrderDetails._.ResetRowIdentifiers();
        await SalesOrderDetail.InsertAsync(salesOrderDetails, ct);

        await transaction.CommitAsync(ct);
    }
}

public Task<int> DeleteSalesOrderAsync(DataSet<SalesOrderHeader.Key> dataSet,
    CancellationToken ct)
{
    return SalesOrderHeader.DeleteAsync(dataSet, (s, _) => s.Match(_), ct);
}

RDO.Data Features, Pros and Cons

RDO.Data Features

  • Comprehensive hierarchical data support
  • Rich declarative business logic support: constraints, automatic calculated filed, validations, etc. for both server side and client side
  • Comprehensive inter-table join/lookup support
  • Reusable view via DbQuery<T> objects
  • Intermediate result store via temporary DbTable<T> objects
  • Comprehensive JSON support, better performance because no reflection required
  • Fully customizable data types and user-defined functions
  • Built-in logging for database operations
  • Extensive support for testing
  • Rich design time tools support
  • And much more...

Pros

  • Unified programming model for all scenarios. You have full control of your data and business layer, no magic black box.
  • Your data and business layer is best balanced for both programmability and performance. Rich set of data objects are provided, no more object-relational impedance mismatch.
  • Data and business layer testing is a first class citizen which can be performed easily - your application can be much more robust and adaptive to change.
  • Easy to use. The APIs are clean and intuitive, with rich design time tools support.
  • Rich feature and lightweight. The runtime DevZest.Data.dll is less than 500KB in size, whereas DevZest.Data.SqlServer is only 108KB in size, without any 3rd party dependency.
  • The rich metadata can be consumed conveniently by other layer of your application such as the presentation layer.

Cons

  • It's new. Although APIs are designed clean and intuitive, you or your team still need some time to get familiar with the framework. Particularly, your domain model objects are split into two parts: the Model/Model<T> objects and DataSet<T> objects. It's not complex, but you or your team may need some time to get used to it.
  • To best utilize RDO.Data, your team should be comfortable with SQL, at least to an intermediate level. This is one of those situations where you have to take into account the make up of your team - people do affect architectural decisions.
  • Although data objects are lightweight, there are some overheads compared to POCO objects, especially for the simplest scenarios. In terms of performance, It may get close to, but cannot beat native stored procedure.

Presentation Layer, the New Way

Presentation logic is complex - it's responsible for overcoming the gap between computer and human being, which is a big (maybe the biggest) challenge in computer science. To encapsulate presentation logic into a separate layer, we need to plan carefully at the very beginning. Unfortunately, existing MVVM implementations, among other similar frameworks, are built as an afterthought.

The Anti-Patterns

The POCO Obsession, Again

When model is arbitrary object (POCO), hiding the model from the view is simply not possible. The presentation layer can do little about it, in many cases, it just exposes the model object via aggregation, without any value-added.

On the other hand, data model cannot be 100% POCO. For example, INotifyPropertyChanged interface is mandatory for most data models, and IDataErrorInfo interface is required if you want to bind to custom validation error. Since these interfaces must be implemented by all data models, they must be dead simple.

In the end, your presentation layer can do little with the data model.

Complex Control

Complex control, such as DataGrid, has very complex presentation logics. Since these controls are built without existing presentation layer, these logics are naturally encapsulated into the control itself - the view. This puts the presentation layer into an embarrassed situation: for simple control without complex view state such as TextBlock, it has little job to do; for complex control such as DataGrid, the control has done the job.

In the end, your presentation layer can do little with the view too.

Put it together, if the presentation layer is an afterthought, there is little room left for implementation, especially at the abstraction level.

The New Way

Thanks to RDO.Data, which provides a rich set of data objects and the separation of model of data, we now have the foundation to implement a comprehensive Model-View-Presenter (MVP) pattern. The following is the architecture of RDO.WPF MVP:

Image 4

  • The model contains the data values and data logic such as computation and validation in a DataSet<T> object. The DataSet<T> object contains collection of DataRow objects and Column objects, similar as two dimensional array. The model provides events to notify data changes, it is not aware of the existence of the presenter at all.
  • The view contains UI components which directly interact with user. These UI components are designed as dumb as possible, all presentation logic is implemented in the presenter. Despite the container UI components such as DataView, BlockView and RowView, or controls depending on presentation logic implemented in presenter (such as ColumnHeader), most UI elements are not aware of the existence of the presenter at all.
  • The presenter is the core to tie model and view together, it implements the following presentation logic:
    • Selection, filtering and hierarchical grouping
    • UI elements life time management and data binding
    • Editing and validation
    • Layout and UI virtualization
  • Since presenting collection of data is extensively and exclusively supported by the presenter, all the complex controls (controls derived from System.Windows.Controls.ItemsControl such as ListBox and DataGrid) are not necessary any more. By using RDO.WPF, your application only need to deal with simple controls such as TextBlock and TextBox, via data binding, in a unified way.

Since presenting collection of data is extensively and exclusively supported by the presenter, all the complex controls (controls derived from System.Windows.Controls.ItemsControl such as ListBox and DataGrid) are not necessary any more. By using RDO.WPF, your application only need to deal with simple controls such as TextBlock and TextBox, via data binding, in an unified way.

Simply derive your data presenter from DataPresenter<T> class, which contains the presentation logic implementation, and put a DataView into your view, you got all the presentation logic such as filtering, sorting, grouping, selection, data binding, editing and layout immediately, without using any complex control. For example, the following code:

C#
namespace DevZest.Samples.AdventureWorksLT
{
    partial class SalesOrderWindow
    {
        private class DetailPresenter : DataPresenter<SalesOrderInfoDetail>,
            ForeignKeyBox.ILookupService, DataView.IPasteAppendService
        {
            public DetailPresenter(Window ownerWindow)
            {
                _ownerWindow = ownerWindow;
            }

            private readonly Window _ownerWindow;

            protected override void BuildTemplate(TemplateBuilder builder)
            {
                var product = _.Product;
                builder.GridRows("Auto", "20")
                    .GridColumns("20", "*", "*", "Auto", "Auto", "Auto", "Auto")
                    .WithFrozenTop(1)
                    .GridLineX(new GridPoint(0, 2), 7)
                    .GridLineY(new GridPoint(2, 1), 1)
                    .GridLineY(new GridPoint(3, 1), 1)
                    .GridLineY(new GridPoint(4, 1), 1)
                    .GridLineY(new GridPoint(5, 1), 1)
                    .GridLineY(new GridPoint(6, 1), 1)
                    .GridLineY(new GridPoint(7, 1), 1)
                    .Layout(Orientation.Vertical)
                    .WithVirtualRowPlacement(VirtualRowPlacement.Tail)
                    .AllowDelete()
                    .AddBinding(0, 0, this.BindToGridHeader())
                    .AddBinding(1, 0, product.ProductNumber.BindToColumnHeader("Product No."))
                    .AddBinding(2, 0, product.Name.BindToColumnHeader("Product"))
                    .AddBinding(3, 0, _.UnitPrice.BindToColumnHeader("Unit Price"))
                    .AddBinding(4, 0, _.UnitPriceDiscount.BindToColumnHeader("Discount"))
                    .AddBinding(5, 0, _.OrderQty.BindToColumnHeader("Qty"))
                    .AddBinding(6, 0, _.LineTotal.BindToColumnHeader("Total"))
                    .AddBinding(0, 1, _.BindTo<rowheader>())
                    .AddBinding
                        (1, 1, _.FK_Product.BindToForeignKeyBox(product, GetProductNumber)
                        .MergeIntoGridCell(product.ProductNumber.BindToTextBlock())
                        .WithSerializableColumns(_.ProductID, product.ProductNumber))
                    .AddBinding(2, 1, product.Name.BindToTextBlock().AddToGridCell()
                        .WithSerializableColumns(product.Name))
                    .AddBinding(3, 1, _.UnitPrice.BindToTextBox().MergeIntoGridCell())
                    .AddBinding
                        (4, 1, _.UnitPriceDiscount.BindToTextBox(new PercentageConverter())
                        .MergeIntoGridCell(_.UnitPriceDiscount.BindToTextBlock("{0:P}")))
                    .AddBinding(5, 1, _.OrderQty.BindToTextBox().MergeIntoGridCell())
                    .AddBinding(6, 1, _.LineTotal.BindToTextBlock("{0:C}").AddToGridCell()
                        .WithSerializableColumns(_.LineTotal));
            }

            private static string GetProductNumber
                           (ColumnValueBag valueBag, Product.PK productKey,
                Product.Lookup productLookup)
            {
                return valueBag.GetValue(productLookup.ProductNumber);
            }

            bool ForeignKeyBox.ILookupService.CanLookup(CandidateKey foreignKey)
            {
                if (foreignKey == _.FK_Product)
                    return true;
                else
                    return false;
            }

            void ForeignKeyBox.ILookupService.BeginLookup(ForeignKeyBox foreignKeyBox)
            {
                if (foreignKeyBox.ForeignKey == _.FK_Product)
                {
                    var dialogWindow = new ProductLookupWindow();
                    dialogWindow.Show(_ownerWindow, foreignKeyBox,
                        CurrentRow.GetValue(_.ProductID));
                }
                else
                    throw new NotSupportedException();
            }

            protected override bool ConfirmDelete()
            {
                return MessageBox.Show(string.Format
                       ("Are you sure you want to delete selected {0} rows?", 
                       SelectedRows.Count), "Delete", 
                       MessageBoxButton.YesNo) == MessageBoxResult.Yes;
            }

            bool DataView.IPasteAppendService.Verify(IReadOnlyList<ColumnValueBag> data)
            {
                var foreignKeys = DataSet<Product.Ref>.Create();
                for (int i = 0; i < data.Count; i++)
                {
                    var valueBag = data[i];
                    var productId = valueBag.ContainsKey(_.ProductID)
                        ? valueBag[_.ProductID] : null;
                    foreignKeys.AddRow((_, dataRow) =>
                    {
                        _.ProductID.SetValue(dataRow, productId);
                    });
                }

                if (!App.Execute((db, ct) =>
                    db.LookupAsync(foreignKeys, ct), Window.GetWindow(View), out var lookup))
                    return false;

                Debug.Assert(lookup.Count == data.Count);
                var product = _.Product;
                for (int i = 0; i < lookup.Count; i++)
                {
                    data[i].SetValue(product.Name, lookup._.Name[i]);
                    data[i].SetValue(product.ProductNumber, lookup._.ProductNumber[i]);
                }
                return true;
            }
        }
    }
}

Will produce the following editable data grid UI, with implementation of foreign key lookup and clipboard support:

Image 5

In the end, you have ALL of your presentation logic in 100% strongly typed, highly reusable clean code.

What's Next

RDO.Net is a comprehensive framework with about 3800 APIs. Don't be scared by this number, this is because it covers many features. And each feature is dead simple!

Start with our step by step tutorial, find out how easy and fun it is to use it!

History

  • 30th November, 2019: First release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)