2
\$\begingroup\$

Background

I'm about to build a LOB application that resembles an e-commerce solution (without paying and being open to the public). It will be used to receive orders for fulfillment purposes based on fulfillment contracts with one or more companies. The UI will be MVC, and it's a going to be a pretty simple site.

Challenge

Most orders that are placed on the site will be fulfilled by our company, however there are orders that will be sent to one or more off-site fulfillment centers (not owned by us, each likely with a different API/workflow). Simply put, there will be multiple order processing workflows. There will also likely be different requirements for calculating inventory on hand on a per-customer basis.

All that being said, I'm more of a front-end UI/UX guy and I want to confirm that the way I'm planning on handling this is reasonable.

If any of the verbiage I'm using below is incorrect, please correct me. Additionally, this is my first time using asyc/await also, so don't be shy about correcting my implementation.

Setup

Note: My boss prefers architectural patters that are as simple as we can make them without sacrificing long-term software viability.

For the sake of this question, we're only going to be dealing with the order object.

My solution currently looks like this:

enter image description here

First I created an interface called IOrderRepository:

public interface IOrderRepositoy
{
    Task<Model.Order> CreateOrderAsync(Model.Order order);
    Task<IEnumerable<Model.Order>> CreateOrderAsync(IEnumerable<Model.Order> orders);
}

Then I created a partial Business.Order class which implements IOrderRepository and inherits Model.Order from entity framework:

namespace AdventureWorks.Business
{
    internal partial class Order : Model.Order, IOrderRepositoy
    {
        public virtual async Task<IEnumerable<Model.Order>> CreateOrderAsync(IEnumerable<Model.Order> orders)
        {
            var db = new AWEntities();
            db.Orders.AddRange(orders);
            await db.SaveChangesAsync();

            return orders;
        }

        public virtual async Task<Model.Order> CreateOrderAsync(Model.Order order)
        {
            var db = new AWEntities();
            db.Orders.Add(order);
            await db.SaveChangesAsync();

            return order;
        }
    }
}

Next, I created a class to house the custom logic for a specific customer called GAC_Order. If custom logic is required, I override the CreateOrderAsync method(s) found in the partial Order class. In the example below, I chose to call base.CreateOrderAsync() as well, but there will likely be cases where base.CreateOrderAsync() does not get called.

namespace AdventureWorks.Business.Repositories
{
    internal class GAC_Order : Business.Order
    {
        public async override Task<Model.Order> CreateOrderAsync(Model.Order order)
        {
            // Custom code to override or add to base funcionality.
            // Can be run before or after base code
            // Add 5 days to required time field
            order.RequiredDate = DateTime.Now.AddDays(5);

            // Run base create order functionality
            await base.CreateOrderAsync(order);

            // Run custom functionality
            return order;
        }

        public async override Task<IEnumerable<Model.Order>> CreateOrderAsync(IEnumerable<Model.Order> orders)
        {
            // Run base create order functionality
            await base.CreateOrderAsync(orders);

            // Run custom functionality
            return orders;
        }
    }
}

Finally, I created a repository factory to determine which implementation of the IOrderRepository would be returned to me. If a custom implementation is not defined at the customer level, the base implementation is used:

namespace AdventureWorks.Business
{
    public static class RepositoryFactory
    {
        public static IOrderRepositoy GetRepository(Model.Customer customer, Type repoType)
        {
            if (repoType == typeof(Business.IOrderRepositoy))
            {
                if (customer.OrderRepository == null || String.IsNullOrEmpty(customer.OrderRepository))
                {
                    return new Business.Order();
                }
                else
                {
                    var assembly = Assembly.GetExecutingAssembly();
                    var type = assembly.GetTypes().First(t => t.Name == customer.OrderRepository);

                    return Activator.CreateInstance(type) as IOrderRepositoy;
                }
            }
            else
            {
                // TODO: Used if/else and type passing to prepare for additional custom logic
            }
        }
    }
}

When I need to create an order, I call it like this:

// Create Order object
Model.Order order = new Model.Order();
order.ShipPostalCode = "30189";
order.OrderDate = DateTime.Now;

// NOTE: This would come from DB in real-world
Model.Customer customer = new Model.Customer();
customer.OrderRepository = "GAC_Order";

// Get order repository
Business.IOrderRepositoy orderRepository = Business.RepositoryFactory.GetRepository(customer, typeof(Business.IOrderRepositoy));

// Create order
orderRepository.CreateOrderAsync(order);

Questions

  • This works, but is this a valid way to account allow for a default workflow, AND also allow for other workflows on a per-customer basis?
  • Am I correct in assuming that this is a simple repository pattern (i.e., am I using the correct terminology here?)?
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Repository pattern

To start with your second question

Am I correct in assuming that this is a simple repository pattern?

No it isn't. When the choice for Entity Framework has been made (which is good), the simplest repository is the one EF provides out of the box: the DbSet. In this case, db.Orders. Any layer on top of that can be useful, but shouldn't be applied just because it seems such a good idea to implement the repository pattern. It's already there! You emphasize that the application is to be simple, so I wouldn't stack layer upon layer prematurely.

The base line

The only thing you really need to persist Orders is the part (slightly rewritten):

using(var db = new AWEntities())
{
    db.Orders.AddRange(orders);
    await db.SaveChangesAsync();
}

All other pieces of code that wrap this part can be deemed redundant. As the ultimate (over) simplification you could even write this code directly in an MVC controller's action method. No added layers involved and the job is done.

Useful layers?

Anything added on top of this base line should be carefully considered. Additions should be useful, not restrictive.

Your proposed architecture is restrictive because it is "vertical". You seem to have a column of abstractions for each entity: Model.Order, Order (as IOrderRepository), Business.Order with subclasses like GAC_Order. Then there is a Model.Customer class, maybe part of a similar column. This architecture has the same drawbacks as Data Access Object: it will lead to multiple isolated queries and repetitive code.

Alternatives?

This columns-per-entity setup defeats the purpose of an OR mapper like Entity Framework, which is to work with object graphs that map to a relational data model. When you need orders and their related customers you can get them in one LINQ query. Likewise, when you want to save orders and customers, you can add them to the context and do one SaveChanges call to save everything in one transaction.

This has made me move to API-oriented architectures. I usually create services that live for the duration of one web request. Each service has a number of methods that execute some business case, like creating orders. For this, the service has one context instance that can pretty freely be used inside the service methods. This works best in combination with dependency injection (or Inversion of Control, IoC), but that's not a prerequisite.

This is, very briefly, what it could look like:

public class OrdersController : Controller
{
    private readonly IOrderService _orderService;

    public OrdersController(IOrderService orderService)
    {
        // Injected by IoC, or just new it up here.
        this._orderService = orderService;
    }

    [HttpPost]
    public ActionResult Create(FormCollection collection)
    {
        var orderDto = new OrderDto {properties from method parameter};
        _orderService.CreateOrder(orderDto);
        // Exception handling, return view, etc.
    }
}

The DTOs serve as an abstraction layer between UI and service, so the UI can change without implications for the service layer and vise versa.

Service layer:

public interface IOrderService
{
    ServiceResult CreateOrder(OrderDto orderDto);
}

class OrderService : IOrderService
{
    private readonly BusinessContext _context;

    public OrderService(BusinessContext context)
    {
        // Injected by IoC, or just new it up here.
        this._context = context;
    }

    public ServiceResult CreateOrder(OrderDto orderDto)
    {
        // Do the business
    }
}

In short, this would mean that each step in a workflow would be represented by a service method. The UI just issues commands and reads results. It doesn't contain any business logic whatsoever.

\$\endgroup\$
3
  • \$\begingroup\$ I really appreciate the detailed response. I realize that EF already provides the repository pattern. The issue that I was trying to answer with an additional layer of abstraction was the multiple-workflow, multiple-business logic issue. \$\endgroup\$
    – James Hill
    Commented Mar 6, 2017 at 11:58
  • \$\begingroup\$ We could have 20 customers in this store, and 10 of them could have different order workflows (meaning they don't get written to our order table, but to an external order table via supplied API). Similarly, we have customers who calc item availability differently. It's not always read and display from the DB. Having said that, it looks like I can handle that similarly to what I've done above but in the service, yes? \$\endgroup\$
    – James Hill
    Commented Mar 6, 2017 at 11:58
  • \$\begingroup\$ That makes things more complicated, but yes, it will all fit in a service-oriented design. For various calculation methods you could use strategy pattern, for instance. For the various client API you could build an abstraction layer, so in you business logic you can do everything the same way. \$\endgroup\$ Commented Mar 6, 2017 at 20:15

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.