Versionering i Windows .NET Frameworket  
Downloads til denne artikel finder du her !

VersionsCheckerClient, en demo af hvorledes du kan checke om der er overensstemmelse mellem versionerne på de assemblies dit program loader og dem der var refereret på compileringstidspunktet.

VersionCheckerClient_vb.zip
VersionChecker demoen i en VB version

VersionCheckerClient_cs.zip
VersionChecker demoen i en C# version

For yderligere information om demoen; læs Readme.txt, der findes i VersionCheckerClient projektet.

Versionel forvirring

Support medarbejderen: "Hvilken version af Windows bruger du"?
Brugeren: "Office XP!"

Måske ikke verdens sjoveste ordveksling, men på sin egen sære måde egentlig ganske betegnende for de problemstillinger, man kan løbe ind i, når man beskæftiger sig med softwareversionering.

Når man i daglig tale siger, at en given applikation eller komponent er af en bestemt version, refererer man oftest til et tal eller en tekst på en given form. Eksempelvis siger man måske, at HenriksSuperApplikation er af version 3.14a, version 17.42.714.117 eller lign. Men i virkeligheden er det jo ganske upræcist - for hvem siger, at vi overhovedet taler om forskellige versioner af den samme applikation?

I .NET er der flere forskellige niveauer af versionering, og i denne artikel skal vi kigge nærmere på, hvori de består.

I de gode gamle dage

Men lad os aller først se på, hvordan versionering blev håndteret i de gode gamle dage. Når vi definerede en version af en almindelig Windows applikation, gjorde vi det, dels ud fra filens navn, dels ud fra et versionsnummer på formen major.minor.revision. Min aktuelle version af Microsoft Word (Microsoft Word 2002) er på dette niveau defineret ud fra filnavnet winword.exe og versionsnummeret 10.0.4524. Taler vi om ganske almindelige applikationer er dette et langt stykke hen af vejen tilstrækkeligt. Men når vi taler om f.eks. de gode gamle COM-komponenter, skal vi have lidt andre informationer. Når vi skal instantiere et objekt ud fra en klasse, skal vi naturligvis have klassens navn, men det er - hvis vi vælger at arbejde late bound - så til gengæld også tilstrækkeligt. I det mest basale eksempel kunne vi i VB6 instantiere et nyt objekt ud fra følgende kode:

  VB6:
  Dim wd As Object
  Set wd = CreateObject("Word.Application")

Men ret beset ved vi jo desværre ikke på design-tidspunktet, hvilken komponent denne kode tager fat i på runtime-tidspunktet - intet forhindrer jo HenrikTheHacker i at lave en komponent ved navn Word indeholdende en klasse ved navn Application. Og hvis denne komponent er den senest registrerede på brugerens maskine, så er det rent faktisk den, der startes op i stedet for som ønsket Microsoft Word.

En GUID (Globally Unique IDentifier) er en 128-bits værdi, som man bl.a. kan få Windows til at generere. GUIDs genereres i hovedscenariet ud fra dels en tidsangivelse dels netværkskortets MAC-addresse; og de er med meget stor sandsynlighed globalt unikke. Et eksempel på en GUID på menneske-læsbar (hexadecimal) form kunne være: 018F996A-0613-4B67-A209-2E87FD456386.

Denne mangel på unik identifikation udgør et oplagt problem, så derfor benytter Microsoft GUIDs til unikt at angive f.eks. klasser i COM-komponenter.

Når man som udvikler laver en early bound reference til en COM-komponent, angiver man basalt set en GUID, som identificerer typebiblioteket for den komponent, man ønsker at benytte. I typebiblioteket er endvidere angivet en række GUIDs til at identificere de enkelte interfaces og klasser. Så længe alle typebiblioteker, interfaces, klasser etc. har unikke GUIDs, vil man dermed også være i stand til entydigt at identificere, hvad det er, man ønsker at få fat på. GUIDs er glimrende til mange ting (også mange ting der intet har med COM at gøre), men de har det i denne sammenhæng store problem, at det er simpelt at kopiere en GUID. Det betyder, at der ikke er noget, der forhindrer HenrikTheHacker i at lave en komponent ved navn winword.exe, med klasser med samme ProgId'er som ProgId'erne i Word ("Word.Application", "Word.Document" o.s.v.) og med præcis de samme GUIDs.

Så selvom GUIDs måske nok er unikke, så er tilstedeværelsen af en given GUID i en komponent altså ikke garanti for noget som helst.

Version.NET

I .NET verdenen kan man arbejde med versionering af assemblies (applikationer/komponenter) på flere forskellige niveauer.

Som udgangspunkt består en versionsangivelse i .NET af fire forskellige informationer:

  • Assemblyens navn
    Mere præcist er det assemblyens filnavn (fraregnet dens fil-extension), der er tale om.
  • Versionsnummer
    Versionsnummeret er på formen <major version>.<minor version>.<build number>.<revision> (f.eks. 17.42.714.117).
    Versionsnummeret sættes v.h.a. assembly-attributten AssemblyVersion som typisk findes i AssemblyInfo-filen. AssemblyVersion-attributten skal ikke forveksles med AssemblyInformationalVersion-attributten, som ikke har nogen programmeringsmæssig betydning, og som udelukkende skal opfattes som en del af produktnavnet, eller som en kommentar om man vil.
  • Culture
    En streng der angiver, hvilken culture assemblyen er lavet til så som "en", "en-GB", "en-US", "de", "de-CH". Den tomme streng er default, og den angiver, at assemblyen er lavet til invariant culture / default culture. Culture sættes v.h.a. assembly-attributten AssemblyCultureAttribute. Se bl.a. Using the CultureInfo Class og CultureInfo Class på MSDN hvis du vil have yderligere information om cultures.
  • Public key token
    Hvis man signerer en assembly v.h.a. et strong name key pair, er denne "public key token" en 8 bytes værdi afledt ud fra den offentlige nøgle. Der følger mere om denne token nedenfor.

Besnærende XCOPY og svage navne

GACen er et centralt repository for en maskines assemblies (komponenter/applikationer). Ved at placere assemblies i GACen muliggør man, at en lang række applikationer på maskinen kan dele den samme fysiske komponent imellem sig. Hvis man ændrer funktionaliteten i assemblyen, samt opdaterer den i GACen vil den ændrede funktionalitet slå igennem i alle applikationer, der refererer den fælles assembly - vel og mærke så længe alle fire versionsinformationer er de samme. På mange måder er målsætningen med GACen tilsvarende det, man søgte at få ud af brugen af System-filkataloget (med de traditionelle dll'er i før-COM-tiden), samt registryet (i COM-tiden). Men til forskel fra tidligere tiders noget rodede og halve løsninger, der hurtigt førte til det såkaldte dll-hell, er GACen et versioneret repository indeholdende entydigt identificerede komponenter.

Når man ser beskrivelser af, hvordan installation af applikationer og tilhørende komponenter foregår i .NET verdenen, beskrives der ofte en såkaldt XCOPY installation. I modsætning til hvad man måske kunne forledes til at tro, er der ikke tale om en ny avanceret installationsmekanisme. Med XCOPY installation menes blot, at man kan installere en applikation v.h.a. simpel filkopiering. Filkopiering er én af de måder, hvorpå man kan installere en applikation, men der er også mange andre så som installation i GACen (Global Assembly Cache), dynamisk installation over internettet og andre lignende scenarier.

Ved simple installationsscenarier (f.eks. ved installation af små simple applikationer) kan en ren XCOPY installation udmærket være en attraktiv mulighed. Udgangspunktet for en XCOPY installation er, at man installerer applikationen, og de komponenter den benytter sig af, i samme applikationsspecifikke filkatalog (bemærk dog at der er mange forskellige variationer over dette tema).

Selv om en versionsangivelse egentlig består af fire selvstændige informationer, kan man godt vælge kun at angive nogle af dem som f.eks. assemblyens navn, versionsnummeret og culturen. I installationsmæssig sammenhæng er det ikke nødvendigt for ens komponent at have en public key token, hvis man ikke har tænkt sig at installere den i GACen.

Lad os se nærmere på versionering i den situation, hvor vi ikke har angivet nogen public key token i assemblyen. Antag at jeg har lavet en komponent kaldet MinKomponent.dll. MinKomponent indeholder en GoddagSiger-klasse med en enkelt særdeles nyttig HelloWorld-metode [udgave A]:

  Public Class GoddagSiger

    Public Shared Function HelloWorld() As String
      Return "Hello"
    End Function

  End Class

Antag endvidere at jeg har lavet en almindelig Windows applikation (MinWindowsApp.exe), der kalder GoddagSigerens HelloWorld-metode. Applikationen kan nu "installeres" ved blot at kopiere filerne MinWindowsApp.exe og MinKomponent.dll ind i et valgfrit filkatalog - naturligvis under forudsætning af at brugeren allerede har Windows .NET Frameworket installeret på sin maskine.

Antag nu at jeg ønsker at lave en ny og bedre udgave af MinKomponent, hvor HelloWorld-metoden er defineret som [udgave B]:

  Public Class GoddagSiger

    Public Shared Function HelloWorld() As String
      Return "Hello World"
    End Function

  End Class

Alt hvad jeg behøver at gøre er at kopiere den nye udgave af MinKomponent ind oveni den gamle udgave. MinWindowsApp-applikationen vil nu umiddelbart og uden problemer køre på den nye udgave.

På et tidspunkt vælger jeg så at lave en langt mere avanceret udgave af MinKomponent. Jeg vil nu stille en fleksibel funktionalitet til rådighed, hvor klienten kan få indflydelse på, hvem der siges "Hello" til [udgave C]:

  Public Class GoddagSiger

    Public Shared Function HelloWhatEver(ByVal place As String) As String
      Return "Hello " + place
    End Function

  End Class

Hvis jeg erstatter udgave B med udgave C af komponenten starter MinWindowsApp-applikationen lige så fint op som før, men i det øjeblik HelloWorld-metoden forsøges kaldt, får klienten naturligvis en run-time exception (System.MissingMethodException), fordi HelloWorld-metoden ikke længere findes i komponenten. Denne situation svarer meget godt til den, der kendes fra scenarier med traditionel late-binding.

Når en komponent ikke er strongly named (signeret med strong name key pairs), er "problemet" (eller featuren om man vil), at .NET Frameworket ikke laver nogen automatisk versionskontrol. Vi kan naturligvis vælge at kode en sådan versionskontrol selv, men det er altså ikke noget vi får per automatik i denne "weakly named" situation.

Stærke navne - stærke bindinger

Lad os se nærmere på hvad det egentlig vil sige at signere en assembly med strong name key pairs. Det key pair, der er tale om, er en offentlig henholdsvis en privat nøgle, der bruges til at foretage signering af assemblies baseret på en RSA krypterings-algoritme. En assembly, der er signeret med et sådan strong name key pair, siges at være strongly named.

Man kan generere et nøgle-par ved at benytte command line utilityen "Strong Name Utility" sn.exe. Den er en del af .NET Framework SDKet og findes på min maskine i filkataloget "C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin". Den kan endvidere eksekveres direkte i "Visual Studio .NET Command Prompten". Følgende kommandolinje vil placere et nygenereret nøglepar i filen CaptatorKeyFile.snk:

    sn -k CaptatorKeyFile.snk

Hvis man efterfølgende udfører kommandolinjen

    sn -tp CaptatorKeyFile.snk

kan man få dels public keyen (den offentlige nøgle) samt den tidligere omtalte public key token at se.

Man kan signere en assembly med den genererede nøgle på flere forskellige måder. En af måderne er at angive keyfilens filnavn som parameter til assembly-attributten AssemblyKeyFile:

    <Assembly AssemblyKeyFile("CaptatorKeyFile.snk")>

Ved kompilering af assemblyen (i det nærværende tilfælde MinKomponent) vil der kort (og lidt forsimplet) fortalt ske følgende: Der beregnes en hash-værdi for assemblyen. Denne hash-værdi krypteres v.h.a. den private nøgle i CaptatorKeyFile.snk filen. Endelig føjes den derved opnåede digitale signatur samt den offentlige nøgle til assemblyen.

Da public keyen er en frygtelig stor størrelse, får man beregnet en hash værdi af denne. De sidste 8 bytes i hash værdien er det, der betegnes som den såkaldte public key token. Denne public key token (som i enhver praktisk henseende kan betragtes som værende unik) benyttes i refererende assemblies manifester til at holde rede på, hvilke komponenter en given assembly har referencer til. En komponents public key token er altså en del af den identifikation, som komponenten refereres med.

Når jeg har kaldt key-filen for CaptatorKeyFile er det for at understrege, at denne keyfile tilhører Captator. Det er yderst vigtigt, at andre ikke får fat i vores keyfile - hvis det skulle ske, ville sikkerheden omkring strong names for vores komponenter være kompromiteret, og man ville ikke længere kunne stole på, at det er Captator, der står bag alle assemblies med et given strong name. Derfor bør sådanne key-files omgåes med den største forsigtighed. Lidt spøgefuldt kan det siges, at man bør opbevare sin private nøgle i et bevogtet pengeskab! Det virker måske nok lidt gammeldags, men det er med til at understrege, at det absolut er fornuftigt at opbevare den sikkert og kun lade enkelte, udvalgte medarbejdere have adgang til at bruge den. Normalt vil et firma af hensyn til overskueligheden kun have en enkelt (eller ihvertfald nogle ganske få) strong name key-file(s).

Hvis vi distribuerer en ny udgave af en strongly named komponent med en ændret intern implementation, men med samme assembly navn, versionsnummer, culture og public key token, så vil komponenten ud fra et rent versioneringsmæssigt synspunkt blive opfattet som værende identisk med den tidligere udgave. Skulle et eller flere interfaces have ændret sig (som der skete fra [udgave B] til [udgave C]) vil det naturligvis stadig medføre en MissingMethodException. Derfor bør man altid nøje checke forskelle imellem den gamle og den nye version af komponenten. Er der - som i dette tilfælde - ændringer i interfacet bør komponenten have et nyt versionsnummer.

Hvis blot en enkelt af versionsinformationerne har ændret sig for en strongly named assembly (som f.eks. at versionsnummeret har ændret sig fra 1.0.0.0 til 2.0.0.0 så træder den automatiske versionskontrol i .NET i kraft og .NET Frameworket kaster en exception af typen System.IO.FileLoadException, hvis den ikke kan finde den ønskede version af assemblyen.

Ved at signere en assembly kan man udnytte .NET Frameworkets automatiske versionscheck, og samtidig er man også garanteret, at der ikke er blevet "fiflet" med assemblyen efter, at den blev signeret. Bemærk dog at denne form for signering ikke (umiddelbart) siger noget om, hvem der rent faktisk har lavet en given komponent.

Summa summarum versionitis

I .NET verdenen har vi selv indflydelse på, hvor meget vi går op i versionering og hvordan vi ønsker at håndtere det.

  • Vi kan vælge at tage versionering afslappet. Vi kan nøjes med den simple ligetil versionering, hvor vi styrer det hele selv - enten manuelt eller via egen kode. Det vil sikkert ofte være valget, hvis vi udvikler komponenter til eget brug (d.v.s. i situationer hvor vi styrer både klient og komponenter).
  • Vi kan også vælge at tage versionering meget seriøst. Vi kan signere assemblies v.h.a. strong name key pairs. Det kan måske godt gøre vores arbejde lidt mere komplekst, men det gør til gengæld, at vi kan være helt sikre på identiteten af vores komponenter. Strong named asssemblies er en forudsætning for en lang række sikkerhedsorienterede features i Windows .NET Frameworket. Og en assembly skal være strong named for, at den kan installeres i GACen.

Denne artikel beskæftiger sig ret snævert med versionering af .NET komponenter. Der er mange andre emner, som jeg i begrænsningens kunst bevidst er gået udenom. Det drejer sig bl.a. om nogle af de mange andre installationsscenarier, der findes ud over XCOPY, om runtime binding (samt herunder brugen af konfigurationsfiler til at styre denne), om brugen af GACen, om delayed signing, og meget, meget andet. Alle disse emner er særdeles interessante og i sig selv oplagte kandidater til fremtidige artikler.

Jeg har lavet en lille utility kaldet VersionChecker, download og afprøv den :-), se sidebar øverst i artiklen. Versionchecker kan hjælpe med til at undersøge versioneringsmæssige differencer i forbindelse med en assembly. Den kan dels undersøge, hvilke assemblies der refereres fra en given klient, dels hvilke assemblies der på runtime tidspunktet rent faktisk i en given installation loades fra klienten. Utilityen er nok mest relevant, når man benytter sig af weakly named komponenter, hvor der jo netop kan være en sådan versionsmæssig forskel.