Click here to Skip to main content
15,868,096 members
Articles / Hosted Services / Azure

Kerosene Dynamic Data Services

Rate me:
Please Sign up or sign in to vote.
4.94/5 (14 votes)
11 Mar 2014CPOL42 min read 25.7K   13   9
Implementing the Repository and Unit of Work Patterns dynamically using Kerosene ORM

Introduction

This article is divided in three main sections. The first one proposes a dynamic and agnostic way of combining both the Repository pattern and the Unit of Work one, into a Dynamic Data Services solution, in such a way that expands their capabilities, and overcomes the annoyances that most of the common implementations we can currently find for them have. This proposal has been built in an agnostic way so it can be implemented using any arbitrary underlying database or ORM technology, as far as the concepts discussed here are supported.

The second section introduces a concrete implementation based upon the Kerosene ORM's "Entity Maps" operational mode. Even it for using this implementation is it absolutely not needed, you may want to take a look at the Kerosene ORM Introductory Article that, along with its accompanying articles, contains all the background information for this library and the extended capabilities it can provide.

The third section contains a set of samples on how to use the proposed Dynamic Data Services solution. It comes with three complete n-Tier projects, each containing its own front-end, mid-tier and back-end libraries, that can be used to follow the samples in this article, as well as if it were templates you can use to quick-start your own projects.

A bit of motivation

Nowadays all but the most trivial enterprise applications use some sort of Repository and Unit of Work patterns. Many rely on the facilities provided by the specific ORM or database technology they are using, even without realizing that sometimes these implement those patterns to some extend only. Others take the abstract route and develop specific interfaces for those patterns, which they have later to tie with the underlying ORM or database technology they are going to use. Either case, in general, current implementations have a number of restrictions and annoyances that make us feel uncomfortable. We are going to try to overcome them in this article.

Let's start by defining what are we going to understand when talking about these patterns. I am not pretending to be original here, so I have copied and adapted what follows from many sources, including Martin Fowler's site:

  • A Repository mediates between the domain objects (also known as Entities or instances of your POCO classes) and the data mapping and database layers, acting as an in-memory collection-like interface for accesing those domain entities. The Repository isolates your application from the details of the underlying database or ORM technologies, providing a place where all the query constructions are concentrated.
  • The Unit of Work mechanism is in charge of maintaining the list of entities that may have been affected by a business-level operation, and when that operation wants to persist it changes in the database, it then coordinates the execution of the the database related commands, under a transactional context, and controlling the possible concurrency problems.

Problems with current Repository pattern implementations

What typically happens with current implementations of the Repository patterns is that we will end having such a proliferation of 'FindByXxx()' methods that maintenance and evolution of our interfaces can become a nightmare (to some extend this is why some authors are calling the Repository pattern as the new Singleton one):

C#
public interface ICustomerRepository
{
   ICustomer FindById(Guid uid);
   ICustomer FindByFirstName(string firstName);
   ICustomer FindByLastName(string lastName);
   ICollection<ICustomer> FindByBirthDateYear(int year);
   ICollection<ICustomer> FindByCountry(Guid countryId);
   ...
}

And this is not our only problem! What happens if the query logic we need to use for a given problem involves several tables? Let's suppose, for instance, that we are asked to find all the employees that belong to a given country. Or to a set of countries. And that then we want to order that list depending upon to what region the country belongs to...

Well, we can always load into memory the list of countries, using its corresponding factory, and then feed into an ad-hoc list the employees we will find per each country. At the end of the day having those lists in memory is not a big deal, right? No, sorry, I cannot agree. In this easy case it might be true, but in the general one we can easily end up loading into memory huge object graphs, potentially the complete database. This is not only inefficient from a memory consumption point of view, it can even generate noticeable delays in the response of our application while those graphs are loaded, not to mention the tons of calls to issue against the database this approach may involve.

Fine, we can always use, Entity Framework, or nHibernate, or almost all other ORMs to address these needs, right? Well, they also have some problems:

  • Firstly, if you try to write any query logic that needs to involve any element for which there is not a member in your POCO classes then you are entering into problems. The way we are forced to write our queries to address this situations is not, by far, intuitive or trivial. And not to mention the fat code such procedure will end up producing and, potentially, again, the ton of calls issued against the database.
  • Secondly, if we take this approach, then we are tying our solution to one of these specific technologies. Its substitution, or evolution, will become a maintenance nightmare. What if tomorrow, for instance, your company decides to change the underlying database engines using a new one that is not yet supported by your ORM? How many half-smiles are you prepared to withstand from, yes, these guys in the other department you appreciate so much?
  • Finally, this approach will inherently implies that we are giving visibility, or 'exporting', the details of our underlying ORM or database technologies up to the business level of our application. And this precisely one of the things we wanted to avoid by using these patterns.

Problems with current Unit of Work pattern implementations

Ok, now you are half convinced. But you do still think that those solutions are a perfect fit for the Unit Of Work pattern specifications. Let me anyhow point out some considerations:

  • They typically require you to write and to maintain external mapping and configuration files, to follow some conventions, or some rules about magic words and configurations, or to pollute your POCO classes with ORM-related stuff. And this breaks, in my modest opinion, the principle of separation of concerns, not to mention that sometimes we are not even in the position of being able to modify these nice corporate libraries that contain our POCO classes.
  • It can be even worse, some solutions will even require you to derive your classes from an ORM-specific one. Buf! I can't do anything but to tremble each time I see one of these.
  • What if you want to use them in an n-Tier application scenario? What if you want to be very strict and so splitting the front-end and the business layer into different machines? How do you currently guarantee that the identity of the client at the HTML side progresses as expected into the protected business layer machine? What if you want to use, for instance, WCF to communicate between this two layers? Many, many times you will be involved in not only some complex configurations but also, at the same time, writing code at both sides whose complexity you cannot really, well, easily justify.
  • And, finally, the same set of considerations apply about tying our solution to a given concrete technology, and being in the need of giving access and/or visibility to the database-related stuff to levels that should not have has this kind of information.

The Agnostic Dynamic Data Services Proposal

I hope that, at least, you think by now that the above considerations are a problem that has to be addressed. The aforementioned solutions are extremely good solutions (not in vain they are the big guys in this game so far), and if you are prepared to live with the annoyances we have briefly seen, this is a perfectly valid choice, of course.

If you rather dislike what we have mentioned, and you are prepared to use a different approach, or even if you are just curious about what this approach is about, we are going to propose in this article an alternative route to overcomes these problems.

The proposal consists in the definition of a small set of interfaces that working together will provide our applications with an "Agnostic and Dynamic Data Services" capability. Its aims are to simplify your application code, easy your maintenance and evolution needs, and to solve the annoyances discussed above.

By agnostic we mean that nothing in our interfaces will be tied to any specific ORM or database technology. Indeed, what they permit is that when these interfaces are used in a front-end scenario they will provide any indication on what would even be the underlying database or supporting ORM technology, if any.

By dynamic we mean that we are going to squeeze to its limits the possibilities offered by C# dynamics and lambda expressions working together. Java will need to wait until it delivers the promise of supporting these features, and in the proper way. There is not an equivalent proposal developed for C++, Visual Basic, or any other languages yet.

For simplicity all the interfaces contained in this proposal have been included in the 'Kerosene.DataServices.Agnostic' assembly that is included in the download. You can drop this class library elsewhere in your solution, include a reference to it into your front-end application, and start enjoying it.

The Data Context Interface

Our first interface will be the 'IDataLink' one. Its jobs is to represent a given data context or connection against an arbitrary database-alike service, completely hidden from our business level, on which we can provide support for the Unit of Work pattern. It will also serve as a factory to create Data Service objects. Its definition is as follows:

C#
public interface IDataLink : IDisposable
{
   void SubmitChanges();
   void DiscardChanges();
   
   IDataService<T> DataService<T>() where T : class;
}

As mentioned, this generic data context can refer to any database-alike service. It could be a standard direct connection with a regular database. But it also could be a WCF connection against a service that emulates such database capabilities in any way it wishes so. Or it could use REST or OData as far as the server-side part can fulfill the capabilities defined in this set of interfaces. In any case the concrete details of this connection are not needed for this interface, and it does not provide any methods or properties to access them. This proposal remains completely agnostic and makes no assumptions about it.

Also, this proposal does not define how this interface have to be implemented, so is left to the implementer to use any appropriate underlying technology. Finally, this proposal also does not define how an object implementing this interface can be created, using a constructor, an IoC framework, or any other mechanism.

Please find later below in the second section of this article an example of a concrete implementation built using the Kerosene ORM "Entity Maps" operational mode, as we have mentioned.

The 'SubmitChanges()' and the 'DiscardChanges()' methods let the interface provide support to the Unit Of Work pattern.

'SubmitChanges()' will persist into the database all pending change operations (defined as the 'Insert', 'Update' and 'Delete' ones) that may have been annotated on any of the entities managed by this data context. We are going to ask this method to open and close any connections needed to the underlying databases on our behalf; that it executes all change operations wrapped by a transactional context, so that them all succeed or fail at once; and that it controls concurrency in an appropriate way. At our business application level we don't need to have any information about how these requirements are fulfilled. Once this method is executed we ask it to guarantee us that there will be no remaining change operations annotated in any of the entities managed by the data context, regardless if the method has succeeded or fail.

The second method, the 'DiscardChanges()' one, will be used when, for whatever circumstances, we don't want to execute the pending change operations that may have been annotated in this data context. When this method is invoked all those pending change operations are cleared and disposed.

This interface implements the 'IDisposable' one so that we can either wait for the GC to handle it, or either we can dispose it along with all resources it may hold, at the moment our application needs to do so. Note that we are going to assume in this proposal that, for the general case, explicit disposal is not mandatory.

As a side note it is assumed that the objects that implement this interface will have some sort of internal cache where to store the entities associated to it. At this business level this proposal provides no methods or properties to access and manage that cache, if any exists, which are capabilities left to the concrete implementation.

Finally, the 'DataService<T>()' method is a factory one that will permit us to instantiate the concrete data service that manages the entities of the type given. If no data service is available in this data context for the entities of the type requested, this method shall return 'null'. Registration of types into the data context, and the concrete definitions about how its members are mapped against columns in the database, is not covered in this proposal. These activities are not considered front-end ones, but rather mid-tier, or even back-end ones, and then shall be addressed at these levels along with the concrete implementation of these interfaces.

We are going to impose the restriction that those entities must be of a reference type, a class not an struct, so that if our application receives back a 'null' reference from a query operation it will be a perfectly valid result, and no exceptions are to be thrown in this case. If your application uses a different approach please feel free to throw your own exceptions when such 'null' value is returned, or if you wish when obtaining an empty list or array of results.

The Data Services Interface

Our second interface will be the data services one itself, as follows:

C#
public interface IDataService<T> : IDisposable where T : class
{
   IDataLink DataLink { get; }
   
   IDataQuery<T> Query();
   IDataQuery<T> Where(Func<dynamic, object> where);
   
   T Find(params Func<dynamic, object>[] specs);
   T Refresh(T entity);
   
   IDataInsert<T> Insert(T entity);
   IDataUpdate<T> Update(T entity);
   IDataDelete<T> Delete(T entity);
}

Its job is to provide us access to the CRUD operations our application will need to execute for its related entities. In the general case we will be able to obtain an object implementing this interface using the factory method implemented by the 'IDataLink' interface.

Indeed, its 'DataLink' property will permit us to access the data context this data service belongs to. Our application may have as many data contexts as needed, each with its own set of data services available, implementing any singleton, per-connection, or per-request pattern its concrete implementation has decided to use. This proposal imposes no restrictions at this regards.

The 'Query()', 'Where()', 'Insert()', 'Update()' and 'Delete()' methods are factory ones that instantiate the appropriate objects that represent the concrete operation their names imply. Each of them will be discussed below in its own section.

The 'Find()' method will immediately return, preferably from the data context's cache, if any is available, an entity whose contents matches the given specifications. If the cache contains no such entity then this method will create and execute a query command to find that entity in the underlying database or service. If, finally, the database contains no such entity, this method will return a 'null' reference.

This method takes a variable list of "Dynamic Lambda Expression" as its arguments. In the general case, these DLEs are defined as lambda expressions where at least one of their arguments is a dynamic one. In our case these DLEs have the 'Func<dynamic, object>' signature, and must resolve into a comparison expression, as for instance happens in the 'x => x.LastName == "Bond"' one. Note that by using DLEs our comparison expression can use any logic we may need, comparing a 'Column' element, or a 'Table.Column' one, to any value, or to any dynamic expression that can be parsed into a valid SQL command. Our entity classes are not even required to have members that correspond to the elements used in these specifications.

The way that these Dynamic Lambda Expressions are parsed and converted into a valid SQL command is out of the scope of this generic proposal (please see later below the Kerosene implementation for more details). It is assumed it can handle any valid SQL syntax the underlying database can understand, and that, while parsing an object or expression, it can extract the arguments it encounters and keep them ready so that they can be used later when generating the appropriate command avoiding SQL injection attacks.

The 'Refresh()' method is used with a given entity as its argument in order to refresh its contents with the most up-to-date ones from the database. This method returns immediately with a reference to an entity with such refreshed contents, or nullif it can not be found in the database. This reference is not, in any case, guaranteed to be a reference to the original entity passed as the argument of the method.

Finally, this interface implement the 'IDisposable' interface in order to signal its data context that it is not needed any longer. The details of this disposal procedure are not covered by this proposal, and are left to the concrete implementation.

The Query Interface

The query interface represents a query operation against the underlying database represented by the data context. Its definition is as follows:

C#
public interface IDataQuery<T> : IDisposable, IEnumerable<T> where T : class
{
   IDataService<T> DataService { get; }
   
   string TraceString();   
   new IDataQueryEnumerator<T> GetEnumerator();
   
   IDataQuery<T> Top(int top);
   IDataQuery<T> Where(Func<dynamic, object> where);
   IDataQuery<T> OrderBy(params Func<dynamic, object>[] orderings);
   
   IDataQuery<T> Skip(int skip);
   IDataQuery<T> Take(int take);
   
   IDataQuery<T> TableAlias(Func<dynamic, object> alias);
   IDataQuery<T> From(params Func<dynamic, object>[] froms);
}

This definition permits us to express any logic we may need to use in a dynamic and flexible way, so avoiding the proliferation of find methods found in other implementations of the Repository pattern while, at the same time, allowing us to express any complex arbitrary logic we may need to use to solve a given business problem.

Its 'DataService' property gets the reference to the data service this command is associated with, and through its own 'DataLink' property the application will have access to the data context where the command will be executed and the one that will, eventually, store the obtained entities in its cache.

Its 'TraceString()' method lets the application obtain a trace string based upon the current specifications of the command, including the SQL command text and its parameter as captured by the parsing procedure. The concrete format of this string is left to the concrete implementation of this method.

Its 'GetEnumerator()' method let the interface implement the 'IEnumerable<T>' one. It returns an object that implements the 'IDataQueryEnumerator' interface, which in turn implement the 'IEnumerator<T>' one, and that will be ultimately the one that executes this command. The way this enumerator is implemented is not defined by this proposal, but it is expected to return actual entities of the type of the data service it is associated with, loaded with the contents fetched from the database or service.

Its 'Top()', 'Where()' and 'OrderBy()' methods permits the application to specify the most common contents of a query operation. Note that these methods will return the reference to its underlying command so that they can all be used in a fluent fashion syntax.

'Top()' takes an integer as its parameter, used to define the contents of the 'Top' clause: the maximum number of records to return. If this integer is a negative value then it is interpreted as a signal to clear the contents of this clause, and it is expected that no exceptions are thrown in this case.

'Where()' permits us to specify the contents of the 'Where' clause: the conditions that the entities shall meet to be returned as a result of this command. It takes a DLE as its argument that will be used to express those conditions, and that shall resolve into a boolean value. This DLE shall accept any valid SQL code that otherwise would be acceptable for the underlying database. This method can be used as many times as needed, and by default the new contents will be concatenated with any previous ones using an 'AND' operator.

This proposal defines a syntax convention to modify this default behavior: the DLE shall resolve into a 'And()' or 'Or()' virtual extension method, whose argument will be the contents of the 'Where' clause to be appended to any previous ones, as follows:

C#
var query = ...
   .Where(x => x.Or( ... ));

A Virtual Extension Method is defined, in the general case, as a method that does not exist on the element it is applied to, but since it is used in the dynamic context of a DLE, it can be used to express any qualification we need on that previous element. In our case the 'Or()' method applied to the dynamic argument itself.

'OrderBy()' permits the application to set the ordering to use to return the results. It takes a variable list of DLEs that shall resolve into the name of the column, or table.column combination, to use for that ordering. The default ordering, unless modified, will be an ascending one. This proposal defines a syntax by which this default ordering can be modified. It consist in appending to the column or table.column specification a virtual extension method whose name is any of the following ones: 'Ascending()', 'Asc()', 'Descending()' or 'Desc()'. For instance:

C#
var query = ...
   .OrderBy(x => x.FirstName.Descending(), ...);

'Skip()' and 'Take()' are used to instruct the the command that it shall discard the first 'skip' records at most, and if there are still results available, then return the 'next' take ones at most. Both methods take an integer as its parameter that, if it is a negative value, will instruct the solution to clear their respective clauses. The concrete implementation of this methods, eventually translating their contents into a valid SQL syntax supported by the underlying database, or even implementing this feature by software, is not defined by this generic proposal.

This interface implement the 'IDisposable' one in order to signal that it is not needed any longer. The details of this disposal procedure are not covered by this proposal, and are left to the concrete implementation. It is anyhow assumed that disposal will not be needed for regular scenarios.

Complex Queries and Extensibility Syntax

This proposal includes a series of methods by which the query commands can incorporate almost any advanced logic the applications may need into it. They use the assumption that, behind the scenes, the data context knows what will the primary table in the database-alike service where to find the entities of the type the data service manages. So, when we were using query logic that involves several tables, there must be a way by which we can assign an alias to that primary table, even without knowing which the concrete one will be, so that there will be no collision of names when the final command is generated and sent to the database.

The 'TableAlias()' method is proposed to deliver precisely this capability. It takes as its argument a DLE that must resolve into the name of alias to use with that hidden primary table:

C#
var query = ...
   .TableAlias(x => x.Emp);

When this method is used we can refer to that primary table, in the logic of this concrete command, by using the alias assigned to it. In the example we are assigning the 'Emp' alias to our primary table, maybe the 'Employees' one but this knowledge is not even required, so that it can be used as needed in the context of this query command only. Other query commands will need to define their own primary table aliases if such is what they needed for their own purposes.

Secondly, to incorporate other sources into the 'From' clause of the query command our application can use the 'From()' method. It takes a variable number of DLEs each specifying an additional table or source of contents:

C#
var query = ...
   .From(x => x.Countries.As(x.Ctry));

This proposal defines a syntax convention to assign an alias to any element in a DLE. It is achieved by using the 'As()' virtual extension method on the element to qualify, whose argument is a DLE that shall resolves into the name of that alias. In our example we are assigning the 'Ctry' alias to the 'Countries' table, and we can use either one to express our query conditions as it would be supported by the underlying database syntax.

Once we have used these two methods we can write query logic that uses the aliases defined, as for instance:

C#
var query = ...
   .Where(x => x.Emp.CountryId == x.Ctry.Id);

Table specifications are not the only source of contents that will be supported by the 'From()' method, we can use any other command or SQL expression as needed. There is one caveat, though: any additional source of contents must be associated with its own alias even if a specific 'As()' method is not supported by that element.

To solve this, and many other extensibility scenarios, this proposal incorporates a way to extend the syntax of an expression by using 'direct invocations' on a previous dynamic element. This mechanism is known as the "escape syntax" one. At any time any element is invoked direcly, as if it were a method or function, the parser must parse any arguments used in that invocation, concatenate them, and inject them as-is. For instance:

C#
var other = ...; // whatever command, expression, or SQL text
var query = ...
   .From(x => x(other).As(x.OtherAlias));

What we have done here was to wrap into a dynamic invocation the additional source of contents we were interested on, and then apply, on the dynamic element returned, the 'As()' virtual extension method. It is assumed that the parser engine is able to identify and use, as appropriate, any aliases it may encounter while building the contents of the method.

This mechanism can also be used to incorporate any element that, being supported by the underlying database, might not be intercepted and treated appropriately by the parsing engine. As an example, if our database service supports a method named 'Foo()' that can be applied to a given previous element in a specification, and if it also supports a 'Bar()' one that takes a number of arguments, we can write something like what follows:

C#
var cmd = ...
   .Where(x => x.Element.Foo() && x.Bar(x.Emp.Id, "whatever"));

For completeness, the 'Where' clause this example will produce shall be as follows:

... WHERE Element.Foo() AND Bar(Emp.Id, 'whatever')

Other Query Methods

Other possibly methods and constructions, as the 'Select', 'Distinct', 'GroupBy', 'Having', and 'With / CTEs' ones, are not included in this proposal. It is left to the concrete implementation to decide whether they bring enough value so that it worth to include them.

In any case, the concrete implementation is required to know what elements to get from the database and how to map them into the appropriate members of the POCO type the data service relates to. So, in the general case, 'Select'-alike methods won't be needed, even it would be undesirable to have them. Similar considerations apply to the other methods mentioned, with the aggravating circumstance that we are expecting to receive unique instances of our POCO classes when using the methods provide by a data service, and not, in this case, records with an arbitrary sets of contents (*).

In any case the Kerosene Dynamic Data Services implementation below is based on <Kerosene ORM "Entity Maps", which do carry these methods and much more. It is a trivial exercise to export them into your customized interfaces if needed.

(*) if this is precisely what your application may need in a given context I do encourage you to take a look at the Kerosene ORM's "Dynamic Records" operational mode, which is explicitly architected to provide such capabilities - at the price of opening the Pandora's box and giving to your business layer visibility or access to the underlying database. But not such power can come without paying for it.

The Insert, Delete and Update Interfaces

The 'IDataInsert', 'IDataUpdate' and 'IDataDelete' interfaces represent the operations their names imply. The three of them share the same common structure that follows:

C#
public interface IDataInsert<T> : IDisposable where T : class
{
   IDataService<T> DataService { get; }
   T Entity { get; }
   
   string TraceString();
   void Submit();
   bool Executed { get; }
}

The 'DataService' property permits our application to access the reference of the data service this command is associated with. We can use its own 'DataLink' property to access the data context, the one where the change operations are going to be annotated.

The 'TraceString()' method lets the application obtain a trace string based upon the current specifications of the command, including the SQL command text and its parameter as captured by the parsing procedure. It is acceptable for this string to be 'null' if there is no need to execute any concrete SQL operation. This case is said to be a 'impotent' command, and it may appear in some scenarios; for instance, when we have an update command defined for an entity and the underlying implementation cannot find any changes to persist in its contents. The format of this string is left to the concrete implementation of this method.

The 'Entity' property maintains a reference to the entity that will be affected by the execution of the command. Or, in other words, the entity where the pending change operation is to be annotated.

Creating an instance of a change operation just create an object able to store its specifications, but nothing else. Executing it is a two step process. The first one is to submit the operation, so that it gets annotated, or associated with its entity, for future execution. This is achieved by using the 'Submit()' method of the change operation interface.

Afterwards, when the application is happy with all the change operations annotated in the entities managed by the data context, it can use the data context's 'SubmitChanges()' method to persist them all at once, as explained before. Remember that the data context interface also has a 'DiscardChanges()' method that can be used to discard all the pending operations without executing them.

Finally, the 'Executed' property returns whether this change operation has been already executed, or not. Note that all these interfaces are intended to be used only once, even if their instances have been already executed. Trying to recycle one of these objects is not supported, and can lead to unexpected results.

These interfaces implement the 'IDisposable' one in order to signal that they are not needed any longer. For instance, the data context's 'DiscardChanges()' method will dispose any pending operations that might be annotated on the entities it manages. Anyhow, the details of this disposal procedure are not covered by this proposal, and are left to the concrete implementation. It assumed that explicit disposal will not be needed for regular scenarios.

The Kerosene Dynamic Data Services Implementation

The 'Kerosene.DataServices.Concrete' class library assembly included in the download contains a concrete implementation of the 'Dynamic Data Services' proposal described in this article, based upon the Kerosene ORM's "Entity Maps" operational mode library.

Indeed, there is almost a one-to-one relationship between the core elements provided by "Entity Maps" and the ones proposed by "Dynamic Data Services". We are not going to repeat here all the discussions regarding the former that can be found in the introductory article and in its accompanying ones. Instead we are going to focus just on the details of how the later has been implemented using it.

The implementation classes

The DataLink class

This is the class that implements the 'IDataLink' interface. Internally it is just a wrapper over the Kerosene ORM's 'KMetaLink' class that provides the functionality we need. In particular, it provides access to the 'IKLink' object that ultimately maintains the agnostic connection with the database service it will use, along with all internal structures it needs to manage the cache of entities. Indeed, its constructor takes that 'IKLink' reference as its argument.

Just for completeness let me reinforce that this 'IKLink' object can refer to any valid mechanism by which it can access a database-alike service, it being a direct connection, a connection against a WCF service, or any other arrangements. As mentioned, the front-end application using the "Dynamic Data Services" interfaces is not even expected to know what would be the concrete one attending its requests.

The DataService<T> class

This is the class that implements the 'IDataService<T>' interface. Internally it is just a wrapper over the 'KMetaMap<T>' class that, as it names implies, provides a way to map your POCO class members into the appropriate columns of a primary table in the underlying database. Its constructor takes an instance of the above 'DataLink' class as its argument.

Its properties and methods are basically the same ones as defined for the generic interface proposed, so we are not going to discuss them here again. Please refer to the introductory article and its accompanying materials for more details.

The Command classes

Similarly, this assembly provides the 'DataQuery<T>', 'DataInsert<T>', 'DataUpdate<T>' and 'DataDelete<T>' classes that implement the methods we have discussed for their respective interfaces in the above section. To prevent this to become an even longer article we are not going to repeat these discussions here.

Just for completeness, the constructor of the 'DataQuery<T>' class takes just one constructor that is the data service this new instance will be associated with. Similarly, the constructors of the 'DataInsert<T>', 'DataUpdate<T>' and 'DataDelete<T>' ones take the same argument as well as the reference to the instance they refer to.

Recommended n-Tier Project Structure

Let me point out some considerations regarding what would be the recommended project structure in an n-Tier scenario. This is really not really part of the 'Kerosene Dynamic Data Services' implementation, but it worth mentioning it here. Please take a look at the following screen capture:

N-Tier Project Structure

This picture reflects the structure of a fictitious application named "Blazer", a minimalist HR system I am using to provide the samples that come with the download. Actually, this capture relates to one of the three samples provided, as we will see later in the third section of this article.

The Front-End Layer

This layer contains a simple console application: I didn't want to enter into the details of a heavy client or MVC implementation, as it would have diverted our attention from what we are talking about. What it is interesting to mention instead is that this level only has visibility on the assemblies lying in the mid-tier one, like the POCO types assembly and the Blazer-related data services ones, as we can see in its references:

Front-End Project References

Yes, we had to add references to the agnostics data services assembly, as well as to the supporting tools library, among the other ones you can have expected. Please bear in mind the second line, the 'Resolver' one, that we are going to discuss in a moment.

In an MVC application context, for instance, we may end adding to this layer a myriad of other elements. An example can be the view models this front-end might be using. In any case, when these view models, or any other element, need to execute any database related operation, they will revert to the data services provided by the mid-tier.

The Mid-Tier layer

The two main assemblies contained in this level are the one that contains the POCO classes ('Blazer.LazyMaps.Pocos' in the example above), and the one that contains the business logic of our application, implemented as a set of data services (the 'Blazer.LazyMaps.DataServices.Agnostic' one in the above example). The references of the POCO assembly are of little interest for our current discussion, but it may help to have handy the references included in the application-level data services one:

Mid-Tier Data Services Project References

Ok, so we only need to have visibility here about the POCOs our application is going to use, and about the generic data services interfaces we have proposed in the first section of this article. Even if it is precisely what it was expected I have always found this fact quite interesting.

As a side note let me point out that the place where to implement your business logic is, at the end of the day, mostly a matter of personal taste. You could have chosen to implement it into your POCO classes if you wish, using the generic data services just to persist and get entities from your underlying database-alike service. I have chosen instead to implement the business logic in the data services themselves, creating a specific interface per each POCO class. For instance:

C#
public interface IEmployeeDataService : IDataService<Employee>
{
   decimal CalculateBonus(int year, int quarter);
}

I really don't want to enter here into a discussion about the pros and the cons of each approach; please choose the one you feel more comfortable with.

This application-related data services assembly also contains an specialized interface for the data context object. At the end of the day it is nothing more than a factory that will permit us to instantiate these application level data services, as follows:

C#
public interface IBlazerDataLink : IDataLink
{
   IRegionDataService CreateRegionDataService();
   ICountryDataService CreateCountryDataService();
   IEmployeeDataService CreateEmployeeDataService();
}

In this case I have opted for a per-request creation pattern but, as we have seen above, we could have also used a singleton or per-connection ones, as it better fits into our application needs. Note that this is an application-level design decision, and has nothing to do with the Kerosene Dynamic Data Services implementation we were discussing.

Let me postpone the discussion about the 'Resolver' assembly for a while. Later it will be clearer its intention.

The Back-End Layer

This layer is the one that contains all our infrastructure related stuff, as the scripts to initialize the database, for instance, along with the specific knowledge on what those databases would be. It is also the place where to implement the concrete data services for our application. in our example this layer contains just one assembly, the 'Blazer.LazyMaps.DataServices.Concrete' one, whose structure and references are as follows:

Back-End Project References and Structure

Let's start with the references first. We need to have visibility of all the elements we have defined in our mid-tier level, hence why the references to the 'Blazer.xxx' assemblies. Then we have added the reference to the agnostic Dynamic Data Services proposal discussed in this article, as well as the reference to the concrete 'Kerosene' implementation we are going to use. Because this implementation uses, behind the scenes, the 'Kerosene ORM' library, in particular its Entity Maps operational mode, we needed to include the references that support that set of libraries. As a side note, we have also included a reference to the customized 'Kerosene ORM' support for MS SQL Server 2008 and 2012 databases because, at this level, we are precisely dealing with this kind of knowledge. All of this is really straightforward.

Let's now move on an take a look at the 'Maps' folder. As we are using Kerosene ORM here we have defined the concrete 'Maps' that link our POCO classes to their respective table in the database. Please, do remember also that these maps are even optional depending upon the concrete maps' mode we are using. I'm going to postpone this discussion till the third section of this article.

Moving up the next folder is the 'Domain' one. This is very interesting as we have here included the implementation of our application-specific data services. If we would have chosen not to use them, then this folder will be empty. In our case, remember we have moved the business logic into this objects, so here we provide the appropriate implementation of their interfaces. Please see the following example:

C#
public class EmployeeDataService : DataService<Employee>, IEmployeeDataService
{
   public EmployeeDataService(BlazerDataLink dataLink) : base(dataLink) { }
   
   public new BlazerDataLink DataLink { get { return (BlazerDataLink)base.DataLink; } }
   IDataLink IDataService<Employee>.DataLink { get { return this.DataLink; } }
   
   public decimal CalculateBonus(int year, int quarter) { ... }
}

This class derives from the base 'DataService<T>' one, that provides the core data services functionality for a given type, and implements the 'IEmployeeDataService' interface our business logic level has defined. Its constructor takes an instance of a customized data context object, as we will see in a minute, and then just implement the interface as appropriate.

Let us now move up again an take a look at the remaining 'BlazerDataLink' class. As it name implies this is customized data context object for our application. In this sample I have chosen to use a per-thread singleton instance combined with a factory pattern to instantiate this object, as follows:

C#
public class BlazerDataLink : DataLink, IBlazerDataLink
{
   [ThreadStatic] static BlazerDataLink _Instance = null;
   static string _ConnectionString = null;
   static string _ServerVersion = null;
   static bool _EnginesAreRegistered = false;
   
   public static void Configure(string cnstr, string version)
   {
      _ConnectionString = cnstr == null ? null : ((cnstr = cnstr.Trim()).Length == 0 ? null : cnstr);
      _ServerVersion = version == null ? null : ((version = version.Trim()).Length == 0 ? null : version);
   }
   
   public static BlazerDataLink Instance { get { ... } }
   private BlazerDataLink(IKLink link) : base(link) { }
   
   ...
 }

This arrangement will permit us to provide back a singleton instance per thread, which is a perfect fit for internet based applications as, in this case, an instance will be created per each connected user, and will be disposed automatically by the GC when this user is not longer connected. Of course in your concrete scenario you can choose to use any other pattern that better fits into your needs.

To support this arrangement I have included two additional elements. The first one is the static 'Instance' property whose getter will return the instance in use by the current thread, creating it if needed:

C#
 ...
public static BlazerDataLink Instance
{
   get
   {
      if(_Instance == null)
      {
         if(!_EnginesAreRegistered)
         {
            KEngineFactory.Register(new KEngineSqlServer2008());
            KEngineFactory.Register(new KEngineSqlServer2012());
            _EnginesAreRegistered = true;
         }

         var link = KLinkDirectFactory.Create(_ConnectionString, _ServerVersion);
         link.AddTransformer<CalendarDate>(x => x.ToDateTime());
         link.AddTransformer<ClockTime>(x => x.ToString());

         new RegionMap(link);
         new CountryMap(link);
         new EmployeeMap(link);

         _Instance = new BlazerDataLink(link);
      }
      return _Instance;
   }
}
...

If the per-thread static instance is created already we are done and we can just return it. Otherwise we need to take some steps. The first one is to make sure that the custom database engines we are going to use are registered into the engine's factory. Please refer to the introductory article for more details.

The second one is to create the internal Kerosene ORM link instance that will support this data link. We have used in this case the direct link factory to achieve this. Along with creating it we have also register into it the transformers for the custom types we may end using as arguments of our commands. For both things, again, please refer to the introductory article for more details.

The third step is to create the maps that our infrastructure will use for the entities it manages. I have chosen to create them all in advance here because it pleases my personal taste of having all these activities concentrated in a single place. But you could have chosen to create them on-demand if you wished so.

Finally, we have used our newly created Kerosene ORM link to create the instance this thread will use, and we have returned it.

The second additional element is the static 'Configure()' method. The idea is that the start-up code can call this method to specify the connection string entry to use, or the name of the engine to use, along with its requested version. Note that this method is optional and, if the start-up code does not use it, Kerosene ORM will use a predefined selector entry in the application's configuration files to locate the appropriate values. Again, sorry, please refer to the introductory article and related materials for more details.

Let's move on. Note that apart from deriving from the base 'DataLink' class it also implements the application-level 'IBlazerDataLink' interface we have defined. Do remember as well that we have created it as a factory for the data services of the entities of our solution. It looks like that it has sense to prevent the creation of any data service that it is not under the ones provided by this interface, so we can end our class definition by writing what follows:

C#
...
IDataService<T> IDataLink.DataService<T> { ... }

public RegionDataService CreateRegionDataService() { return new RegionDataService(this); }
IRegionDataService IBlazerDataLink.CreateRegionDataService() { return this.CreateRegionDataService(); }
...

The first line is just used to hide the capability of creating any generic data service from any external user of this class. Whether you choose to implement is as 'return base.DataService<T>();', or to throw an exception, is completely up to what you do want.

The second and third lines just implement the interface's capability of creating and returning the specific data service for one of our known entity types. I have just included one example because the code for the other known types just follow the same pattern.

The Resolver

Ok, let's just now move into the discussion of the 'Resolver' assembly. Remember that our application-level agnostic data services one defines the 'IBlazerDataLink' interface that will be, ultimately, the one used by our front-end application. The problem lies in how to provide to that application the appropriate instance.

Resolver Project References and Contents

Yes, in production code I would have most probably taken the route of an IoC framework, as Unity, nInject, LinFu or many others. But I wanted not to divert our attention with their particularities, and so I took the hard route of writing this separate assembly, containing just one static 'Factory' class, whose only 'DataLink' property gets back the per-thread instance we have defined before. It had to be a separate assembly to avoid having circular references between the main assemblies in the solution, as shown in the above picture.

And that's it! The morale of this history if that you should have to have a way to instantiate your application-specific data context instance the way you want, and using the pattern that better fits into you concrete application's needs.

Maintenance and Evolution considerations

To finalize this section let me just mention two related facts. The first one is that this is the only place in all our project structure where we have included the details of the underlying ORM technology we are using, in this case the 'Kerosene ORM' one. If in the future we would like to substitute it here is the place to do all modifications, so maintance and evolution is notably simplified.

The second fact relates to our knowledge or our underlying database. At the risk of boring you let me just recall that 'Kerosene ORM' just need a mere indication on what would be the concrete database engine you are going to use, and just a bare minimum knowledge on the structure of your database: basically the just the names of the tables and columns involved. It has not required us to write any external mapping or configuration files, not is has required us to pollute our POCO classes with ORM-related stuff, and, obviously, we have not had to derive those classes from any ORM base one.

Other Project Structures

What the download contains, what we have discussed so far, is probably among the most complex scenarios where you can use the 'Dynamic Data Services' capability we have discussed in this article. Most of the complexity comes from the fact of having used it in a n-Tier scenario, where also we wanted to be very strict regarding who has visibility on what, and not from its definition or concrete implementation.

Indeed, in a heavy-application scenario, for instance, the above structure is much more simpler. What's more, if we wanted to go through the easiest possible route, we could have chosen not to use it at all, and have used the 'Kerosene ORM' "Entity Maps" capabilities instead. In any case if we choose to use the proposal of this article we will obtain the benefit of a decoupled structure between our application-level code and the details of the ORM technology used.

Other possible scenario is the one where the communications between the front-end and the mid-tier levels are carried by using WCF services. This is one among the core scenarios supported by Kerosene ORM and I encourage you to take a look at the introductory article and related materials for more details. In this scenario the above structure is somehow simplified at the price of having to use, well, WCF services that have their own particularities.

There are some other possible configurations. This 'Dynamic Data Services' definition and core implementation is architected in such an agnostic way that it can be easily adapted to almost any possible one, while providing all the benefits we have mentioned so far.

The Sample Applications

To finalize this article let's just take a brief tour on the sample applications that come with the download. It provides with three n-Tier application stacks as the one we have used for the above discussion, each adapted for a specific way of defining the underlying maps:

  • The first one is adapted for 'Table Maps'. In this mode, the maps that you define are basically a mimic of the structure of the tables on your database. This is the default mode of Kerosene ORM "Entity Maps" and, if this approach is feasible for your application, no doubts, it is the one that gets the best performance and, if you feel lazy enough, and no customizations are needed, you don't have even to create classes for your custom maps as their contents can be obtained by default.

If you have navigational or dependency properties, where some of the members of your POCO classes refer to instances of other entities, or list of instances, even if you could handle these dependencies manually, this is not the recommended approach. So two other example stacks are provided:

  • The second one is adapted for 'Eager Maps'. In this scenario your navigational or dependency properties are not virtual, and their contents are obtained immediately from the database along with the contents of the entity they belong to. The main drawback of this approach is that you may end up pre-loading into memory huge object graphs, potentially the whole database depending upon how these properties are configured, which has not only an impact on memory consumption but also on delays while these graphs are loaded into memory, and on performance of the cache management.
  • The third one is adapted for 'Lazy Maps'. In this scenario these properties are marked as virtual, and Kerosene ORM implement the logic needed to load their contents only when these properties are used. This mode outperforms the eager one in almost any circumstances and, in particular, as soon as your tables have more than a couple of thousand records. Its main drawback is that, as happens with any other proxy-based solution, you have to be used to the way it works, and accept that, sometimes, the first time the getters of your properties are used, there will be an additional trip to the database to obtain their contents.

Please, sorry, again, refer to the introductory article and its related materials for more details about these modes and some considerations about how to increase their respective performance.

A Note About Debuggingg

If you compile the sample applications in DEBUG mode you are going to obtain a lot of messages back in their consoles. This is good to understand how the internals of the library work, or for any other debug purpose. You can prevent the debug messages from a given file by un-defining the DEBUG symbol at its top.

Obviously, it goes without mention, you would rather like to compile the libraries in RELEASE mode for your production environment.

What else?

Well, we are done. The 'Dynamic Data Services' capabilities described in this article are currently being used by a number of applications in production environments, using MS SQL Server and Azure databases. I have got also some notices of preliminary deployments using Oracle and MySQL ones. Now is the turn for you to try it in your own environment. Enjoy!

License

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


Written By
Spain Spain
mbarbac has worked in start-ups, multinational tech companies, and consulting ones, serving as CIO, CTO, SW Development Director, and Consulting Director, among many other roles.

Solving complex puzzles and getting out of them business value has ever been among his main interests - and that's why he has spent his latest 25 years trying to combine his degree in Theoretical Physics with his MBA... and he is still trying to figure out how all these things can fit together.

Even if flying a lot across many countries, along with the long working days that are customary in IT management and Consultancy, he can say that, after all, he lives in Spain (at least the weekends).

Comments and Discussions

 
GeneralMy vote of 5 Pin
Paulo Zemek6-Apr-14 15:47
mvaPaulo Zemek6-Apr-14 15:47 
GeneralRe: My vote of 5 Pin
mbarbac7-Apr-14 4:01
mbarbac7-Apr-14 4:01 
QuestionExcellent! 5 stars Pin
danito2341-Apr-14 11:37
danito2341-Apr-14 11:37 
AnswerRe: Excellent! 5 stars Pin
mbarbac3-Apr-14 0:07
mbarbac3-Apr-14 0:07 
GeneralMy vote of 3 Pin
btogkas17-Mar-14 6:39
professionalbtogkas17-Mar-14 6:39 
No Database support other than SQL Server. Seen many open source ORMs that implement the same logic...
GeneralRe: My vote of 3 Pin
mbarbac17-Mar-14 6:52
mbarbac17-Mar-14 6:52 
QuestionI use a generic repository like this Pin
Sacha Barber11-Mar-14 5:30
Sacha Barber11-Mar-14 5:30 
AnswerRe: I use a generic repository like this Pin
mbarbac11-Mar-14 6:19
mbarbac11-Mar-14 6:19 
GeneralRe: I use a generic repository like this Pin
Sacha Barber11-Mar-14 7:22
Sacha Barber11-Mar-14 7:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.