Working on new features.

Roger Torres - May 14th, 2010

The last couple of weeks, I have been working on new CoNatural.Data features with the goal to simplify the persistence of complex object structures. I always try to keep my DAL as basic as possible, with the majority of operations being performed by direct table to object mappings (Table Adapter Pattern), but there are scenarios where more complex object structures should be persisted at once (inside a single transaction and ideally in just one trip to the database). In order to achieve this, I added two new features to the framework: (Note that with the current release we can persist complex structures and this is only to make things simpler)

1) The ability to map indirect references to scalar properties (via Type Mapper classes).
2) The implementation of strategies that navigate the complex object structure, binding instances to commands, and optionally executing the entire operation in a single batch.

Let me show some examples using AdventureWorks2008.

Indirect mappings

In this example I will create some new Stores using the model below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Store {
	public Store() {}
 
	public BusinessEntity BusinessEntity { get; set; }
	public string Name { get; set; }
	public Int32? SalesPersonID { get; set; }
	public string Demographics { get; set; }
	public Guid rowguid { get; set; }
	public DateTime ModifiedDate { get; set; }
 
	public override string ToString() {
		return BusinessEntity.ToString() + ": " + Name;
	}
}
 
public class BusinessEntity {
	public BusinessEntity() {}
 
	public Int32 BusinessEntityID { get; set; }
	public Guid rowguid { get; set; }
	public DateTime ModifiedDate { get; set; }
 
	public override string ToString() {
		return BusinessEntityID.ToString();
	}
}

Now we create the command to insert stores:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class StoreInsert : ICommand {
	public StoreInsert() {}
 
	public string GetScript() { 
		return 
@"
INSERT INTO [Sales].[Store] (
   [BusinessEntityID],
   [Name],
   [SalesPersonID],
   [rowguid],
   [ModifiedDate])
VALUES (
	@BusinessEntityID,
	@Name,
	@SalesPersonID,
	newid(),
	getdate())
"; 
	}
 
	public Int32 BusinessEntityID { get; set; }
	[Parameter(50)] public string Name { get; set; }
	public Int32? SalesPersonID { get; set; }
}

Note that we need to fill the BusinessEntityID parameter, but there is no “matching property” in class Store. The solution is to register with the type mapper factory an indirect property reference to Store.BusinessEntity.BusinessEntityID, so when the type mapper performs the bindings, it can find the property with the input values to feed the command parameters (or the properties that will store the results coming from output parameters). I’m using static reflection in this case to make sure the mapping expression is valid at compile time.

1
2
3
4
5
6
7
public void CreateStore(IConnection c, BusinessEntity be) {
	// set indirect mapping
	c.DefaultTypeMapperFactory.Map<Store>(r => r.BusinessEntity.BusinessEntityID);
 
	var store = new Store { BusinessEntity = be, Name = "Test Store", SalesPersonID = 288 };
	c.Execute(new StoreInsert(), store);
}

We can map multiple indirect references for one type, and we can specify the name of the parameter we want to map in case the referenced property doesn’t match the name. In the next example we will see these cases.

Strategies

Before I present this example, there is a new IConnection method (BeforeExecute) you can override to intercept commands before they are invoked. I will use the following connection to trace some information to the Console:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SqlConnectionWithTrace : CoNatural.Data.SqlClient.SqlClientConnection {
	public SqlConnectionWithTrace(string connectionString)
		: base(connectionString) {
		Trace.Listeners.Add(new ConsoleTraceListener());
	}
 
	public override void BeforeExecute(ICommand iCommand, IDataCommand dataCommand, System.Data.IDbCommand dbCommand) {
		if (iCommand != null)
			Trace.WriteLine("Executing " + iCommand.GetType().ToString());
		foreach (System.Data.IDbDataParameter p in dbCommand.Parameters)
			if (p.Direction != System.Data.ParameterDirection.Output)
				Trace.WriteLine(p.ParameterName + " = " + p.Value);
		Trace.WriteLine(dbCommand.CommandText);
	}
}

Now, going back to strategies, the idea here is to help the engine to traverse the object structure when persisting changes. There is a new StrategyBase class providing a default strategy that understands one-to-many relationships, and can bind to insert/update/delete commands if the objects implement the new IDataObject interface. Let’s take a look at the source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
namespace CoNatural.Data {
	/// <summary>
	/// Implements object tree traversing strategy to persist changes
	/// </summary>
	public interface IStrategy {
		/// <summary>
		/// Adds one-to-many mappings as a guide to traverse the object tree.
		/// </summary>
		/// <typeparam name="T">The type with the many references.</typeparam>
		/// <param name="manyExps">The reference expressions in the form (T) => T.ManyProperty</param>
		void OneToMany<T>(params Expression<Func<T, IEnumerable>>[] manyExps);
 
		/// <summary>
		/// Traverses object tree and returns list of commands bound to objects 
		/// </summary>
		/// <param name="rootObj">The root of the tree.</param>
		/// <returns>The list of bound commands to execute.</returns>
		IEnumerable<BoundCommand> Execute(object rootObj);
 
		/// <summary>
		/// Binds a command and type mapper to a given object instance, returning the resulting bound command
		/// Invoked internally when strategy is executed.
		/// </summary>
		/// <param name="obj">The object instance.</param>
		/// <returns>The bound command or null if no mapping found.</returns>
		BoundCommand Bind(object obj);
	}
}
 
namespace CoNatural.Data {
	/// <summary>
	/// IStrategy base class
	/// Binds commands to IDataObject instances by inspecting the object state (Inserted, Updated, Deleted),
	/// or to the default command otherwise.
	/// </summary>
	public class StrategyBase : IStrategy {
		private Dictionary<Type, ICommand> inserts = new Dictionary<Type, ICommand>();
		private Dictionary<Type, ICommand> updates = new Dictionary<Type, ICommand>();
		private Dictionary<Type, ICommand> deletes = new Dictionary<Type, ICommand>();
		private Dictionary<Type, ICommand> defaults = new Dictionary<Type, ICommand>();
		private Dictionary<Type, List<PropertyInfo>> oneToManyMap = new Dictionary<Type, List<PropertyInfo>>();
 
		/// <summary>
		/// Adds CRUD command mappings 
		/// </summary>
		/// <typeparam name="T">The object type.</typeparam>
		/// <param name="insertCommand">The command used to insert instances of T when T.State = Inserted</param>
		/// <param name="updateCommand">The command used to update instances of T when T.State = Updated</param>
		/// <param name="deleteCommand">The command used to delete instances of T when T.State = Deleted</param>
		public void BindTo<T>(ICommand insertCommand, ICommand updateCommand, ICommand deleteCommand) where T : IDataObject {
			if (insertCommand != null) inserts[typeof(T)] = insertCommand;
			if (updateCommand != null) updates[typeof(T)] = updateCommand;
			if (deleteCommand != null) deletes[typeof(T)] = deleteCommand;
		}
 
		/// <summary>
		/// Adds default command mappings when T is not IDataObject
		/// </summary>
		/// <typeparam name="T">The object type.</typeparam>
		/// <param name="defaultCommand">The default command.</param>
		public void BindTo<T>(ICommand defaultCommand) {
			if (defaultCommand != null) defaults[typeof(T)] = defaultCommand;
		}
 
		#region IStrategy Members
 
		public virtual void OneToMany<T>(params Expression<Func<T, IEnumerable>>[] manyExps) {
			List<PropertyInfo> list;
			if(!oneToManyMap.TryGetValue(typeof(T), out list)) {
				list = new List<PropertyInfo>();
				oneToManyMap.Add(typeof(T), list);
			}
 
			foreach(var manyExp in manyExps) {
				MemberExpression mex = manyExp.Body.ToMemberExpression();
				if(mex == null)
					throw new Exception(string.Format("Invalid many mapping [{0}] -> [{1}].", typeof(T).Name, manyExp.Body.ToString()));
				PropertyInfo property = typeof(T).GetProperty(mex.Member.Name);
				list.Add(property);
			}
		}
 
		public virtual IEnumerable<BoundCommand> Execute(object rootObj) {
			// yield the root first 
			yield return Bind(rootObj as IDataObject);
 
			// traverse one-to-many
			List<PropertyInfo> nav = null;
			if(oneToManyMap.TryGetValue(rootObj.GetType(), out nav)) {
				foreach (var p in nav) {
					IEnumerable children = (IEnumerable)p.GetValue(rootObj, null);
					foreach (var obj in children)
						foreach (var cmd in Execute(obj))
							yield return cmd;
				}
			}
		}
 
		public virtual BoundCommand Bind(object obj) {
			// find the command that applies in the mappings
			ICommand cmd = null;
			IDataObject dataObj = obj as IDataObject;
			if (dataObj == null) {
				defaults.TryGetValue(obj.GetType(), out cmd);
			}
			else {
				switch (dataObj.State) {
					case DataObjectStateEnum.Inserted:
						inserts.TryGetValue(obj.GetType(), out cmd);
						break;
 
					case DataObjectStateEnum.Updated:
						updates.TryGetValue(obj.GetType(), out cmd);
						break;
 
					case DataObjectStateEnum.Deleted:
						deletes.TryGetValue(obj.GetType(), out cmd);
						break;
				}
			}
 
			// return null if no command found in mappings
			if (cmd == null)
				return null;
 
			// create the data-bound command
			return new BoundCommand(obj, cmd);
		}
 
		#endregion
	}
}

The execute method starts traversing the tree from the root, biding objects to commands (following some specific binding rules that can be overridden). In this case there are only one-to-many mappings registered by type but this can also be customized.

The IDataObject interface is used to flag the object as inserted, updated, deleted, or unchanged. This interface only requires one property with the persistence state, and it’s useful if you want to implement data transfer objects (DTOs) that will cross service boundaries and will somehow self-track and send changes back to the DAL. I’m not going to discuss how to do this right now, I will only say that CoNatural.Data is not responsible for tracking changes, that’s the job of other frameworks, and this is an important difference when compared to other tools like LINQ for Entities or NHibernate.

Now let’s make some changes to orders and order details, here is the model (generated with the VS Add-In):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
 
//-------------------------------------------------------
//-- SalesOrderDetail.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:30:55 PM
//-------------------------------------------------------
 
using System;
 
namespace ConsoleApplication1.Model.Sales {
	public class SalesOrderDetail : CoNatural.Data.DataObjectBase {
		public SalesOrderDetail() {}
 
		public Int32 SalesOrderDetailID { get; set; }
		public string CarrierTrackingNumber { get; set; }
		public Int16 OrderQty { get; set; }
		public Int32 ProductID { get; set; }
		public Int32 SpecialOfferID { get; set; }
		public Decimal UnitPrice { get; set; }
		public Decimal UnitPriceDiscount { get; set; }
		public Decimal LineTotal { get; set; }
		public Guid rowguid { get; set; }
		public DateTime ModifiedDate { get; set; }
 
		public SalesOrderHeader SalesOrderHeader { get; set; } // SalesOrderID
	}
}
 
 
//-------------------------------------------------------
//-- SalesOrderHeader.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:30:56 PM
//-------------------------------------------------------
 
using System;
using System.Collections.Generic;
 
namespace ConsoleApplication1.Model.Sales {
	public class SalesOrderHeader : CoNatural.Data.DataObjectBase {
		public SalesOrderHeader() {}
 
		public Int32 SalesOrderID { get; set; }
		public Byte RevisionNumber { get; set; }
		public DateTime OrderDate { get; set; }
		public DateTime DueDate { get; set; }
		public DateTime? ShipDate { get; set; }
		public Byte Status { get; set; }
		public Boolean OnlineOrderFlag { get; set; }
		public string SalesOrderNumber { get; set; }
		public string PurchaseOrderNumber { get; set; }
		public string AccountNumber { get; set; }
		public Int32 BillToAddressID { get; set; }
		public Int32 ShipToAddressID { get; set; }
		public Int32 ShipMethodID { get; set; }
		public Int32? CreditCardID { get; set; }
		public string CreditCardApprovalCode { get; set; }
		public Int32? CurrencyRateID { get; set; }
		public Decimal SubTotal { get; set; }
		public Decimal TaxAmt { get; set; }
		public Decimal Freight { get; set; }
		public Decimal TotalDue { get; set; }
		public string Comment { get; set; }
		public Guid rowguid { get; set; }
		public DateTime ModifiedDate { get; set; }
 
		public Customer Customer { get; set; } // CustomerID
		public SalesPerson SalesPerson { get; set; } // SalesPersonID
		public SalesTerritory SalesTerritory { get; set; } // TerritoryID
 
		// navigation
		public List<SalesOrderDetail> SalesOrderDetails { get; set; }
 	}
}
 
 
 
//-------------------------------------------------------
//-- Customer.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:30:55 PM
//-------------------------------------------------------
 
using System;
 
namespace ConsoleApplication1.Model.Sales {
	public class Customer {
		public Customer() {}
 
		public Int32 CustomerID { get; set; }
		public Int32? PersonID { get; set; }
		public Int32? StoreID { get; set; }
		public Int32? TerritoryID { get; set; }
		public string AccountNumber { get; set; }
		public Guid rowguid { get; set; }
		public DateTime ModifiedDate { get; set; }
	}
}
 
 
 
//-------------------------------------------------------
//-- SalesPerson.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:30:56 PM
//-------------------------------------------------------
 
using System;
 
namespace ConsoleApplication1.Model.Sales {
	public class SalesPerson {
		public SalesPerson() {}
 
		public Int32 BusinessEntityID { get; set; }
		public Int32? TerritoryID { get; set; }
		public Decimal? SalesQuota { get; set; }
		public Decimal Bonus { get; set; }
		public Decimal CommissionPct { get; set; }
		public Decimal SalesYTD { get; set; }
		public Decimal SalesLastYear { get; set; }
		public Guid rowguid { get; set; }
		public DateTime ModifiedDate { get; set; }
	}
}
 
 
 
//-------------------------------------------------------
//-- SalesTerritory.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:30:56 PM
//-------------------------------------------------------
 
using System;
 
namespace ConsoleApplication1.Model.Sales {
	public class SalesTerritory {
		public SalesTerritory() {}
 
		public Int32 TerritoryID { get; set; }
		public string Name { get; set; }
		public string CountryRegionCode { get; set; }
		public string Group { get; set; }
		public Decimal SalesYTD { get; set; }
		public Decimal SalesLastYear { get; set; }
		public Decimal CostYTD { get; set; }
		public Decimal CostLastYear { get; set; }
		public Guid rowguid { get; set; }
		public DateTime ModifiedDate { get; set; }
	}
}

and the commands:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
 
//-------------------------------------------------------
//-- SalesOrderHeaderUpdate.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:29:11 PM
//-------------------------------------------------------
 
using System;
using System.Data;
using CoNatural.Data;
 
namespace ConsoleApplication1.DAL.Sales {
	public class SalesOrderHeaderUpdate : ICommand {
		public SalesOrderHeaderUpdate() {}
 
		public string GetScript() { 
			return 
@"
UPDATE [Sales].[SalesOrderHeader] SET
   [DueDate] = @DueDate,
   [ShipDate] = @ShipDate,
   [Status] = @Status,
   [Comment] = @Comment,
   [ModifiedDate] = getdate()
WHERE
   [SalesOrderID] = @SalesOrderID"; 
		}
 
		public Int32 SalesOrderID { get; set; }
 		public DateTime DueDate { get; set; }
		public DateTime? ShipDate { get; set; }
		public Byte Status { get; set; }
 		[Parameter(128)] public string Comment { get; set; }
	}
}
 
 
//-------------------------------------------------------
//-- SalesOrderHeaderSelectAll.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:29:11 PM
//-------------------------------------------------------
 
using System;
using System.Data;
using CoNatural.Data;
 
namespace ConsoleApplication1.DAL.Sales {
	public class SalesOrderHeaderSelectAll : ICommand {
		public SalesOrderHeaderSelectAll() {}
 
		public string GetScript() { 
			return 
@"
SELECT
   [SalesOrderID],
   [RevisionNumber],
   [OrderDate],
   [DueDate],
   [ShipDate],
   [Status],
   [OnlineOrderFlag],
   [SalesOrderNumber],
   [PurchaseOrderNumber],
   [AccountNumber],
   [CustomerID],
   [SalesPersonID],
   [TerritoryID],
   [BillToAddressID],
   [ShipToAddressID],
   [ShipMethodID],
   [CreditCardID],
   [CreditCardApprovalCode],
   [CurrencyRateID],
   [SubTotal],
   [TaxAmt],
   [Freight],
   [TotalDue],
   [Comment],
   [rowguid],
   [ModifiedDate]
FROM
   [Sales].[SalesOrderHeader]
"; 
		}
 
	}
}
 
 
//-------------------------------------------------------
//-- SalesOrderHeaderInsert.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:29:11 PM
//-------------------------------------------------------
 
using System;
using System.Data;
using CoNatural.Data;
 
namespace ConsoleApplication1.DAL.Sales {
	public class SalesOrderHeaderInsert : ICommand {
		public SalesOrderHeaderInsert() {}
 
		public string GetScript() { 
			return 
@"
INSERT INTO [Sales].[SalesOrderHeader] (
   [RevisionNumber],
   [OrderDate],
   [DueDate],
   [ShipDate],
   [Status],
   [OnlineOrderFlag],
   [PurchaseOrderNumber],
   [AccountNumber],
   [CustomerID],
   [SalesPersonID],
   [TerritoryID],
   [BillToAddressID],
   [ShipToAddressID],
   [ShipMethodID],
   [CreditCardID],
   [CreditCardApprovalCode],
   [CurrencyRateID],
   [SubTotal],
   [TaxAmt],
   [Freight],
   [Comment],
   [rowguid],
   [ModifiedDate])
VALUES (
@RevisionNumber,
@OrderDate,
@DueDate,
@ShipDate,
@Status,
@OnlineOrderFlag,
@PurchaseOrderNumber,
@AccountNumber,
@CustomerID,
@SalesPersonID,
@TerritoryID,
@BillToAddressID,
@ShipToAddressID,
@ShipMethodID,
@CreditCardID,
@CreditCardApprovalCode,
@CurrencyRateID,
@SubTotal,
@TaxAmt,
@Freight,
@Comment,
newid(),
getdate())
SET @SalesOrderID = SCOPE_IDENTITY();
"; 
		}
 
		public Byte RevisionNumber { get; set; }
		public DateTime OrderDate { get; set; }
		public DateTime DueDate { get; set; }
		public DateTime? ShipDate { get; set; }
		public Byte Status { get; set; }
		public Boolean OnlineOrderFlag { get; set; }
		[Parameter(25)] public string PurchaseOrderNumber { get; set; }
		[Parameter(15)] public string AccountNumber { get; set; }
		public Int32 CustomerID { get; set; }
		public Int32? SalesPersonID { get; set; }
		public Int32? TerritoryID { get; set; }
		public Int32 BillToAddressID { get; set; }
		public Int32 ShipToAddressID { get; set; }
		public Int32 ShipMethodID { get; set; }
		public Int32? CreditCardID { get; set; }
		[Parameter(15)] public string CreditCardApprovalCode { get; set; }
		public Int32? CurrencyRateID { get; set; }
		[Parameter(19, 4)] public Decimal SubTotal { get; set; }
		[Parameter(19, 4)] public Decimal TaxAmt { get; set; }
		[Parameter(19, 4)] public Decimal Freight { get; set; }
		[Parameter(128)] public string Comment { get; set; }
		[Parameter(ParameterDirection.Output)] public Int32 SalesOrderID { get; set; }
	}
}
 
 
//-------------------------------------------------------
//-- SalesOrderHeaderDelete.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:29:11 PM
//-------------------------------------------------------
 
using System;
using System.Data;
using CoNatural.Data;
 
namespace ConsoleApplication1.DAL.Sales {
	public class SalesOrderHeaderDelete : ICommand {
		public SalesOrderHeaderDelete() {}
 
		public string GetScript() { 
			return 
@"
DELETE FROM [Sales].[SalesOrderHeader] WHERE
   [SalesOrderID] = @SalesOrderID"; 
		}
 
		public Int32 SalesOrderID { get; set; }
	}
}
 
 
//-------------------------------------------------------
//-- SalesOrderDetailUpdate.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:29:11 PM
//-------------------------------------------------------
 
using System;
using System.Data;
using CoNatural.Data;
 
namespace ConsoleApplication1.DAL.Sales {
	public class SalesOrderDetailUpdate : ICommand {
		public SalesOrderDetailUpdate() {}
 
		public string GetScript() { 
			return 
@"
UPDATE [Sales].[SalesOrderDetail] SET
   [OrderQty] = @OrderQty,
   [UnitPrice] = @UnitPrice,
   [ModifiedDate] = getdate()
WHERE
   [SalesOrderID] = @SalesOrderID AND
   [SalesOrderDetailID] = @SalesOrderDetailID"; 
		}
 
		public Int32 SalesOrderID { get; set; }
		public Int32 SalesOrderDetailID { get; set; }
		public Int16 OrderQty { get; set; }
		[Parameter(19, 4)] public Decimal UnitPrice { get; set; }
	}
}
 
 
 
//-------------------------------------------------------
//-- SalesOrderDetailInsert.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:29:11 PM
//-------------------------------------------------------
 
using System;
using System.Data;
using CoNatural.Data;
 
namespace ConsoleApplication1.DAL.Sales {
	public class SalesOrderDetailInsert : ICommand {
		public SalesOrderDetailInsert() {}
 
		public string GetScript() { 
			return 
@"
INSERT INTO [Sales].[SalesOrderDetail] (
   [SalesOrderID],
   [CarrierTrackingNumber],
   [OrderQty],
   [ProductID],
   [SpecialOfferID],
   [UnitPrice],
   [UnitPriceDiscount],
   [rowguid],
   [ModifiedDate])
VALUES (
@SalesOrderID,
@CarrierTrackingNumber,
@OrderQty,
@ProductID,
@SpecialOfferID,
@UnitPrice,
@UnitPriceDiscount,
newid(),
getdate())
SET @SalesOrderDetailID = SCOPE_IDENTITY();
"; 
		}
 
		public Int32 SalesOrderID { get; set; }
		[Parameter(25)] public string CarrierTrackingNumber { get; set; }
		public Int16 OrderQty { get; set; }
		public Int32 ProductID { get; set; }
		public Int32 SpecialOfferID { get; set; }
		[Parameter(19, 4)] public Decimal UnitPrice { get; set; }
		[Parameter(19, 4)] public Decimal UnitPriceDiscount { get; set; }
		[Parameter(ParameterDirection.Output)] public Int32 SalesOrderDetailID { get; set; }
	}
}
 
 
 
//-------------------------------------------------------
//-- SalesOrderHeaderSelect.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:29:11 PM
//-------------------------------------------------------
 
using System;
using System.Data;
using CoNatural.Data;
 
namespace ConsoleApplication1.DAL.Sales {
	public class SalesOrderRead : ICommand {
		public SalesOrderRead(int orderId) { 
			SalesOrderID = orderId; 
		}
 
		public string GetScript() { 
			return 
@"
-- Read Order Header
SELECT
   [SalesOrderID],
   [RevisionNumber],
   [OrderDate],
   [DueDate],
   [ShipDate],
   [Status],
   [OnlineOrderFlag],
   [SalesOrderNumber],
   [PurchaseOrderNumber],
   [AccountNumber],
   [CustomerID],
   [SalesPersonID],
   [TerritoryID],
   [BillToAddressID],
   [ShipToAddressID],
   [ShipMethodID],
   [CreditCardID],
   [CreditCardApprovalCode],
   [CurrencyRateID],
   [SubTotal],
   [TaxAmt],
   [Freight],
   [TotalDue],
   [Comment],
   [rowguid],
   [ModifiedDate]
FROM
   [Sales].[SalesOrderHeader]
WHERE
   [SalesOrderID] = @SalesOrderID
 
-- Read Order Details
SELECT
   [SalesOrderID],
   [SalesOrderDetailID],
   [CarrierTrackingNumber],
   [OrderQty],
   [ProductID],
   [SpecialOfferID],
   [UnitPrice],
   [UnitPriceDiscount],
   [LineTotal],
   [rowguid],
   [ModifiedDate]
FROM
   [Sales].[SalesOrderDetail]
WHERE
	SalesOrderID = @SalesOrderID
"; 
		}
 
		public Int32 SalesOrderID { get; set; }
	}
}
 
 
 
//-------------------------------------------------------
//-- SalesOrderDetailDelete.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn 
//-- Created on 4/23/2010 12:29:11 PM
//-------------------------------------------------------
 
using System;
using System.Data;
using CoNatural.Data;
 
namespace ConsoleApplication1.DAL.Sales {
	public class SalesOrderDetailDelete : ICommand {
		public SalesOrderDetailDelete() {}
 
		public string GetScript() { 
			return 
@"
DELETE FROM [Sales].[SalesOrderDetail] WHERE
   [SalesOrderID] = @SalesOrderID AND
   [SalesOrderDetailID] = @SalesOrderDetailID"; 
		}
 
		public Int32 SalesOrderID { get; set; }
		public Int32 SalesOrderDetailID { get; set; }
	}
}

We can now start playing with orders:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ConsoleApplication1.Model.Sales;
using ConsoleApplication1.DAL.Sales;
using CoNatural.Data;
using CoNatural.Data.Helpers;
using CoNatural.Data.SqlClient;
using System.Transactions;
using System.Diagnostics;
 
namespace ConsoleApplication1 {
	class Program {
		static IConnection conn;
 
		static void Main(string[] args) {
			conn = new SqlConnectionWithTrace(@"Data Source=PHENOM-II\PHENOM_II;Initial Catalog=AdventureWorks2008;Integrated Security=True");
			//conn.ExecutionMode = System.Data.CommandType.Text;
			conn.ExecutionMode = System.Data.CommandType.StoredProcedure;
 
			// deploy commands
			if(conn.ExecutionMode == System.Data.CommandType.StoredProcedure)
				DeployCommands(conn);
 
			// execute tests
			SalesOrderHeader soh = SaveNewOrderTest(true);
			ModifyOrderTest(soh.SalesOrderID, true);
		}
 
		private static void DeployCommands(IConnection conn) {
			var deployer = new CoNatural.Data.SqlServer.SqlServerCommandDeployer();
			deployer.Deploy(conn, 
			   new SalesOrderHeaderInsert(), 
			   new SalesOrderDetailInsert(),
			   new SalesOrderRead(0),
			   new SalesOrderHeaderUpdate(),
			   new SalesOrderDetailUpdate(),
			   new SalesOrderDetailDelete()
			);
		}
 
		private static SalesOrderHeader SaveNewOrderTest(bool inBatch) {
			SalesOrderHeader order = CreateNewOrder("test1");
			AddNewOrderDetails(order, 1, 706, 33.44M);
			AddNewOrderDetails(order, 2, 707, 55.55M);
			SaveOrder(order, inBatch);
			return order;
		}
 
		private static void ModifyOrderTest(int orderId, bool inBatch) {
			SalesOrderHeader order = ReadOrder(orderId);
			ModifyOrder(order, "new comments", DateTime.Today.AddDays(4));
			DeleteOrderDetail(order.SalesOrderDetails[0]);
			ModifyOrderDetail(order.SalesOrderDetails[1], 1000, 1.23M);
			AddNewOrderDetails(order, 1, 707, 100.100M);
			AddNewOrderDetails(order, 3, 708, 300.3M);
			SaveOrder(order, inBatch);
		}
 
		private static SalesOrderHeader ReadOrder(int orderId) {
			SalesOrderHeader order = null;
			ITypeMaterializer<SalesOrderDetail> detailMaterializer = null;
			conn.ExecuteReader<SalesOrderHeader>(order, new SalesOrderRead(orderId), (o, r, i) => {
				if (i == 1) {
					order = conn.DefaultTypeMaterializerFactory.Materialize<SalesOrderHeader, SalesOrderRead>(r);
					order.SalesOrderDetails = new List<SalesOrderDetail>();
				}
				else {
					if (detailMaterializer == null)
						detailMaterializer = conn.DefaultTypeMaterializerFactory.CreateTypeMaterializer<SalesOrderDetail, SalesOrderRead>(r);
					var detail = detailMaterializer.Callback(r, i);
					detail.SalesOrderHeader = order;
					order.SalesOrderDetails.Add(detail);
				}
			});
 
			return order;
		}
 
		private static void SaveOrder(SalesOrderHeader order, bool inBatch) {
			var f = new TypeMapperFactoryBase();
			//var f = new OptimizedTypeMapperFactoryBase();
			f.Map<SalesOrderHeader>(
				r => r.Customer.CustomerID,
				r => r.SalesTerritory.TerritoryID);
			f.Map<SalesOrderHeader>(r => r.SalesPerson.BusinessEntityID, "SalesPersonID");
			f.Map<SalesOrderDetail>(r => r.SalesOrderHeader.SalesOrderID);
 
 
			var s = new StrategyBase();
			s.BindTo<SalesOrderHeader>(new SalesOrderHeaderInsert(), new SalesOrderHeaderUpdate(), null);
			s.BindTo<SalesOrderDetail>(new SalesOrderDetailInsert(), new SalesOrderDetailUpdate(), new SalesOrderDetailDelete());
			s.OneToMany<SalesOrderHeader>(r => r.SalesOrderDetails);
 
			using (var ts = new TransactionScope()) {
				conn.Execute(order, s, f, inBatch);
				ts.Complete();
			}
		}
 
		private static void SaveOrderDetail(SalesOrderDetail detail, bool inBatch) {
			var f = new TypeMapperFactoryBase();
			//var f = new OptimizedTypeMapperFactoryBase();
			f.Map<SalesOrderDetail>(r => r.SalesOrderHeader.SalesOrderID);
 
			var s = new StrategyBase();
			s.BindTo<SalesOrderDetail>(new SalesOrderDetailInsert(), new SalesOrderDetailUpdate(), new SalesOrderDetailDelete());
 
			using (var ts = new TransactionScope()) {
				conn.Execute(detail, s, f, inBatch);
				ts.Complete();
			}
		}
 
		private static SalesOrderHeader CreateNewOrder(string comment) {
			return new SalesOrderHeader {
				State = DataObjectStateEnum.Inserted,
				RevisionNumber = 1,
				OrderDate = DateTime.Now,
				DueDate = DateTime.Now.AddDays(3),
				ShipDate = DateTime.Now.AddDays(1),
				Status = 5,
				OnlineOrderFlag = false,
				PurchaseOrderNumber = "PO404040404040",
				AccountNumber = "10-4020-000510",
				Customer = new Customer { CustomerID = 29614, AccountNumber = "AW00029614", PersonID = 591, StoreID = 592, TerritoryID = 6 },
				SalesPerson = new SalesPerson { BusinessEntityID = 282, TerritoryID = 6, SalesQuota = 250000, Bonus = 5000, CommissionPct = 0.015M },
				SalesTerritory = new SalesTerritory { TerritoryID = 6, Name = "Canada", CountryRegionCode = "CA", Group = "North America" },
				BillToAddressID = 529,
				ShipToAddressID = 529,
				ShipMethodID = 5,
				CreditCardID = 1566,
				CreditCardApprovalCode = "85817Vi8045",
				CurrencyRateID = 4,
				SubTotal = 50000,
				TaxAmt = 500,
				Freight = 0,
				Comment = comment,
				SalesOrderDetails = new List<SalesOrderDetail>()
			};
		}
 
		private static void AddNewOrderDetails(SalesOrderHeader order, short qty, int productId, decimal unitPrice) {
			if (order.SalesOrderDetails == null)
				order.SalesOrderDetails = new List<SalesOrderDetail>();
			order.SalesOrderDetails.Add(new SalesOrderDetail {
				State = DataObjectStateEnum.Inserted,
				SalesOrderHeader = order,
				CarrierTrackingNumber = "4911-403C-98",
				OrderQty = qty,
				ProductID = productId,
				SpecialOfferID = 1,
				UnitPrice = unitPrice,
				UnitPriceDiscount = 0M
			});
		}
 
		private static void ModifyOrder(SalesOrderHeader order, string comments, DateTime newDueDate) {
			order.Comment = comments;
			order.DueDate = newDueDate;
			order.ShipDate = newDueDate.AddDays(2);
			order.State = DataObjectStateEnum.Updated;
		}
 
		private static void ModifyOrderDetail(SalesOrderDetail detail, short newQty, decimal newUnitPrice) {
			detail.OrderQty = newQty;
			detail.UnitPrice = newUnitPrice;
			detail.State = DataObjectStateEnum.Updated;
		}
 
		private static void DeleteOrderDetail(SalesOrderDetail detail) {
			detail.State = DataObjectStateEnum.Deleted;
		}
	}
 
	public class SqlConnectionWithTrace : CoNatural.Data.SqlClient.SqlClientConnection {
		public SqlConnectionWithTrace(string connectionString)
			: base(connectionString) {
			Trace.Listeners.Add(new ConsoleTraceListener());
		}
 
		public override void BeforeExecute(ICommand iCommand, IDataCommand dataCommand, System.Data.IDbCommand dbCommand) {
			if (iCommand != null)
				Trace.WriteLine("Executing " + iCommand.GetType().ToString());
			foreach (System.Data.IDbDataParameter p in dbCommand.Parameters)
				if (p.Direction != System.Data.ParameterDirection.Output)
					Trace.WriteLine(p.ParameterName + " = " + p.Value);
			Trace.WriteLine(dbCommand.CommandText);
		}
	}
}

Here we are mapping multiple indirect references to properties, with one mapping SalesPerson.BusinessEntityID to SalesPersonID (This is the name of the parameter in all SalesOrderHeader commands).

1
2
3
4
5
f.Map<SalesOrderHeader>(
	r => r.Customer.CustomerID,
	r => r.SalesTerritory.TerritoryID);
f.Map<SalesOrderHeader>(r => r.SalesPerson.BusinessEntityID, "SalesPersonID");
f.Map<SalesOrderDetail>(r => r.SalesOrderHeader.SalesOrderID);

Our strategy is very simple, we just need to register the CRUD commands that will bind to IDataObject types, and set the one-to-many relashionship from SalesOrderHeader to SalesOrderDetail:

1
2
3
4
var s = new StrategyBase();
s.BindTo<SalesOrderHeader>(new SalesOrderHeaderInsert(), new SalesOrderHeaderUpdate(), null);
s.BindTo<SalesOrderDetail>(new SalesOrderDetailInsert(), new SalesOrderDetailUpdate(), new SalesOrderDetailDelete());
s.OneToMany<SalesOrderHeader>(r => r.SalesOrderDetails);

If you compile and execute this example, you should get the following results:

Console

I will release a new version to codeplex in the near future, including a .NET 4.0 build.

Happy coding!

Leave a comment