0

I have several POCO´s (simple classes holding data) that I have need to flush from memory from time to time whenever a collection grows too large. Most often I want to upload the data to a REST service, but if it fails I want to save the data to disk.

In the best of worlds, the code should be able to handle any data type. The problem I'm facing is who should be responsible for the conversion between object -> string | xml | json | ....

I could let all Pocos implement an interface (say ConvertToJson() ). But that would mean all classes that needs to be written has to have that interface and method which seems tedious, instead I would be happy with the default conversion instead of a class-specific.

writer.Write(poco.ConvertToJson());

Simplified code of what I have right now. It lacks support for different conversions (XML, Json).

public interface IWriter
{
    bool Write(string data);
    bool Write(Object data);
}

Writers (File and REST)

public static class FileWriter : IWriter
{
    public bool Write(string data)
    {
        File.WriteAllText("file.txt", data);
    }

    public bool Write(object data)
    {
        XmlSerializer xs = new XmlSerializer(data.GetType());
        using (TextWriter tw = new StreamWriter("file.txt")) { xs.Serialize(tw, data); }
    }
}

public static class RestApiCaller : IWriter
{
    public bool Write(string data)
    {
        MyHttpClient.CallRestService(data);
    }

    public bool Write(object data)
    {
        XmlSerializer xs = new XmlSerializer(data.GetType());
        MyHttpClient.CallRestService(xs.Serialize(data));
    }
}

Example of calling code

    static void Main(string[] args)
    {
         // Write DeathStar as XML to rest service. This was easy!
         List<DeathStar> deathStars = new List<DeathStar>(...);
         if(!RestApiCaller.Write(deathStars))
         {
             // I dont know how to write my deathStar as JSON
             FileWriter.Write(deathStars);
         }
    }

Who should do the conversion?

  • The Poco classes?
  • The Writer?
  • a dedicated converter?
    • if so, who calls the converter, the poco, Writer, or the calling code?

And should a writer only accept string as input? Both XML and Json are strings in the end, but I'm not sure if that is desirable. I would like the answer to be following SOLID.

2 Answers 2

2

The easiest solution is to decide on a single format for your object. Barring that, you are right in that you want to separate the responsibilities of converting to and from the serialized format.

You can have ASP.Net MVC choose the most appropriate format to send your data using a FormatFilter. Bottom line is that you do have to set up some infrastructure to support it. Other frameworks like Java's Spring Boot MVC will use the Content-Type header in the request to choose the response format.

The more your object knows about things other than the reason it was created, the less it is a plain old C# object (POCO). I highly recommend you separate concepts.

The most flexible approach, then is to have the following separation:

  • POCO
  • (De)Serializer
  • Stream

The first and last elements are already well understood concepts. The POCO only contains data, the Stream only reads and writes data. A Serializer converts a POCO to a stream of bytes. A Deserializer converts a stream of bytes to a POCO.

If you were to write an interface to wrap those concepts, it would look something like this:

public interface Deserializer
{
    Task<T> DeserializeAsync<T>(Stream input);
}

public interface Serializer
{
    Task SerializeAsync(object poco, Stream output);
}

The implementation would wrap the serialization/deserialization code that is specific to your JSON or XML conversion process. You can even write a custom one yourself.

At this point, if you want the convenience of extension methods, you could easily write something that handles that. I would have the Stream be the anchor point, though. For example:

public static class ConvertExtenstions
{
    public async Task<T> ReadAsJsonAsync<T>(this Stream input)
    {
        var converter = GetDeserializerFor(StreamType.JSON);
        return await converter.DeserializeAsync<T>(input);
    }

    public async Task WriteAsJsonAsync(this Stream output, object poco)
    {
        var converter = GetSerializerFor(StreamType.JSON);
        await converter.Serialize(poco, input);
    }
}

When deserializing an object from a stream, you will get the POCO you are specifying in T. When serializing an object, we can derive the type from the POCO itself, so it's unneeded there. You'll find that most (de)serializers have the same pattern. Deserialized is generically typed, and serialized is any object.

Note, if you need to work with strings directly, then you can add some more extensions that would convert your string to a MemoryStream, and Vice-Versa. Most of the time you only need to work with the conversion just before reading or writing to a final destination (i.e. response content stream or file system). Rarely is it useful to manipulate strings in code when dealing with JSON, XML, etc.

Which you could use in code like this:

using(var input = new FileStream(filename))
{
    var poco = await input.ReadAsJsonAsync<MyPoco>();
}

or

using(var output = new FileStream(filename))
{
    await output.WriteAsJsonAsync(poco);
}

That's only if you need to manually perform the serialization yourself. Make sure it's not already covered by the frameworks you are using.

3
  • Thank you for taking time to answer. I struggle a bit with the generic 'T' in ReadAsJsonAsync(...). The code doesnt compile and I dont understand how to make T work. I'm also a bit unsure why we go to Stream instead of working with either POCO and strings. Can you expain?
    – David
    Commented Mar 13, 2020 at 8:16
  • Aside from the parts I left for you to fill in (GetSerializerFor and company) I've fixed the code so it will compile. I just missed a couple things transcribing it to the answer. Commented Mar 13, 2020 at 13:43
  • The reason I work with Streams is for flexibility, and not reproducing what is provided by the platform already. For example, you can wrap a the Stream with a StreamReader and await reader.ReadToEndAsync() to get your string if needed. I also know from experience that some of these files can get quite large. Working with streams minimizes the amount of memory overhead needed at any given point. Commented Mar 13, 2020 at 13:51
1

Barring other requirements, in an object-oriented solution you don't flush the objects if they grow big, the objects flush themselves if they grow big.

If you want to do this to the objects from the outside, you would need to export several functionalities:

  • The object must offer some methods or data to determine whether it's big
  • The object must offer some functions to serialize to one or more formats
  • Or, it must publish all of its data for you (or a Format, or Converter or Writer) to be able to serialize.

This creates a technical coupling to the outside which is unnecessary. This is also the problem you are already feeling.

To recap: The object could know how to serialize itself without exposing this functionality, because it could control the process of persisting/flushing also. Of course this would probably require a different non-data-oriented design.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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