Check alle échte Black Friday-deals Ook zo moe van nepaanbiedingen? Wij laten alleen échte deals zien

[C#] Dynamics, JSON en Async vreemd gedrag

Pagina: 1
Acties:

  • NickThissen
  • Registratie: November 2007
  • Laatst online: 18-11 13:07
Dag,

Ik ben bezig met een applicatie die uit server / client combo bestaat. De server en client communiceren via websockets door het sturen van korte JSON berichtjes, welke ik serialize aan de ene kant en daarna deserialize aan de andere kant.

Deze berichten hebben een dynamic property 'Data' waar allerlei data in kan zitten afhankelijk van het doel van het bericht.


Ik merk al enige tijd vreemd gedrag in mijn applicatie wat ik niet kon verklaren en denk dat ik het nu heb terug gebracht tot een simpele test case. Het lijkt erop dat mijn test case zonder fouten verloopt in een niet-async uitvoering, maar "crasht" in een async uitvoering (dat wil zeggen: de debugger kapt er gewoon mee zonder fouten of niks, zelfs met try/catch er omheen). Soms krijg ik wel een foutmelding (zie later) in test cases, maar vaak ook niet, dit ligt misschien aan settings van welke exceptions gevangen worden ofzo...


Dit is mijn test case. Een class Message met dynamic property Data:
C#:
1
2
3
4
public class Message
{
    public dynamic Data { get; set; }
}



Een test object 'TestData' met twee properties die een data object moet voorstellen wat in de Data zou kunnen zitten:
C#:
1
2
3
4
5
public class TestData
{
    public string TestString { get; set; }
    public int TestInt { get; set; }
}



Ik doe nu het volgende:
1. Maak een nieuwe Message.
2. Maak een nieuwe anonymous type met property 'Test' met een instance van TestData en ken die toe aan de Data van de message.
3. Serialize naar json met JSON.NET (Newtonsoft.Json versie 6.0.4).
4. Deserialize terug naar Message object.

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Create message
var sentMessage = new Message();

// Set data
sentMessage.Data = new
{
    Test = new TestData() { TestString = "abcd", TestInt = 42 }
};

// Serialize to json (on client)
var json = JsonConvert.SerializeObject(sentMessage);

// Deserialize back to message (on server)
var receivedMessage = JsonConvert.DeserializeObject<Message>(json);



Nu wil ik in het receivedMessage object wat ik 'terug krijg' (in werkelijkheid is dit dus naar een server verstuurt) het Data property ophalen en daar de TestData object uithalen. Dit TestData object gebruik ik verder in mijn applicatie, wat hier als een simpele method is gedaan waar (thread-safe) een label text gebruikt wordt:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        private void DisplayTestData(TestData testData)
        {
            var testString = testData.TestString;
            this.SetText(testString);
        }

        private void SetText(string text)
        {
            // Set text (thread safe)
            if (label1.InvokeRequired)
            {
                label1.Invoke(new Action<string>(SetText), text);
            }
            else
            {
                label1.Text = text;
            }
        }



Ik zie twee opties om de Data.Test te verkrijgen en naar DisplayTestData op te sturen:

1. Gebruik dynamic en roep gewoon receivedMessage.Data.Test aan:
C#:
1
2
3
4
// Deserialize back to message (on server)
var receivedMessage = JsonConvert.DeserializeObject<Message>(json);

DisplayTestData(receivedMessage.Data.Test);

Geen geklaag tijdens compile, maar tijdens runtime gaat dit mis met een Microsoft.CSharp.RuntimeBinder.RuntimeBinderException. Ik denk dat het probleem is dat 'receivedMessage.Data.Test' een JObject terug geeft en geen dynamic, en DisplayTestData accepteert geen JObject.

2. Ok, prima, dan anders: deserialize de test data gewoon vrolijk nog een keer en deserialize hem deze keer naar een TestData object:
C#:
1
2
3
4
5
6
7
// Deserialize back to message (on server)
var receivedMessage = JsonConvert.DeserializeObject<Message>(json);

// Deserialize test data
var testData = JsonConvert.DeserializeObject<TestData>(receivedMessage.Data.Test.ToString());

DisplayTestData(testData);



Dit werkt prima! Geen geklaag, ook niet in runtime, en alles werkt naar behoren.


Probleem... Zodra ik met async methods ga werken loopt dit helemaal in de soep.

Als ik NIKS verander aan de code tot nu toe, behalve dat ik DisplayTestData async aanroep, dan crasht de boel:

C#:
1
2
3
4
// Deserialize test data
var testData = JsonConvert.DeserializeObject<TestData>(receivedMessage.Data.Test.ToString());

await Task.Run(() => DisplayTestData(testData));


Error: Cannot implicitly convert type 'void' to 'object'

Stack trace:
at CallSite.Target(Closure , CallSite , Form1 , Object )
at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
at WindowsFormsApplication1.Form1.<>c__DisplayClass7.<RunAsyncTest>b__6() in f:\Users\Nick\Dropbox\VS Projects\DynamicJsonTest\WindowsFormsApplication1\Form1.cs:line 51
at System.Threading.Tasks.Task`1.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at WindowsFormsApplication1.Form1.<RunAsyncTest>d__9.MoveNext() in f:\Users\Nick\Dropbox\VS Projects\DynamicJsonTest\WindowsFormsApplication1\Form1.cs:line 51
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.<ThrowAsync>b__0(Object state)
Dit gaat dus mis. Voor mijn gevoel exact dezelfde code, behalve dat ik de method async aanroep met Task.Run.


De oplossing is blijkbaar om het resultaat van DeserializeObject nog eens te casten naar TestData:
C#:
1
2
3
4
// Deserialize test data
var testData = (TestData) JsonConvert.DeserializeObject<TestData>(receivedMessage.Data.Test.ToString());

await Task.Run(() => DisplayTestData(testData));


Met deze extra cast gaat het wel goed. Deze cast is dus niet nodig als ik DisplayTestData niet async aanroep..??


Waar ligt dit aan? En doe ik dit helemaal verkeerd of is dit de juiste manier om met dynamic data en json om te gaan?

Mijn iRacing profiel


  • Jan_V
  • Registratie: Maart 2002
  • Laatst online: 21-11 21:28
Nu volg ik niet het hele proces wat je aan het doen bent, maar in de laatste fout lijkt het alsof je methode DisplayTestData nog steeds void terug geeft. Moet dit niet Task zijn wanneer je Task.Run gebruikt?

Over het gebruik van dynamics om JSON te parsen, daar ben ik het nog niet over uit. In ASP.NET MVC gaat dit redelijk automagisch. Verder lijkt het gebruik van een dynamic mij wel handig.

Battle.net - Jandev#2601 / XBOX: VriesDeJ


  • HMS
  • Registratie: Januari 2004
  • Laatst online: 17-11 00:33

HMS

Jan_V schreef op woensdag 13 augustus 2014 @ 10:44:
Nu volg ik niet het hele proces wat je aan het doen bent, maar in de laatste fout lijkt het alsof je methode DisplayTestData nog steeds void terug geeft. Moet dit niet Task zijn wanneer je Task.Run gebruikt?

Over het gebruik van dynamics om JSON te parsen, daar ben ik het nog niet over uit. In ASP.NET MVC gaat dit redelijk automagisch. Verder lijkt het gebruik van een dynamic mij wel handig.
Dat klopt inderdaad, je doet een await op een Task niet op een void (events zijn een belangrijke uitzondering natuurlijk). Dan slaat de foutmelding ook ergens op: Kan niet van void (return value) naar een object (Task).

[ Voor 13% gewijzigd door HMS op 13-08-2014 10:48 ]


  • Down
  • Registratie: Februari 2005
  • Laatst online: 23:59
JObject heeft gewoon een ToObject<T> method, dat lijkt me iets minder bewerkelijk dan wat je nu doet:

C#:
1
var testData = ((JObject)receivedMessage.Data.Test).ToObject<TestData>();

[ Voor 3% gewijzigd door Down op 13-08-2014 18:49 ]

Mother north, how can they sleep while their beds are burning?


  • NickThissen
  • Registratie: November 2007
  • Laatst online: 18-11 13:07
Het zou goed kunnen dat jullie gelijk hebben hoor, ik gebruik nog niet zo lang async en struikel er nog regelmatig over. Maar waarom zeurt de compiler dan niet gewoon en waarom crasht de boel in plaats van netjes een exception op deze regel?

Anyway, ik moet dus Task teruggeven en niet void gebruiken, ik zal eens kijken of het dan wel goed gaat.
Down schreef op woensdag 13 augustus 2014 @ 18:47:
JObject heeft gewoon een ToObject<T> method, dat lijkt me iets minder bewerkelijk dan wat je nu doet:

C#:
1
var testData = ((JObject)receivedMessage.Data.Test).ToObject<TestData>();
Ah, die kende ik niet, dat lijkt inderdaad beter... Bedankt.

Mijn iRacing profiel


  • xzaz
  • Registratie: Augustus 2005
  • Laatst online: 20-11 17:07
Even iets anders, ik denk niet dat het handig is om een dynamic te gebruiken in een protocol. Juist een protocol moet zorgen voor standaardisatie in gegevensoverdracht. Hoe ga je dit gebruiken wanneer je gaat communiceren met andere systemen die niet C# zijn?

Schiet tussen de palen en je scoort!


  • NickThissen
  • Registratie: November 2007
  • Laatst online: 18-11 13:07
xzaz schreef op donderdag 14 augustus 2014 @ 11:29:
Even iets anders, ik denk niet dat het handig is om een dynamic te gebruiken in een protocol. Juist een protocol moet zorgen voor standaardisatie in gegevensoverdracht. Hoe ga je dit gebruiken wanneer je gaat communiceren met andere systemen die niet C# zijn?
Het is gewoon JSON. De dynamics zijn toch geen onderdeel van het protocol, enkel een handige manier om de JSON gemakkelijk uit te kunnen lezen zonder voor elk bericht (lees: elk soort 'data') een aparte class te hoeven schrijven.

Elke andere taal kan de berichten makkelijk met json lezen en ook eigen berichten met json schrijven, ik zie geen probleem, of wat bedoel je precies?

Mijn iRacing profiel


  • xzaz
  • Registratie: Augustus 2005
  • Laatst online: 20-11 17:07
NickThissen schreef op donderdag 14 augustus 2014 @ 15:31:
[...]
Elke andere taal kan de berichten makkelijk met json lezen en ook eigen berichten met json schrijven, ik zie geen probleem, of wat bedoel je precies?
Hoe moet die taal weten wat er in Data zit? Een int? Moet hij eerst de JSON lezen en dan interpreteren wat het is?

Zie:

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ProtocolWithDynamic
{
    class Program
    {
        static void Main(string[] args)
        {
            var message1 = new Message(MAction.GETMESSAGES);
            message1.Data = new
            {
                Id = 1
            };

            var message2 = new Message(MAction.REMOVEMESSAGE);
            message2.Data = "woo";

            var message3 = new Message(MAction.GETMESSAGES);
            message3.Data = new Data() { Message = "foo" };

            var json1 = JsonConvert.SerializeObject(message1);
            var json2 = JsonConvert.SerializeObject(message2);
            var json3 = JsonConvert.SerializeObject(message3);

            Console.WriteLine(json1);
            Console.WriteLine(json2);
            Console.WriteLine(json3);

            Console.Read();
        }

        class Data
        {
            public String Message { get; set; }
        }

        enum MAction { GETMESSAGES, REMOVEMESSAGE }

        class Message
        {
            public MAction Action { get; set; }
            public dynamic Data { get; set; }

            public Message(MAction action)
            {
                this.Action = action;
            }
        }
    }
}

Schiet tussen de palen en je scoort!


  • NickThissen
  • Registratie: November 2007
  • Laatst online: 18-11 13:07
xzaz schreef op donderdag 14 augustus 2014 @ 20:37:
[...]

Hoe moet die taal weten wat er in Data zit? Een int? Moet hij eerst de JSON lezen en dan interpreteren wat het is?
Die taal moet weten wat er in Data zit omdat het een applicatie is die moet praten met mijn server... Je kan niet verwachten dat een random applicatie met mijn applicatie kan praten zonder te weten wat voor commando's er komen?

Als ik een andere applicatie zou schrijven in een andere taal dan weet ik toch niet steeds wat voor Data er in wat voor geval binnen komt? En als iemand anders dat programma zou schrijven dan zou hij dat toch ook moeten weten, of hij nou dynamic of niet gebruikt? Ik snap je punt niet.

Ik snap het idee van je code ook niet. Je voegt alleen een 'type' toe (Action) aan elk bericht wat aangeeft wat voor bericht het is. Ik heb iets wat exact hetzelfde is (maar dat liet ik hier niet zien), ik maak onderscheid tussen een Command (van client naar server), een Response (van server naar client) en binnen die twee types maak ik onderscheid tussen allerlei verschillende type berichten. Maar de Data die in elk bericht zit is voor elk type bericht anders en ik heb geen zin om voor elk van die types een aparte class te maken.

Mijn iRacing profiel


  • BM
  • Registratie: September 2001
  • Laatst online: 09:03

BM

Moderator Spielerij
Wellicht mis ik wat achtergrond, maar waarom geen base class waar elk bericht van overerft? Ben je de dynamics kwijt, en heb je gelijk een 'vaste' lijst met messages die mogelijk over de lijn gaan. Niet dat ik veel ervaring op dit gebied heb, maar dynamic gebruiken voor dit doel lijkt me raar.

Xbox
Even the dark has a silver lining | I'm all you can imagine times infinity, times three


  • xzaz
  • Registratie: Augustus 2005
  • Laatst online: 20-11 17:07
NickThissen schreef op donderdag 14 augustus 2014 @ 20:55:
[...]
Ik snap het idee van je code ook niet. Je voegt alleen een 'type' toe (Action) aan elk bericht wat aangeeft wat voor bericht het is. Ik heb iets wat exact hetzelfde is (maar dat liet ik hier niet zien), ik maak onderscheid tussen een Command (van client naar server), een Response (van server naar client) en binnen die twee types maak ik onderscheid tussen allerlei verschillende type berichten. Maar de Data die in elk bericht zit is voor elk type bericht anders en ik heb geen zin om voor elk van die types een aparte class te maken.
Laat eens de opbouw van jouw protocol zien, dat is heel wat gemakkelijker.

Ik probeer met mijn code te demonstreren dat dynamic tegen het protocol gedachte ingaat. Het is hoi zeggen tegen de server en niet weten wat je binnen krijgt. Omdat je als client / server niet wordt gedwongen om de juiste code in te voegen. Punt 2 is dat de opbouw niet duidelijk is. Wat zit er in data? Is dat een lijst? Afhankelijk van de 'header' die je meestuurt?

Schiet tussen de palen en je scoort!


  • NickThissen
  • Registratie: November 2007
  • Laatst online: 18-11 13:07
De code die ik liet zien is maar een voorbeeld natuurlijk, in mijn daadwerkelijke code is er een Type property die gebruikt wordt om te bepalen wat voor bericht het is en wat voor actie er ondernomen moet worden (en wat er in Data verwacht wordt).

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
    public abstract class SyncMessage
    {
        public dynamic Data { get; set; }

        public static T FromString<T>(string json)
            where T : SyncMessage
        {
            try
            {
                var message = (T) JsonConvert.DeserializeObject<T>(json);
                return message;
            }
            catch (Exception ex)
            {
                // Unknown message
                return null;
            }
        }

        public override string ToString()
        {
            return JsonConvert.SerializeObject(this);
        }
    }

    /// <summary>
    /// Command from client to server
    /// </summary>
    public class SyncCommand : SyncMessage
    {
        public SyncCommand() : this(CommandTypes.Register) { }

        public SyncCommand(CommandTypes type)
        {
            this.Type = type;
        }

        public CommandTypes Type { get; set; }

        public enum CommandTypes
        {
            Register = 0,
            RequestState,
            UpdateState,
            SyncCameras,
            RequestCameraState,
            SyncPenalties
        }
    }

    public class SyncStateCommand : SyncCommand
    {
        public SyncStateCommand() : base(SyncCommand.CommandTypes.UpdateState)
        {
        }

        public StateUpdateTypes StateUpdateType { get; set; }

        public enum StateUpdateTypes
        {
            WatchedDriverChanged = 0,
            LiveStatusChanged,
            AddPenalty,
            EditPenalty
        }
    }

    /// <summary>
    /// Response from server to client
    /// </summary>
    public class SyncResponse : SyncMessage
    {
        public SyncResponse() : this(ResponseTypes.Connect) { }

        public SyncResponse(ResponseTypes type)
        {
            this.Type = type;
        }

        public ResponseTypes Type { get; set; }

        public enum ResponseTypes
        {
            Connect,
            Disconnect,
            UpdateState,
            SyncCameras,
            UserList,
            Error = 255
        }
    }


En met een simpele switch bepaal ik wat er moet gebeuren, bijv:

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
        private void OnReceive(UserContext context)
        {
            var value = context.DataFrame.ToString();
            var command = SyncMessage.FromString<SyncCommand>(value);
            if (command == null)
            {
                this.OnLog("Unknown message: " + value);
                return;
            }

            switch (command.Type)
            {
                case SyncCommand.CommandTypes.Register:
                    this.RegisterUser(context, command);
                    break;

                case SyncCommand.CommandTypes.RequestState:
                    this.SendState(context);
                    break;

                case SyncCommand.CommandTypes.UpdateState:
                    var stateCommand = SyncMessage.FromString<SyncStateCommand>(value);
                    this.HandleStateUpdate(context, stateCommand);
                    break;

                case SyncCommand.CommandTypes.SyncCameras:
                    this.HandleCameraSync(context, command);
                    break;

                case SyncCommand.CommandTypes.RequestCameraState:
                    this.HandleCameraStateRequest(command);
                    break;
            }
        }


Maar dit was allemaal niet relevant voor m'n probleem uiteraard...

[ Voor 22% gewijzigd door NickThissen op 15-08-2014 10:44 ]

Mijn iRacing profiel

Pagina: 1