CodeTrigger  

Code Generation For C#, WPF, WCF, SQL SERVER/ORACLE/MYSQL and Visual Studio 2013-2019

Tutorial - Unit of Work Pattern with CodeTrigger
CodeTrigger provides support for the UnitOfWork pattern, by decorating its classes with the appropriate interfaces. However you still need to provide an implementation for those interfaces, corresponding to your requirements. Here is a sample that shows one way of doing that.
This guide has been updated for version 5.1.0.0 (released June 10th 2017)

Microsoft Northwind Sample db

STEP 1 - Ensure you have a relevant test database

The CodeTrigger sample applications are designed to work with your existing database schema to ensure that the samples are as relevant as possible. Ensure that you have a TEST SQL Server/Oracle/MYSQL database handy. CodeTrigger and its associated supplier companies do not accept any liability for data loss whilst using the CodeTrigger product. It is important that you use a test database until you are familiar with the functionality of the generated code. This code sample assumes MSQL DB Server/NORTHWIND test database, but should work with your MYSQL/ORACLE db server and your test database of choice.

Step 2 - Create a new CodeTrigger project

Create a new Multi-tier application using the wizard. As a guide you can use the 'DB to Microservice' wizard. Name your project UnitOfWorkSample and generate your application as usual, using CodeTrigger.
Confirm that your project builds and runs successfully.

Step 3 - Modify the CodeTrigger project

Choose the following settings in the Advanced options tab:

Enable for Unit of Work pattern: Ticked
If you are using Northwind sample test database, select 'Employees' and 'Customers' from the Schema Objects tab and 'BOEmployees' and 'BOCustomers' in the Business Objects tab, (If you are not using the Northwind sample test database, select suitable objects from those listed in your Business Objects tab) and click Generate Code to generate the code.

Step 4 - Define your IUnitOfWork interface and Implementation

Define your UnitOfWork interface and implementation to utilize the codetrigger interfaces 'IUnitOfWorkEntity'.
An example implementation is listed here, either use as is, or modify to suit your requirements
Add the IUnitOfWork.cs to the BusinessObjects/Interfaces folder, and the UnitOfWorkImp.cs to the BusinessObjects/Implementation folder.
                    
/*IUnitOfWork Interface Definition - IUnitOfWork.cs*/
using System;

namespace UnitOfWorkSample.BusinessObjects.Interfaces { public interface IUnitOfWork { void Create(IUnitOfWorkEntity busObj); void Update(IUnitOfWorkEntity busObj); void Delete(IUnitOfWorkEntity busObj); bool Commit(out string message); } }


                    
/*UnitOfWork Implementation - UnitOfWorkImp.cs*/
using System;
using System.Collections.Generic;
using UnitOfWorkSample.BusinessObjects.Interfaces;

namespace UnitOfWorkSample.BusinessObjects { public class UnitOfWorkImp : zUnitOfWorkSampleConn_BaseBusiness, IUnitOfWork { private enum OpType { Create, Update, Delete }; List<Tuple<IUnitOfWorkEntity, OpType>> _opList = new List<Tuple<IUnitOfWorkEntity, OpType>>(); public void Create(IUnitOfWorkEntity busObj) { _opList.Add(new Tuple<IUnitOfWorkEntity, OpType>(busObj, OpType.Create)); } public void Update(IUnitOfWorkEntity busObj) { _opList.Add(new Tuple<IUnitOfWorkEntity, OpType>(busObj, OpType.Update)); } public void Delete(IUnitOfWorkEntity busObj) { _opList.Add(new Tuple<IUnitOfWorkEntity, OpType>(busObj, OpType.Delete)); } public bool Commit(out string message) { message = "SUCCESS"; string transName = GetTransactionName(); RegisterObjects(); this.BeginTransaction(transName); try { foreach (var op in _opList) { switch(op.Item2) { case OpType.Create: op.Item1.SaveNew(); break; case OpType.Update: op.Item1.Update(); break; case OpType.Delete: op.Item1.Delete(); break; } } CommitTransaction(); return true; } catch (Exception ex) { base.RollbackTransaction(transName); message = ex.Message; return false; } } private void RegisterObjects() { foreach (var op in _opList) RegisterBusinessObject((zUnitOfWorkSampleConn_BaseBusiness)op.Item1); } private string GetTransactionName() { return "tr-" + DateTime.Now.TimeOfDay.Ticks.ToString(); } } public class UnitOfWorkForwarder : zUnitOfWorkSampleConn_BaseBusiness, IUnitOfWorkEntity { Action _saveFn; Action _updateFn; Action _deleteFn; public UnitOfWorkForwarder(Action saveFn, Action updateFn, Action deleteFn) { _saveFn = saveFn; _updateFn = updateFn; _deleteFn = deleteFn; } public void SaveNew() { if (_saveFn != null) _saveFn(); } public void Update() { if (_updateFn != null) _updateFn(); } public void Delete() { if (_deleteFn != null) _deleteFn(); } } }
Build your project to make sure it builds successfully and fix any namespace issues.

Step 5 - Write some code to test our your UnitOfWork Implementation

You are ready to test out your UnitOfWork implementation. Here is some code that (using the Northwind sample database) shows an update to a customer record and an update to an employee record wrapped as a single unit of work. The update to the customer record occurs first, and when the update to the employee record fails, the entire unit of work is rolled back to the original state.

Add the 'using UnitOfWorkSample.BusinessObjects;' namespace include to your generated UnitOfWorkSample.Svc.Service.cs class, and add the following code to the 'Start()' method so it looks like this:
                
/*UnitOfWorkSample.Svc.Service.cs*/
using System;
using UnitOfWorkSample.BusinessObjects;

namespace UnitOfWorkSample.Svc { public class Service { public bool Start() { Console.WriteLine("Service started..."); string originalcontactName = ""; //Maria Anders string updatedContactName = "Updated despite exceptions"; int invalidReportingLine = -1; BOCustomers boCustomer = new BOCustomers("ALFKI"); BOEmployees boEmployee = new BOEmployees(1); originalcontactName = boCustomer.ContactName; boCustomer.ContactName = updatedContactName; boEmployee.ReportsTo = invalidReportingLine; Console.Write("Try saving the 2 object instances to the db. We expect the employee save to fail with "); Console.Write("an invalid foreign key, thereby rolling back the unit of work"); Console.WriteLine(""); Console.WriteLine(""); UnitOfWorkImp unitOfWork = new UnitOfWorkImp(); unitOfWork.Update(boCustomer); unitOfWork.Update(boEmployee); string message; if (!unitOfWork.Commit(out message)) { Console.WriteLine("As expected, error thrown:" + message); Console.WriteLine(""); Console.WriteLine("However, we can check that the customer instance update was also rolled back"); BOCustomers boCustomerCheck = new BOCustomers("ALFKI"); Console.WriteLine("Customer contact name: '" + boCustomerCheck.ContactName + "'. Original contact name: '" + originalcontactName + "' is the same, so customer name change was rolled back" + ", when subsequent employee update failed."); Console.WriteLine(""); Console.WriteLine(""); Console.WriteLine("Unit of Work rollback " + (boCustomerCheck.ContactName == originalcontactName ? "successful." : "failed.")); } else { Console.WriteLine("---SHOULD NOT GET HERE [If you do, check your constraint values for reportinglineid in your db "); Console.WriteLine("and use an invalid one instead]"); } Console.WriteLine("Press any key to continue.."); Console.ReadLine(); return true; } public bool Stop() { return true; } public bool Pause() { Console.WriteLine("Service paused..."); return true; } public bool Continue() { Console.WriteLine("Service continued..."); return true; } } }

The following code suggestions were contributed by users


Dealing with generated classes that do not inherit IUnitOfWorkEntity (because they do not have a primary key)
These class cannot readily participate in UOW transactions. To get around this, create a partial class definition in an Extensions folder:
public partial class BOUser2Preference : IUnitOfWorkEntity
    {
        public void Update() { throw new NotImplementedException(); }
        public void Delete() { throw new NotImplementedException(); }
    }
This allows you to include instances of this class in a UOW transaction but will alert you if you do anything other than Create/SaveNew on the object.

Getting db auto-generated numbers/keys from one transaction step to the next
Supposing you want to save a new User object to the database, which will assign you a User.UserID auto-generated key. Later on in the transaction you want to save a new Company object to the database which contains a foreign key reference to the new Customer.UserID.
However, without using embedded object references, somehow you need to inject an instruction in-between transaction steps to modify the later step once the earlier step has retrieved a userID. However you still want to be able to roll back the whole lot. We do this with a forwarding action ie:
class UnitOfWorkForwarder : Northwind_BaseBusiness, IUnitOfWorkEntity
    {
        Action _saveFn; Action _updateFn; Action _deleteFn;

        public UnitOfWorkForwarder(Action saveFn, Action updateFn, Action deleteFn)
        {    _saveFn = saveFn; _updateFn = updateFn;  _deleteFn = deleteFn;   }

        public void SaveNew() { if (_saveFn != null) _saveFn(); }
        public void Update() { if (_updateFn != null) _updateFn(); }
        public void Delete() { if (_deleteFn != null) _deleteFn(); }
    }
This allows us to 'forward' assignments to 'commit' time::
string errOut;
UnitOfWorkImpl uow = new UnitOfWorkImpl();

BOUsers newUser = new BOUsers();
newUser.FirstName = param.FirstName;
newUser.LastName = param.LastName;
//newUser.UserID = ?? (auto generated)

BOCompany newCompany = new BOCompany();
newCompany.CompanyName = param.CompanyName;
newCompany.Address1 = param.Address1;
//newCompany.UserID = newUser.UserID; (cant do this yet as we dont know UserID) 

uow.Create(newUser);
uow.Create(new UnitOfWorkForwarder((Action)delegate { newCompany.UserID = newUser.UserID; }, null, null));
uow.Create(newCompany);

uow.Commit(out errOut); 
//at commit time newCompany.UserID is updated before being inserted into the db