Záludnosti v C#

Záludnosti

Uvádzam tu pár zaľudností s ktorými som sa nedávno stretol a nie sú v skutočnom kóde na prvý pohľad jasné, niekedy ani na druhý.
Samozrejme v krátkych ukážkach môžu biť do očí.

Dynamic a overloady metód

Majme triedu (.Net Core 2.1):

class Model
{ 
    public string Foo {get; set; }
}

A tento kód, ktorý asi nikoho neprekvapí:

Model model = new Model() { Foo = null  }; 

StringBuilder sb = new StringBuilder();
sb.AppendFormat("Hello {0}\n", model.Foo);
Console.WriteLine(sb.ToString());

Tento kúsok kódu vypíše Hello . No teraz zmeňme taký malý detail.

dynamic model = new Model() { Foo = null  }; 

StringBuilder sb = new StringBuilder();
sb.AppendFormat("Hello {0}\n", model.Foo);
Console.WriteLine(sb.ToString());

A výsledok vykonania kódu je ArgumentNullException.
Keď sa mi to stalo v produkcii, tak som hodinu nechápal prečo, veď v kóde bolo všetko v poriadku, ešte aj Visual Stduio ukazovalo správny overload StringBuilder.AppendFormat(string format, object arg0). No po kompilácii sa tam dostal StringBuilder.AppendFormat(string format, params object[] args) a ten hádzal výnimku na null hodnote, čo som si potvrdil cez IL Spy.

Fix je jednoduchý:

sb.AppendFormat("Hello {0}\n", arg0: model.Foo);

Async scope v async metóde

V jednom experimentálnom projekte som potreboval vyrobiť async scope aby som mohol v async bloku nastaviť AsycnLocal statickej premennej, no “záhadne” to nefungovalo. Ukážem to na ukážke pomocou Serilogu.

using (await EnshureSession())
{
        await Task.Delay(100);
        Log.Information("Heloo from async.");
        await ExecuteAnyWork();
}

private async Task<IDisposable> EnshureSession()
{
    string session = "abcdefgh";
    await Task.Delay(100); // Simulovanei asynchroneho ziskania session.
    return LogContext.PushProperty("SessionId", session);
}

private async Task ExecuteAnyWork()
{
    await Task.Delay(100);
    Log.Information("Heloo from execute.");
}

V tomto prípade nie je v logoch SessionId. Aj keď by človek čakal, že sa SessionId bude nastavená v rámci asynchrónenho flowu.

No pre správnu funkciu musí byť kód upravený takto:

string session = await EnshureSession();
using (LogContext.PushProperty("SessionId", session))
{
        await Task.Delay(100);
        Log.Information("Heloo from async.");
        await ExecuteAnyWork();
}

private async Task<string> EnshureSession()
{
    await Task.Delay(100);
    return "abcdefgh"
}

private async Task ExecuteAnyWork()
{
    await Task.Delay(100);
    Log.Information("Heloo from execute.");
}

Na aké záludnosti ste narazili vy?

Nieje to az take zakerne, ale stane sa to lahko:

Task<Order> GetOrder(int id) 
{
   using (var dbContext = new MyDbContext()) 
   {
      return dbContext.Orders.FindAsync(id);
   }
}

dbContext je disposnuty skor, ako sa vykona query. Spravne ma byt:

async Task<Order> GetOrder(int id) 
{
   using (var dbContext = new MyDbContext()) 
   {
      return async dbContext.Orders.FindAsync(id);
   }
}

Mimochodom, vedeli ste, ze ked deklarujete using bez zatvoriek, dispose je zavolany az konci aktualneho scope? Prijemne zjednodusenie:

async Task<Order> GetOrder(int id) 
{
   using var dbContext = new MyDbContext();
   // some other code...
   return await dbContext.Orders.FindAsync(id);
}