Monday, December 29, 2008

TDD-friendly CSLA solution – Part 3: Mobilizing the DTO

I thought I’d present a little bit of the detail of the design right now. This particular part of the design is really at the heart of my proposed TDD solution.

Addendum: Let me rephrase what I am trying to achieve. I want all of my business object dependencies to be explicit and I want to pass them in through a constructor. That's just my preference. I am aware that there are other ways to inject dependencies and to achieve most of what I am aiming for - for example, see Peter Stromquist's blog. However, I only want to inject these dependencies once throughout the lifetime of the object (unless of course there is a reason for changing them, then I would expose a setter). In addition, and most importantly, I only ever wish to work with one instance of the business object once it is constructed rather than changing my references after each save.

I am aware that others have achieved similar results to what I am trying to achieve by removing the use of the DataPortal altogether. However, I still want to use the DataPortal mechanism, since it provides for one access point to the server for which all data access requests are serviced.

I have mentioned that ultimately I want to be able to inject validation, authorization, and other services, in addition to data access code. For the purposes of this discussion, however, we only need to consider the data access separation. The concepts I introduce here and in subsequent posts will ultimately allow for the injection of other dependencies.

So, let’s just talk about separating the data access for now…

When developing applications with CSLA, I decouple the specific data access technology by using a shaped data container – a Data Transfer Object (DTO). During a create or fetch, the data access code populates the DTO which the business object then reads from. It also works in reverse – during an insert or update, the business object populates the DTO and the data access code reads from the DTO and subsequently persists data to an underlying datasource (I will make this code available in future posts). This is nothing new. In fact, the CSLA code comes with an example DeepData application that demonstrates the concept.

My point for discussion is this: If we go to the trouble of abstracting out the data access by introducing a DTO, why don’t we send the DTO across the wire rather than the business object? The actual data access code will still run on a server, but we don’t send the business object across the wire.

Sending the DTO accross the wire, rather than the business object, enables the client to continue to work with the same instance of the business object. Others have achieved this by removing the Dataportal altogether. However, I still want to use the Dataportal mechanism, since it provides for one access point to the server for which all data access requests are serviced.

Rather than make the DTO the mobile object, I suggest that we piggyback the serializable DTO on a mobile command object, since the DataPortal has infrastructure in place for a command object.

Note: Since the business object has methods to reconstruct itself from a DTO, we could always create the business object again on the server in order to check validation rules and authorization rules if necessary. If any logic run on the server results in a state change, the state is captured in the DTO and provided back to the business object on the client after the operation is complete. A new instance of the business object does not need to be created on the client, instead the existing instance will update it’s state from the returned DTO.

Here is an example of what the business object constructor will look like (access modifiers on the constructor and other methods will be discussed another time):

[Serializable]
public class Invoice : BusinessBase<Invoice>
{
[NotUndoable]
private IInvoiceDataGateway dataGateway;

// allow the service locator to inject the concrete instance
internal Invoice() : this( ServiceLocator.Current.GetInstance<IInvoiceDataGateway>()) { }

// inject the dependency manually
internal Invoice(IInvoiceDataGateway dataGateway)
{
this.dataGateway = dataGateway;
MarkNew();
}
}

 
What used to be the factory region of the business object looks like this (note that some code has been removed and replaced with comments for now):

new internal void Save()
{
// check authorizations, edit level, validity, isdeleted, isbusy, etc
if (IsDirty)
{
RefreshFromDto(dataGateway.Save(WriteDto()));
}
}

internal Invoice Fetch(long id)
{
// check authorizations

ReadDto(dataGateway.Fetch(id), true);
return this;
}

// Create and Delete methods to be shown later

 
Note that because I am using the command object to execute my CRUD methods, I need to add code to my business class to do things such as check for authorizations before saving, marking itself clean after save, etc – things that the Dataportal fetch, insert, and update methods would normally do for the business object. Most of this code could eventually be templated or put into the business base class. Again, the fact that these are internal will be discussed later.

The business object will also contain DTO assembly methods. The children will also contain these 3 methods. Here they are for you to consider. They will be discussed in future posts.

private void ReadDto(InvoiceDto dto, bool markOld)
{
LoadProperty<long>(IdProperty, dto.Id);
// load all other properties/fields here

LineItems.ReadDto(dto.LineItems, markOld);
// load other children here

if (markOld) MarkOld();
}

private InvoiceDto WriteDto()
{
if (IsDirty)
{
InvoiceDto dto = new InvoiceDto();
dto.IsSelfDirty = IsSelfDirty;
dto.IsDeleted = IsDeleted;
dto.IsNew = IsNew;

dto.Id = ReadProperty<long>(IdProperty);

if (IsSelfDirty)
{
if (!IsDeleted)
{
// copy the remainder of the root state to the dto
}
}

LineItems.WriteDto(dto.LineItems);
// do for all other children

return dto;
}

return null;
}

private void RefreshFromDto(InvoiceDto dto)
{
if (dto != null)
{
if (IsNew) LoadProperty<long>(IdProperty, dto.Id);

timestamp = dto.LastChanged;

LineItems.RefreshFromDto(dto.LineItems);
// refresh other children
MarkOld();
}
}


 
And finally, the concrete instance of the IInvoiceDataGateway will look something like the following. I admit that calling a command object for each method doesn’t look very nice. For now, I am just demonstrating a proof of concept and I want to use existing DataPortal code.

public class MobileInvoiceDataGateway : IInvoiceDataGateway
{
public MobileInvoiceDataGateway() { }

#region IInvoiceDataGateway Members

public InvoiceDto Fetch(long id) { return GetByIdTransactionScript.GetById(id); }

public void Delete(long id) { DeleteTransactionScript.Delete(id); }

public InvoiceDto Save(InvoiceDto data)
{
if (data != null)
{
if (data.IsDeleted)
{
// Details will come later
}
else
{
if (data.IsNew)
return InsertTransactionScript.Insert(data);
else
return UpdateTransactionScript.Update(data);
}
}
return null;
}

#endregion // IInvoiceDataGateway Members

#region Transaction Scripts

[Serializable]
class GetByIdTransactionScript : CommandBase
{
private InvoiceDto data;
private long id;

public GetByIdTransactionScript(long id) { this.id = id; }

public static InvoiceDto GetById(long id) { return DataPortal.Execute<GetByIdTransactionscript>(new GetByIdTransactionScript(id)).data; }

protected override void DataPortal_Execute() { // Details will come later }
}

[Serializable]
class InsertTransactionScript : CommandBase
{
private InvoiceDto data;

public InsertTransactionScript(InvoiceDto data) { this.data = data; }

public static InvoiceDto Insert(InvoiceDto data) { return DataPortal.Execute<InsertTransactionScript>(new InsertTransactionScript(data)).data; }

protected override void DataPortal_Execute()
{
// Details will come later
// note: we can reconstruct the business object if required
}
}

[Serializable]
class UpdateTransactionScript : CommandBase
{
private InvoiceDto data;

public UpdateTransactionScript(InvoiceDto data) { this.data = data; }

public static InvoiceDto Update(InvoiceDto data) { return DataPortal.Execute<UpdateTransactionScript>(new UpdateTransactionScript(data)).data; }

protected override void DataPortal_Execute()
{
// Details will come later
// note: we can reconstruct the business object if required
}
}

[Serializable]
class DeleteTransactionScript : CommandBase
{
private long id;

public DeleteTransactionScript(long id) { this.id = id; }

public static void Delete(long id) { DataPortal.Execute<DeleteTransactionScript>(new DeleteTransactionScript(id)); }

protected override void DataPortal_Execute() { // Details will come later }
}

#endregion // Transaction Scripts
}

 
I am sure that this code may leave more questions than it has answered. I will eventually address all concerns.

1 comment: