Introduction
In this article, I present a set of classes that can be used to create a generic List<T>
from a specified DataSet
object. This could be useful in cases where we have data for a specified entity loaded into a DataSet
and need to easily construct a generic List<>
of that entity type.
To demonstrate the usage, we will consider the following scenario: We have to get an Address Book application running. We are using a database for storage, and have created the following tables:
- Contact - Contains details about a contact
- Address - Contains details about an address
- ContactAddress - Maps a contact to multiple addresses
With this data model in mind, we proceed to write two stored procs, one to get all contacts in the system, and one to get the contact details given the contact ID. While listing all contacts, we only need the contact ID and the contact name to be displayed. While getting the contact details, we need all the contact details and the list of addresses the contact has.
GetContactById
- Gets details of a contact. Also gets all the addresses belonging to that contact.
CREATE PROCEDURE dbo.GetContacts
AS
SET NOCOUNT ON
SELECT ContactId, ContactName
FROM Contact
RETURN
GetContactById
- Gets details of a contact. Also gets all the addresses belonging to that contact.
CREATE PROCEDURE dbo.GetContactById
(
@ContactId UNIQUEIDENTIFIER
)
AS
SET NOCOUNT ON
SELECT ContactId, ContactEmail, ContactName
FROM Contact
WHERE (ContactId = @ContactId)
SELECT Address.AddressId, Address.AddressLine2,
Address.AddressLine1, Address.AddressLine3, Address.AddressType
FROM Address INNER JOIN
ContactAddress ON Address.AddressId = ContactAddress.AddressId
WHERE (ContactAddress.ContactId = @ContactId)
We have simple entity types defined for Contact
, Address
, and ContactInfo
. The Citrus.Data.Core
namespace includes helper classes that will allow us to easily map these entity types with the results of these Stored Procedures.
In our context, an entity is anything that represents a business object. Contact
, ContactInfo
, and Address
are entity types. An entity type can contain other entity types (either as a singular instance or as a list). The Contact
entity contains a list of Address
entities.
Pseudo Logic
The core classes handle all the details of iterating through the DataTable
s found in the DataSet
and mapping DataColumn
s to the entity properties. It provides functions that can then load any given entity object from a specified DataSet
.
While the core classes can be used as is with your existing entity classes, you can also alter their behaviour if required by marking up the properties of your entities with Data*
attributes. This is described in detail in the next section. For example, you can let the EntityLoader
know that a particular property maps to a specific data column using the DataColumn
attribute on that property.
The key method for the EntityLoader
class is Load<T>(DataSet dataSet)
. This method requires the DataSet
with the necessary data and the type T
for which we need to create an entity list. We start the load process with the first data table in the data set:
- Inspect the provided entity type and get a list of all its properties.
- For every property, check if we can map the property to a specified column in the current data table. This is called a column property map.
- Create a new list of the entity type.
- For every data row in the current data table:
- Create an entity of type
T
. - Load all simple properties (
string
, int
, bool
, double
, Guid
etc.) of the new entity based on the data in the current row, by using the map we created. - For all complex properties contained in the current entity (another entity or list of entities):
- If the property is an entity, the data to be loaded would be present in the current data row itself, so get the map for the contained entity and populate it.
- If the property is an entity list, the current data table will not contain the data. If the data table is specified from where this contained entity list can be loaded, do steps 1-4 with that data table.
- Once all the properties of the entity have been loaded, add it to the entity list.
- Return the entity list.
Complete Example with Supported Features
The download sample contains a sample website application which demonstrates the setup and usage of the EntityLoader
class.
Note: The download sample contains the core assemblies, sample web application. and the standalone Address Book database. It also includes binaries off of Microsoft Enterprise Library 4.1 (for Data Access). The solution was created in VS 2008 Express Edition.
In the sample application for the Address Book, we have the following entities:
public class ContactInfo
{
[DataColumn("ContactId")]
public Guid Id { get; set; }
[DataColumn("ContactName")]
public string Name { get; set; }
}
public class Contact
{
[DataColumn("ContactId")]
public Guid Id { get; set; }
[DataColumn("ContactName")]
public string Name { get; set; }
[DataColumn("ContactEmail")]
public string EmailAddress { get; private set; }
[DataTable(DataTableSource.Next)]
public List<Address> Addresses { get; set; }
}
public class Address
{
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string AddressLine3 { get; set; }
public string AddressLine4 { get; set; }
[DataConverter(typeof(AddressTypeConverter))]
public string AddressType { get; set; }
}
The three classes show various combinations of usage of the allowed attributes. In the ContactInfo
class, we use the DataColumn
attribute to specify which column a property should map to explicitly. In the Contact
class, we use the DataTable
attribute to let the EntityLoader
know that the List
of Address
data exists in another data table (in this case, the next one). In the Address
entity class, we see an example of implicit column mapping based on the property name. We also use the DataConverter
attribute to specify that a property needs extra work to be populated - the AddressType
is stored in the database as an int
, but the Address
entity has a string
value for it. The implementation of the AddressTypeConverter
class is shown below:
public class AddressTypeConverter : DataConverterBase<string, int>
{
public override string GetEntity(int entityData)
{
switch (entityData)
{
case 1: return "Home";
case 2: return "Office";
default: return "Unknown";
}
}
public override int GetEntityData(string entity)
{
switch (entity)
{
case "Home": return 1;
case "Office": return 2;
default: return -1;
}
}
}
These attributes help in establishing the mapping between data columns and entity properties.
Data Access Layer
The sample application has a simple data access layer built on top of the Microsoft Enterprise Library 4.1 Data Access Application Block.
The data access layer is defined as:
public static class AddressBook
and has a static constructor like so:
static AddressBook()
{
CachedEntityDefinitionProvider addressBookEntityProvider =
new CachedEntityDefinitionProvider();
addressBookEntityProvider.AddEntityType(typeof(ContactInfo));
addressBookEntityProvider.AddEntityType(typeof(Contact));
addressBookEntityProvider.AddEntityType(typeof(Address));
EntityInspector.EntityProvider = addressBookEntityProvider;
}
The CachedEntityDefinitionProvider
is a simple extension class that is used to tell the entity loading system which types are entity types in the system. In our example, we specify that Contact
, ContactInfo
, and Address
types are entity types. You can create your own version of an entity definition provider and hook it up with the EntityInspector
's EntityProvider
property by implementing the IEntityDefinitionProvider
interface.
The core method that utilizes EntityLoader
is implemented as an extension method on the Database
type as follows:
static List<T> LoadEntity<T>(this Database db,
string commandName, params object[] commandArguments)
{
return EntityLoader.Load<T>(db.ExecuteDataSet(commandName, commandArguments));
}
You can change this in the sample if you do not have EL4.1.
And finally, the data access methods:
public static List<ContactInfo> GetContacts()
{
return DbContext.LoadEntity<ContactInfo>(StoredProcedures.GetContacts);
}
public static List<Contact> GetContactById(Guid id)
{
return DbContext.LoadEntity<Contact>(StoredProcedures.GetContactsById, id);
}
Note that all methods return a list of the specified entity.
Data Attributes
This section enumerates all the attributes available for use:
DataColumn
- Applied on a property. Specifies which data column in a data table maps to this property.DataTable
- Applied on a list<>
property. Specifies the source data table from where the list should be populated. Use as DataTable(DataTableSource.Next)
or DataTable(2)
to specify the relative table position.DataConverter
- Applied to a property. Tells the EntityLoader
system that a specific data converter class needs to be used to load the property data.EntityLoader
- Applied to the entity class. This controls the way in which properties are checked to be loaded with data from a DataSet
. By default, all properties of a given entity will be used for column mapping and data load. You can change this behaviour by specifying an InspectionPolicy
parameter to the EntityLoader
attribute. Possible uses are EntityLoader(InspectionPolicy.OptIn)
or EntityLoader(InspectionPolicy.OptOut)
. If OptIn
is specified, you need to mark properties explicitly with the DataInclude
attribute to ensure that EntityLoader
will use that property. If OptOut
is specified, all properties will be inspected, unless marked with DataExclude
. OptOut
is the default behaviour.
Improvements
Of course, this is just a very early implementation. There are lots more to do in terms of argument validation, better exception handling, and so on.
Also included in the Citrus.Data.Core assembly is a utility class that can do the opposite thing of what we have seen here, convert a List<T>
to a DataSet
.. It's not complete yet, but works for simple entities (ones that do not contain properties that are lists of entities).
In case you can suggest some, please do.
References
Where possible, I have included references in the code itself. Some to look at are: