Praktický průvodce pro vývojáře, kteří chtějí pracovat na samotném WorkTrackeru. Pokud tě zajímá, jak je aplikace navržená, začni v architecture.md. Pokud píšeš plugin, jdi na plugin-development.md.
- Prerekvizity
- Naklonování a build
- Struktura repozitáře
- Spouštění aplikací
- Testy
- Migrace databáze
- Central Package Management
- Coding standards
- Unit of Work — kdy a jak
- Práce se secure storage
- MSAL a device code flow
- Lokalizace
- Logging
- CI/CD
- Release process
- Časté problémy při vývoji
- .NET 10 SDK — projekt používá C# 13, nullable reference types a
TreatWarningsAsErrors = true. Starší SDK build neprojde. - Git
- IDE — Rider nebo Visual Studio 2022+. VS Code s C# Dev Kit také funguje.
- SQLite browser (volitelné) — DB Browser for SQLite nebo
sqlite3CLI pro ad‑hoc inspekciworktracker.db. - Windows pro WPF projekt; Linux/macOS pro zbytek.
- EF Core CLI —
dotnet tool install --global dotnet-ef(pro migrace).
git clone https://github.com/vesnicancz/work-tracker.git
cd work-tracker
dotnet restore
dotnet build
dotnet testPrvní build stáhne ~700 MB závislostí do standardní NuGet cache (typicky ~/.nuget/packages na Linux/macOS nebo %UserProfile%\.nuget\packages na Windows). Konkrétní cesta závisí na tvé lokální konfiguraci NuGetu.
Build by měl projít bez warningů — projekt má TreatWarningsAsErrors = true v Directory.Build.props. Pokud něco bliká warningem, je to CI failure.
work-tracker/
├── src/
│ ├── WorkTracker.Domain/ # Pure business model
│ ├── WorkTracker.Application/ # Use cases, Result<T>, DTOs
│ ├── WorkTracker.Infrastructure/ # EF Core, plugins, MSAL, secure storage
│ ├── WorkTracker.UI.Shared/ # Sdílené ViewModely, orchestrátory, služby
│ ├── WorkTracker.CLI/ # Spectre.Console klient
│ ├── WorkTracker.WPF/ # WPF GUI (Windows)
│ ├── WorkTracker.Avalonia/ # Avalonia GUI (cross-platform)
│ └── WorkTracker.Plugin.Abstractions/ # Plugin API
├── plugins/
│ ├── WorkTracker.Plugin.Atlassian/
│ ├── WorkTracker.Plugin.Office365Calendar/
│ ├── WorkTracker.Plugin.GoranG3/
│ └── WorkTracker.Plugin.Luxafor/
├── tests/ # xUnit + Moq + FluentAssertions
│ ├── WorkTracker.Domain.Tests/
│ ├── WorkTracker.Application.Tests/
│ ├── WorkTracker.Infrastructure.Tests/
│ ├── WorkTracker.UI.Shared.Tests/
│ ├── WorkTracker.Avalonia.Tests/
│ ├── WorkTracker.Plugin.Atlassian.Tests/
│ ├── WorkTracker.Plugin.GoranG3.Tests/
│ ├── WorkTracker.Plugin.Luxafor.Tests/
│ ├── WorkTracker.Plugin.Office365Calendar.Tests/
│ └── WorkTracker.Tests.Common/ # Sdílené test helpery, in-memory DbContext
├── docs/
├── resources/
├── .github/workflows/
│ ├── dotnet.yml # Build + test na PR/push
│ └── release.yml # Multi-platform publish na git tag v*
├── Directory.Build.props # Nullable, warnings as errors, C# 13
├── Directory.Packages.props # Central Package Management
├── global.json # SDK pinning
└── WorkTracker.slnx # Solution (XML formát)
Solution je v novém .slnx formátu (XML, ne .sln). Rider 2024.x+ a VS 17.12+ ho otevřou přímo; starší verze potřebují převod.
dotnet run --project src/WorkTracker.CLI -- help
dotnet run --project src/WorkTracker.CLI -- start PROJ-123 "Bug fix"Pozn.: -- odděluje argumenty pro dotnet run od argumentů pro samotnou aplikaci.
dotnet run --project src/WorkTracker.AvaloniaPři prvním spuštění se vytvoří databáze. Pluginy se discoverují z adresáře vedle binárky — při debugování je to src/WorkTracker.Avalonia/bin/Debug/net10.0/plugins/, kam se plugin dostane buď přes dotnet publish plugin projektu do té složky, nebo ručním zkopírováním DLL.
dotnet run --project src/WorkTracker.WPFPouze Windows (target net10.0-windows).
Pluginy samy nejsou spustitelné. Build:
dotnet build plugins/WorkTracker.Plugin.AtlassianA pro lokální testování v Avalonia / WPF:
dotnet publish plugins/WorkTracker.Plugin.Atlassian -c Debug \
-o src/WorkTracker.Avalonia/bin/Debug/net10.0/plugins/AtlassianProjekt má ~700 testů v xUnit + Moq + FluentAssertions.
# Vše
dotnet test
# Jeden projekt
dotnet test tests/WorkTracker.Application.Tests
# Jeden test
dotnet test --filter "FullyQualifiedName~WorkEntryServiceTests.StartWorkAsync_WithActiveEntry_StopsPrevious"
# Jen metoda/třída, filtr částečný
dotnet test --filter "FullyQualifiedName~OverlapResolution"
# S coverage (Coverlet)
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover- Unit testy (většina) — mock všech závislostí, bez DbContextu.
Domain.Tests,Application.Tests,UI.Shared.Tests, plugin testy. - Integration testy (menšina) —
Infrastructure.Testspoužívá in‑memory SQLite (Microsoft.Data.Sqlites:memory:connection) pro reálný EF Core tok. Helper pro stvořeníDbContextFactoryje vWorkTracker.Tests.Common.
- Třídy se jmenují
{SystemUnderTest}Tests. - Metody
{Method}_{Scenario}_{ExpectedResult}, např.StartWorkAsync_WithEmptyTicketAndDescription_ReturnsFailure. [Fact]pro jednotlivé případy,[Theory]+[InlineData]pro parametrizaci.- Žádné
[Setup]jako v NUnitu — xUnit používá konstruktor třídy aIDisposable/IAsyncLifetimepro cleanup. Fixtures (IClassFixture<T>) pro sdílený stav mezi testy ve třídě. FluentAssertionsvšude:result.IsSuccess.Should().BeTrue();,entries.Should().HaveCount(3);
Moq. Mocky se tvoří explicitně v konstruktoru testovací třídy, ne přes AutoMocker. Důvod: explicitní setup zviditelní závislosti SUT.
public class WorkEntryServiceTests
{
private readonly Mock<IWorkEntryRepository> _repoMock = new();
private readonly Mock<IUnitOfWorkFactory> _uowFactoryMock = new();
private readonly WorkEntryService _sut;
public WorkEntryServiceTests()
{
_sut = new WorkEntryService(_repoMock.Object, _uowFactoryMock.Object, TimeProvider.System);
}
}EF Core migrace se generují s Infrastructure jako target projektem a CLI jako startup projektem:
# Nová migrace
dotnet ef migrations add AddNewColumn \
--project src/WorkTracker.Infrastructure \
--startup-project src/WorkTracker.CLI
# Aplikovat (smaže + znovu z migrations)
dotnet ef database update \
--project src/WorkTracker.Infrastructure \
--startup-project src/WorkTracker.CLI
# Zpět o jednu migraci
dotnet ef database update PreviousMigrationName \
--project src/WorkTracker.Infrastructure \
--startup-project src/WorkTracker.CLI
# Vygenerovat SQL script (užitečné pro review)
dotnet ef migrations script \
--project src/WorkTracker.Infrastructure \
--startup-project src/WorkTracker.CLIRuntime migrace — aplikace při startu volá DependencyInjection.InitializeDatabaseAsync, která spustí DbContext.Database.MigrateAsync(). Žádný ruční dotnet ef database update na produkci.
Migration file naming — EF Core sám přidává timestamp prefix. Název migrace je v PascalCase, dostatečně popisný (AddIsActiveIndex, ne Fix1).
Code‑first — model v WorkTrackerDbContext.OnModelCreating, migrace jsou generovaná z něho. Žádné ruční úpravy migration souborů kromě opravdu speciálních případů (data seedy, custom SQL).
Verze NuGet balíčků jsou centralizované v Directory.Packages.props. V .csproj se uvádí pouze jméno balíčku, bez verze:
<!-- ✅ správně -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<!-- ❌ špatně — verze je v Directory.Packages.props -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />Přidání nového balíčku:
- Otevři
Directory.Packages.props, přidej<PackageVersion Include="..." Version="..." />. - V
.csprojprojektu, který balíček potřebuje, přidej<PackageReference Include="..." />. dotnet restore.
Update verzí:
# Zobrazit zastaralé
dotnet list package --outdated
# Update provést v Directory.Packages.props ručně,
# pak dotnet restore a dotnet testKonvence vynucené přes .editorconfig + Directory.Build.props:
- C# 13, nullable reference types enabled
TreatWarningsAsErrors = true— warningy jsou errory v CI_camelCasepro privátní fieldy,PascalCasepro typy, metody, public membersINamepro interfaces- Tabs pro indentaci (viz
.editorconfig) - Curly braces povinné i pro single‑line
if/else/for/while:
// ✅
if (result.IsFailure)
{
return result;
}
// ❌
if (result.IsFailure) return result;- Všechny I/O metody jsou async.
Task<T>neboValueTask<T>. CancellationTokenjako poslední parametr, bez default value (= default) kromě okrajových případů, kde nemá volající co předat.- Pojmenování: metoda končí
Async.
- Business chyby →
Result<T>/Result, propagace nahoru bez výjimek. - Infrastructure chyby (I/O, HTTP, DbException) → výjimka propaguje; zachycuje se až na hranici UI / CLI handleru a loguje se.
- Programátorské chyby (
nulltam, kde by neměl být, invarianty) →ArgumentNullException/InvalidOperationException. Žádné swallow.
Projekt striktně odmítá:
- Speculative abstractions „pro případ, že by to někdo chtěl“.
- Backwards‑compatibility shims pro necommitnuté/nereleasnuté věci.
- Feature flags, když stačí změnit kód.
- Wrapping trivialit do „služeb“ a
IXyzFactoryjen pro snadnější testování — pokud to nemá víc implementací, interface většinou nemusí existovat.
Když máš pochybnosti, volíme menší, přímočařejší řešení.
- Jen tam, kde logika není samovysvětlující (proč, ne co).
- XML doc komentáře na public API v Application a Plugin.Abstractions — je to reference pro plugin autory i IntelliSense. Interní třídy komentáře nepotřebují.
IUnitOfWork obalí víc zápisů do jedné EF Core transakce. Použij ho, když:
- Operace upravuje víc než jeden záznam a částečný úspěch by byl nekonzistentní.
- Potřebuješ atomicitu mezi repositoryi (typicky ještě není aplikovatelné, protože máme jen jedno
IWorkEntryRepository, ale architektura je připravená).
public async Task<Result<WorkEntry>> StartWorkAsync(
string? ticketId, DateTime? startTime, string? description,
DateTime? endTime, CancellationToken ct)
{
// …validace…
var effectiveStart = startTime ?? _timeProvider.GetLocalNow().DateTime;
await using var uow = await _uowFactory.CreateAsync(ct);
// 1) Zastav aktivní záznam, pokud existuje
var active = await uow.WorkEntries.GetActiveAsync(ct);
if (active != null)
{
active.EndTime = effectiveStart;
active.IsActive = false;
await uow.WorkEntries.UpdateAsync(active, ct);
}
// 2) Vytvoř nový
var entry = new WorkEntry
{
TicketId = ticketId,
Description = description,
StartTime = effectiveStart,
IsActive = endTime == null,
EndTime = endTime,
};
await uow.WorkEntries.AddAsync(entry, ct);
// 3) Commit — oboje nebo nic
await uow.SaveChangesAsync(ct);
return Result<WorkEntry>.Success(entry);
}Pokud metodu dispose‑uješ bez SaveChangesAsync, transakce se rollbackne.
Pro jednoduché CRUD operace se UoW nepoužívá. WorkEntryService dostane injekovanou transient IWorkEntryRepository (factory režim, auto‑save po každé operaci) a rovnou ji volá.
ISecureStorage abstrahuje nad OS credential storem. Implementace (CredentialStoreSecureStorage) používá GitCredentialManager.ICredentialStore, který multiplatformně řeší Windows Credential Manager, macOS Keychain a Linux libsecret.
public interface ISecureStorage
{
string Protect(string plainText, string pluginId, string fieldKey);
string Unprotect(string protectedText);
void Remove(string pluginId, string fieldKey);
}ProtectuložíplainTextdo OS storu pod targetemworktracker://{pluginId}/{fieldKey}a vrátí placeholderCS:{pluginId}:{fieldKey}, který se uloží dosettings.json.Unprotectdostane placeholder nebo plaintext a vrátí plaintext (plaintext vrací beze změn, aby fungoval migration path).Removesmaže položku ze storu.
- Při ukládání plugin konfigurace v
SettingsService— pole typuPassword(nebo API tokeny) se před serializací proženeProtect. - Při čtení před předáním pluginu se pole prožene
Unprotect.
Plugin dostává ve Configuration už rozbalené plaintext hodnoty. Nemusí implementovat šifrování.
Pluginy autentizující se přes Microsoft Entra ID používají device code flow. Důvod viz architecture.md a paměť o MSAL deadlocku v Avalonia.
MsalTokenProviderFactory je registrovaná jako singleton v DI a vystavená pluginům přes ITokenProviderFactory. Plugin si vyžádá token provider:
public class MyPlugin : WorkSuggestionPluginBase
{
private readonly ITokenProviderFactory _tokenProviderFactory;
private ITokenProvider? _tokenProvider;
public MyPlugin(ILogger<MyPlugin> logger, ITokenProviderFactory tokenProviderFactory) : base(logger)
{
_tokenProviderFactory = tokenProviderFactory;
}
protected override async Task<bool> OnInitializeAsync(
IDictionary<string, string> configuration,
CancellationToken cancellationToken)
{
var tenantId = GetRequiredConfigValue("TenantId");
var clientId = GetRequiredConfigValue("ClientId");
var scopes = new[] { "Calendars.Read", "User.Read" };
_tokenProvider = await _tokenProviderFactory.CreateAsync(tenantId, clientId, scopes);
return true;
}
// Na žádost o token:
private async Task<string?> GetTokenAsync(IProgress<string>? progress, CancellationToken ct)
{
var token = await _tokenProvider!.AcquireTokenSilentAsync(ct);
if (token != null)
{
return token;
}
// Silent selhalo (nebo cache prázdná) — spusť device code flow
return await _tokenProvider.AcquireTokenInteractiveAsync(progress, ct);
}
}- Plugin zavolá
AcquireTokenInteractiveAsync(progress, ct). - MSAL vygeneruje user code a verification URL.
MsalTokenProviderzavoláprogress?.Report("Go to https://microsoft.com/devicelogin and enter code ABC-123").- Zároveň se pokusí otevřít browser přes
Process.Start. - Uživatel v browseru zadá code a přihlásí se.
- MSAL mezitím pollne Entra a vrátí token.
- Cache soubor je v
%LocalAppData%\WorkTracker\keys\s názvemmsal_{safeKey}.bin, kdesafeKey = Convert.ToHexString(SHA256(tenantId:clientId)). - Šifrování: DPAPI (Windows), Keychain (macOS), libsecret (Linux) přes
Microsoft.Identity.Client.Extensions.Msal. - Fallback: pokud encrypted cache není dostupná (headless Linux bez secret service), MSAL použije nechráněný soubor
msal_{safeKey}_plain.bin— plugin zaloguje warning.
Texty UI jsou v .resx souborech v src/WorkTracker.UI.Shared/Localization/:
Strings.resx— výchozí (angličtina)Strings.cs.resx— čeština
- Zkopíruj
Strings.resxnaStrings.{culture}.resx(např.Strings.de.resx). - Přelož hodnoty.
- Přidej kulturu do
LocalizationService.AvailableCultures. - Build →
.resources.dllse vygenerují automaticky dobin/{Debug|Release}/{culture}/.
public partial class SettingsViewModel : ObservableObject
{
private readonly ILocalizationService _loc;
public string SaveButtonText => _loc["Settings.Save"];
}V XAML se bindí přes indexer a ILocalizationService jako DataContext:
<Button Content="{Binding Loc[Settings.Save]}" />Služba implementuje INotifyPropertyChanged, takže přepnutí jazyka v runtime regeneruje všechny bindingy.
Projekt používá Serilog, zapojený přes Microsoft.Extensions.Logging interface. Pluginy logují přes ILogger<T> a nevědí, že pod tím je Serilog.
V CLI/Program.cs a Avalonia/App.axaml.cs:
loggerConfiguration
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Warning)
.WriteTo.File(
WorkTrackerPaths.LogFilePath, // %LocalAppData%\WorkTracker\logs\worktracker-.log
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 14);LogInformationpro high‑level události (start aplikace, plugin loaded, worklog submitted).LogWarningpro abnormality, které aplikace zvládne (plugin disabled, retry).LogErrorpro chyby (exception propagated, infrastructure failure) — vždy s exception jako prvním argumentem.LogDebugpro detail, který by byl v Infoverze spam.- Structured logging:
_logger.LogInformation("Plugin {PluginId} loaded in {Elapsed} ms", pluginId, ms);— parametry jako placeholdery, ne string interpolation.
- GUI:
%LocalAppData%\WorkTracker\logs\worktracker-YYYYMMDD.log - CLI:
%LocalAppData%\WorkTracker\logs\worktracker-cli-YYYYMMDD.log - Retence: 14 souborů.
Push + PR na master:
actions/checkout@v5actions/setup-dotnet@v5s.NET 10.0.xdotnet restoredotnet build --no-restoredotnet test --no-build --verbosity normal
Běží na ubuntu-latest. WPF projekt (net10.0-windows) se na Linuxu kompilované builduje díky globálnímu nastavení <EnableWindowsTargeting>true</EnableWindowsTargeting> v Directory.Build.props — kompilace projde, ale runtime scénář (spuštění WPF aplikace) je jen Windows.
Trigger: push tagu v*. Jobs:
- test (ubuntu) — spustí celou test sadu před release.
- publish-cli (matrix: win-x64, linux-x64, osx-x64, osx-arm64) —
dotnet publishsPublishSingleFile=true,SelfContained=false, zip artifact. - publish-wpf (windows-latest) — WPF jen pro Windows, zip přes
Compress-Archive. - publish-avalonia (matrix: win-x64, linux-x64, osx-x64, osx-arm64, win-arm64) — Avalonia pro všechny platformy.
- publish-plugins (matrix: Atlassian, Luxafor, GoranG3, Office365Calendar) — každý plugin samostatně.
- release — stáhne artifacty a vytvoří GitHub Release.
Artifacty jsou framework‑dependent (bez runtime). Kdo chce self‑contained, buildí si sám.
-
Aktualizuj verzi v
Directory.Build.props:<Version>1.2.3</Version> <InformationalVersion>1.2.3</InformationalVersion>
-
Commit + push na master.
-
Vytvoř tag:
git tag v1.2.3 git push origin v1.2.3
-
CI spustí
release.yml, zkompiluje všechny platformy a vytvoří GitHub Release s artifacty. -
Release notes — edituj rovnou na GitHubu po vytvoření releasu (CI vytváří draft / prázdný release). Dělíme podle: Features, Fixes, Plugins, Chores.
TreatWarningsAsErrors = true = každý warning fail buildu. Nejčastější:
- CS8600 / CS8603 — nullability. Oprava:
?,!, nebo skutečná null guard. - CS0618 — obsolete API. Obvykle je v error message už předepsaný migration path.
- Analyzer warning (StyleCop / Roslynator) — nech analyzer vyjet v IDE, oprav podle hintu.
Zavři všechny instance WPF aplikace (i ty v tray), pak dotnet build znovu.
- Zkontroluj, že
.dllskutečně je vplugins/vedle binárky.PluginLoaderscanuje jen tam. - Plugin musí začínat prefixem
WorkTracker.Plugin.(loader filtruje podle jména souboru). - Plugin musí implementovat jedno z
IWorklogUploadPlugin/IWorkSuggestionPlugin/IStatusIndicatorPluginjako non‑abstract třídu. Abstraktní třídy jsou přeskočené. - V logu
PluginLoaderloguje každý soubor, který zkusil načíst, i důvod selhání.
EF Tools potřebují startup projekt s DI bootstrap, který vytvoří DbContext. Proto používáme --startup-project src/WorkTracker.CLI. CLI volá AddInfrastructure, který registruje DbContextFactory, a EF Tools to najdou.
Aktualizuj na Rider 2024.2+, nebo otevři jednotlivé projekty přes File → Open a vybrat .csproj.
Avalonia compiled bindings odhalí type mismatch už v XAML parseru. Pokud komponenta má složitější DataContext, extrahuj ji do samostatného UserControl s explicitním x:DataType — jinak si scope naleje typy z parenta a kompilátor řve.
Projekt nemá husky ani lefthook. Všechny kontroly běží v CI. Před push si ale udělej:
dotnet build
dotnet test