Click here to Skip to main content
Licence CPOL
First Posted 15 Oct 2010
Views 17,006
Bookmarked 27 times

Kynetic ORM (Part 2): An ORM without configuration using C# 4.0 Dynamics, Generics, and Reflection

By | 2 May 2011 | Article
Describes how to create and use maps with the KyneticORM library.
 
Part of The SQL Zone sponsored by
See Also

Introduction

This is the second article on the KyneticORM project. The first one can be found at KyneticORM, and it focused on the description of non-mapped queries and operations.

Just to recapitulate, when you were using the non-mapped approach, what you were obtaining from your iterable commands was, by default, a set of dynamic objects (in the form of instances of the DeepObject class) that can host an arbitrary number of members. These members are associated either with a table on your database or with columns on one of those tables. In the first case, the members of this "first-level" members are the dynamic members associated with the columns returned from this table.

When you want to convert those records to instances of your business classes, you use the ConvertBy() method on each command to transform the dynamic records returned in any way you wanted. Indeed, it will be the only available mechanism when the constructor of your business classes happen to take any parameters.

But there are many circumstances where you want to have this strong-typed approach without having to specify, per command, the conversion mechanism. Instead, what you can do is specify a "map" that associates a given business class with a given table, and let KyneticORM apply this mapping mechanism for all the commands (instantiated in this case using a generic syntax). Before entering into its details, let's see when this approach is applicable:

  • When your business class has a public and parameterless constructor.
  • When there is a clear way to associate the columns in your table with members in your business class, even if these associations require some level of processing.
  • When you need to write back into your database values that do not have a direct correspondence with the first-level members in your business class. For instance, suppose your business object has an "Address" member that is itself an object with its own "Street" and "City" members that, ultimately, will be the columns in your database. The ConvertBy() method is valid for loading into the business objects, reading from the database, but not for writing into it. By using maps, these scenarios can be solved.

What is a Map?

A KMap<T> is the object used to keep the associations you define among the members in a given business class and the columns of a given database. These associations will be used by the "mapped" commands to write the contents from your business objects into the database, and to load the contents they read from the database into instances of your business objects that are created as needed.

Following the configuration-less design principle of KyneticORM, to create a map, you don't have to write an external configuration or mapping file, not use intermediate interfaces or classes, or alter your business classes in any way. Instead, maps are created specifically for a given instance of a KLink object, added to it by using its AddMap<T>() extension method. The "T" generic parameter specifies your class. Its first parameter is a delegate used to specify the table where you want to map your class on. Let's take a look at the following example:

var map = link.AddMap<Employee>( x => x.Employees ).Add(
  x => x.Id.IsKey( true ),
  x => x.Column_Of_First_Name.OnMember( x.Name.FirstName ),
  x => x.LastName,
  x => x.BirthDate,
  x => x.Active,
  x => x.JoinDate,
  x => x.JoinTime,
  x => x.CountryId.OnWrite( (KMapItem<Employee>.OnWriteToDbDelegate)( y => y.CountryId ) )
);

In this case, we are creating a map for your Employee class on the Employees table for the KLink object where we have used its AddMap<T>() extension method. Once a class is registered through a map in a given KLink object, it cannot be registered again in this object (because its type is used as a key to find the map when needed). But nothing impedes you to register your class in as many KLink objects as you wish - for instance, to have different mapping mechanisms in different contexts. On the flip side, a given table can be used in many mappings as you want, even in the same link, without any restrictions.

Once you have a map registered, you specify the associations it holds using its Add() method, where you can specify one or many associations as you wish. These associations can be of different types, as we are going to discuss.

Direct one to one associations

The easiest syntax is the one used in the first association: you just have specified the name of the column in the database. In this case, it is assumed that the class has a member whose name matches exactly (case sensitive) the name of the column. This member can be either a field or a property, and it doesn't matter whether it is public or private: KyneticORM will use Reflection to find it and to write and read its contents. Note that you do not have to specify its type: an internal conversion mechanism is used to convert between database types and C# ones.

x => x.Id,

For most scenarios, you don't have to do anything else: these are the ones where there is a one-to-one correspondence between the column in the database and the member in your class. The only caveat is that the names of the columns are fetched using the DbCaseSensitiveName property of your KLink object, whereas the names of your members in your classes are, obviously, case sensitive as they are regular C# citizens. Not a big deal, but better don't forget.

Using a different class member

Now, let's suppose that the layout of your columns doesn't match with the layout of the members in your class. Take a look at the way we have specified the second association:

x => x.Column_Of_First_Name.OnMember( x.HostName.FirstName ),

In this case, the name of the column in your database is Column_Of_First_Name and it doesn't resemble by any means any similar member you have in your class. What's worse, in your class, the FirstName member is indeed part of a parent member named HostName. No problems, you just have to associate your column with this member by using the OnMember() method appended to the name of your column.

Internally, what happens is that this dynamic delegate is parsed, and when the OnMember() method is found, its argument is annotated to serve as the field to associate with the column. Note that if this field, either a member or a property, and either public or private, doesn't exist, an exception will be thrown.

Specifying on-write behavior

Now let's suppose we have a read-only column in the database. To tell KyneticORM not to try to write into it, we can use the first overload of the OnWrite() method with false as its argument, as in the following example:

x => x.SomeReaadOnlyColumn.OnWrite( false )

Instead, the second overload will permit you to specify how to obtain the value to write into the database for this column. Its argument, in this case, is a delegate that will be invoked to obtain this value, that takes as its dynamic argument the instance of your business class, and that should return the value to write. Take a look at the above example for more details.

Specifying on-load behavior

Similarly, it may happen that even if you want to specify an association for a given column in your database, you don't want to load its contents in your business class instance (it may happen, for instance, that there is not such a member in your class). In this case, you can use the OnLoad() method with false as its argument.

On the flip side, there is, as you are already imagining, an overload that will permit you to specify the exact way you want to load the member or members in your class associated with this column. In this case, the delegate should have the signature Action<dynamic,object> because it returns nothing, but will take a first dynamic argument that is the instance of your class you are going to return, and a second argument that is the value read from the database for this column. This way, if you need to use this value to load several members, you have your instance handy.

See the above example for more details. The only thing worth to mention here is that you have had to cast the value read from the database to the type you are using to load into your instance. Otherwise the run-time will complain.

Working with the Mapped approach

Once you are done creating your maps and your associations, we can move on to see how to use them to send commands to your database and to return instances of your classes instead of generic dynamic records.

Generally speaking, these commands are instantiated using extension methods on your KLink object that use a generic syntax where its type argument is the class you want the results to be converted to. This type argument is going to be used to locate the map you have specified for this class. If the map cannot be located, an exception is thrown.

Mapped queries

The first way to instantiate a mapped query is by using the Where<T>() extension method, as shown in the following example:

var cmd = link.Where<Employee>( x => x.LastName >= "C" );
foreach( Employee emp in cmd ) Console.WriteLine( "- {0}\n", emp );
cmd.Dispose();

It creates, behind the scenes, an instance of a command object that behaves fundamentally the same way as its non-mapped cousin does - so you can use basically all the methods you were used to use. You can notice that you are not needed to use a From() method (that indeed is not supported in this case) because the table used is the one you have specified in your map.

So, how can you specify more tables in your query if you need them? And, in this case, how can you specify an alias for the "main" table if you need so? For these needs, what you have to do is to use the Query<T>() extension method to instantiate your query command, that accepts an optional argument where you can specify the alias to use. Then, to specify the additional tables, you can use the AndFrom() method. Both of these appear in the following example:

var cmd = link.Query<Employee><employee>( x => x.Emp ).Where( 
            x => x.Emp.CountryId == x.Ctry.Id )
  .AndFrom( x => x.Countries.As( x.Ctry ) ).Where( x => x.Ctry.RegionId == x.Reg.Id )
  .AndFrom( x => x.Regions.As( x.Reg ) ).Where( x => x.Reg.ParentId == x.Super.Id )
  .AndFrom( x => x.Regions.As( x.Super ) ).Where( 
            x => x.Super.Name == "Europe, Middle East & Africa" )
  .OrderBy( x => x.Emp.Id );
 
foreach( Employee obj in cmd ) Console.WriteLine( "- {0}\n", obj );
cmd.Dispose();

The AndFrom() method is called this way to reflect the fact that the tables (and aliases if needed) that are going to be used are not to be confused with the main one specified in the map.

Once you have this query command object, you can use it the same way you can use its non-mapped counterpart, as you can see in the example. Indeed, behind the scenes, its non-mapped version is created on your behalf to provide the real functionality of the command.

Mapped updates

There are three versions of the Update<T>() extension method. The first one is basically the mapped version of its non-mapped counterpart:

var cmd = link.Update<Employee>()
  .Where( x => x.FirstName >= "F" )
  .With(
    x => x.ManagerId = null,
    x => x.LastName = x.LastName + "_modif"
  );

Indeed, it works the same way except that the results it gets back are in the form of instances of your business class, as per the associations specified in the map. There is not really much more to say. Just refer to the original article for the discussion on how to specify the columns to modify.

The other two versions operate not on a set of records but rather on a given record, specified by an instance of your business class that you pass to them as the first argument. Once you have finished building your command, you invoke it by using its Execute() method that returns the modified record as it is read from the database. Note that in no circumstance, the original object is modified.

Let's take a look at the second version:

Employee record = ... ;
 
var cmd = link.Update<Employee>( record,
  x => x.ManagerId = null,
  x => x.FirstName = x.FirstName + "_UPDATED1"
);
 
Employee neo = cmd.Execute(); Console.WriteLine( "- {0}\n", neo );
cmd.Dispose();

In this case, we have obtained our original object (record) elsewhere, or we have created it ad-hoc. It is used to build the WHERE statement used to locate the record in the database to update with the column specifications we have included when building the command. When we use the Execute() method, the record that has been updated in the database is read and returned as a new instance of your class.

How does KyneticORM know what columns to use to build these WHERE statements? Internally, each association in a given map has two properties: IsKey and IsUnique, that are used to select the columns to identify univocally a given record. If no columns with the IsKey property set to true are found, then IsUnique columns are used. If then there are no columns with the IsUnique property set to true, there is no way for KyneticORM to univocally find a record, and an exception will be thrown.

By the way, one of the design principles of KyneticORM was that the application will know as less as possible about the way the database and its tables are constructed, and in this particular case, it should not have to know what columns are used to maintain the identity of a record. So, you may don't want to manually set the values of the IsKey and IsUnique properties .

But if you know in advance what are your primary key or unique columns you can use the IsKey(true) and IsUnique(true) methods to set these properties, and when you are done, set the SchemaLoaded property to true to avoid reading them from the database.

Otherwise, what happens when a map is going to be used the first time, before executing any command, is that KyneticORM sends a small SELECT command to the database to read its schema. An arbitrary record is sent back through the wire and then discarded (you don't have to bother about it in almost all circumstances, but it will explain the small delay in the first command executed). Fortunately, this has to be done just once per map because the schema information is cached for further use.

Now, let's move on to the third version of the Update<T>() command:

Employee source = ... ;
 
Employee modified = (Employee)link.MappedClone( source );
modified.Id = "XYZ";
modified.FirstName = "New name";
 
var cmd = link.Update<Employee>( modified, source );
Employee neo = cmd.Execute(); Console.WriteLine( "- {0}\n", neo );
cmd.Dispose();

This version is suited for those scenarios where your application has, for instance, modified your object and you want to reflect those modifications back into the database. If instead of specifying the columns to update, you just specify the modified object, then this is used to modify the record in the database.

This version accepts an optional second argument where you can specify the original object. This is handy when any of the members you have modified are associated with the identity of the record because it is this "original" object that is used to locate the record in the database, and then the values of the columns are obtained from the modified one. If this second argument is not specified (or null is passed), then KyneticORM tries to locate the record using the identity columns in the modified record you have passed.

Finally, just note how I have obtained a copy of the original (source) object before I have modified it in the above example: by using the MappedClone() method. This is explained later, but basically, it uses the associations that exist in the map to create a new instance of your class and load it with the contents of the record passed to it.

Inserts

The Insert<T>() method instantiates a typed insert command that supports the methods you are used to in its non-mapped counterpart. In particular, you have to specify the columns to update using its ColumnValue() and ColumnValues() methods.

The Insert<T>( record ) instantiates a specialized version of the Insert command that can be used to insert a new record in the database based upon the contents of the object you have specified. The only public method of this version is Execute(), that returns the new record inserted.

Deletes

The Delete<T>() method instantiates a typed delete command that supports the methods you are used to in its non-mapped counterpart. In particular, you have to specify the condition to locate the records to delete by using its Where() method.

The Delete<T>( record ) instantiates a specialized version of the delete command that can be used to delete a record in the database located using the identity columns of the object you have passed to it as its argument. The only public method of this version is Execute(), that returns the record deleted.

Can the Mapped approach and the Non-mapped one be mixed?

Absolutely. Regardless of if you have, or not, registered some types and created their maps in your KLink object, its non-generic and generic extension methods can be used without any restrictions.

Goodies

The KMap<T> class comes with a Clone( record ) method that returns a clone of the record given as its argument. In this context, a clone is defined by "executing" the associations in the map found using the type of the object. This is handy when, for instance, you need to save the original record before modifying it, and its class does not implement the ICloneable interface.

References

  • DeepObject: "A multi-level C# 4.0 dynamic object"; describes how to create a dynamic multi-level object, using the dynamic features of C# 4.0. It can be found at: DeepObject.
  • KyneticORM (direct): This is the core non-mapped version of the library, used by the mapped extensions to provide the core of its functionality. It can be found at: KyneticORM.
  • KyneticORM for WCF: This describes the extensions used on the library to provide support for serialization and WCF scenarios. It can be found at: KyneticORM.WCF.

License

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

About the Author

Moises Barba



Spain Spain

Member

Moises Barba works for the Consulting division of a major multinational IT company.
While he has not to develop for a living nowadays, solving complex puzzles has been ever among his main interests - that's why he has spent his latest 20 years trying to combine his degree is in Theoretical Physics with his MBA... and he is still trying to figure how this two things can fit together.
Flying a lot across many countries, along with the long working days that are customary in consultancy, he can say that, after all, he lives in Spain (at least the weekends).

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
GeneralNew question for you :) PinmemberSimon_Whitehead19:03 16 May '11  
GeneralRe: New question for you :) Pinmembermbarbac4:59 17 May '11  
GeneralRe: New question for you :) PinmemberSimon_Whitehead12:21 17 May '11  
GeneralMy vote of 5 Pinmembervbfengshui7:45 2 May '11  
GeneralRe: My vote of 5 Pinmembermbarbac13:04 2 May '11  
QuestionIssue with DateTime? PinmemberSimon_Whitehead12:31 16 Feb '11  
AnswerRe: Issue with DateTime? PinmemberSimon_Whitehead13:09 16 Feb '11  
GeneralRe: Issue with DateTime? Pinmembermbarbac21:04 17 Feb '11  
GeneralRe: Issue with DateTime? PinmemberSimon_Whitehead21:17 17 Feb '11  
GeneralRe: Issue with DateTime? Pinmembermbarbac23:29 17 Feb '11  

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

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

Permalink | Advertise | Privacy | Mobile
Web04 | 2.5.120517.1 | Last Updated 2 May 2011
Article Copyright 2010 by Moises Barba
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid