2

This question is resurrected, because I was wrong in comment "My bad". This issue is there, or I miss something. For popular demand here is a minimal reproducable example:

minimal reproducable example


I have an interface with a default property:

public interface INotification
{
    // ...
    Severity Severity { get; set; } // Severity is an Enum
    // ...
    public string SeverityName => Severity.ToString().ToLower();
}

I have an implementation class

public class Notification : INotification
{
    // ...
    public Severity Severity { get; set; }
    // ...
    // This class does not override default implementation of SeverityName
} 

Question

The Notification class does not have a SeverityName property... this was surprising, but I can live with that, accessing to the notification instance via the INotification interface.

However, I would like to serialize this class with System.Text.Json. How can I serialize SeverityName property too?

g.pickardou
  • 32,346
  • 36
  • 123
  • 268
  • Did you try to serialize the instance of the class? Was there any problem? Which serializer you refer to? xml? json? ...? – MakePeaceGreatAgain May 28 '20 at 07:20
  • 1
    @HimBromBeere OP mentioned `System.Text.Json` – Pavel Anikhouski May 28 '20 at 07:21
  • Have you tried it? What didn't work? This question is very vague – Liam May 28 '20 at 07:22
  • I am referring System.Text.Json, the Core serializer so not Newtonsoft. Yes, the serializer does not see the default property ** because the Notification instance seems to have no such a property** – g.pickardou May 28 '20 at 07:22
  • So where is your code where you do this? – Liam May 28 '20 at 07:23
  • @liam, the code is inside deep in a infra which uses SignalR to communicate with the browser, but I see the json text, and the default property is not there... – g.pickardou May 28 '20 at 07:26
  • Can you provide a [mcve], please? – ProgrammingLlama May 28 '20 at 07:28
  • 1
    @liam it seems you are correct to insist to see the code. It turns out, that it may be the SignalR (internal?) serialization, and not the System.Text.Json. I must see the `_hub.Clients.Clients(connectionIds).SendAsync("JsRuntimeCall", identifier, args)` MS code how the args are serialized. It may be some custome binary? serialization. Anyway, as I wrote I am surprized that the Notification instance not having the property, probably this is the reason why (any?) serializer disregards it when serializing. – g.pickardou May 28 '20 at 07:32
  • 1
    My bad. System.Text.Json serializes the property... I am stuck with the SignalR issue. I am going to delete this post, sorry for using anyone's time. – g.pickardou May 28 '20 at 07:39
  • No worries @g.pickardou, shows the power of a [mcve] – Liam May 28 '20 at 07:40

1 Answers1

2

This is not possible with System.Text.Json in .NET Core 3.1 other than by writing a custom JsonConverter for Notification. The serializer serializes the public properties of a type, but default interface properties are not implemented as class properties, instead they're implemented through some sort of extension mechanism. See the spec: default interface methods

Add support for virtual extension methods - methods in interfaces with concrete implementations. .

Only when the default interface property is overridden does an instance property actually get added to the implementing concrete type, and thereby become serializable.

For confirmation, if we modify the classes from your fiddle as follows:

public interface INotification
{
    Severity Severity { get; set; }
    string Message { get; set; }

    static string MakeDefaultSeverityName<TNotification>(TNotification notification) where TNotification : INotification => notification?.Severity.ToString().ToLower();

    public string SeverityName => MakeDefaultSeverityName(this);
}

public class Notification : INotification
{
    public Severity Severity { get; set; }
    public string Message { get; set; }
}

public class NotificationWithOverride : Notification
{
    public string SeverityName => INotification.MakeDefaultSeverityName(this);
}

And print out the properties and methods of both types using reflection:

Console.WriteLine("Properties of {0}: {1}", typeof(TNotification).Name, string.Join(", ", typeof(TNotification).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Select(p => p.Name)));
Console.WriteLine("Methods of {0}: {1}", typeof(TNotification).Name, string.Join(", ", typeof(TNotification).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Select(p => p.Name)));

We get the following results for Notification:

Properties of Notification: Severity, Message
Methods of Notification: get_Severity, set_Severity, get_Message, set_Message, GetType, MemberwiseClone, Finalize, ToString, Equals, GetHashCode

And for NotificationWithOverride:

Properties of NotificationWithOverride: SeverityName, Severity, Message
Methods of NotificationWithOverride: get_SeverityName, get_Severity, set_Severity, get_Message, set_Message, GetType, MemberwiseClone, Finalize, ToString, Equals, GetHashCode

Notice there's just no instance property or method corresponding to SeverityName in Notification -- but there is in NotificationWithOverride. Lacking a property to serialize, the serializer does not output a SeverityName value.

Notes:

  1. System.Text.Json is consistent with other serializers in this respect; both it and Json.NET generate exactly the same JSON:

    • {"Severity":0,"Message":"Message"} for Notification.
    • {"SeverityName":"info","Severity":0,"Message":"Message"} for NotificationWithOverride.
  2. With Json.NET it might be possible to create a custom contract resolver that automatically adds in the missing default properties, but System.Text.Json does not have public access to its contract resolver; for confirmation see the question System.Text.Json API is there something like IContractResolver to which the answer is, not currently.

  3. In c# 8.0 it is not possible for to override a default interface method in a concrete class and call the base interface method. See How can I call the default method instead of the concrete implementation, Making member virtual prevents calling default interface implementation and causes StackOverflowException in C# 8 and Default interface implementations and base() calls for details.

    To work around this I added a static interface method to INotification that encapsulates the default logic, then called it from both the default interface instance method and the override class method.

A demo fiddle that shows the above can be found here.

dbc
  • 104,963
  • 20
  • 228
  • 340