6
\$\begingroup\$

Related: Something like a LINQ provider


I needed to work with the Sage300 View API. I had never worked with it, but my first impression has been that the API is stringly-typed, and makes you write very procedural and repetitive code.

So I decided to make my life easier, and wrap it with a familiar interface:

public interface IRepository<TEntity> where TEntity : class, new()
{
    /// <summary>
    /// Projects all entities that match specified predicate into a <see cref="TEntity"/> instance.
    /// </summary>
    /// <param name="filter">A function expression that returns <c>true</c> for all entities to return.</param>
    /// <returns></returns>
    IEnumerable<TEntity> Select(Expression<Func<TEntity, bool>> filter);

    /// <summary>
    /// Projects the single that matches specified predicate into a <see cref="TEntity"/> instance.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown when predicate matches more than a single result.</exception>
    /// <param name="filter">A function expression that returns <c>true</c> for the only entity to return.</param>
    /// <returns></returns>
    TEntity Single(Expression<Func<TEntity, bool>> filter);

    /// <summary>
    /// Updates the underlying View for the specified entity.
    /// </summary>
    /// <param name="entity">The existing entity with the modified property values.</param>
    void Update(TEntity entity);

    /// <summary>
    /// Deletes the specified entity from the underlying View.
    /// </summary>
    /// <param name="entity">The existing entity to remove.</param>
    void Delete(TEntity entity);

    /// <summary>
    /// Inserts a new entity into the underlying View.
    /// </summary>
    /// <param name="entity">A non-existing entity to create in the system.</param>
    void Insert(TEntity entity);
}

So, to implement a repository, I make a simple POCO class, and I use a custom MapsToAttribute to tell the "engine" how to map the class and its properties to a View and its fields - note, SageViews is an internal static class exposing nothing but internal const string members:

[MapsTo(SageViews.HeadersViewId)]
public class PurchaseOrderHeader
{
    [MapsTo("PONUMBER")]
    public string Number { get; set; }

    [MapsTo("VDCODE")]
    public string VendorCode { get; set; }

    [MapsTo("PORTYPE")]
    public PurchaseOrderType Type { get; set; }

    [MapsTo("ONHOLD")]
    public bool IsOnHold { get; set; }

    [MapsTo("ORDEREDON")]
    public DateTime OrderDate { get; set; }

    [MapsTo("EXPARRIVAL")]
    public DateTime ExpectedDate { get; set; }

    [MapsTo("FOBPOINT")]
    public string FreeOnBoardPoint { get; set; }

    [MapsTo("VIACODE")]
    public string ShipViaCode { get; set; }

    [MapsTo("VIANAME")]
    public string ShipViaName { get; set; }

    [MapsTo("TERMSCODE")]
    public string TermsCode { get; set; }

    [MapsTo("TERMSCODED")]
    public string TermsName { get; set; }

    [MapsTo("DESCRIPTIO")]
    public string Description { get; set; }

    [MapsTo("REFERENCE")]
    public string Reference { get; set; }

    [MapsTo("COMMENT")]
    public string Comment { get; set; }
}

I decided to implement the interface in a base class first, to avoid having to implement the same reflection code over and over for every entity:

/// <summary>
/// Encapsulates a View and its CRUD operations.
/// </summary>
/// <typeparam name="TEntity">The entity type associated with the view.</typeparam>
/// <remarks>
/// <see cref="TEntity"/> should be a POCO class exposing get/set properties
/// marked with a <see cref="MapsToAttribute"/>.
/// </remarks>
public abstract class SageRepositoryBase<TEntity> : IRepository<TEntity> 
    where TEntity : class, new()
{
    /// <summary>
    /// Uses reflection to discover <see cref="MapsToAttribute"/> mappings on specified <see cref="TEntity"/> type
    /// and reflects on specified <see cref="entity"/> to retrieve property values, mapped to the appropriate field.
    /// </summary>
    /// <param name="entity">The entity object to retrieve mapped values for.</param>
    /// <returns>
    /// Returns a dictionary keyed with View names, where each entry contains all values for that view.
    /// </returns>
    protected IDictionary<string, IEnumerable<EntityPropertyInfo<TEntity>>> DiscoverMappedValues(TEntity entity)
    {
        return entity.GetPropertyInfos()
                     .GroupBy(property => property.ViewName)
                     .ToDictionary(grouping => grouping.Key, grouping => grouping.AsEnumerable());
    }

    /// <summary>
    /// Uses reflection to discover <see cref="MapsToAttribute"/> mappings and fetch 
    /// property values from the mapped views and fields.
    /// </summary>
    /// <returns>
    /// Returns an entity representing the current/active record in the composed views.
    /// </returns>
    protected TEntity ReadEntity()
    {
        var result = new TEntity();
        var properties = result.GetPropertyInfos();

        foreach (var property in properties)
        {
            property.Property.SetValue(result, Views[property.ViewName].Fields.FieldByName(property.FieldName).Value);
        }

        return result;
    }

    /// <summary>
    /// Projects the single that matches specified predicate into a <see cref="TEntity"/> instance.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown when predicate matches more than a single result.</exception>
    /// <param name="filter">A function expression that returns <c>true</c> for the only entity to return.</param>
    /// <returns>Returns the single entity matching specified criteria.</returns>
    public abstract TEntity Single(Expression<Func<TEntity, bool>> filter);

    /// <summary>
    /// Projects all entities that match specified predicate into a <see cref="TEntity"/> instance.
    /// </summary>
    /// <param name="filter">A function expression that returns <c>true</c> for all entities to return.</param>
    /// <returns>Returns all entities matching specified criteria.</returns>
    public abstract IEnumerable<TEntity> Select(Expression<Func<TEntity, bool>> filter);

    /// <summary>
    /// Updates the underlying View for the specified entity.
    /// </summary>
    /// <param name="entity">The existing entity with the modified property values.</param>
    public abstract void Update(TEntity entity);

    /// <summary>
    /// Deletes the specified entity from the underlying View.
    /// </summary>
    /// <param name="entity">The existing entity to remove.</param>
    public abstract void Delete(TEntity entity);

    /// <summary>
    /// Inserts a new entity into the underlying View.
    /// </summary>
    /// <param name="entity">A non-existing entity to create in the system.</param>
    public abstract void Insert(TEntity entity);

    /// <summary>
    /// Gets a dictionary containing all composed views <see cref="TEntity"/> maps to.
    /// Dictionary key is each View's RotoID/name.
    /// </summary>
    protected abstract IDictionary<string, View> Views { get; }
}

Here is the PurchaseOrderHeadersRepository class - I have not yet implemented all CRUD operations, but the implemented ones work perfectly.

public sealed class PurchaseOrderHeadersRepository : SageRepositoryBase<PurchaseOrderHeader>, IDisposable 
{
    private View _headersView;
    private View _commentsView;
    private View _headersOptionalFieldsView;
    private View _requisitionsView;
    private View _functionsView;
    private View _detailsView;
    private View _detailsOptionalFieldsView;
    private View _shipViaAddressesView;

    private View _vendorsView;
    private View _termsView;

    public void Compose(DBLink context)
    {
        _headersView = context.OpenView(SageViews.HeadersViewId);
        _commentsView = context.OpenView(SageViews.CommentsViewId);
        _headersOptionalFieldsView = context.OpenView(SageViews.HeaderOptionalFieldsViewId);
        _requisitionsView = context.OpenView(SageViews.RequisitionsViewId);
        _functionsView = context.OpenView(SageViews.FunctionsViewId);
        _detailsView = context.OpenView(SageViews.DetailsViewId);
        _detailsOptionalFieldsView = context.OpenView(SageViews.DetailsOptionalFieldsViewId);
        _shipViaAddressesView = context.OpenView(SageViews.ShipViaAddressesViewId);

        _vendorsView = context.OpenView(SageViews.VendorsViewId);
        _termsView = context.OpenView(SageViews.TermsViewId);

        _headersView.Compose(new[]{ _commentsView, _detailsView, _requisitionsView, _functionsView, _headersOptionalFieldsView });
        _detailsView.Compose(new[]{ _headersView, _commentsView, _functionsView, null, null, _detailsOptionalFieldsView });
        _commentsView.Compose(new[]{ _headersView, _detailsView });
        _requisitionsView.Compose(new[]{ _headersView, _functionsView });
        _functionsView.Compose(new[]{ _headersView, _commentsView, _detailsView, _requisitionsView });

        _views = new Dictionary<string, View>
        {
            { SageViews.HeadersViewId, _headersView },
            { SageViews.DetailsViewId, _detailsView },
            { SageViews.CommentsViewId, _commentsView },
            { SageViews.RequisitionsViewId, _requisitionsView },
            { SageViews.FunctionsViewId, _functionsView }
        };
    }

    private IDictionary<string, View> _views;

    protected override IDictionary<string, View> Views
    {
        get { return _views; }
    }

    public override PurchaseOrderHeader Single(Expression<Func<PurchaseOrderHeader, bool>> filter)
    {
        var result = Select(filter).ToList();
        return result.Single();
    }

    public override IEnumerable<PurchaseOrderHeader> Select(Expression<Func<PurchaseOrderHeader, bool>> filter)
    {
        var searchFilter = filter.ToFilterExpression();
        _headersView.Browse(searchFilter, true);

        if (!_headersView.GoTop())
        {
            yield break;
        }

        do
        {
            yield return ReadEntity();
        }
        while (_headersView.GoNext());
    }

    public override void Update(PurchaseOrderHeader entity)
    {
        throw new NotImplementedException();
    }

    public override void Delete(PurchaseOrderHeader entity)
    {
        throw new NotImplementedException();
    }

    public override void Insert(PurchaseOrderHeader entity)
    {
        throw new NotImplementedException();
    }

    public void Dispose()
    {
        _headersView.Dispose();
        _commentsView.Dispose();
        _headersOptionalFieldsView.Dispose();
        _requisitionsView.Dispose();
        _functionsView.Dispose();
        _detailsView.Dispose();
        _detailsOptionalFieldsView.Dispose();
        _shipViaAddressesView.Dispose();
    }
}

I don't like that I need to compose the views explicitly, and put that responsibility on the caller - especially since the Compose(DBLink) method isn't part of the interface. I thought of lazy-composing the views on first access, but then I would require the DBLink through the constructor and stored as a field, but I couldn't dispose it because I don't own the object - would it be a good idea to do that?

Other than that.. I'd be happy to hear anyone with experience with the Sage300 View API, tell me what beartrap I might have just stuck my foot in with this code. Or is this "clever code" that will bite me later? Looks clear enough?

All feedback welcome.


Almost forgot - here's the GetPropertyInfos extension method, performing the reflection magic:

public static IEnumerable<EntityPropertyInfo<TEntity>> GetPropertyInfos<TEntity>(this TEntity entity)
    where TEntity : class, new()
{
    var type = typeof (TEntity);
    var mapsToView = type.GetCustomAttribute<MapsToAttribute>();
    if (mapsToView == null)
    {
        throw new InvalidOperationException("Entity type is missing a MapsToAttribute.");
    }

    return from property in typeof (TEntity).GetProperties()
           let mapsToField = property.GetCustomAttribute<MapsToAttribute>()
           where mapsToField != null
           select new EntityPropertyInfo<TEntity>(entity, property, mapsToView, mapsToField);
}

Does it get any cleaner?

\$\endgroup\$
4
\$\begingroup\$

The code looks good, but I can see a couple of things that could be a bit cleaner:

e.g. this method:

public override PurchaseOrderHeader Single(Expression<Func<PurchaseOrderHeader, bool>> filter)
{
    var result = Select(filter).ToList();
    return result.Single();
}

result is a poor name IMO as it's a list (even if it only has one item). I think it should be pluralised: results.

There's no need to call .ToList() which I think makes the intermediate variable pointless:

public override PurchaseOrderHeader Single(Expression<Func<PurchaseOrderHeader, bool>> filter)
{
    return Select(filter).Single();
}

Regarding the DBLink dependency... I definitely think you should either pass it in as a constructor parameter or create a factory method for your repository. Even when you do that, I don't see why you'd need to store the link as a field which means your Dispose implementation would be unchanged.

FWIW, I would go with a Create method over the constructor as you're doing some work there and I don't know whether any of that takes time... Either way, by having an extra method that the caller has to know about before they can use the instance is always frustrating.

You could create it as an extension method on the DBLink if you want to treat that a bit like a unit of work. You could leave your create + initialize as separate steps, or you could create a static factory method.

public class DBLinkExtensions
{
      IRepository<PurchaseOrderHeader> GetPurchaseOrderHeadersRepository(this DBLink link)
     {
         if (link == null) throw new ArgumentNullException("link");
         return new PurchaseOrderHeadersRepository().Compose(link);
         // OR
         // return PurchaseOrderHeadersRepository.Create(link);
     }
}

Then your client code would be:

using (var dbLink = GetTheDBLink(...))
using (var purchaseOrderHeadersRepository = dbLink.GetPurchaseOrderHeadersRepository())
{
    // Your stuff.
}

Just in case you're interested, what you're creating here is called an Anticorruption Layer. You're hiding all of the nastiness of the 3rd party library with a well designed API - a very good idea!

\$\endgroup\$
  • \$\begingroup\$ I... don't have a constructor for now, I'm doing work in a Compose method ;-) the only constructor there is the default one \$\endgroup\$ – Mathieu Guindon Feb 7 '16 at 15:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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