And we welcome Jonne Kats! Are you a Dutch .NET developer, willing to communicate with colleagues working in the same field? Drop me a line!
Get the “Dutch .NET Developers Alliance” opml to be imported in your favorite RSS reader here
Since I’m not feeling to well (shitty fever) I had plenty of opportunity to watch a few Tech-Ed 2004 streams. Hopefully I’ll be able to make it to the DotNed Usergroup meeting next Monday where Juval Lowy and Ingo Rammer will give their talks. I believe Christian Weyer will also be around.
A must see is the ARC402 - Data in Services-Oriented Architecture session. Harry Pierson explains why each model’s strength is simultaneously its weakness related to SOA. A great no nonsense presentation which raises the curtain on the abstractness and hype which seems to be the equivalent of SOA thinking these days.
I have this business requirement to interact with message queues, binding queue and Neo related operations together in a transaction to ensure consistency. My team is comfortable with using COM+ services in the ADO.NET world so I figured why not add COM+ support to Neo. Recently I picked up on this article http://www.apache.org/~hammett/articles/dotnettransactions.html which describes a nice way to satisfy my external transaction requirements.
Listed below is the CustomerOrderBroker class which creates a new transaction. It then fetches the order message from the queue and attempts to process it. Though the code isn’t that useful it actually does express my initial intention. Both the queue and Neo’s operations are rolled back! See to log down below.
Listing CustomerOrderBroker class:
|
[TransactionAware]
public class CustomerOrderBroker : ContextBoundObject
{
private static ILog logger;
public CustomerOrderBroker()
{
if (logger == null)
logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType.ToString());
}
[RequiresTransaction(TransactionOption.RequiresNew)]
public bool ProcessQueuedOrder()
{
try
{
logger.Debug("ProcessQueuedOrder Tx: " + ContextUtil.TransactionId.ToString());
// fetch the new order from queue
MessageQueue neoQueue = new MessageQueue(@".\private$\neo");
Message orderMessage = neoQueue.Receive(new TimeSpan(0, 0, 0, 1), MessageQueueTransactionType.Automatic);
// assume this is a new customer, insert a new
ObjectContext context = NorthwindHelper.GetFreshObjectContext();
Customer customer = new CustomerFactory(context).CreateObject("PGIE");
customer.CompanyName = "ObjectAutomation";
context.SaveChanges();
// imagine we filled a new order entity with the order message data
Order order = new OrderFactory(context).CreateObject(1);
OrderStatus status = customer.PlaceOrder(order);
if (status == OrderStatus.Accepted)
{
ContextUtil.SetComplete();
return true;
}
else
{
// this will rollback both Neo's and msmq's work
ContextUtil.SetAbort();
return false;
}
}
catch (Exception ex)
{
ContextUtil.SetAbort();
throw new Exception("Failed to process queued order.", ex);
}
finally
{
logger.Debug("ProcessQueuedOrder voted: " + ContextUtil.MyTransactionVote.ToString());
}
}
} |
And the transaction aware Customer class.
|
[TransactionAware]
public class Customer : CustomerBase
{
private static ILog logger;
protected Customer(System.Data.DataRow aRow, Neo.Core.ObjectContext aContext) : base(aRow, aContext)
{
if (logger == null)
logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType.ToString());
}
[RequiresTransaction(TransactionOption.Supported)]
public OrderStatus PlaceOrder(Order order)
{
try
{
logger.Debug("PlaceOrder Tx: " + ContextUtil.TransactionId.ToString());
// we did our work
ContextUtil.SetComplete();
// insufficient credit for customer, the coordinating process has to decide
// what action would be appropriate
return OrderStatus.Rejected;
}
catch (Exception ex)
{
ContextUtil.SetAbort();
throw new Exception("Failed to place order.", ex);
}
finally
{
logger.Debug("PlaceOrder voted: " + ContextUtil.MyTransactionVote.ToString());
}
}
} |
And the log produced by the Unit-Test:
|
00:09:15,140: Initialised new transaction sink.
00:09:15,156: Initialised new transaction required enviroment.
00:09:22,000: Initialised new, new transaction required enviroment.
00:09:22,015: ProcessQueuedOrder Tx: 33725a5e-d05c-465b-a306-692bc12b87bf
00:09:22,078: Created new SqlDataStore.
00:09:22,093: Initialised new object context.
00:09:22,093: Initialised new transaction sink.
00:09:22,156: Creating object of type Northwind.Model.Customer
00:09:22,312: Initialised new transaction required enviroment.
00:09:22,328: Initialised new supported transaction enviroment.
00:09:22,328: SaveChanges. Beginning.
00:09:22,359: Opening connection to "data source=localhost;initial catalog=northwind;
user id=sa;password=******"
00:09:22,640: INSERT INTO Customers (Region, PostalCode, CustomerId, Fax, ContactName, City, Address, ContactTitle, Phone, CompanyName, Country) VALUES (@Region, @PostalCode, @CustomerId, @Fax, @ContactName, @City, @Address, @ContactTitle, @Phone, @CompanyName, @Country)
00:09:22,640: @Region =
00:09:22,640: @PostalCode =
00:09:22,640: @CustomerId = PGIE
00:09:22,640: @Fax =
00:09:22,640: @ContactName =
00:09:22,640: @City =
00:09:22,640: @Address =
00:09:22,640: @ContactTitle =
00:09:22,640: @Phone =
00:09:22,640: @CompanyName = ObjectAutomation
00:09:22,640: @Country =
00:09:22,671: 1 rows affected
00:09:22,687: Closed connection to "data source=localhost;initial catalog=northwind;user id=sa;"
00:09:22,703: SaveChanges. Completed.
00:09:22,703: Creating object of type Northwind.Model.Order
00:09:22,718: PlaceOrder Tx: 33725a5e-d05c-465b-a306-692bc12b87bf
00:09:22,718: PlaceOrder voted: Commit
00:09:22,718: ProcessQueuedOrder voted: Abort |
Joel’s latest article confused me a bit. Isn’t it that Visual Basic really sucks, and I mean really sucks! Let’s say it sucks terribly bad for doing anything other then drag and drop RAD development. At least for people that know a thing or two about JAVA. C++ is a great tool for the more experienced developer, but since Microsoft messed up with COM, we’ve been left with a confusing, difficult programming model. Not to mention DCOM, COM+ etc. Joel is right about memory management though, but I personally don’t consider it to be a show stopper.
Now we have .NET, it’s a great platform today, I don’t care about 2006, most likely it’ll be a great platform in 2006+. I’ll even consider investing development time to run my applications on mono (who would have thought of that? I’m not geek enough to fit in with the Tux clan). No ROI, heck I’ll interop. If it wasn’t for .NET I would be doing JAVA (I probably would If I stayed away from CORBA).
So besides Microsoft refusing to fix .NET bugs today, overloading developers with 2006 stuff, what’s the big deal?
Germany vs Netherlands, place your bets
2:1, I'm almost ashamed :D
Neo (1.2.1) has support for Oracle. Erik refactored the classes within the Neo.Database namespace to promote the implementation of additional DataStores. Something that according to Erik should be very easy as of now. Oracle support comes along with a series of extensive Unit Tests with a big thanks to Arjen Poutsma who did the original implementation!
The refactorings include the data store base class, DbDataStore implementing the IDataStore interface which is the class of choice to derive additional DataStores from. The cumbersome SQL generation is encapsulated in the GenericSql92Builder class. One could mistaken the GenericSql92Builder class for the Query Object pattern (PoEAA, Fowler 2003), which in fact it is not. It’s responsibility doesn’t reach further then generate SQL-92 compliant syntax for a DataTable and a specific IDbImplementationFactory instance passed in the constructor. SQL is generated by interpreting Qualifiers and FetchSpecifications which results in all the necessary SQL queries to perform CRUD and find operations on persistent entity data.
The following UML diagram illustrates the classes and interfaces involved.
Download the 1.2.1 distribution
Applications build on top of the Neo framework can use a shared context (one per execution thread to be exact) or create explicit contexts. Personally I prefer using explicit context on a use-case basis. The ObjectContext (the class encapsulating context) handles CRUD operations. The context expressed by the ObjectContext class actually relies on its internal DataSet (remember Neo is an Object Façade over ADO.NET). As you’ve seen in my previous post (and specifically the comment Alex made :D), factories create entity objects within a context. They use the specific context passed to the constructor. The ObjectContext class implements the IDataStore interface which tells us that the ObjectContext class uses a data store to retrieve and save changes to entity objects. Examples of data stores are: sql server instance, DataSet, XML, ASCII etc.
Another cool thing of Neo’s contexts is that they can be nested. This is according to Erik a non-obvious feature which is very useful for rich client applications featuring master / detail forms. You then push the masters context into the detail context, call SaveChanges in the detail context and it’ll escalate into the parents context. Canceling a use-case in the detail form just deletes the detail context. Erik explains this a bit better then I do here.
In a future post I’ll discuss the internals of Neo’s ObjectContext architecture, explaining how it’s hooked into ADO.NET fundaments.
I’d like to discuss finding domain objects using Neo against the Northwind catalog. The code has been tested against Neo 1.2.1 and the following schema:
<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<!DOCTYPE database SYSTEM "file:norque.dtd">
<?neo path="../../../Tools/CodeGen/Resources"?>
<database name="northwind" package="Northwind.Model" defaultIdMethod="none">
<table name="Customers" javaName="Customer">
<column name="CustomerId" javaName="Id" primaryKey="true" required="true" type="CHAR" size="5" />
<column name="CompanyName" javaName="CompanyName" required="true" type="VARCHAR" size="40" />
<column name="ContactName" javaName="ContactName" type="VARCHAR" size="30" />
<column name="ContactTitle" javaName="ContactTitle" type="VARCHAR" size="30" />
<column name="Address" javaName="Address" type="VARCHAR" size="60" />
<column name="City" javaName="City" type="VARCHAR" size="15" />
<column name="Region" javaName="RegionCode" type="VARCHAR" size="15" />
<column name="PostalCode" javaName="PostalCode" type="VARCHAR" size="10" />
<column name="Country" javaName="Country" type="VARCHAR" size="15" />
<column name="Phone" javaName="PhoneNumber" type="VARCHAR" size="24" />
<column name="Fax" javaName="FaxNumber" type="VARCHAR" size="24" />
</table>
</database>
Neo separates finder behavior in it’s entity factories which can be packaged within the domain layer (the layer where you’re model classes reside). You then have three different interfaces to find your domain objects, Templates, Qualifiers, and FetchSpecifications.
Templates
Templates are particularly useful because they have all the properties as the corresponding entity object, including to-one relationships.
[Test]
public void SalesRepresentativeWithTemplate()
{
CustomerList result;
CustomerTemplate t;
t = new CustomerFactory(context).GetQueryTemplate();
t.ContactTitle = "Sales Representative";
result = new CustomerFactory(context).Find(t);
Assertion.AssertEquals("Found wrong number of sales representative", 17, result.Count);
}
If you study the Neo documentation closely you’ll notice the methode FindMatchingObjects being refactored to a simple Find overload. Templates work out really well if being used close to the domain specific code.
Qualifiers
Qualifiers define search criteria’s.
[Test]
public void SalesRepresentativeWithPropertyQualifier()
{
PropertyQualifier q;
IList result;
q = new PropertyQualifier("ContactTitle", new EqualsPredicate("Sales Representative"));
result = new CustomerFactory(context).Find(q);
Assertion.AssertEquals("Found wrong number of sales representative", 17, result.Count);
}
As you might have noticed the find method returns an untyped collection. There’s also an overloaded Find which accepts a qualifier format string and in fact does return a CustomerList collection. Another neat feature of qualifiers is that it can use inlined values and multiple clauses. Qualifiers make perfect sense for relative simple search scenario’s.
FetchSpecifications
Then there is the FetchSpecification. Using a FetchSpecification for searching domain objects enables you to page and sort results. This is done by passing a value indicating the fetch limit, and sort ordering in the constructor.
[Test]
public void SalesRepresentativesWithFetchSpecification()
{
PropertyQualifier q;
FetchSpecification fSpec;
CustomerList result;
q = new PropertyQualifier("ContactTitle", new EqualsPredicate("Sales Representative"));
fSpec = new FetchSpecification(context.EntityMapFactory.GetMap(typeof (Customer)), q);
result = new CustomerFactory(context).Find(fSpec);
Assertion.AssertEquals("Found wrong number of sales representative", 17, result.Count);
}
[Test]
public void SalesRepresentativesWithLimit()
{
PropertyQualifier q;
FetchSpecification fSpec;
CustomerList result;
q = new PropertyQualifier("ContactTitle", new EqualsPredicate("Sales Representative"));
fSpec = new FetchSpecification(context.EntityMapFactory.GetMap(typeof (Customer)), q, 10);
result = new CustomerFactory(context).Find(fSpec);
Assertion.AssertEquals("Should only fetch up to limit.", 10, result.Count);
}
Overview finders interface implemented in CustomerFactory class: