3
\$\begingroup\$

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:

  1. RecordClear header to initialize the fields.
  2. Set the fields in the header.
  3. RecordClear detail to initialize the fields.
  4. Set the fields in the detail.
  5. Insert detail.
  6. Go to step 3 if there are more details.
  7. 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?

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

If you're unsure whether the API is going to throw a ViewException or a COMException, or under which circumstances it's going to throw which, your exception handling has holes.

And it's not very DRY.

Instead of repeating this block everywhere (well, you're not repeating it everywhere, but it's not clear whether that's intended):

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);
}

Consider replacing it with a shorter, Pokemon catch block:

catch (Exception exception)
{
    OnSessionErrorException(exception);
    throw;
}

And then have that OnSessionErrorException method centralize exception handling:

private void OnSessionErrorException(Exception exception)
{
    var session = View.Parent.Parent;
    var sessionError = session.Errors.Count > 0
        ? session.Errors[0]
        : null;

    var message = sessionError == null ? exception.Message : sessionError.Message;
    if (exception is ViewException)
    {
        throw new SessionErrorException(message, exception.InnerException);
    }

    throw new SessionErrorException(message, exception);
}

That way you'll be throwing a SessionErrorException with the correct inner exception and message everytime, with minimal code repetition.

Since it's a "catch 'em all" handler in a ViewSet class, perhaps SessionErrorException would be better off with a less specific ViewSetException type name; the handler could then be renamed OnViewSetException.


The dual try blocks in BeginInsert, make it harder to read than necessary. Just wrap the whole block in a single try block instead:

public void BeginInsert(TEntity entity)
{
    try
    {
        if (HasAutomaticKey(entity))
        {
            View.RecordCreate(ViewRecordCreate.DelayKey);
        }
        else
        {
            View.RecordClear();
        }

        var properties = entity.GetPropertyInfos().Where(property =>
            property.ViewName == View.ViewID
            && (property.KeyType == null || property.KeyType == KeyType.Manual)
            && property.EditMode != EditMode.ReadOnly);

        WriteEntity(properties);
    }
    catch (Exception exception)
    {
        OnViewSetException(exception);
        throw;
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.