Viser opslag med etiketten csharp. Vis alle opslag
Viser opslag med etiketten csharp. Vis alle opslag

2010-10-17

Simpel Behavior Driven Development

Her er et eksempel på en meget simpel metode til at at lave BDD style tests med MS Test frameworket.

Der findes utallige komponenter til at at lave mere flydende og måske også mere læsbare test cases til BDD, men jeg lide denne mere simple metode, som ikke kræver andet end diciplin fra dem der skriver test koden og så selvfølgelig gode user stories at tage udgangspunkt i.

Jeg har her til lejligheden opfundet en ny regnemaskine, der har en række krav, som afviger lidt fra en normal lommeregner.

Feature: Addition

 User story: 
  As an accountant I would like to add two numbers.

 Acceptance Criteria:
  Given a positive number
  When added to a positive number
  Then the result should be the sum of the numbers

  Given a zero
  When added to a positive number
  Then the result should be zero

  Given a negative number
  When added to a positive number
  Then the result should be zero

Som man kan se er dette ikke helt almindelige krav til en regnemaskine, men det er heller ikke det der er det vigtige i denne omgang.

Disse krav kunne sagtens testes via "gammeldags" TDD og få det ønskede resultat. Men det som jeg ser som målet med BDD er at kunne vise kunden (Product Owner) at de krav som der er stillet er opfyldt, længe før at et evt brugerinterface er lavet færdigt, og dette egner de fleste testresultater fra TDD sig ikke til. Selvom man laver gode navne på sine tests og variable, så er det jo ikke ensbetydende med at de er forståelige for kunden.

Derfor kan man istedet udnytte det fulde potentiale af udviklingsmiljøet og bruge namespaces, klasse- og metodenavne til at beskrive hvilken user story med tilhørende krav der bliver testet.


Her er et eksempel på dette
namespace Feature_Addition._As_an_Accountant._I_would_like_to
{
    [TestClass]
    public class _add_two_numbers
    {

        Calculator sut;
        [TestInitializeAttribute]
        public void SetUp()
        {
            sut = new Calculator();
        }

        [TestMethod]
        public void Given_a_positive_number_When_added_to_a_positive_numbers_Then_result_should_be_the_sum()
        {
            Assert.AreEqual(3, sut.Add(1, 2));
        }

        [TestMethod]
        public void Given_a_zero_When_added_to_a_positive_number_Then_result_should_be_0()
        {
            Assert.AreEqual(0, sut.Add(0, 2));
        }

        [TestMethod]
        public void Given_a_negative_number_When_added_to_a_positive_number_Then_result_should_be_0()
        {
            Assert.AreEqual(0, sut.Add(-1, 2));
        }
    }
}

Som man kan se er hele user storyen med alle 3 acceptance criteria repræsenteret i denne test klasse. Og når man kører disse test, så får man følgende resultat i Test Results vinduet


Jeg har tilføjet kolonnen "Full Class Name" til listen, for bedre at kunne se hele user storien i test resultatet. Alternativt skal man angive hele navnet i hver test metode, men det vil kun gøre navnene besværlige at læse og  kræve at den samme sætning bliver gentaget igen og igen.

Dette test resultat kunne man selvfølgelig sagtens levere, som dokumentation for at kundens krav er opfyldt, men det er nok ikke så læsbart som man kunne ønske. Istedet kan man udnytte at MSTest laver en XML fil med resultaterne fra hver testkørsel. Denne XML fil kan man konvertere til HTML format ved hjælp af værktøjet trx2html så man istedet for får dette resultat


Her er et resultat der kan bruges til at dokumentere om kundens krav er opfyldt løbende under udviklingsfasen. Det eneste der mangler er at få denne html rapport automatisk genereret når build serveren har kørt unit tests efter en build.

2010-10-01

Test af array

Inspireret af dette indlæg på StackOverflow: http://bit.ly/atgBod har jeg forsøgt at lave 2 forskellige test metoder, for at vise forskellen mellem unit og integrationstest.

Jeg mener at forfatterens eksempel er en test der i realiteten ikke er nødvendig, hvis man istedet for at teste på værdierne i et array, istedet testede det der genererede array'et

Her er først et eksempel på integrationstesten:

[TestMethod]
public void ArrayTest()
{
    IBlackBox sut = new BlackBox();
    Assert.AreEqual(0, sut.items[0] % 2);
    for (int i = 1; i < sut.items.Length; i++)
    {
       Assert.AreEqual(sut.items[i - 1] + 2, sut.items[i]);
       Assert.AreEqual(0, sut.items[i] % 2);
    }
}

Den kunne sagtens deles op i nogle seperate tests, men som man kan se, så antager testen det array der testes på allerede eksisterer og at værdierne er fyldt i uden egentlig at teste hvordan arrayet er genereret.

Hvis man ser hvordan arrayet bliver genereret kan man pludselig se en del flere hjørner der bør testes, fx negative værdier og ulige tal.

public class WhiteBox : IWhiteBox
{
    public int GenerateNextItem(int previousItem)
    {
        if (previousItem < 0) return 0;
        if (previousItem % 2 != 0) previousItem--;
        int nextItem = previousItem += 2;
        return nextItem;
    }
}
 
public class BlackBox : IBlackBox
{
    public int[] items
    {
        get
        {
            IWhiteBox whiteBox = new WhiteBox();
            int value = 0;
            IList<int> result = new List<int>();
            for (int i = 0; i < 4; i++)
            {
                value = whiteBox.GenerateNextItem(value);
                result.Add(value);
            }
            return result.ToArray();
        }
    }
}

De tilhørende tests til denne implementering kunne så være

[TestMethod]
public void GivenUnevenPreviousValueReturnsNextEvenValue()
{
    IWhiteBox sut = new WhiteBox();
    int actual = sut.GenerateNextItem(1);
    int expected = 2;
    Assert.AreEqual(expected, actual);
}

[TestMethod]
public void GivenEvenPreviousValueReturnsNextEvenValue()
{
    IWhiteBox sut = new WhiteBox();
    int actual = sut.GenerateNextItem(2);
    int expected = 4;
    Assert.AreEqual(expected, actual);
}

[TestMethod]
public void GivenNegativePreviousValueReturnsZero()
{
    IWhiteBox sut = new WhiteBox();
    int actual = sut.GenerateNextItem(-1);
    int expected = 0;
    Assert.AreEqual(expected, actual);
}

Nu er der selvfølgelig en del flere tests, til "bare" at teste en så simpel funktion, tilgengæld så er der ingen tvivl om hvordan arrayet bliver genereret og at "ulovlige" værdier er håndteret, hvis nogen skulle finde på at ændre i implementeringen af array genereringen. Den første test ville fx ikke finde fejl ved negative numre i arrayet.

2010-03-09

TinyIoC - Lille sej DI container

Jeg faldt fornylig over en lille simple DI container kaldet TinyIoCtwitter. filosofien bag denne DI container er, at man ikke behøver at skulle tilføje en ekstra assembly (eller flere) for at kunne bruge en DI container. Den er selvfølgelig heller ikke egnet til løsninger med mange og komplekse afhængigheder, men til mindre løsninger kan den sagtens anvendes.

Her er et eksempel på hvor simpelt det er at anvende den. Jeg har lavet et konsolprojekt og har kopieret TinyIoC.cs ind i projektet.

Jeg har herefter lavet et par interfaces og klasser, som jeg vil benytte TinyIoC til at oprette instanser af. Selvom det er nogle små klasser og et lidt specialt mønster, håber jeg at man kan umiddelbart kan gennemskue dette - her er et klassediagram over disse klasser:


For at initalisere en instans af Cat på traditionel vis (Poor man's DI) skal jeg skrive følgende:

Farmer farmer = new Farmer();
Milk milk = new Milk(farmer);
IFood food = new Food();
ICat cat = new Cat(food,milk);

Hvis jeg istedet anvender TinyIoC containeren til at initialisere en Cat kan jeg nøjes med at skrive følgende:

var container = TinyIoCContainer.Current;
container.AutoRegister();
ICat cat = container.Resolve<ICat>();

Jeg benytter her muligheden for at auto registere objekterne og interfacene i DI containeren, jeg kunne også have regisreret dem enkeltvis, men det ville i dette tilfælde have gjort eksemplet for komplekst, til at man ville kunne se en fordel ved at bruge TinyIoC istedet for Poor Man's DI.

2009-11-07

Optionelle parametre i C#4.0

Introduktion

I den foregående blogpost har jeg skrevet om dynamics i C#4.0. I denne post vil jeg skrive om optionelle parametere og se på, hvordan de kan bruges - både i forhold til når man skal kommunikere med et api der indeholder metoder med optionelle parametre og når man vil bruge dem i sine egne metoder i C#

I denne post, har jeg udover nunit også anvendt moq til at lave kode eksempler. Jeg vil ikke komme nærmere ind på moq her, men det er et mocking framework der kan anvendes i kombination med et unittest framework, til at lave tests der ikke er afhængig af en bestemt implementering af logik.

Optionelle parametre

Optionelle parametre er en ting, som VB programmører har kunnet anvende i mange år. Helt enkelt gør det ud på, at man kan undlade parametre, hvis den metode man kalder har en defineret default værdi for parameteren.

Hvis man fx forestiller sig følgende interface til et api, der anvender optionelle parametre

public interface IApiFunctions
{
    void ApiFunction(int arg1, int arg2=1);
}

Så kan man som vist herunder, vælge at kalde metoden ApiFunction uden at angive en værdi for parameteren arg2

[Test]
public void GivenOnly1Paramter()
{
    var mock = new Moq.Mock<IApiFunctions>();
    IApiFunctions sut = mock.Object;

    Assert.DoesNotThrow(() => sut.ApiFunction(1));
}

Man kan selvfølgelig også angive begge parametrene, hvis man har behov for det

[Test]
public void GivenBothParamters()
{
    var mock = new Moq.Mock<IApiFunctions>();
    IApiFunctions sut = mock.Object;
    Assert.DoesNotThrow(() => sut.ApiFunction(1, 2));
}

Navngivne parametre

En anden mulighed som man også har, er at angive navnet på parametrene som man angiver. Hvis man forstiller sig at api'et indeholder endnu en metode der ligner den følgende

void ApiFunction2(int arg1, int arg2 = 1,string arg3="NaN");

Så kan man vælge at kalde den uden at angive arg2, ved at angive at det er arg3, som man

[Test]
public void UsingNamedParamters()
{
    var mock = new Moq.Mock<IApiFunctions>();
    IApiFunctions sut = mock.Object;
    Assert.DoesNotThrow(() => sut.ApiFunction2(1, arg3: "Mojn"));
}

Dynamics

Et lidt andet eksempel på optionelle parametre, som egentlig mere hører til under min sidste post om dynamics, er hvor man kalder en metode i et api, som har optionelle parametre. Jeg har som eksempel tilføjet følgende metode til det ironpython script, som jeg tidligere har anvendt.
def Add(arg1,arg2=2):
 return arg1+arg2
Som man kan se, så har denne metode en parameter arg2 som har en default værdi. Hvis jeg fx. prøver at kalde denne metode i en ironpython consol, så får jeg følgende resultat
IronPython 2.6 (2.6.10920.0) on .NET 2.0.50727.4200
Type "help", "copyright", "credits" or "license" for more information.
>>> def Add(arg1,arg2=2):
...     return arg1+arg2
...
>>> Add(3,1)
4
>>> Add(3)
5

Dvs at jeg kan kalde metoden, med 1 eller 2 parametre og få det forventede resultat. Hvis jeg forsøger at kalde metoden vha dynamics fra C#, får jeg som man kan se herunder samme resultat

[Test]
public void CanHandleOptionalParameterInIronPython()
{
    var py = Python.CreateRuntime();
    dynamic ironPython = py.UseFile("IronPythonFunctions.py");
    Assert.AreEqual(5, ironPython.Add(3));
}

[Test]
public void CanHandleOptionalParameterInIronPython()
{
    var py = Python.CreateRuntime();
    dynamic ironPython = py.UseFile("IronPythonFunctions.py");
    Assert.AreEqual(4, ironPython.Add(3,1));
}

Dvs at man via af dynamics kan udnytte andre sprogs mulighed for optionelle parametre.

Overloading

En anden situation, hvor man i nogen tilfælde, kan udnytte optionelle parametre, er når man laver 1 eller flere overloads af en metode, for at kunne kalde den uden at angive bestemte paramtere. Se fx. følgende klasse

class MethodOverloads
{
    public void DoStuff(int StuffID)
    {
        string name = "Stuff";
        DoStuff(StuffID, name);
    }

    public void DoStuff(int StuffID, string name)
    {
        double factor = 2;
        DoStuff(StuffID, name, factor);
    }

    public void DoStuff(int StuffID, string name, double factor)
    {
        /* DoStuff */
    }
}

Der er egentlig ikke noget i vejen med at lave ovenstående konstruktion af metoder, og med lidt kommentarer kunne man sagtens overskue hvilken af metoderne man skulle kalde. Men man kunne med optionelle parametre istedet skrive følgende

class MethodOverloads
{
   public void DoStuffAllInOne(int StuffID, string name="Stuff", double factor=2)
    {
        /* DoStuff */
    }
}

Her kan man med det samme, når man vil kalde metoden, se hvilke parametre man skal angive. Men det som den sidste løsning giver mulighed for, er at man også kan kalde metoden uden at angive parameteren name. For at kunne gøre dette i det klassiske eksempel, skulle man have lavet endnu en overload. Som man kan se i den følgende test, er der ingen forskel på, hvordan det ser ud når man kalder de to forskellige løsninger

[Test]
public void CallNormalMethod()
{
    MethodOverloads sut = new MethodOverloads();
    Assert.DoesNotThrow(() => sut.DoStuff(1));
    Assert.DoesNotThrow(() => sut.DoStuff(1,"Stuff"));
    Assert.DoesNotThrow(() => sut.DoStuff(1,"Stuff",2));
}

[Test]
public void CallAllInOneMethod()
{
    MethodOverloads sut = new MethodOverloads();exit()
    Assert.DoesNotThrow(() => sut.DoStuffAllInOne(1));
    Assert.DoesNotThrow(() => sut.DoStuffAllInOne(1, "Stuff"));
    Assert.DoesNotThrow(() => sut.DoStuffAllInOne(1, "Stuff", 2));
}

Der er sikkert mange meninger for og imod at bruge optionelle parametre, men det er ihverfald noget som der kan anvendes især, når man arbejder med ms office automation, hvor der typisk er mange optionelle parametre i de forskellige metoder i interfacet.

Næste post

I næste post vil jeg beskrive endnu en ny feature i C#4.0, eller dvs faktisk om to features nemlig Covariance og Contravariance.

2009-11-02

Dynamics i C#4.0

Introduktion

Dette er nummer 2 post i en række af artikler om C#4.0. I den første post, har jeg beskrevet, hvordan jeg har konfigureret visual studio 2010 beta 2 express, til at kunne lave kode eksempler. I denne post, vil jeg kigge nærmere på dynamics, som er en af de nye ting i C#4.0

Overblik

Den nye ved typen dynamic er, at instanser af denne type ikke bliver kontrolleret ved kompilering, men først i runtime. Se fx følgende anvendelse af en dynamic variabel

[TestFixture]
public class UseDynamicTest
{
    [Test]
    public void Dynamic_Property_Test()
    {
        UseDynamic sut = new UseDynamic();
        sut.DynamicObject = "Some String";
        sut.DynamicObject *= 4;
        Assert.AreEqual("Some string"*4,sut.DynamicObject);
    }
}

public class UseDynamic
{
    public dynamic DynamicObject { get; set; }
}

Her tildeler jeg først en streng til en dynamic variabel og bagefter forsøger jeg at gange strengen med 4. Dette får jeg ikke nogen kompile fejl ved. Hvis jeg derimod forsøger at udføre det samme direkte på strengen, som jeg har vist i assert'en, så får jeg som forventet med det samme en kompile fejl.

Hvis jeg ændrer min test så den kan kompilere

[Test]
public void Dynamic_Property_Test()
{
    UseDynamic sut = new UseDynamic();
    sut.DynamicObject = "Some String";
    sut.DynamicObject *= 4;
    Assert.AreEqual("Some string" + "Some string" + 
                    "Some string" + "Some string",
                    sut.DynamicObject); 
}

Får jeg følgende fejl, når jeg forsøger at afvikle testen:

1) Test Error : DynamicTest.UseDynamicTest.Dynamic_Property_Test
   Microsoft.CSharp.RuntimeBinder.RuntimeBinderException : Operator '*=' cannot be applied to operands of type 'string' and 'int'
   at CallSite.Target(Closure , CallSite , Object , Int32 )
   at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
   at DynamicTest.UseDynamicTest.Dynamic_Property_Test()

Jeg kan derimod sagtens skrive og afvikle følgende
[Test]
public void Dynamic_Property_Test()
{
    UseDynamic sut = new UseDynamic();
    sut.DynamicObject = "Some String";
    Assert.IsTrue(sut.DynamicObject is string);
    sut.DynamicObject = 4;
    Assert.IsTrue(sut.DynamicObject is int);
    Assert.AreEqual(4, sut.DynamicObject);
}

Dvs at den dynamiske variabel først er en streng type og derefter en int type uden at jeg skal udføre nogen typecast.

Anvendelses områder

En af de steder, hvor man fremhæver brugen af dynamics, er i forhold til kald af metoder i dynamiske sprog som fx IronPython og IronRuby. Det lyder umiddelbart ikke noget, man kan bruge i .net verdenen, men da jeg først så, hvad det drejer som om - kunne jeg pludselig se en masse muligheder i det.

Jeg viser her et par eksempler på hvordan man kan kalde metoder i IronPython fra C#. For at kunne dette har jeg hentet IronPython 2.6 CTP for .NET 4.0 Beta 2. Der er i skrivende stund også en 2.6 Release Candidate 2, men den har jeg ikke kunne få til at fungere.

Her er først den python fil, som indeholder det script jeg gerne vil kalde:

# IronPythonFunctions.py
import sys;

# Class
class IronPythonFunctions:
 def __init__(self):
  pass;

 def Add(self,arg1,arg2):
  res=arg1+arg2
  print "Add {0} and {1} - Result is {2}".format(arg1,arg2,res)
  return res

# Factory method
def GetIronPythonFunctions():
 return IronPythonFunctions()

En kort forklaring til denne fil, er at den indeholder en klasse "IronPythonFunctions" med en metode "Add" der tager 2 parametre arg1 og arg2. Desuden indeholder filen en metode der returnerer en instans af klassen "IronPythonFunctions".

Jeg så lavet nogle forskellige test af denne klasse fra C#. For at kunne kalde IronPython fra C# har jeg tilføjet en reference til følgende filer fra IronPython 2.6 CTP for .NET 4.0 Beta 2:
IronPython.dll
IronPython.Modules.dll
Microsoft.Dynamic.dll
Microsoft.Scripting.dll
Microsoft.Scripting.Debugging.dll

Først har jeg testet om jeg kan få returneret en instans af IronPythonFunctions
[Test]
public void Can_Get_IronPythonFunctions_Instance()
{
    var py = Python.CreateRuntime();
    dynamic ironPython = py.UseFile("IronPythonFunctions.py");
    dynamic ironPythonFunctions = ironPython.GetIronPythonFunctions();
    Assert.IsNotNull(ironPythonFunctions);
}

Her efter har jeg testet en række forskellige kald af metoden Add på ironPythonFunctions instancen.
[Test]
public void Can_Add_Numbers()
{
    var py = Python.CreateRuntime();
    dynamic ironPython = py.UseFile("IronPythonFunctions.py");
    dynamic ironPythonFunctions = ironPython.GetIronPythonFunctions();
    Assert.AreEqual(5, ironPythonFunctions.Add(2, 3));
}

[Test]
public void Can_Add_Strings()
{
    var py = Python.CreateRuntime();
    dynamic ironPython = py.UseFile("IronPythonFunctions.py");
    dynamic ironPythonFunctions = ironPython.GetIronPythonFunctions();
    Assert.AreEqual("hello world", ironPythonFunctions.Add("hello ", "world"));
}

[Test]
public void Cannot_Add_Strings_And_Numbers()
{
    var py = Python.CreateRuntime();
    dynamic ironPython = py.UseFile("IronPythonFunctions.py");
    dynamic ironPythonFunctions = ironPython.GetIronPythonFunctions();
    Assert.Throws(typeof(ArgumentTypeException),(()=> ironPythonFunctions.Add("hi ", 5)));
}

Som man kan se, er det rimeligt simpelt at kalde metoder fra IronPython - man skal selvfølgelig passe på og lave grundige tests af sin kode, da man jo først ser om det fungerer runtime. Jeg kunne fx have skrevet følgende test
[Test]
public void Can_Multiply_Numbers()
{
    var py = Python.CreateRuntime();
    dynamic ironPython = py.UseFile("IronPythonFunctions.py");
    dynamic ironPythonFunctions = ironPython.GetIronPythonFunctions();
    Assert.AreEqual(25, ironPythonFunctions.Multiply(5, 5));
}
og stadig kunne kompilere uden fejl. Det er først når jeg kører testen eller i værste tilfælde produktionskoden, at jeg får en fejl.

Dette er en meget kort gennemgang af dynamics, men det viser nogenlunde, hvad man kan anvende det til. Man vil helt sikkert snart se mange flere anvendelses muligheder med dynamics end blot kald til IronPython og lignende.

Næste post

I næste post vil jeg kigge på optionelle parametre og hvad de kan bruges til.

2009-10-24

Installation og konfigurering af Visual Studio 2010

View this post in english here: http://www.soerenlarsen.dk/wiki/InstallAndConfig

Introduktion


Dette er den første post i en serie af artikler om C#4.0. Jeg vil her starte med at introducere hvordan jeg har installeret Visual Studio og konfigureret det, så det kan anvendes til denne artikelserie.

Installation

Jeg har valgt at installere en Visual C# 2010 Beta 2 (VCS2010B2), som jeg vil anvende til lave kodeeksempler i. VCS2010B2 kan downloades her: http://www.microsoft.com/express/future/





Tilpasning


Da express versionerne af visual studio ikke har indbygget et unit test framework, har jeg også installeret NUnit. Nunit kan downloades som msi pakke her: http://www.nunit.org/index.php?p=download.

Den aktuelle version af NUnit (2.5.2) er ikke lavet så den understøtter C#4.0 har jeg ved hjælp af følgende beskrivelse ændret, således at den kan. (kilde: http://bit.ly/2j4u9U)

Find filen nunit-console.exe.config i %PROGRAMFILES%/nunit 2.5.2/bin/net-2.0 og tilføj følgende:

Under <configuration> tilføjes:

<startup>
  <requiredRuntime version="v4.0.20506" />
</startup>

og under <runtime> tilføjes:

<loadFromRemoteSources enabled="true" />


Jeg har desuden lavet en genvej til at køre unit tests direkte fra visual studio. Det første man skal gøre, er at aktivere expert settings under menuen tools->settings.
Herefter kan man under punktet tools->external tools oprette følgende:





Command: %PROGRAMFILES%\NUnit 2.5.2\bin\net-2.0\nunit-console.exe
Arguments: $(TargetName).dll /nologo /nodots
Initial directory: $(ProjectDir)/bin/release

Jeg har desuden lavet en genvejs tast, så jeg kan køre unit tests uden at skulle aktivere menuen:
Åben menuen punktet tools>options og find keyboard indstillinger under environment



Har har jeg fundet kommandoen Tools.ExternalCommand1, der refererer til det først oprettede eksterne værktøj. Ved at sætte markøren i feltet Press shortcut keys kan man så vælge den genvejs tast man ønsker og  her har jeg valgt Alt-R og derefter trykket på Assign.


Hello world


Så er det tid til at teste om det hele fungerer.

HelloWorldTest.cs

using NUnit.Framework;
using HelloWorldBLL;

namespace HelloWorldTest
{
    [TestFixture()]
    public class HelloWorldTest
    {
        [Test]
        public void WhenCallHallo_HelloWorldReturns()
        {
            string actual = HelloWorld.SayHallo();
            string expected = "Hello World";
            Assert.That(actual == expected);
        }
    }
}

HelloWorldBLL.cs

namespace HelloWorldBLL
{
    public class HelloWorld
    {
        public static string SayHallo()
        {
            return "Hello World";
        }
    }
}

Resultat

ProcessModel: Default    DomainUsage: Single
Execution Runtime: net-4.0.21006.1

Tests run: 1, Errors: 0, Failures: 0, Inconclusive: 0, Time: 0,082 seconds
  Not run: 0, Invalid: 0, Ignored: 0, Skipped: 0


Som det kan ses her, er det lykkedes at køre testen uden fejl, hvilket betyder at jeg nu kan begynde at arbejde med systemet.

Næste post

I næste post vil jeg kigge nærmere på en af de nye ting, som er en del af C#4.0, nemlig dynamic.

Ny blog

Jeg vil her på denne blog skrive om mine opstartserfaringer med Visual Studio 2010 og C#4.0