1

I apologize in advance for the length of this question; it requires some explanation. I'll try to make it as clear as possible.

I am developing and maintaining a mobile app (Xamarin, F#) that communicates with a web API (ASP.NET Core, F#). The server-side API exists only to communicate with the app and do database operations and other HTTP requests as needed. The domain model, so to speak, is more or less exactly the same in the app and the API. For example, both the app and the API have a concept of a CardId with the same notions of what exactly constitutes a valid card ID.

Since apps can't be instantly upgraded, I must version the API so that users of outdated app versions can still use the service. Currently the app and server communicates by serializing/deserializing simple DTO-like types (sent as JSON payloads over HTTP) defined in a common assembly referenced by both the app and API. For example, a card ID is represented as a simple string. The types in this shared assembly are versioned, and the app and API convert between these types and their corresponding types that they use internally.

  • On the app side, the app converts between internal representations (defined in the app assemblies) and the latest version of these types (since the app only ever needs to work with a single API version).

  • On the API side it's similar, but the API contains converters for all versions of the shared types, since it needs to support older versions too.

This all means that any concept/type shared between the app and server needs at least 3 types (for a single version) - one in the app, one in the API, and one in the shared assembly to be serialized/deserialized - as well as 2-4 converters (1-2 each in the app/API, depending on whether it needs to only receive/send the types or both). This is very robust as it decouples the internal representation (and the logic working on them) from the communication between app and server, but it's a heck of a lot of boilerplate. Also, it leads to me often cutting logic from the app - for example, a card ID in the app is just modeled as a simple string, and the API does validation on it and can return an error code indicating "invalid card ID".

Now, what I really want to do is to simply share objects between client and server. If they could somehow share memory (absurd, but explains my point), that would simplify things a lot. Since both the client and server use the same platform/language (.NET/F#), and this is unlikely to ever change (if so, one could always go back to the current solution), I have considered collapsing all three versions of a type (app, API, and shared) into a single type (still versioned, of course), and simply serialize/deserialize that. For example using some kind of binary serialization (I've no experience with that).

This ensures that the server and app are truly speaking the same language and that equal concepts are treated equally. For example, I can share complex objects with guarantees on some invariants by construction (such as the abovementioned CardID) and know that all invariant still holds once I deserialize it on the other end, because it's the exact same object. I could then also get rid of several server-side checks and errors like "Invalid card ID" because the app could never construct and send an invalid card ID in the first place.

It also means that all across my app and API, there would be references to a specific version of objects, but I don't think that would be any worse to maintain than the current solution.

However, I can't shake the feeling that I'd somehow be shooting myself in the foot, or that versioning would be more difficult and brittle, though I can't pinpoint why I feel that way (perhaps it just feels more robust with dedicated objects that I use for serialization/deserialization and convert to/from internally, but I'm still relying on serialization/deserialization of these DTO objects, so why not go all the way?). The question is therefore:

In light of the above explanation, is sharing objects between client and server using e.g. binary serialization a good option, or have I missed some clear problems with that approach? Have I perhaps missed another better option entirely?

  • are you trying to share the actual code of the object, ie serialise the methods and send them to the client, or just use the same dll on both client and server? – Ewan Dec 7 '17 at 10:45
  • Both app and server reference the same DLL with the shared types. – cmeeren Dec 7 '17 at 10:48
  • Currently the types in the shared assembly are the DTO types to be serialized/deserialzied to/from JSON. My suggestion is to have the types in the shared assembly be (most of) the actual domain model, and serialize/deserialize those directly. – cmeeren Dec 7 '17 at 10:50
  • If there is a public network between your app and your API, you can't guarantee that a de-serialized object is completely valid from a business perspective. You can't guarantee that I did not impersonate your app and sent a message to the API with a invalid CardID. – Bart van Ingen Schenau Dec 7 '17 at 11:10
  • You are of course right, and some kind of validation happens anyway, just by layers "behind" the server maintained by others. Anyway, in this specific case I'm not particularly concerned with someone figuring out the binary serialization and sending invalid data. – cmeeren Dec 7 '17 at 11:17
2

Binary serialization can be very brittle. I would only use binary serialization in very special cases where you need the economy that such serialization would provide (e.g. file transfers), you don't have to pass through firewalls, you have complete control over both ends of the communication, and the data format will seldom change.

Sending arbitrary JSON over the wire; using Newtonsoft.JSON to deserialize it to a dynamic, a dictionary of key/value pairs or an ExpandoObject; and then mapping those arbitrary fields to a concrete representation or using them directly; is a perfectly reasonable thing to do. Potentially, you can include some metadata at the top of the JSON that will hint at what type of data is being transmitted.

You could also experiment with something like Protocol Buffers.

If you want to stick with static typing, sharing DTO classes between the client and the server is kind of inevitable, unless you want to write a factory method that produces the correct object.

0

I'm a bit confused by your setup, let me lay my usual approach out:

  • Shared Anaemic Models
  • Client logic services - perform client side logic
  • Versioned Client - serialised/deserialises to JSON (or alternative) and sends to server
  • Versioned Server - deserialises to Model and calls service layer
  • Service Layer - performs business logic, returns Model

Technically of course everything is versioned. but for the matching up of client and server its really only the communication which counts.

When you have more than one version live, you have different end points for each version. (which the versioned client knows about and automatically deals with)

Now the key difference here seems to be that I have different logic on the client and server. I tend to use the ADM approach and code this logic as services, but you could just as well put it in objects.

I have no need to share any logic between the server and client because they do different things. If I do have some shared logic, say credit card validation, I would put it in a separate service/dll and share that. Injecting it into other services as required.

Now I could take the serialised data and put it directly into a shared object/service but I wouldn't really save any code.

Instead of de/serialising in the client and server, I would have that code in the object constructor. Mapping to private members of the object rather than the public members of the DTO.

This would tightly couple my de/serialisation logic to my business logic classes and all Im saving is some class definition code.

My feeling is that your versioning complexity comes from running server code that can cope with multiple versions. I would suggest you split that, creating one server deployment per version, so that you code needs only worry about the one version. You can decommission old versions as your client update slowly takes place.

Then instead of multiple converters, you just have multiple versions of the same converter and your codebase and testing is considerably simplified.

JSON vs Binary vs XML or something else doesn't really matter. .Net is memory managed, so (as far as i know!) there is no way to take a binary stream, write it directly to memory and then call that memory as if it were an already instantiated class.

You can't bypass the constructor and just say, that thing you just downloaded, that's a MyObject. So whatever you do you always have to take the incoming data and say, that bit is a string and I am assigning it to MyObject.Id or whatever

  • We use SVN without branching, so one server deployment per version sounds like a maintenance nightmare, seeing as I might have to go back and update previous versions if a dependent service changes. – cmeeren Dec 7 '17 at 12:08
  • Also, FsPIckle seems to be fully able to deserialize a single-case DU with a private constructor (haven't tested anything else). – cmeeren Dec 7 '17 at 12:08
  • not branching will make your life difficult yes. The point is you dont go back and update old versions though. you release new versions – Ewan Dec 7 '17 at 12:18
  • Sure, but if a 3rd party service that the server uses suddenly changes their protocol, then I need to update the old versions because otherwise they'd be broken, and the whole point is to have them continue functioning. Thanks anyway :) – cmeeren Dec 7 '17 at 12:31
  • yes, if that happened between the release of a new version and the full upgrade of your client base AND you weren't allowed to branch for religious reasons. yes you would have a problem. However..... – Ewan Dec 7 '17 at 17:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.