Tuesday, March 16, 2010

LINQ-To-Business: Don’t fight the ORM - keep your BLL

Previously, I posted here about DDD, repositories, ORMs and .NET and how they fit in today's software topology. Well, here we go for round 2.
I want my BLL!

Okay, I said it before. I'll say it again: Until the day comes that an ORM either [1] becomes a silver bullet (heh) or [2] an ORM evolves into a full fledged runtime (like a "lightweight" version of BizTalk), I REFUSE to coerce my business-centric stuff into an ORM for reasons that should be obvious (must I articulate them?). While the ORM (ADO.NET Entities, etc.) is a godsend for DBMS-to-OOPL, I'm still left with a sort of impedance mismatch with my dedicated business layer. I ventured off in search of a solution for this and, after much digging, began to finally get enough to start the soaking process (some call it design). As in my last post, I started a concoction, in concept, that wasn't so bad. The only bothersome part was that it didn't neatly square up with the existing technology. But then I discovered a fresh approach by Randolph Cabral in which the essence of his approach was to actually wrapper the DataContext with a BusinessContext. Another fellow whom which deserves mention is Mike Hadlow on his blog about incorporating and using a generic repository implementation for which, along with a few other similar “IRepository” implementations, I base my design approach around. As you will see later, the generic Repository along with LINQ, succinctly becomes the main pillar. He was the general concept:

  public class NorthwindDataContext : DataContext {
    // ...
  }

  public class CustomerBus {
    public CustomerBus() {
      //Validators.Add(new SimpleDataValidator());
    }

    internal CustomerBus(Customer linqEntity) 
             : this() {
      LinqEntity = linqEntity;
    }

    private Customer _linqEntity = null;

    internal Customer LinqEntity {
      get {
        if (_linqEntity == null)
          _linqEntity = new Customer();

        return _linqEntity;
      }
      set {
        _linqEntity = value;
      }
    }
  }

  public class NorthwindBusinessContext 
                             : IDisposable {
    protected NorthwindDataContext DataContext { 
      get; set; 
    }

    public CustomerBus GetCustomerBusBy(
                            string customerID) {
      var linqEntity = DataContext
                  .GetTable<Customer>()
                .Single(entity => 
                  entity.CustomerID == customerID);
      var ret = new CustomerBus(linqEntity);
      return ret;
    }

    // TODO: Implement other methods here

    #region IDisposable Members

    public void Dispose() {
      DataContext.Dispose();
    }

    #endregion
  }

Figure 1

Admittedly, I had an oh duh moment. It makes sense! The concept is great, for a bunch of reasons:
  • can leverage template-based code generation (e.g. T4/visual studio templates, designers)
  • gives a clean abstraction to underlying ORM technology
  • leverages/reuses many of the core technology constructs
  • has the same familiar feeling and developer experience
  • follows the technology's investment trend/path (i.e. future proof)
LINQ-To-Business, Anyone?
It hit me: Wow, this looks pretty darn close to a LINQ provider! So I said to myself, "No problem, I'll write a LINQ provider. We'll call it LinqToBusiness or LinqToDomain (I prefer the former; I'm old school contemporary :) )." So for my needs, I wanted the following additional things:
  • deferred querying (so that it got optimized all the way down to the data source)
  • a reusable base framework so that I only have to write it once
  • a simple model mapping scheme to make it designable
  • business objects to be completely persistence ignorant (no DAO or ActiveRecord injection)
  • support out-of-the-box support for generic repositories that can be wrapped by declarative repositories
  • have the same semantics and interface feeling as other LINQ providers
  • support Updateable/Observable LINQ extensions
Revisiting the Case
As a common case, I still have a profound desire to have a dedicated business layer. With any involved business - especially at a B2B/enterprise level, it isn't uncommon to deal with more than one data source within a given system. Surely many of us know or have experienced that. That is the primary and fundamental rationale for a dedicated business layer. As an illustration, My "Order" entity actually may have many different data sources (not limited to a classic DBMS, mind you; what if it is from an external/federated service?) for each department within a business "domain". The Northwind warehouse probably has some oldie goldie legacy system for the order item picker, purchasing probably has some reference to monitor stock fulfillment, and so on. (Microsoft had (has?) a cool lab demonstrating a true enterprise order system showcasing BizTalk, WCF, WF and so on), complete with a COBOL/CICS legacy product picker (Hello, green and black). Behind my nifty business services layer would be my good old component-oriented business layer wrapped by coarse grained wrappers where needed.

The Need
So in a nutshell, I'm looking to establish a "general purpose" BusinessContext such that I could generate/model it using T4 templates or whatever and independently model the true, sure enough business domain.

The Solution?
Consider the following (needs touch-up to compile, but for concept):


public interface IBusinessRootEntity {  }

public interface IBusinessEntity { string Id { get; set; }
}

public interface IRepository where T : class, IBusinessRootEntity {
// To provide coarse-grained/controlled interface over the // SQO where needed; factor into a different interface.
//IBusinessEntity GetBy(string id);
void Add(T entity);
void Remove(T entity);
}

public class Repository : IRepository, IEnumerable where T : class, IBusinessRootEntity {
BusinessContext biz;
BusinessMappingSource mappingSource;

internal Repository(BusinessContext businessContext, BusinessMappingSource mappingSource) {
biz = businessContext;
this.mappingSource = mappingSource;
BusinessMetaDataModel model = mappingSource.GetModel(typeof(T));
}
}

public class BusinessContext {
//IDictionary modelMap = new Dictionary();
BusinessMappingSource mappingSource;
string ConnectionResource { get; set; } public BusinessContext(string connectionResource) {
mappingSource = new BusinessMappingSource(connectionResource, this.GetType());
ConnectionResource = connectionResource;
// TODO: Model map should be read from the mapping source, given // the optional connectionResource (e.q. config section, resource // manifest name, etc.) //modelMap
}

public virtual Repository GetRepository() where T : class, IBusinessRootEntity {
Repository repo = (Repository)Activator .CreateInstance(typeof(Repository<>) .MakeGenericType(new Type[] { typeof(T) }), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, new object[] { this, mappingSource }, null);
return repo;
}
}

public class BusinessMappingSource {
private BusinessMetaDataModel model;
public BusinessMappingSource(string uri, Type contextType) {
model = new BusinessMetaDataModel(uri, typeof(TracsDwDataContext), this);
// other stuff here
}

public BusinessMetaDataModel GetModel(Type context) {
return model;
}
// implement other supporting stuff ...
}

public class BusinessMetaDataModel {
private object identity = new object();

// TODO: Add mapping container here and implement supporting mapping provider logic protected BusinessMetaDataModel() { }
internal BusinessMetaDataModel(string connectionSource, Type contextType, BusinessMappingSource mappingSource) {
ConnectionSource = connectionSource;
ContextType = contextType;
MappingSource = mappingSource;
}

public Type ContextType { get; set; }
public string ConnectionSource { get; set; }
internal object Identity { get { return this.identity; } }
public BusinessMappingSource MappingSource { get; }
} 


Now for the declarative, solution-specific model (VERY thin):


public class NorthwindBusinessContext : BusinessContext {
NorthwindDataContext db;

public NorthwindBusinessContext(string connection) : base(connection) { }
// DEVNOTE: Table = data-centric view of an data MVC (i.e. ORM)
// Repository = business-centric view of the business MVC
public Repository Orders { get { return this.GetRepository(); } }
}

And to consume it:


// The context is a unit of work, a MVC and a LINQ-provider all in one.
// Let's make it transactional too, shall we?
using(var biz = new NorthwindBusinessContext()) {
var q = from c in biz.Customers
// NOTE: We're in the business context; no surrogate keys
// visible, unless also is the "business key"
// (Remember, in an OLAP system it may be CustomerKey, in a SQL Server OLTP
// CustomerID and in a oldie COBOL VSAM system CUSTOMER-ID PIC(X)
// noting EBCIDIC character set)
where c.Id == "ALFKI"
select c;

var c = q.Single();
Order o = new Order();
o.OrderID = new Guid();
// set the order ...
// careful, we are in business context now, so should speak
// "business lingo" not data lingo; InsertOnSubmit is wrong lingo.
c.Orders.Add(o);

// mark the unit of work as done and by default commit
biz.Save();
}

With this approach, everything would stay neat and clean and leverage all of the latest facilities. The only real requirement on the consuming developer’s part is specifying the business model and its relationship to the underlying data sources. That’s where the custom LINQ-To-Business query provider comes into play and will save for a later post.

The ultimate goal is to stay within the technology paradigm so that we can squeeze out every last drop of  goodness from the technology environment. Leveraging the power of generics, lambdas, anonymous types, type inferencing, extension methods and of course the LINQ infrastructure, you get the very best experience possible. I’d argue that, of these, probably the most profound of benefits is compile-time support via the type safety features (generics, inferencing) and the query ability via LINQ SQO.

This is only the beginning of this idea, so stay tuned...

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.