Jeg har fornylig lavet min første rigtige ASP.NET MVC løsning.  Det er faktisk super cool at arbejde med.  Det eneste, jeg ikke er så vil med, er behovet for at lave sine views à la klassisk ASP, hvor man bruger <% %> tags.  Til fordel fra klassisk ASP snakker vi dog nu typestærkt C#.  Desværre bliver alle views først kompileret runtime, så eventuelle fejl i views opdages først under debugging.

En virkelig lækker ting ved ASP.NET MVC er, at det er utroligt nemt at få til at spille sammen med Dependency Injection frameworks, som f.eks. Unity.  Med Unity kan man f.eks. uden det store besvær lave constructor DI på sine controller klasser.  Der findes masser af eksempler på det på nettet.

Derudover har jeg gjort mig et par erfaringer, som jeg hermed giver videre:

  • Benyt altid IIS'en til at udvikle på og ikke ASP.NET development server, som VS.NET starter op under debug (det gælder vel altid og ikke kun for MVC).
  • Sørg for at sætte routing og IIS'en op til at benytte .mvc extension fra start af (gælder kun IIS 6) - eller brug url-rewriting.  Jeg har ikke noget problem med at bruge .mvc extensions, for pæne urls er ikke mit formål med MVC, men nogle finder det grimt, og så er url-rewriting tilsyneladende en løsning.
  • Benyt hjælpefunktionerne på Html klassen så meget som muligt.  Det gælder især Html.ActionLink, Html.RouteLine og Html.BeginForm.  På den måde sikrer man, at alle links altid er rigtige ift. websitets root folder.

Følgende stump kode er den anbefalede måde at bruge monitors på, og det er identisk med den kode, som C#'s lock statement genererer.

Monitor.Enter(lockobject);
try
{
    //...
}
finally
{
    Monitor.Exit(lockobject);
}

 

Der er faktisk en potentiel bug i denne kode (iflg. Joe Duffy).  Kompileren er i sin fulde ret til at indsætte instruktioner mellem Monitor.Enter og try.  Et forsøg viser, at C# kompileren ganske rigtigt indsætter en NOP instruktion i debug build. 

Det betyder, at man (hvis man er meget uheldig) kan opleve asynkrone thread exceptions for netop denne instruktion.  Det kan f.eks. ske, hvis en anden tråd kalder Thread.Abort.  Sker det, vil finally blokken aldrig eksekvere, og vi har en såkaldt abandoned monitor.  Det er nok de færreste, for hvilke det er et reelt problem, men det kan ske.

Det viser sig, at C# teamet har taget højde for dette.  I release builds vil C# kompileren ikke indsætte ekstra instruktioner efter Monitor.Enter, og Monitor.Enter er lavet på en speciel måde, så næste instruktion i vores kode ovenfor vil foregå inde i try blokken.

Joe Duffys bog fik mig til at tænke på connections.  Hvis DbConnection.Open står uden for try blokken, kan man i sjældne tilfælde risikere, at finally blokken indeholdende DbConnection.Close ikke bliver kaldt pga. en exception.  Heldigvis er det et krav til DbConnection.Close, at man skal kunne kalde den flere gange uden at få en exception i hovedet uanset om ens connection er åben eller ej.  Derfor kan man med sindsro flytte DbConnection.Open indenfor try blokken.

En gammel ven og Nikon D300 ejer gjorde mig opmærksom på, at der er kommet et nyt website med tips og tricks videoer for Nikon DSLR brugere: http://www.dtowntv.com/

Desværre fokuserer de kun på de nyere kameraer og dermed ikke D80, men derfor er det meget sjovt at se.

Et meget hyppigt anvendt argument for at bruge stored procedures frem for parametriserede queries på SQL Server er performance.  Argumentet er, at eksekveringsplanen for sprocs kan caches på serveren.  Det er korrekt, at sådan forholdt det sig på SQL Server 7 og måske også på 2000, men efter min bedste overbevisning er SQL Server nu i stand til på samme måde at lave caching for parametriserede queries.

Det er svært at finde dokumentation af dette (hvis du har links, så giv dem til mig!), så jeg besluttede at lave et lille forsøg: Jeg lavede en lille testtabel og prøvede at indsætte 100.000 rækker 50 gange med en sproc og med en parametriseret INSERT statement.

Konklusionen?

Jeg kunne ikke se nogen forskel.  Selvfølgelig svinger tiderne noget i forhold til, hvad serveren ellers laver, men man kan ikke se, at den ene metode er hurtigere end den anden.

Der kan selvfølgelig være mange andre argumenter for at sprocs - f.eks. security.  Det vil jeg ikke gå yderligere ind i her, for det plejer at kunne få folk helt op at køre.

Historien er en anden med dynamisk opbygget SQL.  Her kan SQL Server ikke cache.  Men dynamisk opbygget SQL er i forvejen bandlyst pånær i særlige tilfælde, da det øger risikoen for SQL injection.

Jeg synes, at man rundt omkring på forskellige blogs kan spore en vis tvivl om, hvorvidt vi virkelig har brug for alle de programmeringssprog, der skyder frem (se f.eks. Martin Fowler, Daniel F og Neal Ford).

Jeg tror på, at man såvidt muligt skal bruge det rigtige værktøj til at løse en opgave.  Mange inklusive undertegnede lader sig styre af følelser og "religion" ved valg af programmeringssprog.  Det er i bund og grund usagligt og useriøst.  Hvis Excel og VBA er det rigtige værktøj til opgaven, så skal man vælge det.

Derfor er det vigtigt, at man er dygtig til et bredt udvalg af programmeringssprog, så man har et arsenal at vælge fra.  Ellers ender det med, at man strækker sit favoritværktøj til det yderste for at kunne løse en specifik opgave.  I mine øjne er det ligegyldigt, om man bruger VB.NET eller C#.  Man kan have sin favorit, men i bund og grund kan de to sprog det samme.  Det var noget andet med f.eks. VB6 og Visual C++.  De tog værktøjer havde vidt forskellige anvendelser.  Netop VB6 er et godt eksempel på et værktøj, som blev brugt til meget andet end det var beregnet til.  VB6 havde så mange muligheder for at lave lækre memory leaks og andre mærkelige ting, at det i nogle henseender faktisk var sværere at bruge end Visual C++.  Men den typiske VB6 programmør kunne ikke andet.  Til gengæld var den typiske VC++ programmør for stolt til at nedværdige sig til at bruge VB6, selvom det havde været det rigtige valg i nogle tilfælde.

Det stiller høje krav at være god til flere sprog, men de fleste mestrer allerede adskillige sprog som C#, VB.NET, C++, JavaScript, VBS, PowerShell og SQL.  Ellers kommer man jo ingen steder.

Nu har vi så fået F#.

Jeg tror, det er umagen værd at lære F# af én bestemt grund: Parallel processing.

Funktionel programmering har eksisteret i mange år.  Fordelen for mig ved F# ift. f.eks. Erlang er integrationen med .NET.  F# er som udgangspunkt funktionelt, men man kan kommunikere med anden .NET kode, hvis man har behovet.

Man hører mange andre argumenter for at brug F# - herunder god performance.  Jeg tror gerne på, at F# kan give bedre udviklerperformance på nogle typer opgaver som f.eks. matematisk opgaver, som bestemt er relevant for aktuarer.  Mht. runtimeperformance er jeg lidt mere tvivlende.  F# kode kompileres til IL-kode, og IL er i bund og grund objektorienteret.  Så for at kunne lave lækre funktionelle ting som f.eks. ufuldstændige funktionskald (eller hvad det nu hedder på dansk), bliver IL'en virkelig sær.  Det tror jeg vil gå ud over performance i de fleste tilfælde, medmindre selvfølgelig at JIT-kompileren kan gennemskue det og optimere.

Designet bag Linq to Sql er tænkt som et Unit of Work pattern.  Som navnet måske antyder, skal ens DataContext klasse betragtes som en context.  Det betyder, at tanken er, at man holder sin DataContext i live under hele sin session eller for WinForms eventuelt i hele applikationens levetid.  Hvis man åbner og lukker DataContexts i flæng, kan man bl.a. løbe ind i Attach problemer ved updates.  Det betyder også, at de genererede entityklasser skal betragtes som egentlige domæneklasser.

I det følgende vil jeg give et eksempel på, hvorfor jeg ikke synes, at designet for Linq to Sql holder.

Lad os betragte en simpel databasemodel med to tabeller, der har en en-til-mange relation:

database

Lad os sige, at jeg ønsker at trække en bestemt Parent ud, og lidt efter ønsker jeg at trække et tilhørende Child ud med navnet "Child1".  Min første naive tilgang kan se ud som følgende:

using (DataClassesDataContext db = new DataClassesDataContext())
{
    db.Log = Console.Out;

    var p = (from parents in db.Parents
             where parents.ParentId == 1
             select parents).Single();

    // Noget kode...
   
    var c = from children in p.Childs
            where children.Name == "Child1"
            select children;
    Console.WriteLine(c.Count());
}

 

Følgende viser output:

output1

Som det fremgår, er det ikke en god metode.  I den genererede SQL bliver min where-clause fuldstændig ignoreret, og det er først på klienten, at Linq to Sql filtrerer.  Eftersom jeg ikke filterer på primærnøglen, svarer det logisk til en tablescan - dvs. en for-løkke på mit EntitySet, hvor man tester et objekt ad gangen.  Det er ikke smart, hvis jeg har 8 mio. rækker i min Child tabel, som i øvrigt også først skal hives henover netværket.

Den anbefalede løsning fra eksperterne er at bruge en LEFT OUTER JOIN på Child tabellen vha. LoadWith funktionen:

using (DataClassesDataContext db = new DataClassesDataContext())
{
    db.Log = Console.Out;

    DataLoadOptions options = new DataLoadOptions();
    options.LoadWith<Parent>(parent => parent.Childs);
    db.LoadOptions = options;
    db.DeferredLoadingEnabled = false;

    var p = (from parents in db.Parents
                 where parents.ParentId == 1
                 select parents).Single();

    // Noget kode.

    var c = from children in p.Childs
             where children.Name == "Child1"
             select children;
    Console.WriteLine(c.Count());
}

 

Dette giver følgende output:

output2

LoadWith giver en mere effektiv SQL, bortset fra en SELECT COUNT(*) på Child.  Jeg har svært ved at gennemskue, hvorfor den er nødvendig.

Problemet med denne løsning er, at LoadOptions sættes på det "globale" datacontext objekt.  Hvis vi på et senere tidspunkt ønsker at benytte deferred loading på samme DataContext, får vi en InvalidOperationException, for man må ikke ændre LoadOptions efter første load på en DataContext.

Man kan også bruge DataLoadOptions.AssociateWith i stedet for en where-clause på Child:

using (DataClassesDataContext db = new DataClassesDataContext())
{
    db.Log = Console.Out;

    DataLoadOptions options = new DataLoadOptions();
    options.AssociateWith>Parent>(parent => parent.Childs.Where(child => child.Name == "Child1"));
    db.LoadOptions = options;

    var p = (from parents in db.Parents
                 where parents.ParentId == 1
                 select parents).Single();

    // Noget kode.

    var c = from children in p.Childs
             select children;
    Console.WriteLine(c.Count());
}

 

Resultatet er en noget mærkelig SQL med en (unødvendig) nested SQL til at finde parentid, og fordi DataContext.LoadOptions ikke kan ændres, er vi nu til "evig" tid bundet op på én where-clause:

output3

Konklusionen er, at man skal passe meget på, hvordan man bruger Linq to Sql.  Faktisk tør jeg ikke rigtig bruge det efter hensigten, men kun til små lukkede funktionskald, hvor jeg kan tillade mig at smide DataContext væk efter brug.  Men nu er der selvfølgelig også noget, der tyder på, at Linq to Sql vil lide en stille død.

Før collection initializers blev indført med C# 3.0, var det nødvendigt at initialisere static collections i static constructors (medmindre man gjorde det "dovent" ifm. et funktionskald).  Hvis man selv giver en klasse en static constructor, vil C# kompileren fjerne beforefieldinit flaget fra klassen, hvilket i nogle tilfælde kan give dårligere performance.  Med collection initializers kan man fylde data i en static collection inline og dermed beholde beforefieldinit:

static Dictionary<int, string>; someStaticCollection = new Dictionary<int, string>()
{
    { 42, "The answer" },
    { 90, "ne0sans new Nikon camera" }
};

IL'en viser, at C# kompileren putter beforefieldinit på klassen:

beforefieldinit

Jeg kom ved en fejl til at bruge Excel i dag.  Det crashede, og jeg benyttede lejligheden til at trykke på knappen "Send en fejlrapport til Microsoft".  Jeg har engang set en lille "dokumentar"-film fra Microsoft, hvor de påstod, at en Microsoft-medarbejder får stød, hver gang man trykker på knappen.  Jeg bruger den hver gang i håb om, at jeg får ram på en af dem jeg kender.

Efter fejlrapporten var sendt, dukkede følgende besked op, hvor jeg naturligvis trykkede på linket.  Jeg venter stadig på løsningen.

fejlrapportering

Den er da meget sød :-)

Da jeg var ung, kunne man i Visual C++ 6.0 selv definere, hvordan typer skulle vises i watch-vinduet ved at rette i en hemmelig tekstfil i notepad.  Det er også muligt i VS.NET, men det kræver kode i stedet for konfiguration.  Man kan nemlig gøre det vha. DebuggerDisplayAttribute.  I modsætning til VC++ løsningen kan man derfor kun ændre visningen for sine egne typer.

Jeg vil her vise, hvordan DebuggerDisplayAttribute bruges.  Vi tager udgangspunkt i følgende type:

class MyClass
{
    public int Number { get; set; }
    public string Text { get; set; }
}

Følgende skærmbillede af watch-vinduet viser standardvisningen for vores type:

default

Hvis man ønsker at vise noget andet end den ikke specielt sigende tekst "{ConsoleApplication3.MyClass}", er den nemme løsning at override ToString:

public override string ToString()
{
    return String.Format("Number={0}, Text=\"{1}\"", Number, Text);
}

Overrides af ToString vil altid blive kaldt af debuggeren i watch-vinduet.  Med den metode kan man få vist, hvad man vil - f.eks.:

tostring

Hvis man ikke ønsker at override ToString bare for at vise noget pænt ved debugging, er DebuggerDisplayAttribute vejen at gå.  Koden kommer til at se således ud:

[DebuggerDisplay("Number={Number}, Text={Text}")]
class MyClass
{
    public int Number { get; set; }
    public string Text { get; set; }
}

 

Resultatet er det samme som ved brug af ToString.  DebuggerDisplay vil altid blive brugt i stedet for ToString af debuggeren.  Bemærk at debuggeren selv indsætter anførselstegn omkring strenge ved brug af DebuggerDisplay.

Som standard viser watch-vinduet alle properties og fields (public såvel som private), når man klikker på '+' ved siden af variabelnavnet.  Vha. en anden attribute, DebuggerBrowsable, kan man helt fjerne properties fra visningen:

[DebuggerDisplay("Number={Number}, Text={Text}")]
class MyClass
{
    public int Number { get; set; }
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public string Text { get; set; }
}

Det giver følgende visning:

DebuggerBrowsable

Man har også mulighed for helt at ændre denne visning vha. en tredje attribute, DebuggerTypeProxy:

[DebuggerDisplay("Number={Number}, Text={Text}")]
[DebuggerTypeProxy(typeof(MyClassDebugView))]
class MyClass
{
    public int Number { get; set; }
    public string Text { get; set; }

    internal class MyClassDebugView
    {
        MyClass c;

        public MyClassDebugView(MyClass c)
        {
            this.c = c;
        }

        public string NumberAndText { get { return c.Number.ToString() + ", " + c.Text; } }
    }
}

 

Debuggeren kalder constructoren for MyClassDebugView med det objekt af typen MyClass, som bliver vist i watch.  Nedenfor er vist, hvordan det kommer til at se ud i watch-vinduet.  Bemærk at debuggeren selv tilføjer et raw-view.

DebuggerTypeProxy

Så er TechEd vel overstået, og jeg er tilbage i DK.

Det hotte emne i år var parallel programming, så de fleste af mine sessioner handlede om det samt om det nye Geneva framework (the framework formerly known as Zermatt).  Til en session om parellel programming af Joe Duffy lykkedes det mig at vinde en signeret version af hans nye bog (på omkring 43225235 sider):

412yJeVFALL__SS500_

Det blev også til et par andre bøger, som jeg dog måtte betale for:

f# 51UY5afgcFL__BO2,204,203,200_PIsitb-sticker-arrow-click,TopRight,35,-76_AA240_SH20_OU02_  

Jeg havde kameraet med til Barcelona.  Desværre var der ikke tid til megen sightseeing, men her er et lille udpluk:

DSC_0039 DSC_0050 DSC_0084 DSC_0094  DSC_0105 DSC_0088