The road to Salamanca

To content | To menu | To search

Tag - Data Access

Entries feed - Comments feed

Wednesday, January 28 2009

First maintenance release : 0.9.1

We discovered an issue in the current release of the Salamanca libraries (0.9.0) that was important enough to make us deliver a maintenance release : version 0.9.1. You can use these new libraries as a drop in replacement for the old ones.

Note that the issue discovered concerns only the libraries that depend on Enterprise Library :

We previously compiled these libraries against version 4.1 of Enterprise Library, which requires the .NET Framework 3.5. As our libraries were only supposed to target the .NET Framework 3.0 at best, this introduced a very unwelcome dependency. So the new version is compiled against version 3.1 of Enterprise Library, which only requires the .NET Framework 2.0.

As for the next release (1.0.0), these dependencies has been fixed as followed :

  • .NET Framework 2.0, 3.0 targeted libraries : depend on Enterprise Library 3.1.
  • .NET Framework 3.5 targeted libraries : depend on Enterprise Library 4.1.

Did I just write about version 1.0.0 ? Here is in two sentences what is to be expected :

  • updated libraries, now targeting the .NET Framework 3.5 as well.
  • our first SDML designer ! Still limited, but entirely functional, with code generation from SQL to C# entities…

image

Stay tuned !

Tuesday, September 23 2008

QuickStart : the Domain Model

Now that the first release is out, let's start playing with it. The first step is to design our business objects ; we'll eventually have a DSL for this (the SDML), but basic UML will do for now.

Quick Start (UML)We'll begin with a (very) simple model. Sorry, I could not think of anything more simple ;-) Our application will deal with products, whose sole property is their name.

The associated database could be created with the following script :

CREATE TABLE Products (
ProductID INT IDENTITY(1, 1) NOT NULL,
ProductName VARCHAR(40) NOT NULL
CONSTRAINT PRIMARY KEY (ProductID)
);

Now we can create our Data Access Layer. What we need first is a class to handle the primary key for our product. We have a base class for it, in the Salamanca.DataAccess namespace :

using System;
using Salamanca.DataAccess;

namespace QuickStart
{
public class ProductPrimaryKey:
PrimaryKey<int>
{
public ProductPrimaryKey():
base()
{
}

public ProductPrimaryKey(int key):
base(key)
{
}

public static implicit operator ProductPrimaryKey(int key)
{
return new ProductPrimaryKey(key);
}

public static explicit operator int(ProductPrimaryKey key)
{
return key.Value;
}
}
}

Then we need a class to be used as a data holder. More specifically, it will be used as a Data Transfer Object :

using System;
using Salamanca.DataAccess;

namespace QuickStart
{
public class ProductDataTransfer:
IDataTransferObject
{
public object Clone()
{
return MemberwiseClone();
}

public int Id;
public string Name;
}
}

What remains to be defined is our Domain Model and our Data Mapper. To avoid too tight coupling between both, we'll define an interface that will have to be implemented by a product related Data Mapper :

using System;
using Salamanca.DataAccess;
using Salamanca.DataAccess.Collections;

namespace QuickStart
{
public interface IPersonMapper:
IDataMapper
{
Product Find(ProductPrimaryKey key, DataMapperCollection dataMappers);
DomainModelKeyedCollection<Product> FindAll(DataMapperCollection dataMappers);
}
}

For a starter, we'll only need two methods :

  • one to get a specific product instance from the data backend.
  • one to get all the products form the data backend. The DomainModelKeyedCollection returned can be used a regular Collection, as well as a Dictionary where products are indexed with their primary keys.

Now we're ready to define our Domain Model :

using System;
using System.Collections.Generic;
using Salamanca.DataAccess;
using Salamanca.DataAccess.Collections;
using Salamanca.DataRules;


namespace QuickStart
{
public class Product:
DomainModel<ProductPrimaryKey, ProductDataTransfer>
{

public Product():
base()
{
}

public Product(DataMapperCollection dataMappers):
base(dataMappers)
{
}

protected override ProductPrimaryKey CreateId()
{
return new ProductPrimaryKey(Data.Id);
}

public static Product Find(ProductPrimaryKey key, DataMapperCollection dataMappers)
{
return ((IProductMapper)dataMappers[typeof(Product)]).Find(key, dataMappers);
}

public static IList<Product> FindAll(DataMapperCollection dataMappers)
{
return ((IProductMapper)dataMappers[typeof(Product)]).FindAll(dataMappers);
}

internal void SetId(int id)
{
Data.Id=id;
}

[StringLengthRule(40)]
[NotNullRule]
public string Name
{
get
{
Load(true);
return Data.Name;
}
set
{
if (Data.Name!=value)
{
Data.Name=value;
RaisePropertyChanged("Name");
}
}
}
}
}

As you can see, this is quite straightforward : just inherit from Salamanca.DataAccess.DomainModel, override CreateId and add your properties. The static methods have just been defined here for convenience. Also, note the attributes above the Name property, that define our first business rules.

Final step : define a Data Mapper for our product. We'll choose the ADO .NET based abstract implementation that is found in the Salamanca.DataAccess.Data namespace as a base :

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using Salamanca.DataAccess;
using Salamanca.DataAccess.Collections;
using Salamanca.DataAccess.Data;

namespace QuickStart
{
public partial class ProductMapper:
DataMapper<Product , ProductDataTransfer ProductPrimaryKey>,
IProductMapper
{
public ProductMapper(DbConnection connection, ICacheManager cacheManager):
base(connection, DomainModelCreator.CreateDefault<Product>(), cacheManager)
{
}

public DomainModelKeyedCollection<Product> FindAll(DataMapperCollection dataMappers)
{
IDbCommand[] commands=new IDbCommand[1];

commands[0]=Connection.CreateCommand();
commands[0].CommandType=CommandType.Text;
commands[0].CommandText="SELECT ProductID, ProductName FROM Products";

return Load(commands, dataMappers);
}

protected override IList<IDbCommand> CreateSelectCommands(ProductPrimaryKey key)
{
IDbCommand[] commands=new IDbCommand[1];

commands[0]=Connection.CreateCommand();
commands[0].CommandType=CommandType.Text;
commands[0].CommandText="SELECT ProductName FROM Products WHERE ProductID=@Id";

IDbDataParameter p=commands[0].CreateParameter();
p.ParameterName="@Id";
p.DbType=DbType.Int32;
p.Value=key.Value;
p.Direction=ParameterDirection.Input;
commands[0].Parameters.Add(p);

return commands;
}

protected override IList<IDbCommand> CreateDeleteCommands(ProductDataTransfer data)
{
IDbCommand[] commands=new IDbCommand[1];

commands[0]=Connection.CreateCommand();
commands[0].CommandType=CommandType.Text;
commands[0].CommandText="DELETE FROM Products WHERE ProductID=@Id";

IDbDataParameter p=commands[0].CreateParameter();
p.ParameterName="@Id";
p.DbType=DbType.Int32;
p.Value=data.Id;
p.Direction=ParameterDirection.Input;
commands[0].Parameters.Add(p);

return commands;
}

protected override IList<IDbCommand> CreateInsertCommands(ProductDataTransfer data)
{
IDbCommand[] commands=new IDbCommand[1];

commands[0]=Connection.CreateCommand();
commands[0].CommandType=CommandType.Text;
commands[0].CommandText="INSERT INTO Products (ProductName) VALUES(@ProductName)";

IDbDataParameter p=commands[0].CreateParameter();
p.ParameterName="@Name";
p.DbType=DbType.String;
p.Value=data.Name;
p.Direction=ParameterDirection.Input;
commands[0].Parameters.Add(p);

return commands;
}

protected override IList<IDbCommand> CreateUpdateCommands(ProductDataTransfer data)
{
IDbCommand[] commands=new IDbCommand[1];

commands[0]=Connection.CreateCommand();
commands[0].CommandType=CommandType.Text;
commands[0].CommandText="UPDATE Products SET ProductName=@Name WHERE ProductID=@Id";

IDbDataParameter p=commands[0].CreateParameter();
p.ParameterName="@Name";
p.DbType=DbType.String;
p.Value=data.Name;
p.Direction=ParameterDirection.Input;
commands[0].Parameters.Add(p);

p=commands[0].CreateParameter();
p.ParameterName="@Id";
p.DbType=DbType.Int32;
p.Value=data.Id;
p.Direction=ParameterDirection.Input;
commands[0].Parameters.Add(p);

return commands;
}

protected override ProductDataTransfer GetDataTransferObject(IList<IDataRecord> records)
{
ProductDataTransfer data=new ProductDataTransfer();

int ord=records[0].GetOrdinal("ProductID");
if (!records[0].IsDBNull(ord))
data.Id=records[0].GetInt32(ord);

ord=records[0].GetOrdinal("ProductName");
if (!records[0].IsDBNull(ord))
data.Name=records[0].GetString(ord);

return data;
}

protected override void OnInserted(Product domainModel, IList<IDbCommand> commands)
{
IDbCommand command=Connection.CreateCommand();
command.CommandType=CommandType.Text;
command.CommandText="SELECT @@IDENTITY";
command.Transaction=commands[0].Transaction;

domainModel.SetId(Convert.ToInt32(command.ExecuteScalar()));

base.OnInserted(domainModel, commands);
}
}
}

That's it : derive from DataMapper and fill the gaps with pure ADO .NET code. You can already understand that this is where code generation will greatly improve the developer productivity.

Our Data Access Layer is now complete : we can build a simple application based on it. The only (?) tricky thing is that you will need to create an instance of DataMapperCollection. For our sample, simply use this code :

DataMapperCollection _DataMappers=new DataMapperCollection();
_DataMappers.Add(typeof(Product), new ProductMapper(Connection, new NoCacheManager()));

It creates a new collection based on a prexisting Connection, and our data is simply not cached in memory.

You can find a complete working example, as a Visual Studio 2008 solution, here. As you can see, we can build a simple application that integrates our Domain Model in a very few lines of code, with business rules validation (the underlying database is Northwind on SQL Server, for which a data file is included in the downloadable sample).

Quick Start Sample

Next time, we will create our first activity. Stay tuned !

Monday, September 1 2008

Inside the libraries (II) : Data Access

This one is simple : Data Access is an (ORM) library. As such, it owes a lot to the experience of NourY Solutions in the field of data management. But our ability to structure this experience and put it into words has been greatly enhanced by the remarkable work of Martin Fowler. Many concepts in use in this library are described in his book Patterns of Enterprise Application Architecture.

There are many .NET ORM libraries out there. As evoked before, we don't believe "one size fits all" applies when it comes to ORM ; the mismatch between the object world and the relational one is so huge that it can only be solved by specific solutions under specific sets of constraints. Data Access has been designed with these constraints in mind :

  • integration with Data Rules.
  • easy serialization of business object instances (XML for use in Web Services, for Ajax applications...).
  • persistence backend abstraction : business objects should easily be persisted against any database backend, like a standard RDBMS solution (Sql Server, Oracle, SQLite...), or a web service, or even a flat file.
  • the developer always has to be in control : for instance, no on-the-fly SQL generation (though this could be added later as an option).
  • easy code generation (we want to build a Software Factory, after all !).

This library is organized around 4 interfaces :Basics

  • IDomainModel : is implemented by a . There is also an abstract class (DomainModel) that can be used as a base implementation for your business objects. It provides Data Rules implementation, serialization, and UI integration (through the implementation of the standard IDataErrorInfo, IEditableObject and INotifyPropertyChanged interfaces).
  • IDataMapper : is implemented by a Data Mapper, which basically provides the persistence backend abstraction. The library provides base implementation for persistence based on standard ADO .NET, Enterprise Library Data Access Application Block or Web Services.
  • IDataTransferObject : this interface is implemented by a Data Transfer Object. It is used to hold the data of a Domain Model, to transfer it between a Domain Model and a Data Mapper and to serialize our Domain Models.
  • IPrimaryKey : is implemented by custom Identity Fields types.

A DSL to design our Domain Models would look a lot like a class diagram. And it does actually ;-) (more on that later).

Our plans in the future for this library are :

  • Unity integration.
  • Ability to serialize trees of objects.
  • JSON serialization.
  • Powershell integration.
  • SSIS integration.

Next time will be the time to introduce Data Activities.

Wednesday, August 20 2008

Visiting Salamanca

Fundamentally, Salamanca is designed around three sets of libraries :Architecture

  1. Data Access is the easiest to explain in a few words : think of it as (yet) an (other) (ORM) library. Why another ? We believe that there is not nor will ever be a perfect solution for this problem ; call us "ORM limitations acceptors". Our belief is that an ORM solution has to be designed to fulfill a specific set of issues under a specific set of constraints (which definitely sounds like a product line to me). After all, if you want to go around and around in circles without going anywhere, you are more likely to win the race with a formula one car. If you want to drive home, you will be better off with a standard commercial car : it has been designed for average roads, and priced accordingly. This library will help us handle business specific data in a (DAL).
  2. Data Activities is a library that helps define the business data lifecycle into a specific layer. The latter is a which lies at the core of our architecture : it manages business data, drives the HMI, and as such acts as a perfect (?) .
  3. Data Rules is a validation framework. It will be used to implement our business rules in our activities transitions and our data.

These libraries represent the fundamental abstractions around which our applications will be built. Best practices can be documented around their use, that can be written with our own (DSLs). We have defined two DSLs so far :

  1. SDML (Salamanca Domain Modeling Language) : this language will help us structure our data in order to implement our DAL. But more than that, it will help us create reusable states for our activities, or reusable component for data edition (user controls in Windows Forms for instance). Its representation is very close to a UML Class Diagram.
  2. SAML (Salamanca Activities Modeling Language) : this will help us define our activities. Activities reuse concepts defined in a SDML model. Its representation is inspired by the (BPMN).

Implementing an application with our libraries once the models are properly defined should be quite straightforward. So straightforward that we will be gain a lot by creating two tools that will greatly enhance our productivity :

  1. A Modeler : a graphical tool that will help us visually model our application and serialize it in SDML and SAML formats.
  2. A Generator : a tool that will implement our best practices and take our model as input, in SDML and SAML formats, and generate our C# code. Though it is imaginable to generate a whole functioning application, the developer will have the possibility to customize the generated code to make it more usable, or implement parts that could not be modeled. Usually, developers will ... develop a usable HMI on top of the generated code.

As for now, the libraries are quite in an advanced state, the DSLs definition is in progress and the tools are part of our to do list.