Programmeren met .NET, zoveel mogelijkheden terwijl je eigenlijk alleen de BASICs gebruikt

Ingediend door Dirk Hornstra op 12-feb-2024 21:23

Ik ben bezig om mijn computerhok "uit te mesten". Als je een FIFA Panini album van het WK van 2014 tegenkomt, dan weet je dat er al zaken "heel lang" liggen... en er niets mee gedaan wordt. Mocht je dit artikel later teruglezen, dit is een blog-post van 2024, dus ja: het lag hier al 10 jaar.

De reden dat dit zo is, is simpel: te veel andere zaken waar de focus op ligt. Want ja, een 36-urige werkweek, soms in eigen tijd nog wel even wat dingetjes doen, wat voor andere mensen fixen, natuurlijk ook ontspannen door naar concerten, het theater te gaan, 2x in de week sporten. Hier elke week proberen een blogpost te delen. Podcasts beluisteren.

Maar goed, op het moment dat er bijna geen gangpad meer is, dan weet je dat je aan de bak moet...

Zo kom ik hier nog een uitgeprinte versie tegen van MSDN training, Programming with the Microsoft .NET Framework (Microsoft Visual C# .NET).
Ik denk dat dit ook wel 10 jaar oud is. Of nog ouder.... Dus gooi ik het weg of ga ik er nog iets mee doen? Toch maar het laatste, want ik heb het toen uitgeprint omdat ik bepaalde zaken nog eens wilde doorlezen. Om kennis op te doen. Het is mogelijk dat die kennis inmiddels verouderd is (Microsoft heeft in de latere versies steeds meer zaken toegevoegd, zaken makkelijker gemaakt). En sommige zaken, daar deed je niet zoveel mee en doe je nu misschien ook niets meer mee. Maar... een stuk basiskennis, dat kan nog wel eens van pas komen.  Want hoe zat het ook alweer met versienummers? Welke tools krijg je bij Visual Studio? Hoe zat het ook alweer met strong naming? Als je zaken niet dagelijks doet, dan verstoft het, mocht je het een keer nodig hebben, dan is het fijn dat je nog ergens wat parate kennis hebt.

 

De printjes beginnen bij Module 4, de eerste 3 modules waren waarschijnlijk "algemeen en wel bekend".

Module 4, deployment en versioning.


Ik doe het meeste met web-deployments. Dat betekent dat de DLL bestanden allemaal in dezelfde map staan.
In het voorbeeeld in deze module staat 1 van de DLL's in een andere map. In het config-bestand van de executable kun je aangeven waar je die DLL die nodig is voor het programma kunt vinden:


<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="pad-naar-andere-dll" />
    </assemblyBinding>
  </runtime>
</configuration>


Strong Names Assemblies

Hiermee plaats je DLL's in de GAC. Mocht je die willen verwijderen, dan gebruik je daarvoor dit statement:

gacutil /u Naam-Dll
Met gacutil /l kun je zien welke DLL's in de GAC staan (en of die van jou nu ook echt verwijderd is).

Om een "strong name" te genereren gebruik je sn.exe.
Voorbeeld-aanroep: sn.exe -k orgVerKey.snk


In het .CS bestand plaats je dan deze attributen:

#if STRONG
[assembly: System.Reflection.AssemblyVersion("2.0.0.0")]
[assembly: System.Reflection.AssemblyKeyFile("ogVerKey.snk")]
#endif

Voor compilatie gebruik je dit commando:
csc /define:STRONG /target:library /out:AReverser_v2.0.0.0\AReverser.dll AReverser_v2.0.0.0\AReverser.cs


Versienummers

De opbouw van een versienummer is:
major version . minor version . revision . build number

Wanneer "update je wat"?
Major en Minor gebruik je om aan te geven dat deze "incompatible" is met de vorige versie.
Revision geeft aan dat deze "mogelijk compatible" is.
En build is QFE: Quick Fix Engineering, bijvoorbeeld een security-fix.


Binding Policy

Je kunt aangeven dat een andere versie van een gedeelde DLL gebruikt kan/mag worden na compilatie (dus zonder dat jouw applicatie opnieuw gebuild moet worden).
Dat kan in verschillende stadia:

  • application-policy resolution

- <probing privatePath="pad-naar-andere-dll" /> wat we eerder zagen is hier een voorbeeld van.

  • publisher-policy resolution

- dit is op GAC niveau, dus een soort server-pack update

  • administrator-policy resolution

- te vinden in windows/Microsoft.NET/Framework/v1.0.FinalBuildNumber/CONFIG map en hier machine.config


Binding aan een specifieke assembly versie

Veel van deze zaken gebeuren "automatisch" in Visual Studio. Als je een nuget-package update, wordt web.config automatisch bijgewerkt. Waarbij ik soms nog wel eens tegen een probleem aangelopen ben en toen zelf in het bestand ben gaan spitten.
Je ziet dan zaken zoals deze:


<bindingRedirect oldVersion="2.0.0.0-2.0.0.9" newVersion="2.0.1.0" />

Het boek laat ook zien hoe je aan "dezelfde versie" kunt koppelen, maar die je in een andere map hebt staan. Mogelijk omdat je even iets wilt testen?


<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="MyStringer"/>
      <publisherPolicy apply="no"/>
      <dependentAssembly>
        <assemblyIdentity name="AReverser" publicKeyToken="52398B7804D8A95C" culture="" />
        <publisherPolicy apply="no" />
        <bindingRedirect oldVersion="2.0.0.0" newVersion="2.0.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

Het publicKeyToken kun je opvragen met de gacutil /l


Localization

Het is zo makkelijk, in de view-delen van je applicatie teksten typen. In je code bepaalde meldingen terug geven. Maar het hoort daar niet. Want waar je nu jouw Nederlandse teksten intypt, mogelijk wordt je app / site een groot internationaal succes en moeten die meldingen in het Engels. En Duits. En Frans (Duits). Doe het dus goed:

  • scheid je standaard resources van je code en plaats de je teksten in een tekstbestand
  • compileer dat tekstbestand naar een resource bestand
  • package je standaard resources in het hoofd-assembly bestand met je compiler
  • maak satelliet resources voor .NET Framework cultures met hulp van de Alink tool
  • deploy die satelliet resources naar een een directorystructuur onder de hoofdapplicatie
  • schrijf de code om die resources tijdens run-time te kunnen benaderen


Packaging en Deployment Tools

Dit hoofdstuk toont een lijstje met tools, misschien nog wel handig om te weten "wat er allemaal is":

  • Assembly Linker (Al.exe)
  • Global Assembly Cache tool (Gacutil.exe)
  • MSIL Disassembler (ildasm.exe)
  • Strong Name (sn.exe)
  • Native Image Generator (Ngen.exe)
  • Assembly Binding Log Viewer (Fuslogvw.exe)
  • .NET Framework Configuration Tool (Mscorcfg.msc)
  • Code Access Security Policy Tool (Caspol.exe)


Een aantal items waren al benoemd, dus die sla ik nu over.

  • ildasm.exe - als je de structuur (eigenschappen, methodes) in een DLL/EXE wilt bekijken.
  • ngen.exe - alleen in noodgevallen gebruiken. Je code wordt altijd JIT gecompileerd. Maar als dat traag is, een hoge CPU load heeft dan zou je dit kunnen proberen. Hiermee wordt de code in cache op de lokale computer geplaatst in "native format", dus al gecompileerd.
  • fuslogvw.exe - mocht je applicatie/site niet starten "omdat een assembly niet gevonden kan worden", dan kan deze tool je mogelijk verder helpen.
  • mscorcfg.msc - als je met remoting werkt, hiermee kun je in een grafische interface met je security policies en applicaties aan de slag. Zit binnen MMC.
  • caspol.exe - code access security policy tool waarmee je machine, user en enterprise-level code access security policies kunt bekijken en aanpassen.



Module 5, Common Type System.


Bit Flags

Volgens mij door onze voormalige stagiair Jan Julius gebruikt, namelijk voor het aangeven van de rollen van gebruikers in een web-applicatie. Want iemand kan een combinatie hebben van meerdere rollen. In het boek wordt het voorbeeld gegeven van weer-condities. Ook die kun je combineren:



[Flags] enum WeatherDisplay : ushort
{ Sunny=1, Cloudy=2, Rain=4, Snow=8, Fog=16}

var wd = WeatherDisplay.Sunny | WeatherDisplay.Cloudy;
Console.WriteLine(wd.ToString());
wd = (WeatherDisplay)System.Enum.Parse(typeof(WeatherDisplay), "Rain, Snow");
Console.WriteLine(wd.GetHashCode());

// output is hier Sunny, Cloudy en 12


Interfaces

Zelf heb ik redelijk veel met "concrete classes" gewerkt. Totdat je wilt gaan testen. En in .NET Core doe je dat sowieso niet meer, omdat met Dependency Injection je standaard met Interfaces werkt en de concrete class dan doorgeeft. Het maakt je systeem veel meer "plug-en-play". Wordt implementatie 1 niet meer ondersteund, maar kan implementatie 2 met class X dat wel? Zolang die maar voldoet aan de structuur van de Interface kun je deze straffeloos vervangen. Nog wel goed testen, want intern kan de werking natuurlijk wel anders zijn ;)

Dit geeft wel aan wat een Interface is. Het is een contract, je geeft aan wat de methodes en eigenschappen zijn. Een interface heeft geen implementatie, dat zit in de classes die gebaseerd zijn op de interface.


Object Oriented Characteristics

In den beginne waren er ponskaarten, daarna kwam machinetaal, programmeerde je in assembly. En uiteindelijk ontstond OOP - object oriënted programming. Waarmee je code meer structuur krijgt, het beter te onderhouden en te testen is. Er zijn een aantal zaken verbonden met deze structuur en die worden hier genoemd.

  • abstraction. Het is moeilijk voor mensen om een telefoonnummer te onhouden (10 cijfers). Maar door deze op te splitsen naar een netnummer (050 - Groningen) en de resterende naar een pre-fix (eerstvolgende nummers) en uiteindelijk de resterende getallen wordt het makkelijker. Zo kun je een groot stuk code uitsplitsen naar verschillende classes met hun eigen eigenschappen en functies en zo het "grote probleem" splitsen naar "kleine deelproblemen"
  • encapsulation. Wat binnen een class gebeurt, soms wil je dat wel delen met de buitenwereld, maar soms ook niet. Gebruik je een interne variabele om berekeningen te doen, maar mag de aanroepende code daar niets mee doen. Met public/protected/internal/private kun je instellen wie-wat mag doen.
  • inheritance. Soms heb je een class die iets kan/mag en heb je een andere class die daarop lijkt, maar net iets meer mag. Of waarbij de implementatie een klein beetje afwijkt, maar voor 90% gelijk is. Je kunt die class dan laten overerven, waardoor je alles van de hoofd-class beschikbaar hebt. En je kunt dan via "new" zorgen dat jouw code doet wat het zou moeten doen. Ook kan een class "abstract" zijn, dan is deze alleen bedoeld om de hoofdstructuur op te bouwen. Maar is er geen algemene implementatie van een functie, dan geef je hiermee aan dat elke class die "erft" dat zelf moet implementeren via het "override" key-word. In .NET kun je maar van 1 class erven, wel van meerdere interfaces. En wil je voorkomen dat mensen van jouw class zaken overnemen,dan zet je voor jouw class NAAM het key-word "sealed", daarmee sluit je jouw class af voor de buitenwereld.
  • polymorphism. Bij inheritance noemde ik abstract en override, maar dat kun je ook bereiken met virtual/override. In dat geval heb je in de base-class met virtual wel een standaard implementatie. En als je class die overerft niet een eigen implementatie heeft, wordt die functie aangeroepen. Wordt in de ervende class wel een eigen implementatie aangemaakt, dan wordt die aangeroepen. Je implementatie van een class, het concrete object is polymorf, het kan een functie aanroepen uit class A (de hoofd-class), maar dus ook uit class B (de ervende class).

 

Module 6, Working with Types

Om te kijken of 2 objecten eigenlijk "hetzelfde object zijn", dus beide wijzen naar hetzelfde object in het geheugen, dat kun je controleren met de functie Object.ReferenceEquals.
Met .Equals kun je zien of 2 waardes gelijk zijn. Onder water wordt de .ReferenceEquals uitgevoerd, waarbij je dus eigenlijk niet de waarde maar de objectverwijzing controleert.
Als je zelf je eigen types maakt en wilt controleren of 2 objecten met "dezelfde waarde" gelijk zijn, dan moet je zelf de .Equals methode overriden.

Als je die override implementeert, dan zijn er een paar tips:

  • override óók de GetHashCode methode. Als 2 objecten "hetzelfde" zijn, moeten ze ook dezelfde hashcode terug geven. De standaard implementatie doet dat niet!
  • als je de == methode override, override dan ook != en .Equals. HashTable en ArrayList gebruiken de Equals functie, dus die zullen dan hetzelfde resultaat geven als je andere vergelijkingen op dit object.
  • als je class een implementatie van de IComparable interface is, implementeer de Equals methode en bij voorkeur ook de andere vergelijkings operatoren.
  • heel belangrijk: Equals en GetHashcode zouden NOOIT excepties moeten opgooien! Naast het feit dat vaak niet "verwacht" wordt dat deze "basic functies" excepties opleveren, moeten ze snel resultaat teruggeven en voorkomen worden dat er allehande exceptie-afhandeling omheen gebouwd moet worden.

 

De ToString() functie overriden

Soms wil je nog wel eens wat data "even" tonen en dat doe je met .ToString(). Maar als iets een object is, dan krijg je [Object] en daar heb je niets aan. Door zelf een override van ToString() te doen kun je meer relevante data tonen, zelf heb ik dat wel een aantal maal toegepast.
Tip van dit boek is dat als je iets terug wilt geven in localized formaat, je dat NOOIT moet doen. Je moet dan namelijk de IFormattable interface implementeren en daar ToString methode van gebruiken, zo voorkom je problemen.

Het volgende onderdeel gaat over static constructors en private constructors. De rest van die module heb ik niet geprint, dus daar kan ik verder geen informatie over delen.

Externe code gebruiken

We krijgen een voorbeeld:


[DllImport("user32.dll", Charset<CharSet.Ansi]
public static extern int MessageBox(int h, string m, String c, int type);

public static void Main()
{
  string pText = "Hello World";
  string pCaption = "PInvoke Test";
  MessageBox(0, pText, pCaption, 0);
}

En er volgt nog een korte uitleg hoe je .NET code kunt laten exporteren naar COM dll's. Kort geleden een podcast van .NET Rocks gehoord, waarbij een ontwikkelaar dat gebruikte om .NET code in een groot VB6 project te krijgen.
Je gebruikt hiervoor tlbexp.exe en voor het registreren van de DLL gebruik je regasm.exe.
Die regasm.exe herinner ik me nog van de tijd dat ik bij HSCG werkte en je de Sitedev DLL's importeerde om ze in je classic ASP web-applicaties te kunnen gebruiken.

Om COM-classes in je .NET code te krijgen gebruik je tblimp.exe

 

Module 7, Static and prive constructors

Hier heb ik verder geen printjes van liggen, waarschijnlijk omdat ik het toen "niet nodig" vond. Ik werk het hier nog wel even uit, want inmiddels vind ik het wel vermeldenswaardig.

En maar even de documentatie erbij gezocht.
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors

Hier zie je dat als je "alleen bij de eerste aanroep" bepaalde waardes in wilt stellen, die "statisch" beschikbaar zijn door de applicatie, dan maak je jouw constructor static.
Zou je een ticketingsysteem hebben en een class TicketRequest en je zou willen weten wanneer de allereerste aanvraag geweest is, dan kun je met een static property DateTime firstRequest dat instellen.

Het andere item is private constructors.

Als je een nieuw object van een class aan wilt maken doe je bijna altijd een

var instantie = new Object();

Maar soms krijg je een foutmelding dat dit niet kan/mag. Door ervaring weet je dan dat het vaak de volgende aanroep moet worden:

var instantie = Object.Create(...);

Maar waarom zou je dat doen? Bij mijn eigen "classes" heb ik dat tot nu toe nog niet gedaan.

Ook hier even de documentatie erbij gezocht:
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/private-constructors

Het voorbeeld is iets anders, daar is het eigenlijk meer een "static class". Standaard (dus zonder dat je er iets voor hoeft te doen) krijg een class een parameterloze constructor.
Als je dat niet wilt (omdat jouw class altijd bepaalde input nodig heeft) maak je de private constructor aan. Maar als je class niet static is en je wilt wel "een instantie" aanmaken
en teruggeven, dan gebruik je daar de .Create(...) aanroep voor (daar zul je dan bepaalde parameters aan mee moeten geven, laatste statement in Create(..) zal zijn return this;


Module 8, Delegates en Events

Delegates zijn zaken waar ik niet zoveel mee doe. Ik kan me zo niet herinneren dat ik het in een web-project gebruikt hebt. Volgens mij wel bij een desktop-applicatie waar bij het scannen van een pasje bepaalde zaken uitgevoerd moesten worden.
In ieder geval, even kijken of de onderstaande voorbeelden van het boek wat zaken duidelijk maken:



delegate void MyDelegate(string s);

public class MyClass
{
  public static void Hello(string s) {
   // ...
  }

  public void AMethod(string s) {
   // ...
  }
}

var a = new MyDelegate(MyClass.Hello);
var myClass = new MyClass();
var b = new MyDelegate(myClass.AMethod);

a("World");
b("World");

Het boek geeft dan het voorbeeld van een delegate void SwitchFlipped(SwitchPosition switchState), class Light, class Switch en een OnFlip event.
En 2 "switches", een lichtschakelaar voor de badkamer en de slaapkamer. Maar waarom je nu een delegate zou gebruiken en niet een eigen class-implementatie is mij niet helemaal duidelijk.
Hier geldt waarschijnlijk het credo: "als je zelf iets moet bouwen, er een delegate voor nodig hebt, dat je dan snapt hoe het werkt".

Events komen ook nog even voorbij. Daar heb ik inderdaad wel gebruik gemaakt van de Button.OnClick += new ButtonClickHandler....


Module 9, Memory and Resource Management

Van dit hoofdstuk heb ik maar 1 printje. Waarschijnlijk omdat ik toen al zaken liet overerven van IDisposable. En later door het gebruik van using (...) zorg je dat zaken netjes vrijgegeven worden.
En ik heb vaak genoeg gehoord dat je "niet zelf de Garbage Collection" aan moet roepen, want dat doet het systeem veel efficiënter.

Op dit blaadje staan de System.GC.Collect() (wat je dus eigenlijk niet zou moeten aanroepen), de System.GC.WaitForPendingFinalizers() (uitstellen, wachten tot de Queue van Finalizers leeg is),
System.GC.ReRegisterForFinalize(object b), als iets opgeruimd is maar "op magische wijze weer actief gemaakt is" nogmaals registreren om opgeruimd te worden.
En de System.GC.SuppressFinalize(object obj) om te zorgen dat iets niet opgeruimd wordt.


Module 11, Internet Access

Toen ik dit uitprintte was het waarschijnlijk nieuw voor mij, maar dit is allemaal bekend spul. Encoding.UTF8.GetString(bytes...), WebRequest, authenticatie door een NetworkCredential te gebruiken, zo moeilijk is dat niet.


Module 12, Serialization

Er ging al data over de lijn, meeste in XML-formaat, dat is nog steeds zo, maar nu zal het grootste deel in het JSON-formaat over de lijn gaan.
Je doet dat door op de class het attribuut [Serializable]  toe te voegen. In het voorbeeld van het boek wordt dit gedaan:

 

var o = new SerializableClass();
IFormatter formatter = new SoapFormatter();
var toStream = new FileStream("test.xml", FileMode.Create, FileAccess.Write, FileShare.None);
formatter.Serialize(toStream, o);
toStream.Close();

Met de .Deserialize kun je ingelezen tekst weer omzetten in een object in code.


Module 14, Threading en asynchronous programming

Dit heeft Microsoft een stuk makkelijker gemaakt door zaken als async en await in functies te kunnen gebruiken. Wat er "onder de motorkap" allemaal voor moet gebeuren, dat is niet zichtbaar voor de developer.
Die kan zich richten op de code die hij asynchroon wil laten uitvoeren.

Dat gezegd hebbende, je kunt natuurlijk nog wel zelf Thread-objecten aanmaken. Ik heb dat zelf gebruikt bij een Windows-service. Die draait al jaren en volgens mij zonder problemen (afkloppen!).

Je kunt verschillende zaken aanpassen. Zo kun je met t.IsBackground = true; instellen dat die thread een "background-thread" is.
Als "de voorgrond thread" stopt, dan stoppen al die background-threads ook. Dat is wel een goede reminder, want vaak wil je bepaalde processen op de achtergrond laten uitvoeren. Maar die wil je wel hun werk af laten maken!

Met Thread.Sleep(..) kun je een thread even laten rusten, met t.Join() zorg je dat je wacht tot de andere thread klaar is.
Met .WaitAll() wacht je tot alles klaar is. Met de Thread.Sleep(Timeout.Infinite) zorg je dat deze weer actief wordt als een een andere thread een .Interrupt of .Abort aanroept.
Met .Suspend laat je de thread wachten tot een andere thread je weer actief maakt via .Resume.

Je moet er altijd goed opletten dat je dit "slim" programmeert, doe je het niet goed dan staat je CPU op 100% te blazen, gaat er iets niet goed met het slapen/suspenden kun je een deadlock krijgen waarbij alle processen op elkaar staan te wachten.
Met het attribuut [ThreadStatic] zorg je dat het veld niet met de standaard NULL geïnitialiseerd wordt.

Je hebt het [Synchronization()] attribuut. Hiermee kun je instellen dat meerdere threads wel static velden kunnen lezen, maar voor eigen variabelen en functies, daar kan alleen 1 thread per keer bij.
Je doet dat bij een class die erft van ContextBoundObject.

Als je bepaalde code wilt beschermen (er wordt bijvoorbeeld een schrijf-actie uitgevoerd), dan zet je er het lock keyword voor.
Op een functie kun je het attribuut [MethodImplAttribute(MethodImplOptions.Synchronized)] toevoegen om te zorgen dat 1 thread er bij kan.
Je hebt ook het Monitor object met functies .Wait, .Pulse en .PulseAll.

Met de Thread.Timer kun je zaken inregelen. Via de TimerCallback-functie kun je zorgen dat er acties uitgevoerd worden.

Volgende item is een ThreadPool. Handig om schaalbaar te worden, maar er zijn wel beperkingen. Als niet elke thread dezelfde prioriteit heeft, je langdurige taken hebt, threads moeten van elkaar onderscheiden kunnen worden, dan is dit geen geschikte oplossing.
Met het attribuut [STAThread] kun je een rechtstreekse aanroep op een COM-dll uitvoeren. Ik hoop dat je die functionaliteit niet nodig hebt, want dan heb je het volgens mij over unmanaged code, iets wat je applicatie instabiel kan maken. Gedoe :)