1
\$\begingroup\$

Currently I have 3 different classes for the same type of entity. One is the main class that contains logic and everything the entity needs. Then I have a DB object, that has the same structure as the entity inside the database. The third is a data transfer object (DTO) that only contains the values the client is supposed to know about.
Now whenever I add a variable, I have to add the variable in all 3 classes and copy the DTO class over to the client too. Also, I have to add the variable to each converter method, that creates the DTO and DB objects from the main object and need to add a check to the DTO converter, so that I can differentiate which variables actually need to be updated to lessen the network usage. It's a huge mess and the bigger those classes become, the more I hate my current approach.
What's a smarter approach to this? Maybe there is a way to use C# attributes, but before I try to learn about how to make my own and implement it, I'd love to know if it's even a good choice.

edit 1:

following method checks which values have been updated and not sent to the client yet and returns a new DTO object to send, if the client is not up-to-date. The DTO object is serialized to json and sent as a string. Then its deserialized on the client and the client checks for valid values to know which one to update:

public CharacterDTO GetNetworkDTOValues()
{
    bool shouldUpdate = false;
    CharacterDTO returnDTO = new CharacterDTO();

    if (_clientInstance == null)
    {
       _clientInstance = new CharacterDTO();
    }
    if (_clientInstance.Level != Level)
    {
       returnDTO.Level = Level;
       shouldUpdate = true;
    }
    if (_clientInstance.Experience != Experience)
    {
       returnDTO.Experience = Experience;
       shouldUpdate = true;
    }
    if (_clientInstance.Life != Life)
    {
       returnDTO.Life = Life;
       shouldUpdate = true;
    }

    [...] (there are around 30 more variables)

    if (shouldUpdate)
    {
       _clientInstance = CreateDTO(this);
       return returnDTO;
    }

    return null;
}
\$\endgroup\$
4
  • \$\begingroup\$ Sometimes it helps to plan beforehand what you actually want to make, what it should be able to do or handle. While that does not remove totally the need to add a missing attribute, you should not be anymore in the situation where you have to upgrade once again your current implementation. \$\endgroup\$
    – Zibelas
    Commented Mar 26, 2021 at 13:31
  • \$\begingroup\$ @Zibelas Yes, that's how I always try to go about things. The game I'm developing has endless growth possibilities though, so I try to find a way that works with it. \$\endgroup\$
    – besplash
    Commented Mar 26, 2021 at 13:46
  • \$\begingroup\$ Not sure if/why you need a dedicated DTO. To send things over the network, they must be serialized anyways. Can't you diff serialized data? \$\endgroup\$ Commented Mar 26, 2021 at 19:49
  • \$\begingroup\$ @ShadowsInRain I don't want the client to know about all the values that my classes contain. They don't need to know about database ids and they aren't allowed to access some of the data that they need to explore. I don't understand what you mean by "Can't you diff serialized data?". I serialize them by converting them to a json object and sending them as a string. If I were to remove the sensitive data from that object, I would still need a client class which is essentially just a mirror of my current DTO anyway \$\endgroup\$
    – besplash
    Commented Mar 27, 2021 at 14:57

2 Answers 2

1
\$\begingroup\$

Using DTO solely to convert to and from JSON means given DTO defines a JSON shema. Good news is that JSON can work without (explicitly defined) schema just fine and JSON does not care what class to serialize from / deserialize to — which allows to map JSON to a dictionary (and back).

It means you can get rid of a DTO entirely. Moreover, if de/serialization is as straightforward as simply copying selected fields, you can entrust serialization to JSON framework: just annotate fields that you want to be skipped. For more complex conversions you can define custom converters.

enter image description here

Mapping database entities directly into game objects certainly has a code smell to it, because it pokes a leak in a neat onion architecture. Just remember that you are taking technical debt here.

As for diffing, try diffing serialized JSON. Would it prove being too slow, try double-buffering next: maintain a second copy of a DB object, do a CLR diff and propagate it to a JSON dump.

\$\endgroup\$
2
  • \$\begingroup\$ With this, I would remove the server-side DTO and could have a dynamic way of comparing 'DTOs', so if I added another stat/variable, I would only need to add it once to the server-side object, once to the DB object and once to the client object and wouldn't need to manually add a comparison, right? Diffing and custom converters may be exactly what I need. \$\endgroup\$
    – besplash
    Commented Mar 27, 2021 at 20:18
  • \$\begingroup\$ @besplash Don't even need to add any fields to JSON objects, it can be fully automatic or would require 2 mapping functions in the worst case. \$\endgroup\$ Commented Mar 27, 2021 at 20:29
0
\$\begingroup\$

It looks to me like you could accomplish a lot of this by re-using a common struct between the client instance, the on-the-wire data, and the server instance.

public class Character {

    // Data that needs replication over the network.
    public struct DTO {
        public int level;
        public int experience;
        public int life;
        // ...etc.
    }

    DTO _replicated;

    // Client-only data;
    int _localIndex;
    // ...etc.
}

When you need to add a new variable to Character, if it's one that needs to be replicated, you add it (once) to the Chatacter.DTO struct, and access it via _replicated.newVariableName.

When you need to send something over the wire, you can grab the whole _replicated struct as a unit, rather than fishing out one variable at a time.

Then on your server, you can do something like this...

public class ServerCharacter {
    // Data to replicate.
    Character.DTO _clientSnapshot;
    Character.DTO _liveData;

    // Private server data.
    uint _databaseID;
    // ...etc.    

    public bool ShouldSendDTO(ref Character.DTO dataToSend) {
        if (_clientSnapshot == _liveData)
            return false;

        _clientSnapshot = _liveData;
        dataToSend = _liveData;
        return true;
    }
}

As long as your struct is set up correctly, no references or padding, the default comparison operator will do a fast memory compare of the two instances' bits, without you having to check and assign each variable one by one.

You could likely even implement this generically with an unmanaged type constraint to both enforce the no-references rule and save you some repeated code.

\$\endgroup\$
2
  • \$\begingroup\$ How would this work for nested complex classes? If Character has an array of Items, that also have DTOs and DB objects, how would you then compare it? In my case I still need to convert each item to a DTO because CharacterDTO is only allowed to contain DTOs, not the full objects, because the full objects aren't allowed to be sent to the client. I don't see how using a struct like this is any different to a class, I feel like I'm already doing the same thing, except using 'class' instead of 'struct' \$\endgroup\$
    – besplash
    Commented Mar 27, 2021 at 20:13
  • \$\begingroup\$ The advantage here is when you want to add a new value type variable, like experience, you can add it in just one place: the over-the-wire struct, and you don't need to duplicate that definition anywhere else. It's already included in the change check and assignment shown in the ShouldSentDTO method, with no extra if clause to write and maintain, and you can bulk copy the struct more efficiently than assigning each field. This approach does not handle nested references on its own - to get answers about that case, make sure it's demonstrated clearly in the example code in your question. \$\endgroup\$
    – DMGregory
    Commented Mar 27, 2021 at 21:11

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .