Question

I am building my first web application, it links to the serverside through RESTfull Web API (Angular on client side, ASP.Net Core and EF Core on serverside, Automapper to map API Resources to/from domain models).
In the application I have two main models (simplified below as Delivery and Order) and each has its own API controller and endpoint (api/deliveries and api/orders). The business rules are such that: 1. an Order can be created before Delivery exists, 2. Delivery can hold an array of Order that will be included in the delivery, 3. Order can be amended after being included in Delivery.

My domain models (API resources are close to identical):

public class Delivery
{
    public int Id { get; set; }
    public Customer Customer { get; set; }
    public Address Address { get; set; }
    public DateTime EstimatedDelivery { get; set; }
    public DeliveryOrder[] Orders { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public Product Product { get; set; }
    public int Quantity { get; set; }
}

public class DeliveryOrder
{
    public int DeliveryId { get; set; }
    public Delivery Delivery { get; set; }
    public int OrderId { get; set; }
    public Order Order { get; set; }
}

The client user interface has a view to create/amend order (instance of Order) and a view to create/amend delivery (instance of Delivery) which includes the ability to create/amend orders. As a result of the business rules, in the delivery view, I could update order individually and directly, or update delivery.orders. I am struggling to come to a suitable implementation.

An example of what the delivery object looks like on client side, with the first order being an existing order and the second order being a new order:

{
    "id": 5,
    "customer": {
        "id": 15,
        "name": "Jane"
    },
    "address": {
        "street": "Something Ave",
        "number": "145"
    },
    "estimatedDelivery": "2019-05-21T13:28:06.419",
    "orders": [
        {
            "orderId": 6,
            "order": {
                "id": 6,
                "product": {
                    "id": 112,
                    "categoryId": 36
                },
                "quantity": 4
            }
        },
        {
            "order": {
                "product": {
                    "id": 115,
                    "categoryId": 36
                },
                "quantity": 6
            }
        }
    ]
}

Solutions I have considered:

First. In the view (Angular component) for delivery have two steps in a "save" action. In this case I need to check if order has an id or not and either put an amended order or post a new order to my api/orders. In this case my Delivery API (through Automapper profile) ignores Delivery.Orders.Order and only compares values of Delivery.Orders.OrderId. My client action would look similar to:

saveAction() {
    delivery.orders.forEach(o => {
        if (o.order.id) { this.httpClient.put('api/orders/' + o.order.id, o.order).subscribe( ... ); } 
        else { this.httpClient.post('api/orders/', o.order).subscribe( ... ) });
    this.httpClient.put('api/deliveries' + delivery.id, delivery).subscribe( ... );
}

Reading from other topics I understand it is not recommended to create multiple HTTP requests with a for loop to manage the number of subscriptions. Perhaps this is less of an issue if the subscriptions are combined in a forkJoin?

Second. Instead of deciding wether a specific order existed before the save action, send the whole delivery.orders to the api and let the serverside decide on whether instances of Order need to be created or amended. In that case I would patch the whole array, as discussed here. Again, my Delivery API (through Automapper profile) ignores Delivery.Orders.Order and only compares values of Delivery.Orders.OrderId. The saving action would look something like:

saveAction() {
    this.httpClient.patch('api/orders/' delivery.orders).subscribe(data => {
            delivery.orders = data;
            this.httpClient.put('api/deliveries' + delivery.id, delivery).subscribe( ... )
        });
}

Because this would require work on my serverside, I thought maybe it would be better to manage the whole thing on my serverside, which is the third solution I am considering.

Third: Let the server side consume Delivery.Orders in full and do a custom mapping so that the members in Delivery.Orders.Order are correctly amended or created. This could be done in either the Delivery controller or perhaps in a MappingProfile. The data is then simply sent through:

saveAction() {
    this.httpClient.put('api/deliveries' + delivery.id, delivery).subscribe(data => delivery = data);
}

Question(s): I have never done this and I don't feel confident about any of my solutions. Does my third solution make most sense, or should I not have Delivery API endpoint take responsibility of creating/amending the nested Order? Is there a design pattern that is considered best practice or at least more commonly used than what I have thought of?

Was it helpful?

Solution

I am treating the Delivery which is a physical shipment as separate to the DeliveryInstructions which are the address, contact, time, etc...

Does Order depend on Delivery?

In your case it appears that an Order can be made independently of a Delivery.

There for Order should be its own API end-point.

Is Delivery really delivering orders, or products on an Order?

I know this is not what you asked, but there is a semantic lurking down there. Either:

  • An order should not be altered when it is being delivered (how on earth are you going to add the plushie, or remove the socks when its on the truck?)
  • The individually ordered products are sent in a Delivery, and the order can only remove undelivered items, or add new items to the order (for subsequent and separate delivery).

In the first case Delivery does not hold Order objects, simply a reference to them (like an identifier or a url).

In the second case the Delivery refers to a set of line items on an Order. Those Line items also refer to their order/delivery.

Transactionality

Who is responsible for transactionally updating a set of objects on the server?

You could make a workflow, which essentially performs a single update on a single url at a time. Each transform should always leave the system in a consistent state (even if its not the desired end state).

You could drive this workflow from the client:

  • You run the risk of the client timing out
  • Customers might be a little upset if their order isn't actioned if you display that it has been on their UI.

You could pass this workflow to the server, something like /request.

  • You will need to provide an api for monitoring progress of the workflow.
  • You will need to provide several UI views for observing progress.
  • However once received the client can disconnect, and your service will still follow through.

Of course if you are passing it directly to the server you could simply perform the request inline.

  • No need to provide a progress UI.
  • The update can be done in a single SQL/NoSQL transaction.
  • The workflow cannot take too long as it will tie up WebServer Threads/Client Connections.

Take your pick on what works best for you.

OTHER TIPS

In the end I decided to go with the third solution. This was greatly facilitated by using Automapper and even more so by using AutoMapper.EquivalencyExpression. For complicated nested relationships, I felt I had no choice but to create references on the client side before posting/putting the whole graph to my server.

I decided to use GUID's (or UUID) for these in my client application. On the server side I made these Alternate Keys so that I could rely on this property as the foreign key. By doing so I did not need to write any logic in my application for processing the different scenarios (post versus put and existing nested objects versus new instances of the nested objects). For the abstraction, the pattern is unnecessarily complicated. However for my actual implementation it allowed me to cross-reference between nested objects in Delivery and Order even before these were created on the server side. I have included an example below of my approach:

Programmer related domain models

public class Programmer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Keyboard> Keyboard { get; set; }
    public List<Finger> Fingers { get; set; }
}

public class Finger
{
    public int Id { get; set; }
    public Guid Guid { get; set; }
    public Hand Hand { get; set; }
    public int Sequence { get; set; }
    public List<KeyStroke> KeyStrokes { get; set; }

}

public enum Hand
{
    Left = 1,
    Right = 2
}

Keyboard related domain models, where KeyStroke is the class used as joining table between Key and Finger:

public class Keyboard
{
    public int Id { get; set; }
    public int SerialNumber { get; set; }
    public Programmer Programmer { get; set; }
    public List<Key> Keys { get; set; }
}

public class Key
{
    public int Id { get; set; }
    public string KeyValue { get; set; }
    public List<KeyStroke> KeyStrokes { get; set; }
}

public class KeyStroke
{
    public int KeyId { get; set; }
    public Key Key { get; set; }
    public Guid FingerGuid { get; set; }
    public Finger Finger { get; set; }
}

The DbContext profile where the Guid is marked as alternate key:

modelBuilder.Entity<Programmer>();
modelBuilder.Entity<Finger>()
    .HasAlternateKey(f => f.Guid);
modelBuilder.Entity<KeyStroke>()
    .HasKey(ks => new {ks.FingerGuid, ks.KeyId});

modelBuilder.Entity<KeyStroke>()
    .HasOne(ks => ks.Finger)
    .WithMany(f => f.KeyStrokes)
    .HasForeignKey(ks => ks.FingerGuid)
    .HasPrincipalKey(f => f.Guid);

My Resources (or ViewModel):

public class ProgrammerResource
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<KeyboardResource> Keyboard { get; set; }
    public List<FingerResource> Fingers { get; set; }
}

public class FingerResource
{
    public int Id { get; set; }
    public Guid Guid { get; set; }

    public Hand Hand { get; set; }
    public int Sequence { get; set; }
}

public class KeyboardResource
{
    public int Id { get; set; }
    public int SerialNumber { get; set; }
    public List<KeyResource> Keys { get; set; }
}

public class KeyResource
{
    public int Id { get; set; }
    public string KeyValue { get; set; }
    public List<KeyStrokeResource> KeyStrokes { get; set; }
}

public class KeyStrokeResource
{
    public int KeyId { get; set; }
    public Guid FingerGuid { get; set; }
}

In this structure Keyboard can be created and posted in it's own controller, or can be included in the Programmer related graph and then posted into Programmer controller.

Programmer controller:

[HttpPost("programmer")]
public async Task<IActionResult> CreateProgrammer([FromBody] ProgrammerResource programmerResource)
{
    var programmer = mapper.Map<ProgrammerResource, Programmer>(programmerResource);

    context.Programmers.Add(programmer);
    await context.SaveChangesAsync();

    var savedProgrammer = await context.Programmers
        .Include(p => p.Fingers)
        .Include(p => p.Keyboard).ThenInclude(kb => kb.Keys).ThenInclude(k => k.KeyStrokes)
        .SingleOrDefaultAsync(v => v.Id == programmer.Id);

    var result = mapper.Map<Programmer, ProgrammerResource>(savedProgrammer);

    return Ok(result);
}

With Automapper profile:

CreateMap<Keyboard, KeyboardResource>();
CreateMap<KeyboardResource, Keyboard>()
    .EqualityComparison((kr, k) => kr.Id == k.Id);

CreateMap<KeyResource, Key>()
    .EqualityComparison((kr, k) => kr.Id == k.Id);
CreateMap<FingerResource, Finger>()
    .EqualityComparison((fr, f) => fr.Id == f.Id);
CreateMap<KeyStrokeResource, KeyStroke>()
    .ForMember(k => k.Finger, opt => opt.Ignore())
    .ForMember(k => k.Key, opt => opt.Ignore());

This setup allows me to post a whole graph, and have it create the relationship between Programmer and Keyboard as well as the relationship between Finger and Key even though instances of Programmer and Keyboard do no yet exist on posting.

{
    "name": "John Doe",
    "keyboard": [
        {
            "serialNumber": 6584654,
            "keys": [
                {
                    "keyValue": "A",
                    "keyStrokes": [
                        {
                            "fingerGuid": "f911ac90-48fc-4eae-ae3d-b727f005eb53"
                        },
                        {
                            "fingerGuid": "52ea936a-3922-41e6-95f2-1408fa19af74"
                        }
                    ]
                },
                {
                    "keyValue": "B",
                    "keyStrokes": []
                }
            ]
        }
    ],
    "fingers": [
        {
            "guid": "f911ac90-48fc-4eae-ae3d-b727f005eb53",
            "hand": 1,
            "sequence": 1
        },
        {
            "guid": "52ea936a-3922-41e6-95f2-1408fa19af74",
            "hand": 1,
            "sequence": 2
        }
    ]
}

Posting the above will create the whole relationship correctly (and changes through Put are executed as expected):

{
  "id": 4,
  "name": "John Doe",
  "keyboard": [
    {
      "id": 3,
      "serialNumber": 6584654,
      "keys": [
        {
          "id": 5,
          "keyValue": "A",
          "keyStrokes": [
            {
              "keyId": 5,
              "fingerGuid": "f911ac90-48fc-4eae-ae3d-b727f005eb53"
            },
            {
              "keyId": 5,
              "fingerGuid": "52ea936a-3922-41e6-95f2-1408fa19af74"
            }
          ]
        },
        {
          "id": 6,
          "keyValue": "B",
          "keyStrokes": []
        }
      ]
    }
  ],
  "fingers": [
    {
      "id": 6,
      "guid": "f911ac90-48fc-4eae-ae3d-b727f005eb53",
      "hand": 1,
      "sequence": 1
    },
    {
      "id": 7,
      "guid": "52ea936a-3922-41e6-95f2-1408fa19af74",
      "hand": 1,
      "sequence": 2
    }
  ]
}

For the creation of the Guid on the client application I used the very straightforward GUID creation class by Steve Fenton.

Licensed under: CC-BY-SA with attribution
scroll top