How to consume nested objects in API resource
https://softwareengineering.stackexchange.com/questions/390419
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?
La 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.
Autres conseils
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.