CodeMaid er for alle os, der gør en dyd ud af at arrangere sin kode pæn og læsbar. Desværre ved vi jo også af erfaring, at man ikke skal være langt inde i et projekt før der bliver fokuseret mere at skidtet virker og kan leveres til tiden, end at koden kan læses næste gang man har næsen nede i linjerne i en given del af applikationen.

Jeg har længe brugt StyleCop til at forcere ensartet kodestil, men nogle gange er det noget trivielt at flyttet rundt på metoder, slette blanke linjer m.v. for at tilfredsstille StyleCop. Dette er heldigvis én af CodeMaids mange features, og yderligere kan metoder, felter etc. sorteres alfabetisk, så navigationen bliver endnu lettere.

Når man så er i gang med at rydde op i sin kode, så kan man også kaste et blik på den McCabe complexity score, som beregnes. Den siger noget om hvor kompleks en metode er og vil give en god indikation af hvor en refaktorering er nødvendig (eller hvor man skal starte sit code review).

Du finder CodeMaid som Visual Studio Extension her. Selvom den aktuelle version pt. er 0.4.2 så er den stabil og fungerer upåklageligt i VS2010.

I det, der efterhånden har udviklet sig til en lille følgeton om FILESTREAMs i SQL Server, er vi nu kommet til FULLTEXT søgning. For selvom SQL Server håndterer FILESTREAM data som eksterne filer, understøttes FULLTEXT søgning i filerne, og søgningen kan endda gøres afhængig af formatet på indholdet i filerne.  HTML filer er et godt eksempel på dette.

Hvis man forestiller sig et website, hvor brugeren har mulighed for at søge i en række artikler, der ligger gemt som HTML, vil brugerne af et sådant website formentlig have en forventning om, at de gennem søgningen kun søger i artiklernes egentlige tekstindhold.  De forventer ikke at kunne søge efter de HTML tags, der tilfældigvis findes i de gemte artikler.  Så hvis der søges efter “h1”, skal søgningen kun give et resultat, hvis en artikels brødtekst rente faktisk indeholder “h1”, og ikke hvis artiklen har en overskrift med et “<h1>” tag.

FULLTEXT søgning i FILESTREAMs understøtter netop et sådant scenarie.  Ved at fortælle SQL Server, hvilket format de gemte filer er i, vil SQL Server sørge for, at søgningen tilpasses filformatet.  Dette gøres ved at angive en TYPE COLUMN, når man opretter sit FULLTEXT INDEX:

   1:  CREATE TABLE [Article]
   2:  (
   3:      [ArticleId] [uniqueidentifier] ROWGUIDCOL  NOT NULL,
   4:      [Title] [nvarchar](4000) NOT NULL,
   5:      [Content] [varbinary](max) FILESTREAM  NULL,
   6:      [FileExtension] [nvarchar](5) NOT NULL,
   7:      CONSTRAINT [PK_Article] PRIMARY KEY CLUSTERED 
   8:      (
   9:          [ArticleId] ASC
  10:      ) ON [PRIMARY] FILESTREAM_ON [FILESTREAM1]
  11:  ) ON [PRIMARY] FILESTREAM_ON [FILESTREAM1]
  12:  GO
  13:   
  14:  CREATE FULLTEXT CATALOG ftCatalog AS DEFAULT;
  15:  GO
  16:   
  17:  CREATE FULLTEXT INDEX ON Article
  18:  (
  19:      Title,
  20:      Content TYPE COLUMN FileExtension
  21:  ) KEY INDEX PK_Article;
  22:  GO

I linje 20 angives, at kolonnen FileExtension indeholder filtypen for Content, hvor artiklernes indhold ligger som FILESTREAM.  For at lade SQL Server foretage en fornuftig søgning i vores HTML artikler, skal vi bare sørge for, at kolonnen FileExtension indeholder teksten “.htm”.

SQL Server understøtter som standard en lang række formater.  Der findes et catalog view kaldet sys.fulltext_document_types, som returnerer de filformater, SQL Server understøtter.  Og flere kan tilføjes – man kan endda selv implementere nogen (i COM).

Med SQL Server 2008 forbedrede Microsoft muligheden for at gemme filer i databasen ved at introducere FILESTREAM.  Med FILESTREAM bliver filer gemt som fysiske filer på disk, mens al tilgang til filerne gik gennem T-SQL.  Dette blev gjort for at forbedre performance betragteligt ift. tidligere versioner af SQL Server, hvor BLOBs blev anvendt til at gemme filer.

Med SQL Server 2012 udbygger Microsoft FILESTREAM yderligere ved at tillade, at man tilgår de fysiske filer direkte gennem Windows I/O API’en.  Den nye funktionalitet kaldes FileTables, og det ser ret interessant ud.  FileTables tillader, at man kan tilgå filerne uden at skulle gå den tunge vej gennem transaktioner, samtidig med at filerne administreres af SQL Server, og dermed indgår i den daglige vedligehold med backups etc.  Fordi FileTables bygger på almindelige filer, tillader det også bruge af standard Windows directories.  FileTables bygger ovenpå FILESTREAM, hvillket betyder, at f.eks. FULLTEXT search virker i filerne.

Det er en længere historie at sætte FILESTREAM og derefter FileTables op i SQL Server 2012.  Når det er gjort, har SQL Server oprettet et fil share på serveren, som man skal bruge til at tilgå filerne, hvis man ønsker at bruge Windows I/O API’en.  Funktionen FileTableRootPath bruges til at få stien til dette share:

   1:  SELECT FileTableRootPath();

For at benytte FileTables kræves en speciel tabel databases.  Denne tabel indeholder en række for hver fil, der gemmes i det oprettede fil share.  Det betyder, at SQL Server intercepter alle I/O til dette fil share.  Filsystemet i Windows understøtter som sådan ikke ACID transaktioner, men så vidt jeg kan forstå, benytter SQL Server diverse locks på tabellen til at styre concurrency

Med et share og en FileTable på plads, kan man nu meget nemt skrive filer til databasen uden brug af SQL (men husk – det er uden en transaktion):

   1:  string rootShare = @"\\machine\blabla";
   2:  for (int i = 0; i < 10; i++)
   3:  {
   4:      File.WriteAllLines(rootShare + String.Format("\\TextFile_{0}.txt", i), new string[] { "Hello " + i });
   5:  }

Følgende SQL afslører, at der rent faktisk er dukket 10 rækker op i vores FileTable:

   1:  SELECT file_stream.GetFileNamespacePath()
   2:  FROM MyFileTable

FileTables er tilgængeligt i den SQL Server 2012, der er til download d.d. (RCO hedder den vist) – og det er også tilgængeligt i Express edition. Sweet.

Det kan undertiden være nyttigt at skulle gemme filer i SQL Server.  Før SQL Server 2008 havde man den mulighed at gemme filer i en BLOB, dvs. man gemte filen binært i en kolonne af type VARBINARY.  Flere har talt imod denne løsning, fordi den – i hvert fald hvis man skal tro rygterne – kunne være ekstremt skadelig for performance.  Flere anbefalede i stedet en løsning, hvor filerne fysisk bliver gemt på disk på et fileshare, og man i stedet nøjes med at gemme filens placering i en kolonne på SQL Server.  Det er naturligvis en lidt skrøbelig løsning, fordi man altid skal huske at opdatere fileshare og database som ét.  Det kan gøres i en transaktion, men det er indiskutabelt en lidt besværlig løsning.

Til det formål introducerede SQL Server 2008 type FILESTREAM.  Med FILESTREAM gemmes filerne fysisk på disk et sted, som SQL Server administrerer, men al tilgang til filerne går gennem SQL Server med T-SQL.

FILESTREAM er som default ikke tilgængeligt på en nyinstalleret SQL Server, så der skal en lille smule arbejde til for at kunne benytte det.

Vha. ADO.NET kan man læse og skrive til en FILESTREAM kolonne.  Det er ikke svært, men det kræver lidt arbejde, hvor man skal have fat i mindre fashionable ting som SqlFileStream og GET_FILESTREAM_TRANSACTION_CONTEXT.  Desuden er der krav om, at både læsning og skrivning omkranses af en transaktion, som man selv er ansvarlig for at oprette.  Alt i alt kunne man godt ønske sig, at der var en lidt mere elegant måde at gøre det på.

Heldigvis findes der en sådan elegant løsning (ellers ville jeg ikke have noget at skrive om), og den findes i Entiry Framework.

Givet følgende tabel med en FILESTREAM kolonne,

   1:  CREATE TABLE Ad
   2:  (
   3:    AdId UNIQUEIDENTIFIER NOT NULL ROWGUIDCOL PRIMARY KEY,
   4:    UserId UNIQUEIDENTIFIER NOT NULL,
   5:    Photo VARBINARY(MAX) FILESTREAM NULL,
   6:    Url NVARCHAR(512) NOT NULL,
   7:    ExpiryDate DATETIME2 NOT NULL
   8:  )

vil Entity Framework automatisk bl.a. generere en property Ad.Photo af typen byte[].  Bemærk i øvrigt, at en tabel, der benytter FIILESTREAM, skal have en ROWGUIDCOL kolonne.  Som i gamle dage er kolonnen med filen stadig af typen VARBINARY, men med et ekstra lille FILESTREAM bagefter, som sørger for al den filbaserede magi.

Med Entity Framework kan man nu gemme sine filer i SQL Server meget nemt ved blot at omsætte sin fil til et byte array:

   1:  using (var db = GetContext())
   2:  {
   3:      Stream fs = File.OpenRead(filePath);
   4:      using (var reader = new BinaryReader(fs))
   5:      {
   6:          var ad = new Ad()
   7:          {
   8:              // Set other properties here.
   9:              Photo = reader.ReadBytes((int) fs.Length)
  10:          };
  11:          db.Ad.AddObject(ad);
  12:          db.SaveChanges();
  13:      }
  14:  }

På tilsvarende vis kan filer læses meget simpelt.  Entity Framework sørger for alt det beskidte arbejde.

Hvis du har en masse test data i en Excel fil er der ingen grund til at kopiere det ind i dine tests - xUnit extensions kan ordne det for dig. Det er endda på en mege elegant måde.

Jeg har noget test data fra et gammel system som indeholder nogle tal for forskellige aldre. F.eks.

Age, Gamma, Sigma, Lambda, S

Jeg har 130 aldre i skridt på et kvart år. Hvert af de øvrige koloner svarer til det ønskede resultat af en funktion med samme navn givet alderen. Der findes altså funktioner Gamma(age), Sigma(age), Lambda(age) og S(age) på min klasse. Jeg vil gerne teste disse funktioner i hver deres test metode - ikke noget med at teste alle mulige funtioner i samme test metode (fy fy).

Det første du skal gøre er at finde dit test data frem i Excel og navngive området - inkl. kolonenavne. Enten ved at du bruger navneboksen til venstre for formellinjen eller "Name Manager" under "Formulas" (Du skal bruge "Name Manager" hvis du vil ændre eller slette navne). Dernæst gemmer du dit test data i Excel 97-2003 format (*.xls). Jeg har ikke fået det til at virke med det nye fancy pancy format.

Nu skriver du dine test metoder og klistrer de magiske attributer på:

[Theory]
[ExcelData("MyData.xls", "SELECT Age, Gamma FROM TestArea51")]
public void Gamma_Test(double age, double expected)
{
  // TODO: Place tests here (sorry about the methodname)
  var actual = ...

  Assert.Equal(expected, actual);  
}

Begge attributer kræver Xunit.Extensions som har sin egen Nuget pakke (og namespace med samme navn).

Det lille fancy SQL statement er muligt fordi vi har valgt at kolonenavne skal indgå i TestArea51 området. Så slipper vi for at ændre vores test hvis vi senere ønsker at udvidere området med mere test data. Bemærk at Excel navne svarer til et område inkl. arknavnet. Man kan altså ikke have samme område navn på forskellige ark - tilgengæld skal man heller ikke angive arknavnet i attributten ovenfor.

Når du kører testen vil den blive kaldt for hver række i dit test data - også selvom nogle rækker skulle fejle. Input data fremgår i test rapport hvis det fejler, så det er nemt at finde de rækker som ikke lever op til dine krav. Vær opmærksom på at GUI test runneren godt kan kløjs lidt i det hvis alt for mange rækker fejler. Jeg har med et snuptag fået 2000+ test bare på dette data - det er ret meget output hvis alle tests fejler.

Jeg vil fremover ikke længere poste på dotninjas.dk under navnet “t4rzsan”, men i stedet bruge det lidt mere sigende navn “Jakob Christensen” Smiley.

Jeg er træt af spam.  Jeg forstår ikke pointen med spam, for det må være meget begrænset, hvad der tjenes af penge på at sende spam ud.  Nogle spam mails indeholder endda kun volapyk, så jeg har ingen idé om, hvorfor folk sender dem ud.  Det er ikke fordi, jeg får oceaner af mails, men der dukker vel omkring 5 spam mails op om dagen.

På det seneste har jeg fået lidt mere seriøse og målrettede spam mails fra østeuropa og Indien med tilbud om at indgå samarbejde om outsourcing af udviklingsarbejde.  Disse mails bærer præg af, at de ikke bare er sendt ud i flæng, og at der rent faktisk står et reelt firma bag, som beskæftiger sig med programmering.  Det skumle ved disse mails er, at de er sendt til en af mine emailadresser, hvor mit navn ikke indgår, og ikke desto mindre er disse mails adresseret til “Jakob”.  Jeg har ingen idé om, hvordan afsenderen har fundet ud af at koble mailadresse og navn sammen, og det gør mig mistænksom.

Derfor har jeg en mistanke om, at et eller andet firma et eller andet sted har solgt min emailadresse.

Inspireret af Jeff Blankenburg har jeg derfor nu opsat en catchall emailadresse på mit domæne styrdindiabetes.dk, og hver gang jeg opretter en profil ved registrering på et eller andet website, benytter jeg en emailadresse opfundet til formålet.  Emailadressen findes ikke i virkeligheden, men når man sender til den, ryger den i min catchall mailbox, og jeg kan se, hvilken adresse, der er sendt til.

Hvis jeg f.eks. skal købe noget fra www.foobar.com, oplyser jeg mailadressen foobar på domænet styrdindiabetes.dk.  På den måde kan jeg forhåbentlig se, hvem synderen er, hvis jeg en dag begynder at modtage spam på den email.

Ulempen ved en catchall er, at man risikerer spam fra folk, der bare spammer tilfældige mailadresser på domænet.

Nu vil tiden vise, om idéen holder.

Mark Seemann skrev forleden et godt indlæg om de udfordringer, man møder, når man skal mappe DTO objekter til domain objekter og videre til viewmodel objekter.

Jeg er en af dem, der har forsøgt sig med at snyde og lade DTO objekter vandre op igennem forretningslaget, og det bliver hurtigt grimt. Bare det at DTO klasserne alle har default constructor gør, at vedligeholdelse af koden bliver en pine. Mark er inde på, at mapning mellem DTO objekter og domain objekter kan være svær, fordi ORM frameworks ikke understøtter mapping, når der ikke findes en default constructor og alle propeties ikke har en setter.

Jeg har haft nogenlunde succes med at bruge AutoMapper til det. Det viser sig, at AutoMapper er i stand til at mappe properties på source objektet til constructor parametre på destination objektet. Man skal bare sørge for, at parametre og properties hedder det samme (de behøver ikke have samme casing), hvilket i mine øjne i øvrigt er god ting at have i sin kodestandard alligevel (konsistent navngivning).

Lad os tage udgangspunkt i følgende klasser Track og DbTrack (tyvstjålet fra Mark’s indlæg):

   1:  public class Track
   2:  {
   3:      private readonly int id;
   4:      private string name;
   5:      private string artist;
   6:   
   7:      public Track(int id, string name, string artist)
   8:      {
   9:          if (name == null)
  10:              throw new ArgumentNullException("name");
  11:          if (artist == null)
  12:              throw new ArgumentNullException("artist");
  13:   
  14:          this.id = id;
  15:          this.name = name;
  16:          this.artist = artist;
  17:      }
  18:   
  19:      public int Id
  20:      {
  21:          get { return this.id; }
  22:      }
  23:   
  24:      public string Name
  25:      {
  26:          get { return this.name; }
  27:          set
  28:          {
  29:              if (value == null)
  30:                  throw new ArgumentNullException("value");
  31:   
  32:              this.name = value;
  33:          }
  34:      }
  35:   
  36:      public string Artist
  37:      {
  38:          get { return this.artist; }
  39:          set
  40:          {
  41:              if (value == null)
  42:                  throw new ArgumentNullException("value");
  43:   
  44:              this.artist = value;
  45:          }
  46:      }
  47:  }
  48:   
  49:  public class DbTrack
  50:  {
  51:      public int Id { get; set; }
  52:      public string Name { get; set; }
  53:      public string Artist { get; set; }
  54:  }

Track har ikke en default constructor og har setter properties for Name og Artist, men ikke for Id. AutoMapper kan – uden nogen egentlig konfiguration – benyttes til at mappe fra DbTrack til Track på følgende måde:

   1:  Mapper.CreateMap<DbTrack, Track>();
   2:   
   3:  var source = new DbTrack()
   4:  {
   5:      Id = 12,
   6:      Name = "Name12",
   7:      Artist = "PSB"
   8:  };
   9:   
  10:  var destination = Mapper.Map<Track>(source);

Kaldet til Mapper.Map i linje 10 vil først kalde constructoren på Track med de rigtige parametre bestemt ud fra parametrenes navne, og da Track klassen har setters på to properties, vil disse properties blive kaldt efterfølgende. Hvis Track klassen kun havde getters på alle properties, ville det naturligvis kun være constructoren, der blev kaldt. Hvis Track klassen har parametre i constructoren, som har navne, der ikke kan matches med properties på DbTrack klassen, vil AutoMapper fejle med en exception.

TFS er en god ting at have (især når man tidligere brugte VSS), men da antallet af aktive udviklere lige i min afdeling (som er et aktuariat) kan tælles på en finger, er “vi” ikke storforbrugere af de avancerede TFS features med automatisk builds og what-not.  Der er derfor nok at lære, når man ind imellem støder på noget ud over det lidt mere sædvanlige som check-ins og bug reports.

Forleden løb jeg ind i problemer, da jeg havde fat i et projekt, der kun sjældent bliver rettet i.  Det viste sig, at en kollega, der ikke længere sidder i aktuariatet, havde nogle udestående pending changes på nogle filer.

En søgning ledte mig hurtigt til “tf undo” kommandoen, som giver mulighed for at lave undo på pending changes for andre brugere i andre workspaces.  Så med følgende kommando var jeg hurtigt kørende igen:

tf undo /workspace:kollegasmaskinnavn;kollegasinitialer /recursive $/Foldernavn/Projektnavn /server:TFSservernavn

Jeg har for lang tid siden skrevet et indlæg på dotninjas om, hvordan man laver en single file generator (også kaldet et custom tool) til Visual Studio.  Desværre ligger det så lang tid tilbage, at jeg ikke kan grave et link frem.  En single file generator benyttes mange steder til at genere kode ud fra indholdet af en anden fil.  For eksempel benyttes det af Entity Framework til at lave klasser ud fra en XML fil som vist på billedet herunder, hvor Custom Tool er sat til “EntityModelCodeGenerator”.

CustomTool

For at lave en Single File Generator skal man implementere nogle bestemte COM interfaces og registrere sin dll på en bestemt måde i registry, og metoden fra min gamle (bortkomne) artikel har fungeret fint indtil fornylig, hvor jeg langt om længe fik mig en Windows 7 64 bit boks på arbejdet.  Jeg har prøvet alt på den maskine både med Wow32To64, og hvad ved jeg.  Men VS 2010 blev ved med fuldstændig at ignorere mit gamle custom tool.

På MSDN er der dokumentation af, hvordan man laver Single File Generators, og der findes også et sample i VS SDK’en, som beskriver, hvordan man kan lave en Single File Generator vha. VS Extensions.  Eksemplet virker fint, men det er ikke særlig godt beskrevet.  Især er det ikke beskrevet, hvordan man sætter projektet op til at blive kompileret helt korrekt, så registry entries bliver genereret korrekt.  Det håber jeg, at dette indlæg kan råde bod på.

Målet er at lave en Single File Generator, der nemt kan distribueres vha. af en .vsix pakke, så den nemt kan installeres som en Visual Studio Extension.  Dette er noget nemmere end min gamle metode, hvor der skulle registreres en dll vha. regasm.exe.

Det er en forudsætning, at man har Visual Studio Service Pack 1 samt Visual Studio SDK Service Pack 1 installeret.  Når SDK’en er installeret, er der adgang til et par VS projekttemplates, som kan bruges til at lave VS Extensions.  Til at lave en Single File Generator, skal man bruge den template, der hedder “VSIX Project”.

vsixproject

Efter projektet er oprettet, skal man først tilføje nogle referencer til en række dll’er for at få fat i de rigtige COM interfaces.  Det drejer sig om følgende:

  • EnvDTE
  • EnvDTE80
  • Microsoft.VisualStudio.Designer.Interfaces
  • Microsoft.VisualStudio.OLE.Interop
  • Microsoft.VisualStudio.Shell.10.0
  • Microsoft.VisualStudio.Immutable.10.0
  • Microsoft.VisualStudio.Interop
  • Microsoft.VisualStudio.Interop.8.0
  • Microsoft.VisualStudio.Interop.9.0
  • Microsoft.VisualStudio.Interop.10.0
  • VSLangProj
  • VSLangProj2
  • VSLangProj80

Det er muligt, at nogle enkelte referencer ikke er nødvendige.  Det har jeg ikke testet.  Det er i øvrigt vigtigt, at alle referencer har “Embed Interop Types” sat til “False”.

EmbedInteropType

Med det ovennævnte sample følger nogle standardimplementationer af to interfaces IVsSingleFileGenerator og IObjectWithSite.  Det nemmeste er at bruge disse implementationer, så vi kopierer BaseCodeGenerator.cs og BaseCodeGeneratorWithSite.cs.  Bemærk, at BaseCodeGenerator indeholder nogle Trace.WriteLine statements, man skal fjerne for at kompilere, hvis man kopierer de to klasser direkte fra eksempelkoden.

Dernæst tilføjes filen CodeGeneratorRegistrationAttribute.cs, som kommer med SDK’en, og som standard ligger under “C:\Program Files (x86)\Microsoft Visual Studio 2010 SDK SP1\VisualStudioIntegration\Common\Source\CSharp\RegistrationAttributes\”.  Denne klasse indeholder en implementation af en custom attribute, som VS benytter til at generere de nødvendige registry entries i en såkaldt .pkgdef fil.  Mere om det om lidt.

Det er nu tiden til at implementere selve kodegenereringen.  Dette gøres ved at tilføje en klasse, som skal nedarve fra BaseCodeGeneratorWithSite.

    [ComVisible(true)]
    [Guid("A4008ECD-02C6-418F-A0BD-083461479064")]
    [CodeGeneratorRegistration(typeof(DotNinjasCodeGenerator), 
        "DotNinjas sample C# code generator", 
        vsContextGuids.vsContextGuidVCSProject, 
        GeneratesDesignTimeSource = true)]
    [ProvideObject(typeof(DotNinjasCodeGenerator))]
    public class DotNinjasCodeGenerator : BaseCodeGeneratorWithSite
    {
        protected override byte[] GenerateCode(string inputFileContent)
        {
            return Encoding.ASCII.GetBytes("// Some generated code goes here");
        }
    }

Ikke overraskende er det metoden GenerateCode, der kaldes af VS, når man kører sit custom tool.  Parameteren inputFileContent vil indeholde indholdet af den fil, som vores custom tool bliver anvendt på.

Bemærk den imponerende række attributes på klassen.

Klassen skal nødvendigvis være COM visible og dermed også have en Guid attribute (husk også at gøre hele assembly COM visible).  CodeGeneratorRegistration benyttes som nævnt til at generere de nødvendige registry settings automatisk.  Disse registry settings ryger i en .pkgdef fil.  Bemærk vsContextGuids.vsContextGuidVCSProject som angiver, at vores kodegenerator vil generere C# kode.  ProvideObject fortæller VS, hvilken klasse der skal oprettes til at generere koden.  Det er et krav, at klassen markeret med ProvideObject skal have en default constructor.  Bemærk at det lidt overraskende er ProvideObjectAttribute, der bestemmer navnet på vores generator.  Dvs. i dette eksempel er det “DotNinjasCodeGenerator”, der skal benyttes som navnet på vores custom tool i VS.

Projektet er nu klar til at kompilere, og der kommer ganske rigtigt en .vsix fil ud som resultat.  Desværre er det ikke nok.  Der mangler en dll og der mangler en .pkgdef fil med de nødvendige registry entries.  For mig var det lidt et mysterium, hvordan jeg fik disse filer genereret, men det kræver, at man redigerer direkte i .csproj filen.  Så start din favorit csproj editor (såsom notepad) og slet følgende linjer:

    <GeneratePkgDefFile>false</GeneratePkgDefFile>
    <IncludeAssemblyInVSIXContainer>false</IncludeAssemblyInVSIXContainer>
    <IncludeDebugSymbolsInVSIXContainer>false</IncludeDebugSymbolsInVSIXContainer>
    <IncludeDebugSymbolsInLocalVSIXDeployment>false</IncludeDebugSymbolsInLocalVSIXDeployment>
    <CopyBuildOutputToOutputDirectory>false</CopyBuildOutputToOutputDirectory>
    <CopyOutputSymbolsToOutputDirectory>false</CopyOutputSymbolsToOutputDirectory>

GeneratePkgDefFile skal væk, så vi får vores .pkgdef fil, og CopyBuiltOutputToOutputDirectory forhindrer, at der bliver kompileret en dll.  Tilføj desuden følgende PropertyGroup i .csproj filen og gem den:

  <PropertyGroup>
    <RegisterOutputPackage>true</RegisterOutputPackage>
    <RegisterWithCodebase>true</RegisterWithCodebase>
  </PropertyGroup>

Efter reload af den nye projektfil, vil en kompilering give følgende output:

projectoutput

Man kan nu teste pakken ved at trykke F5 for debug, hvilket vil starte en ny instans af VS, hvor man kan afprøve vores custom tool.  Vi vil i stedet prøve vores vores custom tool i den virkelige verden.  Så start med at lukke alle instanser af VS og dobbeltklik på .vsix filen og tryk “Install”.  Dette vil installere vores custom tool som en VS Extension, og ved opspart af VS vil man ganske rigtigt kunne finde DotNinjasSingleFileGenerator som installeret extension.  Ønsker man at angive en lidt mere sigende beskrivelse end bare “Empty VSIX project”, kan det gøre ved at rette i source.extension.vsixmanifest filen i VS.

extension

For at teste DotNinjasSingleFileGenerator, opret et nyt C# projekt, tilføj en fil af en type, som normalt ikke benyttes i et C# projekt (f.eks. en .txt fil men ikke en .cs fil) og angiv DotNinjasCodeGenerator som custom tool, da det var DotNinjasCodeGenerator, der blev angivet som type til ProviderObjectAttribute.

customtoolinaction

Højreklik på din fil og vælg “Run custom tool”, hvilket vil resultere i en .cs fil indholdende den genererede kode.