Using Ports and Adapters to Persist Restaurant Table Configurations
Using Ports and Adapters to persist restaurant table configurations #
Excerpt #
A data architecture example in C# and ASP.NET.
A data architecture example in C# and ASP.NET.
This is part of a small article series on data architectures. In the first instalment, you’ll see the outline of a Ports and Adapters implementation. As the introductory article explains, the example code shows how to create a new restaurant table configuration, or how to display an existing resource. The sample code base is an ASP.NET 8.0 REST API.
Keep in mind that while the sample code does store data in a relational database, the term table in this article mainly refers to physical tables, rather than database tables.
While Ports and Adapters architecture diagrams are usually drawn as concentric circles, you can also draw (subsets of) it as more traditional layered diagrams:
Here, the arrows indicate mappings, not dependencies.
HTTP interaction # #
A client can create a new table with a POST
HTTP request:
POST /tables HTTP/1.1
content-type: application/json
{ <span>"communalTable"</span>: { <span>"capacity"</span>: 16 } }
Which might elicit a response like this:
HTTP/1.1 201 Created
Location: https://example.com/Tables/844581613e164813aa17243ff8b847af
Clients can later use the address indicated by the Location
header to retrieve a representation of the resource:
GET /Tables/844581613e164813aa17243ff8b847af HTTP/1.1
accept: application/json
Which would result in this response:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{<span>"communalTable"</span>:{<span>"capacity"</span>:16}}
By default, ASP.NET handles and returns JSON. Later in this article you’ll see how well it deals with other data formats.
Boundary # #
ASP.NET supports some variation of the
model-view-controller (MVC) pattern, and Controllers handle HTTP requests. At the outset, the action method that handles the POST
request looks like this:
[<span>HttpPost</span>]
<span>public</span> <span>async</span> <span>Task</span><<span>IActionResult</span>> <span>Post</span>(<span>TableDto</span> <span>dto</span>)
{
<span>ArgumentNullException</span>.<span>ThrowIfNull</span>(<span>dto</span>);
<span>var</span> <span>id</span> = <span>Guid</span>.<span>NewGuid</span>();
<span>await</span> <span>repository</span>.<span>Create</span>(<span>id</span>, <span>dto</span>.<span>ToTable</span>()).<span>ConfigureAwait</span>(<span>false</span>);
<span>return</span> <span>new</span> <span>CreatedAtActionResult</span>(<span>nameof</span>(<span>Get</span>), <span>null</span>, <span>new</span> { id = <span>id</span>.<span>ToString</span>(<span>"N"</span>) }, <span>null</span>);
}
As is
idiomatic in ASP.NET, input and output are modelled by
data transfer objects (DTOs), in this case called TableDto
. I’ve already covered this little object model in the article
Serializing restaurant tables in C#, so I’m not going to repeat it here.
The ToTable
method, on the other hand, is a good example of how trying to cut corners lead to more complicated code:
<span>internal</span> <span>Table</span> <span>ToTable</span>()
{
<span>var</span> <span>candidate</span> =
<span>Table</span>.<span>TryCreateSingle</span>(SingleTable?.Capacity ?? -1, SingleTable?.MinimalReservation ?? -1);
<span>if</span> (<span>candidate</span> <span>is</span> { })
<span>return</span> <span>candidate</span>.Value;
<span>candidate</span> = <span>Table</span>.<span>TryCreateCommunal</span>(CommunalTable?.Capacity ?? -1);
<span>if</span> (<span>candidate</span> <span>is</span> { })
<span>return</span> <span>candidate</span>.Value;
<span>throw</span> <span>new</span> <span>InvalidOperationException</span>(<span>"Invalid TableDto."</span>);
}
Compare it to the TryParse
method in the
Serializing restaurant tables in C# article. That one is simpler, and less error-prone.
I think that I wrote ToTable
that way because I didn’t want to deal with error handling in the Controller, and while I test-drove the code, I never wrote a test that supply malformed input. I should have, and so should you, but this is demo code, and I never got around to it.
Enough about that. The other action method handles GET
requests:
[<span>HttpGet</span>(<span>"</span><span>{</span>id<span>}</span><span>"</span>)]
<span>public</span> <span>async</span> <span>Task</span><<span>IActionResult</span>> <span>Get</span>(<span>string</span> <span>id</span>)
{
<span>if</span> (!<span>Guid</span>.<span>TryParseExact</span>(<span>id</span>, <span>"N"</span>, <span>out</span> <span>var</span> <span>guid</span>))
<span>return</span> <span>new</span> <span>BadRequestResult</span>();
<span>var</span> <span>table</span> = <span>await</span> <span>repository</span>.<span>Read</span>(<span>guid</span>).<span>ConfigureAwait</span>(<span>false</span>);
<span>if</span> (<span>table</span> <span>is</span> <span>null</span>)
<span>return</span> <span>new</span> <span>NotFoundResult</span>();
<span>return</span> <span>new</span> <span>OkObjectResult</span>(<span>TableDto</span>.<span>From</span>(<span>table</span>.Value));
}
The static TableDto.From
method is identical to the ToDto
method from the
Serializing restaurant tables in C# article, just with a different name.
To summarize so far: At the boundary of the application, Controller methods receive or return TableDto
objects, which are mapped to and from the Domain Model named Table
.
Domain Model # #
The Domain Model Table
is also identical to the code shown in
Serializing restaurant tables in C#. In order to comply with the
Dependency Inversion Principle (DIP), mapping to and from TableDto
is defined on the latter. The DTO, being an implementation detail, may depend on the abstraction (the Domain Model), but not the other way around.
In the same spirit, conversions to and from the database are defined entirely within the repository
implementation.
Data access layer # #
Keeping the example consistent, the code base also models data access with C# classes. It uses Entity Framework to read from and write to SQL Server. The class that models a row in the database is also a kind of DTO, even though here it’s idiomatically called an entity:
<span>public</span> <span>partial</span> <span>class</span> <span>TableEntity</span>
{
<span>public</span> <span>int</span> Id { <span>get</span>; <span>set</span>; }
<span>public</span> <span>Guid</span> PublicId { <span>get</span>; <span>set</span>; }
<span>public</span> <span>int</span> Capacity { <span>get</span>; <span>set</span>; }
<span>public</span> <span>int</span>? MinimalReservation { <span>get</span>; <span>set</span>; }
}
I had a command-line tool scaffold the code for me, and since
I don’t usually live in that world, I don’t know why it’s a partial class
. It seems to be working, though.
The SqlTablesRepository
class implements the mapping between Table
and TableEntity
. For instance, the Create
method looks like this:
<span>public</span> <span>async</span> <span>Task</span> <span>Create</span>(<span>Guid</span> <span>id</span>, <span>Table</span> <span>table</span>)
{
<span>var</span> <span>entity</span> = <span>table</span>.<span>Accept</span>(<span>new</span> <span>TableToEntityConverter</span>(<span>id</span>));
<span>await</span> context.Tables.<span>AddAsync</span>(<span>entity</span>).<span>ConfigureAwait</span>(<span>false</span>);
<span>await</span> context.<span>SaveChangesAsync</span>().<span>ConfigureAwait</span>(<span>false</span>);
}
That looks simple, but is only because all the work is done by the TableToEntityConverter
, which is a nested class:
<span>private</span> <span>sealed</span> <span>class</span> <span>TableToEntityConverter</span> : <span>ITableVisitor</span><<span>TableEntity</span>>
{
<span>private</span> <span>readonly</span> <span>Guid</span> id;
<span>public</span> <span>TableToEntityConverter</span>(<span>Guid</span> <span>id</span>)
{
<span>this</span>.id = <span>id</span>;
}
<span>public</span> <span>TableEntity</span> <span>VisitCommunal</span>(<span>NaturalNumber</span> <span>capacity</span>)
{
<span>return</span> <span>new</span> <span>TableEntity</span>
{
PublicId = id,
Capacity = (<span>int</span>)<span>capacity</span>,
};
}
<span>public</span> <span>TableEntity</span> <span>VisitSingle</span>(
<span>NaturalNumber</span> <span>capacity</span>,
<span>NaturalNumber</span> <span>minimalReservation</span>)
{
<span>return</span> <span>new</span> <span>TableEntity</span>
{
PublicId = id,
Capacity = (<span>int</span>)<span>capacity</span>,
MinimalReservation = (<span>int</span>)<span>minimalReservation</span>,
};
}
}
Mapping the other way is easier, so the SqlTablesRepository
does it inline in the Read
method:
<span>public</span> <span>async</span> <span>Task</span><<span>Table</span>?> <span>Read</span>(<span>Guid</span> <span>id</span>)
{
<span>var</span> <span>entity</span> = <span>await</span> context.Tables
.<span>SingleOrDefaultAsync</span>(<span>t</span> => <span>t</span>.PublicId <span>==</span> <span>id</span>).<span>ConfigureAwait</span>(<span>false</span>);
<span>if</span> (<span>entity</span> <span>is</span> <span>null</span>)
<span>return</span> <span>null</span>;
<span>if</span> (<span>entity</span>.MinimalReservation <span>is</span> <span>null</span>)
<span>return</span> <span>Table</span>.<span>TryCreateCommunal</span>(<span>entity</span>.Capacity);
<span>else</span>
<span>return</span> <span>Table</span>.<span>TryCreateSingle</span>(
<span>entity</span>.Capacity,
<span>entity</span>.MinimalReservation.Value);
}
Similar to the case of the DTO, mapping between Table
and TableEntity
is the responsibility of the SqlTablesRepository
class, since data persistence is an implementation detail. According to the DIP it shouldn’t be part of the Domain Model, and it isn’t.
XML formats # #
That covers the basics, but how well does this kind of architecture stand up to changing requirements?
One axis of variation is when a service needs to support multiple representations. In this example, I’ll imagine that the service also needs to support not just one, but two, XML formats.
Granted, you may not run into that particular requirement that often, but it’s typical of a kind of change that you’re likely to run into. In REST APIs, for example, you should use content negotiation for versioning, and that’s the same kind of problem.
To be fair, application code also changes for a variety of other reasons, including new features, changes to business logic, etc. I can’t possibly cover all, though, and many of these are much better described than changes in wire formats.
As described in the introduction article, ideally the XML should support a format implied by these examples:
<span><</span><span>communal-table</span><span>></span>
<span> <</span><span>capacity</span><span>></span>12<span></</span><span>capacity</span><span>></span>
<span></</span><span>communal-table</span><span>></span>
<span><</span><span>single-table</span><span>></span>
<span> <</span><span>capacity</span><span>></span>4<span></</span><span>capacity</span><span>></span>
<span> <</span><span>minimal-reservation</span><span>></span>3<span></</span><span>minimal-reservation</span><span>></span>
<span></</span><span>single-table</span><span>></span>
Notice that while these two examples have different root elements, they’re still considered to both represent a table. Although at the boundaries, static types are illusory we may still, loosely speaking, consider both of those XML documents as belonging to the same ’type'.
To be honest, if there’s a way to support this kind of schema by defining DTOs to be serialized and deserialized, I don’t know what it looks like. That’s not meant to imply that it’s impossible. There’s often an epistemological problem associated with proving things impossible, so I’ll just leave it there.
To be clear, it’s not that I don’t know how to support that kind of schema at all. I do, as the article Using only a Domain Model to persist restaurant table configurations will show. I just don’t know how to do it with DTO-based serialisation.
Element-biased XML # #
Instead of the above XML schema, I will, instead explore how hard it is to support a variant schema, implied by these two examples:
<span><</span><span>table</span><span>></span>
<span> <</span><span>type</span><span>></span>communal<span></</span><span>type</span><span>></span>
<span> <</span><span>capacity</span><span>></span>12<span></</span><span>capacity</span><span>></span>
<span></</span><span>table</span><span>></span>
<span><</span><span>table</span><span>></span>
<span> <</span><span>type</span><span>></span>single<span></</span><span>type</span><span>></span>
<span> <</span><span>capacity</span><span>></span>4<span></</span><span>capacity</span><span>></span>
<span> <</span><span>minimal-reservation</span><span>></span>3<span></</span><span>minimal-reservation</span><span>></span>
<span></</span><span>table</span><span>></span>
This variation shares the same <table>
root element and instead distinguishes between the two kinds of table with a <type>
discriminator.
This kind of schema we can define with a DTO:
[<span>XmlRoot</span>(<span>"table"</span>)]
<span>public</span> <span>class</span> <span>ElementBiasedTableXmlDto</span>
{
[<span>XmlElement</span>(<span>"type"</span>)]
<span>public</span> <span>string</span>? Type { <span>get</span>; <span>set</span>; }
[<span>XmlElement</span>(<span>"capacity"</span>)]
<span>public</span> <span>int</span> Capacity { <span>get</span>; <span>set</span>; }
[<span>XmlElement</span>(<span>"minimal-reservation"</span>)]
<span>public</span> <span>int</span>? MinimalReservation { <span>get</span>; <span>set</span>; }
<span>public</span> <span>bool</span> <span>ShouldSerializeMinimalReservation</span>() =>
MinimalReservation.HasValue;
<span>// Mapping methods not shown here...</span>
}
As you may have already noticed, however, this isn’t the same type as TableJsonDto
, so how are we going to implement the Controller methods that receive and send objects of this type?
Posting XML # #
The service should still accept JSON as shown above, but now, additionally, it should also support HTTP requests like this one:
POST /tables HTTP/1.1
content-type: application/xml
<span><</span><span>table</span><span>><</span><span>type</span><span>></span>communal<span></</span><span>type</span><span>><</span><span>capacity</span><span>></span>12<span></</span><span>capacity</span><span>></</span><span>table</span><span>></span>
How do you implement this new feature?
My first thought was to add a Post
overload to the Controller:
[<span>HttpPost</span>]
<span>public</span> <span>async</span> <span>Task</span><<span>IActionResult</span>> <span>Post</span>(<span>ElementBiasedTableXmlDto</span> <span>dto</span>)
{
<span>ArgumentNullException</span>.<span>ThrowIfNull</span>(<span>dto</span>);
<span>var</span> <span>id</span> = <span>Guid</span>.<span>NewGuid</span>();
<span>await</span> <span>repository</span>.<span>Create</span>(<span>id</span>, <span>dto</span>.<span>ToTable</span>()).<span>ConfigureAwait</span>(<span>false</span>);
<span>return</span> <span>new</span> <span>CreatedAtActionResult</span>(
<span>nameof</span>(<span>Get</span>),
<span>null</span>,
<span>new</span> { id = <span>id</span>.<span>ToString</span>(<span>"N"</span>) },
<span>null</span>);
}
I just copied and pasted the original Post
method and changed the type of the dto
parameter. I also had to add a ToTable
conversion to ElementBiasedTableXmlDto
:
<span>internal</span> <span>Table</span> <span>ToTable</span>()
{
<span>if</span> (Type == <span>"single"</span>)
{
<span>var</span> <span>t</span> = <span>Table</span>.<span>TryCreateSingle</span>(Capacity, MinimalReservation ?? 0);
<span>if</span> (<span>t</span> <span>is</span> { })
<span>return</span> <span>t</span>.Value;
}
<span>if</span> (Type == <span>"communal"</span>)
{
<span>var</span> <span>t</span> = <span>Table</span>.<span>TryCreateCommunal</span>(Capacity);
<span>if</span> (<span>t</span> <span>is</span> { })
<span>return</span> <span>t</span>.Value;
}
<span>throw</span> <span>new</span> <span>InvalidOperationException</span>(<span>"Invalid Table DTO."</span>);
}
While all of that compiles, it doesn’t work.
When you attempt to POST
a request against the service, the ASP.NET framework now throws an AmbiguousMatchException
indicating that “The request matched multiple endpoints”. Which is understandable.
This lead me to the first round of
Framework Whac-A-Mole. What I’d like to do is to select the appropriate action method based on content-type
or accept
headers. How does one do that?
After some web searching, I came across a Stack Overflow answer that seemed to indicate a way forward.
Selecting the right action method # #
One way to address the issue is to implement a custom ActionMethodSelectorAttribute:
<span>public</span> <span>sealed</span> <span>class</span> <span>SelectTableActionMethodAttribute</span> : <span>ActionMethodSelectorAttribute</span>
{
<span>public</span> <span>override</span> <span>bool</span> <span>IsValidForRequest</span>(<span>RouteContext</span> <span>routeContext</span>, <span>ActionDescriptor</span> <span>action</span>)
{
<span>if</span> (<span>action</span> <span>is</span> <span>not</span> <span>ControllerActionDescriptor</span> <span>cad</span>)
<span>return</span> <span>false</span>;
<span>if</span> (<span>cad</span>.Parameters.Count != 1)
<span>return</span> <span>false</span>;
<span>var</span> <span>dtoType</span> = <span>cad</span>.Parameters[0].ParameterType;
<span>// Demo code only. This doesn't take into account a possible charset</span>
<span>// parameter. See RFC 9110, section 8.3</span>
<span>// (https://www.rfc-editor.org/rfc/rfc9110#field.content-type) for more</span>
<span>// information.</span>
<span>if</span> (<span>routeContext</span>?.HttpContext.Request.ContentType == <span>"application/json"</span>)
<span>return</span> <span>dtoType</span> <span>==</span> <span>typeof</span>(<span>TableJsonDto</span>);
<span>if</span> (<span>routeContext</span>?.HttpContext.Request.ContentType == <span>"application/xml"</span>)
<span>return</span> <span>dtoType</span> <span>==</span> <span>typeof</span>(<span>ElementBiasedTableXmlDto</span>);
<span>return</span> <span>false</span>;
}
}
As the code comment suggests, this isn’t as robust as it should be. A content-type
header may also look like this:
Content-Type: application/json; charset=utf-8
The exact string equality check shown above would fail in such a scenario, suggesting that a more sophisticated implementation is warranted. I’ll skip that for now, since this demo code already compromises on the overall XML schema. For an example of more robust content negotiation implementations, see Using only a Domain Model to persist restaurant table configurations.
Adorn both Post
action methods with this custom attribute, and the service now handles both formats:
[<span>HttpPost</span>, <span>SelectTableActionMethod</span>]
<span>public</span> <span>async</span> <span>Task</span><<span>IActionResult</span>> <span>Post</span>(<span>TableJsonDto</span> <span>dto</span>)
<span>// ...</span>
[<span>HttpPost</span>, <span>SelectTableActionMethod</span>]
<span>public</span> <span>async</span> <span>Task</span><<span>IActionResult</span>> <span>Post</span>(<span>ElementBiasedTableXmlDto</span> <span>dto</span>)
<span>// ...</span>
While that handles POST
requests, it doesn’t implement content negotiation for GET
requests.
Getting XML # #
In order to GET
an XML representation, clients can supply an accept
header value:
GET /Tables/153f224c91fb4403988934118cc14024 HTTP/1.1
accept: application/xml
which will reply with
HTTP/1.1 200 OK
Content-Length: 59
Content-Type: application/xml; charset=utf-8
<span><</span><span>table</span><span>><</span><span>type</span><span>></span>communal<span></</span><span>type</span><span>><</span><span>capacity</span><span>></span>12<span></</span><span>capacity</span><span>></</span><span>table</span><span>></span>
How do we implement that?
Keep in mind that since this data-architecture variation uses two different DTOs to model JSON and XML, respectively, an action method can’t just return an object of a single type and hope that the ASP.NET framework takes care of the rest. Again, I’m aware of middleware that’ll deal nicely with this kind of problem, but not in this architecture; see Using only a Domain Model to persist restaurant table configurations for such a solution.
The best I could come up with, given the constraints I’d imposed on myself, then, was this:
[<span>HttpGet</span>(<span>"</span><span>{</span>id<span>}</span><span>"</span>)]
<span>public</span> <span>async</span> <span>Task</span><<span>IActionResult</span>> <span>Get</span>(<span>string</span> <span>id</span>)
{
<span>if</span> (!<span>Guid</span>.<span>TryParseExact</span>(<span>id</span>, <span>"N"</span>, <span>out</span> <span>var</span> <span>guid</span>))
<span>return</span> <span>new</span> <span>BadRequestResult</span>();
<span>var</span> <span>table</span> = <span>await</span> <span>repository</span>.<span>Read</span>(<span>guid</span>).<span>ConfigureAwait</span>(<span>false</span>);
<span>if</span> (<span>table</span> <span>is</span> <span>null</span>)
<span>return</span> <span>new</span> <span>NotFoundResult</span>();
<span>// Demo code only. This doesn't take into account quality values.</span>
<span>var</span> <span>accept</span> =
<span>httpContextAccessor</span>?.HttpContext?.Request.Headers.Accept.<span>ToString</span>();
<span>if</span> (<span>accept</span> == <span>"application/json"</span>)
<span>return</span> <span>new</span> <span>OkObjectResult</span>(<span>TableJsonDto</span>.<span>From</span>(<span>table</span>.Value));
<span>if</span> (<span>accept</span> == <span>"application/xml"</span>)
<span>return</span> <span>new</span> <span>OkObjectResult</span>(<span>ElementBiasedTableXmlDto</span>.<span>From</span>(<span>table</span>.Value));
<span>return</span> <span>new</span> <span>StatusCodeResult</span>((<span>int</span>)<span>HttpStatusCode</span>.NotAcceptable);
}
As the comment suggests, this is once again code that barely passes the few tests that I have, but really isn’t production-ready. An accept
header may also look like this:
accept: application/xml; q=1.0,application/json; q=0.5
Given such an accept
header, the service ought to return an XML representation with the application/xml
content type, but instead, this Get
method returns 406 Not Acceptable
.
As I’ve already outlined, I’m not going to fix this problem, as this is only an exploration. It seems that we can already conclude that this style of architecture is ill-suited to deal with this kind of problem. If that’s the conclusion, then why spend time fixing outstanding problems?
Attribute-biased XML # #
Even so, just to punish myself, apparently, I also tried to add support for an alternative XML format that use attributes to record primitive values. Again, I couldn’t make the schema described in the introductory article work, but I did manage to add support for XML documents like these:
<span><</span><span>table</span><span> </span><span>type</span><span>=</span>"<span>communal</span>"<span> </span><span>capacity</span><span>=</span>"<span>12</span>"<span> /></span>
<span><</span><span>table</span><span> </span><span>type</span><span>=</span>"<span>single</span>"<span> </span><span>capacity</span><span>=</span>"<span>4</span>"<span> </span><span>minimal-reservation</span><span>=</span>"<span>3</span>"<span> /></span>
The code is similar to what I’ve already shown, so I’ll only list the DTO:
[<span>XmlRoot</span>(<span>"table"</span>)]
<span>public</span> <span>class</span> <span>AttributeBiasedTableXmlDto</span>
{
[<span>XmlAttribute</span>(<span>"type"</span>)]
<span>public</span> <span>string</span>? Type { <span>get</span>; <span>set</span>; }
[<span>XmlAttribute</span>(<span>"capacity"</span>)]
<span>public</span> <span>int</span> Capacity { <span>get</span>; <span>set</span>; }
[<span>XmlAttribute</span>(<span>"minimal-reservation"</span>)]
<span>public</span> <span>int</span> MinimalReservation { <span>get</span>; <span>set</span>; }
<span>public</span> <span>bool</span> <span>ShouldSerializeMinimalReservation</span>() => 0 < MinimalReservation;
<span>// Mapping methods not shown here...</span>
}
This DTO looks a lot like the ElementBiasedTableXmlDto
class, only it adorns properties with XmlAttribute
rather than XmlElement
.
Evaluation # #
Even though I had to compromise on essential goals, I wasted an appalling amount of time and energy on yak shaving and Framework Whac-A-Mole. The DTO-based approach to modelling external resources really doesn’t work when you need to do substantial content negotiation.
Even so, a DTO-based Ports and Adapters architecture may be useful when that’s not a concern. If, instead of a REST API, you’re developing a web site, you’ll typically not need to vary representation independently of resource. In other words, a web page is likely to have at most one underlying model.
Compared to other large frameworks I’ve run into, ASP.NET is fairly unopinionated. Even so, the idiomatic way to use it is based on DTOs. DTOs to represent external data. DTOs to represent UI components. DTOs to represent database rows (although they’re often called entities in that context). You’ll find a ton of examples using this data architecture, so it’s incredibly well-described. If you run into problems, odds are that someone has blazed a trail before you.
Even outside of .NET, this kind of architecture is well-known. While I’ve learned a thing or two from experience, I’ve picked up a lot of my knowledge about software architecture from people like Martin Fowler and Robert C. Martin.
When you also apply the Dependency Inversion Principle, you’ll get good separations of concerns. This aspect of Ports and Adapters is most explicitly described in Clean Architecture. For example, a change to the UI generally doesn’t affect the database. You may find that example ridiculous, because why should it, but consult the article Using a Shared Data Model to persist restaurant table configurations to see how this may happen.
The major drawbacks of the DTO-based data architecture is that much mapping is required. With three different DTOs (e.g. JSON DTO, Domain Model, and ORM Entity), you need four-way translation as indicated in the above figure. People often complain about all that mapping, and no: ORMs don’t reduce the need for mapping.
Another problem is that this style of architecture is complicated. As I’ve argued elsewhere, Ports and Adapters often constitute an unstable equilibrium. While you can make it work, it requires a level of sophistication and maturity among team members that is not always present. And when it goes wrong, it may quickly deteriorate into a Big Ball of Mud.
Conclusion # #
A DTO-based Ports and Adapters architecture is well-described and has good separation of concerns. In this article, however, we’ve seen that it doesn’t deal successfully with realistic content negotiation. While that may seem like a shortcoming, it’s a drawback that you may be able to live with. Particularly if you don’t need to do content negotiation at all.
This way of organizing code around data is quite common. It’s often the default data architecture, and I sometimes get the impression that a development team has ‘chosen’ to use it without considering alternatives.
It’s not a bad architecture despite evidence to the contrary in this article. The scenario examined here may not be relevant. The main drawback of having all these objects playing different roles is all the mapping that’s required.
The next data architecture attempts to address that concern.
Next: Using a Shared Data Model to persist restaurant table configurations.