Budowanie paczki Nugetowej (przykład)

Buduję właśnie nugeta z solucji, która ma ponad 20 projektów. Ok 10 z nich ma wylądować w paczce. Reszta to Unit Testy i Functional Testy, których nie potrzebujemy. Paczka ta jest silnikiem obliczeniowym i będzie konsumowana przez większy system wewnątrz firmy. Nuget będzie hostowany tylko wewnętrznie.

Wygląda to tak:

  • Plik nuspec, w którym najważniejsze rzeczy to zdefiniowane id (unikalna nazwa nugeta) oraz pattern jakie pliki wybrać, wygląda on prosto:
    <files>
     <file src="YourCompany.YourProject.*" target="lib\netstandard1.3" exclude="*.deps.json;*Tests*" />
    </files>
    
  • Ze skryptu PowerShellowego podaję odpowiednią versję w jakieś będzie nuget. Wersję pobieram z jednej DLLki (hardcoded)
  • Skrypt buduje solucję do folderu „BuildArtifacts”, pełne odpalanie wszystkich testów.
  • Przekazuję więc też gdzie szukać plików (powyższe „BuildArtifacts”).
  • Nuget zbudowany komendą:
    Write-Host "Building NuGet package" -ForegroundColor Green
    & nuget.exe pack "$code_dir\SolutionName.nuspec" -Version $nugetPackageVersion -Properties Debug -BasePath $build_artifacts_dir
    
  • Jeśli paczka w takiej samej wersji jest już na Serverze Nugetowym to skrypt ostrzega, ale nic nie podmienia. Nie chcemy przypadkiem nadpisać wersji, którą ktoś ściągnął. Jest wypisana sugestia, że może zapomnieliśmy podbić wersji.
  • Skrypt pyta czy wrzucić na Server Nugetowy (no dobra, na razie tylko folder share’owany), ale tylko jeśli jej tam nie ma, jak w punkcie powyżej.

Pliki, które lądują w paczce

Jeszcze w szczegółach chciałbym omówić, które pliki lądują w paczce a które nie i dlaczego.

<file src="YourCompany.YourProject.*" exclude="*.deps.json;*Tests*" />
  • „YourCompany.YourProject.*” – pozwala odfiltrować wszystkie dodatkowe biblioteki ściągnięte nugetem.
  • „exclude=”*.deps.json;*Tests*” – odfiltrowuje DLLki naszych własnych testów, oraz pliki *deps.json powstałe podczas kompilacji.
  • chce mieć pliki *.pdb dołączone do paczki, więc ich nie filtruje. Dzięki temu osobom korzystający z paczki będzie łątwiej debugować kod, a większy rozmiar paczki nie jest aż tak istotny, bo to paczki Nugetowe i nie lądują w kontroli wersji.

Pełny kod skryptu

Task (korzystam z psake) który tworzy paczkę nugetową wygląda więc tak:

Task Create-And-Publish-Nuget -Depends Build {
    $libraryPath = Resolve-Path "$build_artifacts_dir\Jetbrains.Rider.dll"

    $assemblyVersion = get-assembly-version($libraryPath)

    $nugetPackageVersion = [string]$assemblyVersion.Major + "." + [string]$assemblyVersion.Minor + "." + [string]$assemblyVersion.Build
    
    Write-Host "Building NuGet package" -ForegroundColor Green
    & $nuget_exe_path pack "$code_dir\Rider.nuspec" -Version $nugetPackageVersion -Properties $build_configuration -BasePath $build_artifacts_dir

    $packageName = "Jetbrains.Rider." + $nugetPackageVersion + ".nupkg";
    
    $nugetServerDestinationPath = "$private_nuget_share\$packageName"

    $isAlreadyOnNugetServer = Test-Path $nugetServerDestinationPath

    if ($isAlreadyOnNugetServer ){
        Write-Host "Package in current version already exists on nuget server! Maybe you forgot to bump version?" -foregroundcolor "yellow"
    } else {
        while ($choice -notmatch "[y|n]"){
            $choice = read-host "Are you sure you want to update Jetbrains.Rider package on $private_nuget_share host? (Y/N)";
        }

        if ($choice -eq "y"){
            Copy-Item -Path "$packageName" -Destination $nugetServerDestinationPath
            Write-Host "New package is copied on server!" -foregroundcolor "green"
        }
    }    
}

function get-assembly-version() {
    param(
        [Parameter(Mandatory=$true)]
        [string] $file
    )
    
    $version = [System.Reflection.AssemblyName]::GetAssemblyName($file).Version;
    
    $version
}
Reklamy
Opublikowano DevOps | Otagowano , , , , | Dodaj komentarz

Jeden wspólny AssemblyInfo dla całej solucji

Tak wygłada AssemblyInfo.cs stworzony przez Visual Studio w nowym projekcie:

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following 
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("YourProjectName")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("YourCompanyName")]
[assembly: AssemblyProduct("YourProjectName")]
[assembly: AssemblyCopyright("Copyright © YourCompanyName 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible 
// to COM components.  If you need to access a type in this assembly from 
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("7a08ceec-6dc3-45a1-8c2d-338ab1bac448")]

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version 
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Build and Revision Numbers 
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

Ja tu zawsze widzę dużo śmieci. Przy kilkunastu/kilkudziesięciu projektach tych śmiecie jest liniowo więcej. Może czasem niektóre z nich w Waszym kontekście są potrzebne. Moje doświadczenia są jednak takie, że ten plik mógłby wyglądać:

using System.Reflection;

[assembly: AssemblyVersion("1.0.0")]

bo jedyne co naprawdę mnie interesuje to podbijanie wersji. I od tej wersji się ostatnio zaczęło, bo trzeba to podbijać i najlepiej wszędzie na raz. Można to zrobić skryptem buildującym (np u mnie obecnie PowerShell). A można skorzystać z współdzielenia jednego AssemblyInfo między wszystkimi projektami.

Źródło: Shared AssemblyInfo for uniform versioning across the solution

Skorzystałem z ręcznego dodawania linków, które jest opisane w drugiej odpowiedzi do powyższego pytania, a wygląda:

  1. Right click on the project, in which you wish to add the Shared assembly file, and select Add -> Existing Item…
  2. Select the file “SharedAssemblyInfo.cs” from the solution folder.
  3. Instead of Add, click on the the arrow next to Add and click “Add as Link”
  4. Drag down the added linked file alongside AssemblyInfo.cs in the same folder.
  5. Repeat steps 1 – 4 for all projects for which you wish to add shared assembly file.

Wersję więc podbijam ręcznie, nie potrzebuję żadnych skryptów. Plik SharedAssemblyInfo.cs wrzuciłem na tym samym poziomie co solucja i dodałem do folderu „Solution Items” w solucji.

shared assemblyinfo in solution explorer

Oprócz tego czasem pojawia się zwykły plik AssemblyInfo.cs per projekt. Jedyną rzeczą, która w nim jest to attrybut InternalsVisibleTo:

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("YourCompany.YourProject.Core.Tests")
Opublikowano DevOps | Otagowano , , , , , | 5 Komentarzy

Zestaw nugetów, które poznałem w ostatnim roku

Według mojej subiektywnej oceny „przydania się ostatnio”:

  1. MadiatR – implementacja mediatora (dla Command & Query)
  2. TypeLITE – generuje C#->TypeScript, opisane przeze mnie TypeLITE: generator C# => TypeScript
  3. Squirrel.WindowsSquirrel: It’s like ClickOnce but Works™, zaoszczędziło nam duuuuużo czasu.
  4. Microsoft.AspNet.SignalR.Client – SignalR znam już dawno i dobrze, po prostu dowiedziałem się że jest też klient w C# i można komunikację Client<->Server oprzeć też na SignalR (czy się powinno to inna kwestia).
  5. StyleCopAnalyzersStyleCopa (wcześniej FxCopa) lubiłem zawsze, jednak ta bilbioteka jest oparta na Roslynie i jest przez to super prosta w użyciu. Mój plik configurujący https://github.com/kmorcinek/dotnet-tools-settings/blob/master/StyleCopDefault.ruleset
  6. JetBrains.Annotations – już to chyba było w projektach w których brałem udział, ale teraz sam zrobiłem setup i użyłem. Skorzystałem z Hunt your bugs in design time
  7. SolutionScripts – dzięki temu z konsoli nugeta w VS mogę korzystać ze skrótów do długich komend (jak np update-database z EF z opcjami który projekt i gdzie szukać konfiguracji).
  8. MoreLINQ – rozszerzenia do LINQ. Jedyna metoda, z której korzystamy, to „DistinctBy” – ale już dlatego warto. Inne się kiedyś pozna.
Opublikowano Programowanie | Otagowano , | Dodaj komentarz

TypeLITE: generator C# => TypeScript

W projekcie Webowym z bogatą logiką FrontEndową zawsze w pewnym momencie stajemy przed problemem synchronizacji klas, które mamy w C# z tymi po stronie FE. Jednym z ułatwień jest korzystanie z TypeScript, który udostępnia silne typowanie po stronie FE. Ciągle jednak może być tak, że klasa w C# rozjedzie się z klasą w TypeScript (np. po refaktoringu).

TypeLITE

TypeLITE is an utility, that generates TypeScript definitions from .NET classes. It supports all major features of the current TypeScript specification, including modules and inheritance.

TypeLITE generuje C# => TypeScript. Oczywiście tylko definicje właściwości/pól, ick implementacja nie miałaby sensu.

C#:

public class Person {
    public string Name { get; set; }
    public List<address> Addresses { get; set; }
}
  
public class Employee : Person {
    public decimal Salary { get; set; }
}
  
public class Address {
    public string Street { get; set; }
}

TypeScript:

interface Person {
    Name: string;
    Addresses: Address[];
}
  
interface Employee extends Person {
    Salary: number;
}
  
interface Address {
    Street: string;
}

Przykład użycia

Poniżej opiszę przykład z mojego aktualnego projektu. W trakcie implementacji pojawiło się kilka problemów.

  • TypeLITE uruchamia się w momencie wykonywania „Run custom tool” (albo zapisujemy) na pliku TypeLite.Net4.tt i generuje klasy do plików TypeLite.Net4.d.ts oraz Enums.ts. Takie ustawienie było domyślnie i tak zostało. Przy takim ustawieniu czasami można zapomnieć wygenerować wszystko TypeLITEm i poprawić definicje w FE. Wyjdzie w trakcie CodeReview lub w Runtime
  • Jest kilka sposobów na wskazanie klas z C#, które mają być wygenerowane. Te fajne (otagowanie atrybutem) jakoś nie zadziałały. Więc w pliku konfiguracji TypeLite.Net4.tt zdefiniowałem je explicite:
    //...	
    <# var ts = TypeScript.Definitions()
    		.For<KMorcinek.Examples.UserDto>()
    		.For<KMorcinek.Examples.Features.ListAdultUsersQuery>()
    //...	
    

    Nie musiałem wskazywać każdej klasy, wystarczy że podałem te „ostateczne”. Gdy UserDto posiada AddressDto to AddressDto również zostanie wygenerowane.

  • Następnie explicite podałem, z które projekty należy przeszukiwać. Początek pliku w moim przypadku wyglądał tak:
    <#@ template debug="false" hostspecific="True" language="C#" #>
    <#@ assembly name="$(TargetDir)TypeLite.dll" #>
    <#@ assembly name="$(TargetDir)TypeLite.Net4.dll" #>
    <#@ assembly name="$(TargetDir)$(TargetFileName)" #> // means current WebApp project
    <#@ assembly name="$(TargetDir)YourProject.Domain.dll" #>
    <#@ assembly name="$(TargetDir)YourProject.Application.dll" #>
    
  • skorzystałem z customizacji, która sprawiała że nazwy w TS są camelCase
    <# var ts = TypeScript.Definitions()
    //...
        .WithMemberFormatter(identifier => 
            Char.ToLower(identifier.Name[0]) + identifier.Name.Substring(1)
        );
    

    podejście dosyć proste (zmienia tylko pierwszą literę), ale działa. Jeśli chcesz aby „VAT” zostało zmienione na „VAT” zamiast „vAT” to trzeba chyba podpiąć kod podobny do JsonNetSerializera.

  • kolejna customizacja polegała na podmianie namespace na coś krótszego.
    <# var ts = TypeScript.Definitions()
    //...	
        .WithModuleNameFormatter(module => { 
            return module.Name == "System" ? "System" : "YouApplicationName"; 
        })
    

    Dla Guid’ów (w namespace System) był System, dla pozostałych „YouApplicationName”. Nie chcę się bawić ze zbyt długimi namespace’ami.

  • Konfiguracja generowania podczas procesu buildowania – nie powiodła się, chociaż poszło na to kilka godzin. Należało doinstalować dodatkowy pakiet do VS, ale było to zbyt kruche rozwiązane i nie miałem czasu, żeby to rozkminić. Na razie zostawiłem jak jest, poczekam na lepsze czasy.

Problem zapętlenia budowania C# i TypeScript’a

Narzędzie TypeLITE potrzebuje do prawidłowego działania zbudowanych dll’ek projektów, z których pobiera klasy/modele.

Problem #1:

  1. Załóżmy, że zmieniamy coś w definicji klasy, np. dodaliśmy nową

    właściwość

  2. Wykonujemy „Run custom tool” (albo zapisujemy) na TypeLite.Net4.tt
  3. Ale … coś poszło źle, generator miał błędy i wygenerowany plik TypeLite.Net4.d.ts jest pusty
  4. W związku z powyższym nie możemy przebudować całej solucji ponieważ TypeScript rzuca błędami. Z tego miejsca już nie da się ładnie wrócić

Rozwiązanie:

  1. git checkout -- *.Net4.d.ts wracamy do poprzedniej wersji wygenerowanego pliku, teraz FE się builduje prawidłowo
  2. Poprawiamy w C# to co było źle
  3. Przebudowujemy wszystko
  4. Wykonujemy „Run custom tool” (albo zapisujemy) TypeLite.Net4.tt, co powoduje wygenerowanie poprawnego pliku TypeLite.Net4.d.ts

Problem #2:

W przypadku dodania nowej klasy do C# i dodania do niej ścieżki w pliku TypeLite.Net4.tt, i nie przebudowanu całej solucji przed regenerowaniem aktualnego TypeScriptu znowu może pojawić się się problem z brakiem definicji w plikach TypeLite.Net4.d.ts oraz Enums.ts.

Rozwiązanie:

Rozwiązanie dokładnie takie samo jak opisane w Problemie #1.

Inne

Jak tak o tym piszę to widzę że „Net4.” jest zbyt często powtarzane i prawdopodobnie mogę to usunąć.

Miejsce na improvement:
będę wdzięczny gdyby ktoś fajnie opisał jak debugować to coś 🙂

Opublikowano Programowanie | Otagowano , , | 1 komentarz

Nazywanie commitów

Ostatnio miałem małe Pair Programming poza pracą (tak można się w ten sposób uczyć!).

Padł zarzut że nie powinno się nazywać commitów w stylu “Create PatientService class” tylko jaka jest funkcjonalność dodana. Zgodzę się z tym. Z tym, że aby tak było to trzeba najpierw mieć napisaną całą funkcjonalność krok po krok, przejść process CodeReview (za pomocą Pull Request), następnie wyczyścić commity (git rebase interactive) i wtedy wychodzi nam jeden (kilka?) commitów mających domenowy sens. I tak staram się robić.

Niestety rzeczywistość obok mnie jest mniej różowa, czasem poprawka goni poprawkę. Wtedy siłą rzeczy commit wyglądają jak “Fix: change number variable to long”, “Add missing PatientService class”, “Filter itemsRepository by PartId”. Naprawdę niewiele zmienia gdy na początku commita doklei się ticket z JIRY czy inne TFS. W historii jest po prostu sieczka.

Przy okazji, a może przede wszystkim: How to Write a Git Commit Message

Opublikowano git | Otagowano , , , , | Dodaj komentarz

Literówki w kodzie, komentarzu i na głównej

Mam już taki wzrok, że widzę literówki i nie przepuszczam im. Po prostu taki już jestem, tak mój mózg pracuje. Oczywiście nie toczę wojen w zespołach o to. Są już automatyczne narzędzia które to wychwytują (dodatek ReSpeller do ReSharpera), a resztę sobie poprawię sam, to sama przyjemność.

Można więc myśleć, że to aż tak się nie zwraca bo w sumie kod się uruchomi niezależnie od tego czy klasa się będzie nazywać ArticleTpeConwerter czy ArticleTypeConverter. I że ten porządek, pomoże generalnie projektowi, ale przejdzie to na pewno niezauważone.

Ale, ale

Właśnie zdałem sobie sprawę, że te wszystkie niechluje, które się niczym nie przejmują (i walą literówki i nawet nie chce im się poprawiać po sobie) robią to samo z tekstami, które idą do klienta.

Inspiracja (można przeczekać minutę o CV, później jest o pracy):

Opublikowano Programowanie | Otagowano | 2 Komentarze

Reguła DRY – tip #1

DRY – Don’t Repeat Yourself. Łamanie tej reguły ma tyle twarzy, że postanowiłem każdy mały przykład wrzucać.

Gorszy kod:

string GetFilePath()
{
    return IsForDebug
        ? $@"{Folder}\{Name}.{Extension}.{VersionNumber}"
        : $@"{Folder}\{Name}.{Extension}";
}

Lepszy kod:

string GetFilePathBetter()
{
    var filePath = $@"{Folder}\{Name}.{Extension}";

    if (IsForDebug)
    {
        filePath += $".{VersionNumber}";
    }

    return filePath;
}

Wiem że nie powinno się sklejać ścieżki w ten sposób tylko użyć Path.Combine(). Przykład z kodu.

Po komentarzu @marfusios2 zdałem sobie sprawę, że można to ulepszyć do

string GetFilePathMaybeBetter()
{
    var postfix = IsForDebug
        ? $".{VersionNumber}"
        : "";

    return $@"{Folder}\{Name}.{Extension}{postfix}";
}

Ciągle jest jeden zbędny string, ale już zdecydowanie krótszy.

Opublikowano Programowanie | Otagowano , | 3 Komentarze