Dela via


Självstudie: Skapa en anpassad uppgift för kodgenerering

I den här självstudien skapar du en anpassad uppgift i MSBuild i C# som hanterar kodgenerering och sedan använder du uppgiften i en version. Det här exemplet visar hur du använder MSBuild för att hantera de rena och återskapande åtgärderna. Exemplet visar också hur du stöder inkrementell version, så att koden endast genereras när indatafilerna har ändrats. De tekniker som visas gäller för en mängd olika scenarier för kodgenerering. Stegen visar också användningen av NuGet för att paketera uppgiften för distribution, och självstudien innehåller ett valfritt steg för att använda BinLog-visningsprogrammet för att förbättra felsökningsupplevelsen.

Förutsättningar

Du bör ha en förståelse för MSBuild-begrepp som uppgifter, mål och egenskaper. Se MSBuild-begrepp.

Exemplen kräver MSBuild, som installeras med Visual Studio, men som också kan installeras separat. Se även Ladda ned MSBuild utan Visual Studio.

Introduktion till kodexemplet

Exemplet tar en indatatextfil som innehåller värden som ska anges och skapar en C#-kodfil med kod som skapar dessa värden. Även om det är ett enkelt exempel kan samma grundläggande tekniker tillämpas på mer komplexa scenarier för kodgenerering.

I den här självstudien skapar du en anpassad MSBuild-uppgift med namnet AppSettingStronglyTyped. Uppgiften läser en uppsättning textfiler och varje fil med rader med följande format:

propertyName:type:defaultValue

Koden genererar en C#-klass med alla konstanter. Ett problem bör stoppa bygget och ge användaren tillräckligt med information för att diagnostisera problemet.

Den fullständiga exempelkoden för den här handledningen finns på Anpassad uppgift – kodgenerering i .NET-exempelarkivet på GitHub.

Skapa Projektet AppSettingStronglyTyped

Skapa ett .NET Standard-klassbibliotek. Ramverket ska vara .NET Standard 2.0.

Observera skillnaden mellan fullständig MSBuild (den som Visual Studio använder) och den bärbara MSBuild, den som paketerades på .NET Core-kommandoraden.

  • Fullständig MSBuild: Den här versionen av MSBuild finns vanligtvis i Visual Studio. Körs på .NET Framework. Visual Studio använder detta när du kör Build på din lösning eller ditt projekt. Den här versionen är också tillgänglig från en kommandoradsmiljö, till exempel Kommandotolken för Visual Studio Developer eller PowerShell.
  • .NET MSBuild: Den här versionen av MSBuild paketeras på .NET Core-kommandoraden. Den körs på .NET Core. Visual Studio anropar inte direkt den här versionen av MSBuild. Det stöder endast projekt som bygger med Microsoft.NET.Sdk.

Om du vill dela kod mellan .NET Framework och andra .NET-implementeringar, till exempel .NET Core, bör biblioteket rikta in sig på .NET Standard 2.0och du vill köra i Visual Studio, som körs på .NET Framework. .NET Framework stöder inte .NET Standard 2.1.

Välj den MSBuild API-version som ska refereras till

När du kompilerar en anpassad uppgift bör du referera till den version av MSBuild API (Microsoft.Build.*) som matchar den lägsta versionen av Visual Studio och/eller .NET SDK som du förväntar dig att stödja. För att till exempel stödja användare i Visual Studio 2019 bör du skapa mot MSBuild 16.11.

Skapa den anpassade arbetsuppgiften AppSettingStronglyTyped MSBuild

Det första steget är att skapa den anpassade MSBuild-uppgiften. Information om hur du skriver en anpassad MSBuild-uppgift kan hjälpa dig att förstå följande steg. En anpassad MSBuild-uppgift är en klass som implementerar ITask-gränssnittet.

  1. Lägg till en referens till Microsoft.Build.Utilities.Core- NuGet-paketet och skapa sedan en klass med namnet AppSettingStronglyTyped som härletts från Microsoft.Build.Utilities.Task.

  2. Lägg till tre egenskaper. Dessa egenskaper definierar parametrarna för den uppgift som användarna anger när de använder uppgiften i ett klientprojekt:

    //The name of the class which is going to be generated
    [Required]
    public string SettingClassName { get; set; }
    
    //The name of the namespace where the class is going to be generated
    [Required]
    public string SettingNamespaceName { get; set; }
    
    //List of files which we need to read with the defined format: 'propertyName:type:defaultValue' per line
    [Required]
    public ITaskItem[] SettingFiles { get; set; }
    

    Aktiviteten bearbetar SettingFiles och genererar en klass SettingNamespaceName.SettingClassName. Den genererade klassen har en uppsättning konstanter baserat på textfilens innehåll.

    Uppgiftsutdata ska vara en sträng som ger filnamnet för den genererade koden:

    // The filename where the class was generated
    [Output]
    public string ClassNameFile { get; set; }
    
  3. När du skapar en anpassad uppgift ärver du från Microsoft.Build.Utilities.Task. Om du vill implementera uppgiften åsidosätter du metoden Execute(). Metoden Execute returnerar true om aktiviteten lyckas och false annat. Task implementerar Microsoft.Build.Framework.ITask och tillhandahåller standardimplementeringar av vissa ITask medlemmar och dessutom vissa loggningsfunktioner. Det är viktigt att du skickar status till loggen för att diagnostisera och felsöka aktiviteten, särskilt om ett problem uppstår och uppgiften måste returnera ett felresultat (false). Vid fel signalerar klassen felet genom att anropa TaskLoggingHelper.LogError.

    public override bool Execute()
    {
        //Read the input files and return a IDictionary<string, object> with the properties to be created. 
        //Any format error it will return false and log an error
        var (success, settings) = ReadProjectSettingFiles();
        if (!success)
        {
                return !Log.HasLoggedErrors;
        }
        //Create the class based on the Dictionary
        success = CreateSettingClass(settings);
    
        return !Log.HasLoggedErrors;
    }
    

    Uppgifts-API:et tillåter att man returnerar falskt, vilket indikerar fel, utan att visa användaren vad som gick fel. Det är bäst att returnera !Log.HasLoggedErrors i stället för en boolesk kod och logga ett fel när något går fel.

Loggfelaktigheter

Det bästa sättet när du loggar fel är att ange information, till exempel radnummer och en distinkt felkod när du loggar ett fel. Följande kod parsar textindatafilen och använder metoden TaskLoggingHelper.LogError med radnumret i textfilen som skapade felet.

private (bool, IDictionary<string, object>) ReadProjectSettingFiles()
{
    var values = new Dictionary<string, object>();
    foreach (var item in SettingFiles)
    {
        int lineNumber = 0;

        var settingFile = item.GetMetadata("FullPath");
        foreach (string line in File.ReadLines(settingFile))
        {
            lineNumber++;

            var lineParse = line.Split(':');
            if (lineParse.Length != 3)
            {
                Log.LogError(subcategory: null,
                             errorCode: "APPS0001",
                             helpKeyword: null,
                             file: settingFile,
                             lineNumber: lineNumber,
                             columnNumber: 0,
                             endLineNumber: 0,
                             endColumnNumber: 0,
                             message: "Incorrect line format. Valid format prop:type:defaultvalue");
                             return (false, null);
            }
            var value = GetValue(lineParse[1], lineParse[2]);
            if (!value.Item1)
            {
                return (value.Item1, null);
            }

            values[lineParse[0]] = value.Item2;
        }
    }
    return (true, values);
}

Med hjälp av de tekniker som visas i föregående kod visas fel i syntaxen för textindatafilen som byggfel med användbar diagnostikinformation:

Microsoft (R) Build Engine version 17.2.0 for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.

Build started 2/16/2022 10:23:24 AM.
Project "S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild" on node 1 (default targets).
S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\error-prop.setting(1): error APPS0001: Incorrect line format. Valid format prop:type:defaultvalue [S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild]
Done Building Project "S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild" (default targets) -- FAILED.

Build FAILED.

"S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild" (default target) (1) ->
(generateSettingClass target) ->
  S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\error-prop.setting(1): error APPS0001: Incorrect line format. Valid format prop:type:defaultvalue [S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild]

     0 Warning(s)
     1 Error(s)

När du får undantag i uppgiften använder du metoden TaskLoggingHelper.LogErrorFromException. Detta kommer att förbättra felrapporteringen, till exempel genom att hämta anropsstacken där undantaget utlöstes.

catch (Exception ex)
{
    // This logging helper method is designed to capture and display information
    // from arbitrary exceptions in a standard way.
    Log.LogErrorFromException(ex, showStackTrace: true);
    return false;
}

Implementeringen av de andra metoderna som använder dessa indata för att skapa texten för den genererade kodfilen visas inte här. se AppSettingStronglyTyped.cs i exempelrepo.

Exempelkoden genererar C#-kod under byggprocessen. Uppgiften är som vilken annan C#-klass som helst, så när du är klar med den här självstudien kan du anpassa den och lägga till de funktioner som krävs för ditt eget scenario.

Generera en konsolapp och använd den anpassade uppgiften

I det här avsnittet skapar du en .NET Core-standardkonsolapp som använder uppgiften.

Viktig

Det är viktigt att undvika att generera en anpassad MSBuild-uppgift i samma MSBuild-process som kommer att använda den. Det nya projektet ska finnas i en separat Visual Studio-lösning, eller så använder det nya projektet en dll som genereras i förväg och återplaceras från standardutdata.

  1. Skapa .NET Console-projektet MSBuildConsoleExample i en ny Visual Studio-lösning.

    Det normala sättet att distribuera en uppgift är via ett NuGet-paket, men under utveckling och felsökning kan du inkludera all information om .props och .targets direkt i programmets projektfil och sedan gå över till NuGet-formatet när du distribuerar uppgiften till andra.

  2. Ändra projektfilen så att den använder kodgenereringsaktiviteten. Kodlistan i det här avsnittet visar den ändrade projektfilen efter att ha refererat till uppgiften, angett indataparametrarna för aktiviteten och skrivit målen för hantering av rena och återskapade åtgärder så att den genererade kodfilen tas bort som förväntat.

    Uppgifter registreras med hjälp av elementet UsingTask (MSBuild). Elementet UsingTask registrerar uppgiften. den talar om för MSBuild namnet på uppgiften och hur du hittar och kör sammansättningen som innehåller aktivitetsklassen. Sammansättningssökvägen är relativ till projektfilen.

    PropertyGroup innehåller de egenskapsdefinitioner som motsvarar de egenskaper som definierats i aktiviteten. De här egenskaperna anges med hjälp av attribut och aktivitetsnamnet används som elementnamn.

    TaskName är namnet på uppgiften som ska refereras till från sammansättningen. Det här attributet bör alltid använda fullständigt angivna namnområden. AssemblyFile är filsökvägen för kompileringen.

    Om du vill anropa aktiviteten lägger du till aktiviteten i rätt mål, i det här fallet GenerateSetting.

    Målet ForceGenerateOnRebuild hanterar rengörings- och återuppbyggnadsoperationerna genom att ta bort den genererade filen. Den är inställd på att köras efter CoreClean-målet genom att ange attributet AfterTargets till CoreClean.

    <Project Sdk="Microsoft.NET.Sdk">
        <UsingTask TaskName="AppSettingStronglyTyped.AppSettingStronglyTyped" AssemblyFile="..\..\AppSettingStronglyTyped\AppSettingStronglyTyped\bin\Debug\netstandard2.0\AppSettingStronglyTyped.dll"/>
    
        <PropertyGroup>
            <OutputType>Exe</OutputType>
            <TargetFramework>net6.0</TargetFramework>
            <RootFolder>$(MSBuildProjectDirectory)</RootFolder>
            <SettingClass>MySetting</SettingClass>
            <SettingNamespace>MSBuildConsoleExample</SettingNamespace>
            <SettingExtensionFile>mysettings</SettingExtensionFile>
        </PropertyGroup>
    
        <ItemGroup>
            <SettingFiles Include="$(RootFolder)\*.mysettings" />
        </ItemGroup>
    
        <Target Name="GenerateSetting" BeforeTargets="CoreCompile" Inputs="@(SettingFiles)" Outputs="$(RootFolder)\$(SettingClass).generated.cs">
            <AppSettingStronglyTyped SettingClassName="$(SettingClass)" SettingNamespaceName="$(SettingNamespace)" SettingFiles="@(SettingFiles)">
            <Output TaskParameter="ClassNameFile" PropertyName="SettingClassFileName" />
            </AppSettingStronglyTyped>
            <ItemGroup>
                <Compile Remove="$(SettingClassFileName)" />
                <Compile Include="$(SettingClassFileName)" />
            </ItemGroup>
        </Target>
    
        <Target Name="ForceReGenerateOnRebuild" AfterTargets="CoreClean">
            <Delete Files="$(RootFolder)\$(SettingClass).generated.cs" />
        </Target>
    </Project>
    

    Not

    I stället för att åsidosätta ett mål som CoreCleananvänder den här koden ett annat sätt att ordna målen (BeforeTarget och AfterTarget). SDK-liknande projekt har en implicit import av mål efter den sista raden i projektfilen. Det innebär att du inte kan åsidosätta standardmål om du inte anger dina importer manuellt. Se åsidosätt fördefinierade mål.

    Attributen Inputs och Outputs hjälper MSBuild att bli effektivare genom att tillhandahålla information för inkrementella versioner. Datumen för indata jämförs med utdata för att se om målet behöver köras, eller om utdata från den tidigare versionen kan återanvändas.

  3. Skapa en textfil med det tillägg som ska upptäckas. Använd standardtillägget och skapa MyValues.mysettings i roten med följande innehåll:

    Greeting:string:Hello World!
    
  4. Bygg igen, och den genererade filen ska skapas och byggas. Kontrollera projektmappen för filen MySetting.generated.cs.

  5. Klassen MySetting finns i fel namnområde, så gör nu en ändring för att använda vårt appnamnområde. Öppna projektfilen och lägg till följande kod:

    <PropertyGroup>
        <SettingNamespace>MSBuildConsoleExample</SettingNamespace>
    </PropertyGroup>
    
  6. Återskapa igen och observera att klassen finns i MSBuildConsoleExample namnrymd. På så sätt kan du omdefiniera det genererade klassnamnet (SettingClass), texttilläggsfilerna (SettingExtensionFile) som ska användas som indata och platsen (RootFolder) för dem om du vill.

  7. Öppna Program.cs och ändra den hårdkodade "Hello World!" till den användardefinierade konstanten:

    static void Main(string[] args)
    {
        Console.WriteLine(MySetting.Greeting);
    }
    

Kör programmet; den skriver ut hälsningen från den genererade klassen.

(Valfritt) Logga händelser under byggprocessen

Det går att kompilera med ett kommandoradskommando. Gå till projektmappen. Du använder alternativet -bl (binär logg) för att generera en binär logg. Den binära loggen har användbar information för att veta vad som händer under byggprocessen.

# Using dotnet MSBuild (run core environment)
dotnet build -bl

# or full MSBuild (run on net framework environment; this is used by Visual Studio)
msbuild -bl

Båda kommandona genererar en loggfil msbuild.binlog, som kan öppnas med MSBuild Binary och Structured Log Viewer. Alternativet /t:rebuild innebär att köra målet för ombyggnad. Det framtvingar återskapande av den genererade kodfilen.

Grattis! Du har skapat en uppgift som genererar kod och använt den i en version.

Paketera uppgiften för distribution

Om du bara behöver använda din anpassade uppgift i några projekt eller i en enda lösning kan det vara allt du behöver använda uppgiften som en rå sammansättning, men det bästa sättet att förbereda din uppgift att använda den någon annanstans eller dela den med andra är som ett NuGet-paket.

MSBuild-uppgiftspaket har några viktiga skillnader jämfört med bibliotekets NuGet-paket:

  • De måste paketera sina egna sammansättningsberoenden i stället för att exponera dessa beroenden för det förbrukande projektet
  • De paketerar inte nödvändiga sammansättningar i en lib/<target framework> mapp, eftersom det skulle göra att NuGet inkluderar sammansättningarna i alla paket som använder uppgiften
  • De behöver bara kompilera mot Microsoft.Build-samlingar – under körning tillhandahålls dessa av den faktiska MSBuild-motorn och behöver därför inte ingå i paketet.
  • De genererar en särskild .deps.json fil som hjälper MSBuild att läsa in aktivitetens beroenden (särskilt interna beroenden) på ett konsekvent sätt

För att uppnå alla dessa mål måste du göra några ändringar i standardprojektfilen utöver de som du kanske är bekant med.

Skapa ett NuGet-paket

Att skapa ett NuGet-paket är det rekommenderade sättet att distribuera din anpassade uppgift till andra.

Förbereda för att generera paketet

Förbered för att generera ett NuGet-paket genom att göra några ändringar i projektfilen för att ange den information som beskriver paketet. Den första projektfilen som du skapade liknar följande kod:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.0.0" />
    </ItemGroup>

</Project>

Om du vill generera ett NuGet-paket lägger du till följande kod för att ange egenskaperna för paketet. Du kan se en fullständig lista över MSBuild-egenskaper som stöds i Pack-dokumentationen:

<PropertyGroup>
    ... 
    <IsPackable>true</IsPackable>
    <Version>1.0.0</Version>
    <Title>AppSettingStronglyTyped</Title>
    <Authors>Your author name</Authors>
    <Description>Generates a strongly typed setting class base on a text file.</Description>
    <PackageTags>MyTags</PackageTags>
    <Copyright>Copyright ©Contoso 2022</Copyright>
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    ...
</PropertyGroup>

Egenskapen CopyLocalLockFileAssemblies krävs för att kontrollera att beroenden kopieras till utdatakatalogen.

Markera beroenden som privata

Beroendena för din MSBuild-uppgift måste paketeras i paketet. de kan inte uttryckas som normala paketreferenser. Paketet exponerar inga vanliga beroenden för externa användare. Detta tar två steg att utföra: markera dina sammansättningar som privata och faktiskt bädda in dem i det genererade paketet. I det här exemplet förutsätter vi att din uppgift är beroende av Microsoft.Extensions.DependencyInjection för att fungera, så lägg till en PackageReference till Microsoft.Extensions.DependencyInjection i version 6.0.0.

<ItemGroup>
    <PackageReference 
        Include="Microsoft.Build.Utilities.Core"
        Version="17.0.0" />
    <PackageReference
        Include="Microsoft.Extensions.DependencyInjection"
        Version="6.0.0" />
</ItemGroup>

Markera nu alla beroenden för det här aktivitetsprojektet, både PackageReference och ProjectReference med attributet PrivateAssets="all". Tala om för NuGet att inte exponera dessa beroenden till några projekt alls. Du kan läsa mer om att kontrollera beroendetillgångar i NuGet-dokumentationen.

<ItemGroup>
    <PackageReference 
        Include="Microsoft.Build.Utilities.Core"
        Version="17.0.0"
        PrivateAssets="all"
    />
    <PackageReference
        Include="Microsoft.Extensions.DependencyInjection"
        Version="6.0.0"
        PrivateAssets="all"
    />
</ItemGroup>

Paketera beroenden i paketet

Du måste också bädda in körtidstillgångar för våra beroenden i uppgiftspaketet. Det finns två delar i detta: ett MSBuild-mål som lägger till våra beroenden i BuildOutputInPackage ItemGroup och några egenskaper som styr layouten för dessa BuildOutputInPackage objekt. Du kan läsa mer om den här processen i NuGet-dokumentationen.

<PropertyGroup>
    ...
    <!-- This target will run when MSBuild is collecting the files to be packaged, and we'll implement it below. This property controls the dependency list for this packaging process, so by adding our custom property we hook ourselves into the process in a supported way. -->
    <TargetsForTfmSpecificBuildOutput>
        $(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage
    </TargetsForTfmSpecificBuildOutput>
    <!-- This property tells MSBuild where the root folder of the package's build assets should be. Because we are not a library package, we should not pack to 'lib'. Instead, we choose 'tasks' by convention. -->
    <BuildOutputTargetFolder>tasks</BuildOutputTargetFolder>
    <!-- NuGet does validation that libraries in a package are exposed as dependencies, but we _explicitly_ do not want that behavior for MSBuild tasks. They are isolated by design. Therefore we ignore this specific warning. -->
    <NoWarn>NU5100</NoWarn>
    <!-- Suppress NuGet warning NU5128. -->
    <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
    ...
</PropertyGroup>

...
<!-- This is the target we defined above. It's purpose is to add all of our PackageReference and ProjectReference's runtime assets to our package output.  -->
<Target
    Name="CopyProjectReferencesToPackage"
    DependsOnTargets="ResolveReferences">
    <ItemGroup>
        <!-- The TargetPath is the path inside the package that the source file will be placed. This is already precomputed in the ReferenceCopyLocalPaths items' DestinationSubPath, so reuse it here. -->
        <BuildOutputInPackage
            Include="@(ReferenceCopyLocalPaths)"
            TargetPath="%(ReferenceCopyLocalPaths.DestinationSubPath)" />
    </ItemGroup>
</Target>

Paketa inte Assemblyn Microsoft.Build.Utilities.Core

Som vi nämnt ovan kommer det här beroendet att tillhandahållas av MSBuild själv vid körning, så vi behöver inte inkludera det i paketet. Det gör du genom att lägga till attributet ExcludeAssets="Runtime" i PackageReference för det

...
<PackageReference 
    Include="Microsoft.Build.Utilities.Core"
    Version="17.0.0"
    PrivateAssets="all"
    ExcludeAssets="Runtime"
/>
...

Generera och bädda in en deps.json fil

Den deps.json filen kan användas av MSBuild för att säkerställa att rätt versioner av dina beroenden läses in. Du måste lägga till vissa MSBuild-egenskaper för att filen ska genereras, eftersom den inte genereras som standard för bibliotek. Lägg sedan till ett mål för att inkludera det i våra paketutdata, på samma sätt som du gjorde för våra paketberoenden.

<PropertyGroup>
    ...
    <!-- Tell the SDK to generate a deps.json file -->
    <GenerateDependencyFile>true</GenerateDependencyFile>
    ...
</PropertyGroup>

...
<!-- This target adds the generated deps.json file to our package output -->
<Target
        Name="AddBuildDependencyFileToBuiltProjectOutputGroupOutput"
        BeforeTargets="BuiltProjectOutputGroup"
        Condition=" '$(GenerateDependencyFile)' == 'true'">

     <ItemGroup>
        <BuiltProjectOutputGroupOutput
            Include="$(ProjectDepsFilePath)"
            TargetPath="$(ProjectDepsFileName)"
            FinalOutputPath="$(ProjectDepsFilePath)" />
    </ItemGroup>
</Target>

Inkludera MSBuild-egenskaper och mål i ett paket

För bakgrundsinformation i det här avsnittet, läs om egenskaper och mål och sedan hur du kan inkludera egenskaper och mål i ett NuGet-paket.

I vissa fall kanske du vill lägga till anpassade byggmål eller egenskaper i projekt som använder ditt paket, till exempel att köra ett anpassat verktyg eller en process under bygget. Det gör du genom att placera filer i formuläret <package_id>.targets eller <package_id>.props i mappen build i projektet.

Filer i projektroten skapa mapp anses lämpliga för alla målramverk.

I det här avsnittet ska du koppla in uppgiftsimplementeringen i .props och .targets filer, som kommer att ingå i vårt NuGet-paket och automatiskt läsas in från ett referensprojekt.

  1. Lägg till följande kod i aktivitetens projektfil AppSettingStronglyTyped.csproj:

    <ItemGroup>
        <!-- these lines pack the build props/targets files to the `build` folder in the generated package.
            by convention, the .NET SDK will look for build\<Package Id>.props and build\<Package Id>.targets
            for automatic inclusion in the build. -->
        <Content Include="build\AppSettingStronglyTyped.props" PackagePath="build\" />
        <Content Include="build\AppSettingStronglyTyped.targets" PackagePath="build\" />
    </ItemGroup>
    
  2. Skapa en mapp och lägg två textfiler i den mappen: AppSettingStronglyTyped.props och AppSettingStronglyTyped.targets. AppSettingStronglyTyped.props importeras tidigt i Microsoft.Common.propsoch egenskaper som definieras senare är inte tillgängliga för den. Undvik därför att referera till egenskaper som ännu inte har definierats. de skulle utvärderas till tomma.

    Directory.Build.targets importeras från Microsoft.Common.targets när du har importerat .targets filer från NuGet-paket. Därför kan den åsidosätta egenskaper och mål som definierats i de flesta bygglogik, eller ange egenskaper för alla dina projekt oavsett vad de enskilda projekten har angett. Se importorder.

    AppSettingStronglyTyped.props innehåller uppgiften och definierar vissa egenskaper med standardvärden:

    <?xml version="1.0" encoding="utf-8" ?>
    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <!--defining properties interesting for my task-->
    <PropertyGroup>
        <!--The folder where the custom task will be present. It points to inside the nuget package. -->
        <_AppSettingsStronglyTyped_TaskFolder>$(MSBuildThisFileDirectory)..\tasks\netstandard2.0</_AppSettingsStronglyTyped_TaskFolder>
        <!--Reference to the assembly which contains the MSBuild Task-->
        <CustomTasksAssembly>$(_AppSettingsStronglyTyped_TaskFolder)\$(MSBuildThisFileName).dll</CustomTasksAssembly>
    </PropertyGroup>
    
    <!--Register our custom task-->
    <UsingTask TaskName="$(MSBuildThisFileName).AppSettingStronglyTyped" AssemblyFile="$(CustomTasksAssembly)"/>
    
    <!--Task parameters default values, this can be overridden-->
    <PropertyGroup>
        <RootFolder Condition="'$(RootFolder)' == ''">$(MSBuildProjectDirectory)</RootFolder>
        <SettingClass Condition="'$(SettingClass)' == ''">MySetting</SettingClass>
        <SettingNamespace Condition="'$(SettingNamespace)' == ''">example</SettingNamespace>
        <SettingExtensionFile Condition="'$(SettingExtensionFile)' == ''">mysettings</SettingExtensionFile>
    </PropertyGroup>
    </Project>
    
  3. Den AppSettingStronglyTyped.props filen inkluderas automatiskt när paketet installeras. Sedan har klienten uppgiften tillgänglig och vissa standardvärden. Men det används aldrig. För att sätta den här koden i praktiken definierar du vissa mål i den AppSettingStronglyTyped.targets filen, som också inkluderas automatiskt när paketet installeras:

    <?xml version="1.0" encoding="utf-8" ?>
    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    
    <!--Defining all the text files input parameters-->
    <ItemGroup>
        <SettingFiles Include="$(RootFolder)\*.$(SettingExtensionFile)" />
    </ItemGroup>
    
    <!--A target that generates code, which is executed before the compilation-->
    <Target Name="BeforeCompile" Inputs="@(SettingFiles)" Outputs="$(RootFolder)\$(SettingClass).generated.cs">
        <!--Calling our custom task-->
        <AppSettingStronglyTyped SettingClassName="$(SettingClass)" SettingNamespaceName="$(SettingNamespace)" SettingFiles="@(SettingFiles)">
            <Output TaskParameter="ClassNameFile" PropertyName="SettingClassFileName" />
        </AppSettingStronglyTyped>
        <!--Our generated file is included to be compiled-->
        <ItemGroup>
            <Compile Remove="$(SettingClassFileName)" />
            <Compile Include="$(SettingClassFileName)" />
        </ItemGroup>
    </Target>
    
    <!--The generated file is deleted after a general clean. It will force the regeneration on rebuild-->
    <Target Name="AfterClean">
        <Delete Files="$(RootFolder)\$(SettingClass).generated.cs" />
    </Target>
    </Project>
    

    Det första steget är att skapa en ItemGroup, som representerar textfilerna (det kan vara fler än en) att läsa och det blir en del av vår uppgiftsparameter. Det finns standardvärden för platsen och tillägget som vi letar efter, men du kan åsidosätta de värden som definierar egenskaperna i klientens MSBuild-projektfil.

    Definiera sedan två MSBuild-mål. Vi utökar MSBuild-processen, åsidosätter fördefinierade mål:

    • BeforeCompile: Målet är att anropa den anpassade uppgiften för att generera klassen och inkludera klassen som ska kompileras. Uppgifter i det här målet infogas innan kärnkompileringen görs. Indata- och utdatafält är relaterade till inkrementell byggnad. Om alla utdataobjekt är av typen up-to-date, hoppar MSBuild över målet. Den här inkrementella byggningen av målet kan avsevärt förbättra prestandan för dina byggningar. Ett objekt anses up-to-date om dess utdatafil är samma ålder eller senare än dess indatafil eller filer.

    • AfterClean: Målet är att ta bort den genererade klassfilen när en allmän rensning har inträffar. Aktiviteter i den här måltavlan infogas efter att kärnstädfunktionen har anropats. Det tvingar kodgenereringssteget att upprepas när målet Återskapa körs.

Generera NuGet-paketet

Om du vill generera NuGet-paketet kan du använda Visual Studio (högerklicka på projektnoden i Solution Exploreroch välja Pack). Du kan också göra det med hjälp av kommandoraden. Navigera till mappen där uppgiftsprojektfilen AppSettingStronglyTyped.csproj finns och kör följande kommando:

// -o is to define the output; the following command chooses the current folder.
dotnet pack -o .

Grattis! Du har genererat ett NuGet-paket med namnet \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkg.

Paketet har ett tillägg .nupkg och är en komprimerad zip-fil. Du kan öppna den med ett zip-verktyg. Filerna .target och .props finns i mappen build. Filen .dll finns i mappen lib\netstandard2.0\. Filen AppSettingStronglyTyped.nuspec är på rotnivå.

(Valfritt) Stöd för multitargeting

Du bör överväga att stödja både Full (.NET Framework) och Core (inklusive .NET 5 och senare) MSBuild-distributioner för att stödja bredast möjliga användarbas.

För "normala" .NET SDK-projekt innebär multitargeting att flera TargetFrameworks anges i projektfilen. När du gör detta utlöses byggen för både TargetFrameworkMonikers och de övergripande resultaten kan paketeras som en enda artefakt.

Det är inte hela historien för MSBuild. MSBuild har två primära fraktfordon: Visual Studio och .NET SDK. Det här är mycket olika körningsmiljöer; en körs på .NET Framework-runtime-miljön och den andra på CoreCLR. Det innebär att även om din kod kan rikta in sig på netstandard2.0 kan aktivitetslogik ha skillnader beroende på vilken MSBuild-körningstyp som används för närvarande. Eftersom det finns så många nya API:er i .NET 5.0 och senare är det praktiskt att både multitarget din MSBuild-uppgiftskällkod för flera TargetFrameworkMonikers samt multitarget din MSBuild-mållogik för flera MSBuild-körningstyper.

Ändringar som krävs för multitarget

För att rikta in sig på flera TargetFrameworkMonikers (TFM):

  1. Ändra projektfilen så att den använder net472 och net6.0 TFM:er (den senare kan ändras baserat på vilken SDK-nivå du vill rikta in dig på). Du kanske vill rikta in dig på netcoreapp3.1 tills .NET Core 3.1 inte stöds. När du gör detta ändras paketmappstrukturen från tasks/ till tasks/<TFM>/.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. Uppdatera dina .targets-filer så att de använder rätt TFM för att ladda dina uppgifter. Den TFM som krävs ändras baserat på vilken .NET TFM du valde ovan, men för ett projekt som är inriktat på net472 och net6.0skulle du ha en egenskap som:

<AppSettingStronglyTyped_TFM Condition=" '$(MSBuildRuntimeType)' != 'Core' ">net472</AppSettingStronglyTyped_TFM>
<AppSettingStronglyTyped_TFM Condition=" '$(MSBuildRuntimeType)' == 'Core' ">net6.0</AppSettingStronglyTyped_TFM>

Den här koden använder egenskapen MSBuildRuntimeType som proxy för den aktiva värdmiljön. När den här egenskapen har angetts kan du använda den i UsingTask för att läsa in rätt AssemblyFile:

<UsingTask
    AssemblyFile="$(MSBuildThisFileDirectory)../tasks/$(AppSettingStronglyTyped_TFM)/AppSettingStronglyTyped.dll"
    TaskName="AppSettingStrongTyped.AppSettingStronglyTyped" />

Nästa steg

Många uppgifter omfattar att köra ett program. I vissa scenarier kan du använda Exec-uppgiften, men om begränsningarna för Exec-uppgiften är ett problem kan du också skapa en anpassad uppgift. I följande självstudie går vi igenom båda alternativen med ett mer realistiskt scenario för kodgenerering: skapa en anpassad uppgift för att generera klientkod för ett REST-API.

Eller lär dig hur du testar en anpassad uppgift.