Following-up on my Linq-to-Sage implementation, I ended up implementing all CRUD operations, following the Sage 300 view protocols.
So, the entities derive from classes that look like this:
namespace SageAPI.Views.PurchaseOrders
{
/// <summary>
/// Defines view and key field mappings for 'Purchase Orders' view.
/// </summary>
[MapsTo("PO0620")]
public class PO0620
{
[Key(KeyType.GeneratedByView)]
[MapsTo("PORHSEQ")]
public decimal Key { get; set; }
}
}
The KeyType
enum determines how keys are generated:
public enum KeyType
{
/// <summary>
/// Indicates that the key value is specified manually.
/// </summary>
Manual,
/// <summary>
/// Indicates that the key value is handled by the view, like an identity column.
/// </summary>
GeneratedByView,
/// <summary>
/// Indicates that the key value is handled through composition.
/// Use for a foreign key column referring to the parent view in a composite-key setup.
/// </summary>
GeneratedByHeader
}
...Which determines how the API is being used for inserting/updating these views. So I added 2 static helper methods in the ViewSet<TEntity>
class:
private static bool HasAutomaticKey(TEntity entity)
{
return GetKeys(entity).Any(key => key.KeyType == KeyType.GeneratedByView || key.KeyType == KeyType.GeneratedByHeader);
}
private static IEnumerable<EntityPropertyInfo<TEntity>> GetKeys(TEntity entity)
{
return entity.GetPropertyInfos().Where(property => property.KeyType != null);
}
The WriteKeys
and WriteEntity
methods are used for writing to the active record:
private void WriteEntity(IEnumerable<EntityPropertyInfo<TEntity>> properties)
{
foreach (var property in properties)
{
try
{
View.Fields.FieldByName(property.FieldName).SetValue(property.Value, false);
}
catch (COMException exception)
{
var error = View.Parent.Parent.Errors[0];
throw new SessionErrorException(error.Message, exception);
}
}
}
private void WriteKeys(TEntity entity)
{
var keys = entity.GetPropertyInfos().Where(property =>
property.ViewName == View.ViewID && property.KeyType != null);
WriteEntity(keys);
}
I had to add an EditMode
attribute, because ReadOnly
wasn't good enough since I needed to be able to insert into fields that, later, couldn't be updated. So I have this enum as a parameter to the EditModeAttribute
:
public enum EditMode
{
/// <summary>
/// Field can be inserted and updated.
/// </summary>
Editable,
/// <summary>
/// Field cannot be inserted or updated.
/// </summary>
ReadOnly,
/// <summary>
/// Field can be inserted, but not updated.
/// </summary>
InsertOnly
}
Then, I was ready to implement the actual operations:
Insert
/// <summary>
/// Inserts a single new record into a flat view, or of a detail record in a composed header/detail view.
/// </summary>
/// <param name="entity">The entity that contains the key field and values to insert.</param>
public void Insert(TEntity entity)
{
BeginInsert(entity);
FinalizeInsert();
}
/// <summary>
/// Begins the insertion of a header record into a composed header/detail view.
/// </summary>
/// <param name="entity">The entity that contains </param>
public void BeginInsert(TEntity entity)
{
if (HasAutomaticKey(entity))
{
try
{
View.RecordCreate(ViewRecordCreate.DelayKey);
}
catch (COMException exception)
{
var error = View.Parent.Parent.Errors[0];
throw new SessionErrorException(error.Message, exception);
}
}
else
{
try
{
View.RecordClear();
}
catch (COMException exception)
{
var error = View.Parent.Parent.Errors[0];
throw new SessionErrorException(error.Message, exception);
}
}
WriteEntity(entity);
}
/// <summary>
/// Finalizes the insertion of a header and its details, in a composed header/detail view.
/// </summary>
public void FinalizeInsert()
{
try
{
View.Insert();
}
catch (COMException exception)
{
var error = View.Parent.Parent.Errors[0];
throw new SessionErrorException(error.Message, exception);
}
}
Update
/// <summary>
/// Updates a single new record into a flat view, or of a detail record in a composed header/detail view.
/// </summary>
/// <param name="entity">The entity that contains the key field and values to update.</param>
public void Update(TEntity entity)
{
BeginUpdate(entity);
FinalizeUpdate();
}
/// <summary>
/// Begins the insertion of a header record into a composed header/detail view.
/// </summary>
/// <param name="entity">The entity that contains the key field and values to update.</param>
public void BeginUpdate(TEntity entity)
{
WriteKeys(entity);
try
{
if (View.Read(false))
{
WriteEntity(entity);
}
else
{
throw new InvalidKeyException();
}
}
catch (COMException exception)
{
var error = View.Parent.Parent.Errors[0];
throw new SessionErrorException(error.Message, exception);
}
}
/// <summary>
/// Finalizes the updating of a header and its details, in a composed header/detail view.
/// </summary>
public void FinalizeUpdate()
{
try
{
View.Update();
}
catch (COMException exception)
{
var error = View.Parent.Parent.Errors[0];
throw new SessionErrorException(error.Message, exception);
}
}
Delete
/// <summary>
/// Deletes the specified existing entity.
/// If this is a header entity in a header/detail view, details will also be deleted.
/// </summary>
/// <param name="entity">The entity to delete.</param>
public void Delete(TEntity entity)
{
try
{
WriteKeys(entity);
if (View.Read(false))
{
View.Delete();
}
else
{
throw new InvalidKeyException();
}
}
catch (ViewException exception)
{
var error = View.Parent.Parent.Errors[0];
throw new SessionErrorException(error.Message, exception.InnerException);
}
catch (COMException exception)
{
var error = View.Parent.Parent.Errors[0];
throw new SessionErrorException(error.Message, exception);
}
}
I had to split Insert
and Update
methods into Begin[Insert|Update]
and Finalize[Insert|Update]
because, per API documentation, in order to insert into composed header/detail views I have to:
- RecordClear header to initialize the fields.
- Set the fields in the header.
- RecordClear detail to initialize the fields.
- Set the fields in the detail.
- Insert detail.
- Go to step 3 if there are more details.
- Insert header. (This will Post the details)
So my client code for inserting looks something like this:
context.PurchaseOrderHeaders.BeginInsert(header); context.PurchaseOrderDetails.Insert(detail1); context.PurchaseOrderDetails.Insert(detail2); context.PurchaseOrderDetails.Insert(detail3); context.PurchaseOrderHeaders.FinalizeInsert();
And for deleting:
var header = context.PurchaseOrderHeaders.SingleOrDefault(po => po.Number == "987654"); if (header != null) { context.PurchaseOrderHeaders.Delete(header); }
The exceptions thrown by the API are COMException
, or ViewException
if I'm lucky. Either way, the useful information is in the Session.Errors
collection, which I'm accessing from the View
- so I'm grabbing the message from there, and the stack trace from the thrown COMException
(a ViewException
has an inner COMException
), and throw my own SessionErrorException
with that.
I can't shake the feeling that I've gone overboard with try...catch
blocks. What else could be cleaned up?