Should I share types between a Web API service and its client ? What are the other options?
We are developing a Web API RESTful service to provide access to common data to all the applications of our enterprise. To help we will also publish a Client API that encapsulate all the HttpClient details and provides a strongly-typed access to the data.
Our goal is to start small and progressively add features, while still keeping backwards compatibility with already deployed versions of the client API (compatibility with the clients of the same Major version)
While talking about the design, our team just had a very long discussion about whether or not we should share types between the server and the client (for instance via versionned NuGet packages that both server and client would depend on), and ended up with pros and cons ... and we do not manage to decide one way or another.
Sharing types (shared assembly) between client and server
- Client model and Server model are always up to date
- no serializing/deserializing problems because same types are serialized/deserialized
- no duplication
- need to find a way to share the types between server and client
- non semantic modifications can break existing client applications (changing the name of a class or its namespace in the server model) even though it has no impact on the serialized json and should therefore have no impact
- risk of breaking the clint without realizing
Separate (but structurally equivalent) types for client and server
- client "model" is less coupled to the server implementation (just a mirror of the Json output by the server, but no hard "same type" relationship)
- server model can evolve without risk of breaking any client
- enable to enhance the client-side model independently from the server model
- the client model is part of the client package, no "shared package" to maintain between server and client
- duplication between server code and client code
- error-prone task of keeping server-side and client-side structure in sync
There seems to be a 50/50 preference for each solution in our team.
I personally have a preference for the second option, because I believe RESt is all about decoupling, and decoupling means the client should not care about how the server side is implemented (which types, or whether it is a .NET app anyway) but wish we could get rid of the possible duplication, maybe thanks to code generation or something like that, but could not find any guidance on the subject
Are there other pros and cons to sharing types between client and server ?
If we do not share them, are there ways to lower the maintenance costs when trying to keep client model and server model in sync ?
I would argue that if you are not careful, the second option could end up being less RESTful than the first. REST is less about de-coupling and more about managing and focusing the coupling between client and server.
In a restful system you know the coupling between client and server lies in the media type definitions and the link relation definitions.
In both options, you are effectively sharing types between the client and the server. In the first option this sharing is explicit via a concrete implementation that could be managed as a nuget package and versioned independently of client and server.
In the second option you have two implementations of the shared types. However, I'm guessing you are not planning on defining a media type that explicitly defines what the properties of those types are. Therefore you have no single source of truth, you have nothing to define what the data contract between client and server is. How do you know when you are going to make a change that will break a client? At least with a shared library you can know that the server is now using version 1.4.7 of the shared types and the client is using 1.3.9. You can use semantic versioning on the shared type library to know when you are making a breaking change that will force the client to update.
With the second option, you have a client and a secer that will be independently versioned and it will be much harder to keep track of whether there are breaking changes between the two versions.
Explict Media types are always the best way to capture the contracts and version the contracts between HTTP clients and servers. However, if you don't want to go there, then the shared nuget library is the best next step because you are isolating the part of the system that is shared from the client and server implementations. This is one of the key objectives of REST. The fact that you are actually sharing an implementation library of that shared contract only affects consumers live on other platforms that can't consume that library.
We had a similar discussion -- with similar pros and cons -- and we took a hybrid approach. We shared an assembly between the client and the server, but only shared interfaces. Then we created classes based on the interfaces on the client side. The advantage was that the actual objects on the client and the server could change independently.
Domain model classes are primarily defined to be used by the server. On the server side, model types are used by the methods defined inside controllers to access data, for example by using entity framework.
But, for some reasons you may prefer to pass another version of the model object to the client. A known approach is to define DTO classes which are very similar but not exactly the same as model types.
In each method in the controller, when you fetch data from database, you need to map the retrieved data from its model type format to its comparable DTO class. AutoMapper makes this mapping easier.
Therefore you need these steps to be done:
- Define your model types inside the server project.
- Add these packages to your server project as dependencies:
- AutoMapper.Extensions.Microsoft.DependencyInjection (version 1.2.0)
- Define a MappingProfile (or MappingConfiguration) inside the server project and use services.AddAutoMapper() in ConfigureServices method in Startup.cs
- Modify your controller methods to do proper mapping on the retrieved data and return equivalent DTO as the output of the method.
- In parallel, you can make a new project containing your DTO classes. This project is shared between server and client projects.
Then, on the client side you do not need to know any details of model types. Your client only works with DTO classes. These classes contain all the necessary data on the client side. In some cases you may need to combine data of multiple model objects to provide a single container for client side.