Języki programowania na platformie .NET / Programowanie .NET

Rok akademicki: 2026/27

Instrukcja laboratoryjna nr 3: Eksploracja Systemu Plików (System.IO), Generowanie Raportów OpenXML (EPPlus) oraz Czysta Architektura z DI

Prowadzący: Tomasz Goluch
Wersja: 1.2


1. Wprowadzenie: Standard OpenXML i Generowanie Raportów w .NET

Cel: Zrozumienie budowy dokumentów Office Open XML (.xlsx), porównanie podejść do programowego generowania arkuszy kalkulacyjnych na platformie .NET oraz poznanie modelu licencyjnego i architektury biblioteki EPPlus.

W aplikacjach biznesowych i analitycznych jednym z najczęstszych wymagań jest eksport przetworzonych danych oraz statystyk do postaci skoroszytów programu Microsoft Excel. Współczesny format .xlsx (wprowadzony w standardzie Office Open XML – OOXML, ECMA-376 / ISO/IEC 29500) nie jest monolitycznym plikiem binarnym (jak historyczny format .xls), lecz skompresowanym archiwum ZIP zawierającym hierarchię dokumentów XML opisujących strukturę danych, style, relacje oraz elementy graficzne:

[ Plik raportu: Raport.xlsx (Archiwum ZIP) ]
   ├── [Content_Types].xml          (Rejestr typów MIME wszystkich części pakietu)
   ├── _rels/.rels                  (Główne powiązania wewnątrz pakietu)
   ├── docProps/core.xml            (Metadane: Autor, Tytuł, Firma, Data utworzenia)
   └── xl/
        ├── workbook.xml            (Definicja skoroszytu i lista arkuszy)
        ├── styles.xml              (Czcionki, kolory tła, obramowania, formaty liczb)
        ├── sharedStrings.xml       (Słownik unikalnych ciągów tekstowych – deduplikacja)
        ├── worksheets/
        │    ├── sheet1.xml         (Komórki, wartości, formuły i konspekt arkusza 1)
        │    └── sheet2.xml         (Komórki i dane arkusza 2)
        └── drawings/
             └── charts/chart1.xml  (Definicje wykresów i serii danych)

Porównanie technologii obsługi plików Excel w ekosystemie .NET

Cecha Microsoft Office Interop (COM) Open XML SDK (DocumentFormat.OpenXml) Biblioteka EPPlus (OfficeOpenXml)
Wymagania systemowe Wymaga zainstalowanego programu MS Excel i systemu Windows Brak (czysta biblioteka .NET) Brak (wieloplatformowa biblioteka .NET 8/9+)
Wydajność i stabilność Bardzo niska (uruchamia proces EXCEL.EXE w tle, ryzyko wycieków COM) Bardzo wysoka (szczególnie w trybie strumieniowym SAX) Wysoka (optymalizowana pamięć z RecyclableMemoryStream)
Poziom abstrakcji API Wysoki, ale przestarzały model obiektowy COM Bardzo niski (ręczne operacje na węzłach drzewa XML) Wysoki i intuicyjny (Cells["A1"], stylizacja, wykresy, tabele przestawne)
Zastosowanie na serwerze / w Dockerze Niewspierane przez Microsoft Wspierane W pełni wspierane (Linux / Windows / macOS)

🧠 Zrozumieć ewolucję licencjonowania EPPlus (Wersje 4 vs 5–7 vs 8+):


2. Kompilacja Biblioteki EPPlus ze Źródeł i Zarządzanie Referencjami

Cel: Opanowanie pracy z zewnętrznym kodem otwartoźródłowym, dostosowanie docelowych frameworków (Multi-Targeting), uruchamianie i debugowanie testów jednostkowych biblioteki oraz dołączanie lokalnie zbudowanych zestawów .dll.

W codziennej pracy najprostszym sposobem instalacji biblioteki jest użycie menedżera pakietów NuGet:

dotnet add package EPPlus

(lub w Konsoli Menedżera Pakietów Visual Studio: Install-Package EPPlus -Project <NazwaProjektu>).

W celach inżynierskich i diagnostycznych ogromną wartość daje jednak pobranie i samodzielna kompilacja kodu źródłowego biblioteki. Pozwala to na pracę z najnowszą wersją kodu, analizę architektury testów jednostkowych twórców oraz bezpośrednie wchodzenie debugerem (F11) do wnętrza metod biblioteki.

1. Pobranie repozytorium i konfiguracja docelowych frameworków

  1. Sklonuj oficjalne repozytorium biblioteki EPPlus z serwisu GitHub:
    git clone [https://github.com/EPPlusSoftware/EPPlus.git](https://github.com/EPPlusSoftware/EPPlus.git)
  2. Otwórz rozwiązanie znajdujące się w podkatalogu src\EPPlus.sln.
  3. Projekty biblioteki oraz testów w oficjalnej solucji wykorzystują mechanizm Multi-Targeting (kompilację na wiele wersji platformy .NET jednocześnie, w tym historyczne wersje .NET Framework):
    <TargetFrameworks>net8.0;net6.0;netstandard2.1;netstandard2.0;net462;net35</TargetFrameworks>
    Jeśli na stacji roboczej nie zainstalowano starszych pakietów SDK (Targeting Packs dla .NET Framework 3.5 / 4.6.2), kompilacja wszystkich celów zakończy się błędem.
  4. Aby przyspieszyć kompilację i uniknąć błędów brakujących SDK, w plikach projektów (EPPlus.csproj oraz projekcie testowym EPPlusTest.csproj) ogranicz listę docelowych frameworków do wersji używanej w laboratorium (np. net8.0 lub net9.0):
    <TargetFrameworks>net8.0</TargetFrameworks>

2. Budowanie biblioteki i debugowanie testów jednostkowych

Po zbudowaniu biblioteki (dotnet build lub Ctrl+Shift+B w Visual Studio) możesz zweryfikować poprawność jej działania, uruchamiając zestaw testów jednostkowych.

3. Dołączanie zbudowanej biblioteki do własnego projektu

Aby wykorzystać samodzielnie skompilowaną bibliotekę w nowym projekcie (tworzonym w osobnej solucji):

  1. Dodaj w swoim projekcie bezpośrednią referencję do skompilowanego pliku EPPlus.dll oraz EPPlus.Interfaces.dll (znajdujących się w katalogu src\EPPlus\bin\Debug\net8.0\):
    • W Visual Studio: Kliknij prawym przyciskiem myszy na węzeł Zależności (Dependencies) → Dodaj odwołanie do projektu (Add Project Reference...) → zakładka Przeglądaj (Browse) → Przeglądaj... i wskaż pliki EPPlus.dll oraz EPPlus.Interfaces.dll.
    • Bezpośrednio w pliku .csproj:
      <ItemGroup>
        <Reference Include="EPPlus">
          <HintPath>..\..\EPPlus\src\EPPlus\bin\Debug\net8.0\EPPlus.dll</HintPath>
        </Reference>
        <Reference Include="EPPlus.Interfaces">
          <HintPath>..\..\EPPlus\src\EPPlus\bin\Debug\net8.0\EPPlus.Interfaces.dll</HintPath>
        </Reference>
      </ItemGroup>
  2. Nie instaluj pakietu EPPlus przez NuGet w tym samym projekcie – doprowadziłoby to do konfliktu i niejednoznaczności typów w przestrzeni nazw OfficeOpenXml.
  3. Ponieważ ręczne dodanie referencji do pliku .dll nie przenosi automatycznie zależności przechodnich (transitive dependencies) zadeklarowanych w NuGet, środowisko może zgłosić brak bibliotek pomocniczych w czasie uruchomienia. Należy je wówczas doinstalować w swoim projekcie za pomocą NuGet CLI:
    dotnet add package Microsoft.IO.RecyclableMemoryStream
    dotnet add package Microsoft.Extensions.Configuration.Json
💡 Wyjaśnienie: Jak działa debugowanie kodu zewnętrznej biblioteki .dll i jak automatycznie wyeksportować wszystkie jej zależności? (Kliknij, aby rozwinąć/zwinąć)

1. Rola plików .pdb przy referencji binarnej

Gdy dodajesz referencję bezpośrednio do pliku EPPlus.dll skompilowanego w konfiguracji Debug, w tym samym folderze znajduje się wygenerowany plik bazy symboli EPPlus.pdb. Plik ten zawiera pełne mapowanie instrukcji MSIL na ścieżki plików .cs z Twojego dysku.

Dopóki nie usuniesz sklonowanego folderu ze źródłami EPPlus, naciśnięcie klawisza F11 (Step Into) na wywołaniu np. ws.Cells.AutoFitColumns() w Twojej aplikacji sprawi, że Visual Studio automatycznie otworzy odpowiedni plik źródłowy .cs biblioteki EPPlus i pozwoli na debugowanie jej algorytmów linijka po linijce!

2. Automatyczne kopiowanie EPPlus.Interfaces.dll podczas kompilacji

W najnowszych wersjach EPPlus pakiet EPPlus.Interfaces jest pobierany jako zależność NuGet podczas budowania projektu EPPlus.csproj. Domyślnie .NET SDK nie kopiuje bibliotek z pakietów NuGet do folderu bin\Debug\net8.0 dla projektów będących bibliotekami klas (Class Library).

Aby wymusić skopiowanie pliku EPPlus.Interfaces.dll (oraz pozostałych zależności) bezpośrednio obok pliku EPPlus.dll, wystarczy dodać do pliku src\EPPlus\EPPlus.csproj w sekcji <PropertyGroup> właściwość:

<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

lub zbudować projekt poleceniem dotnet publish -c Debug.


3. Operacje na Plikach i Katalogach (System.IO) oraz Odporna Rekurencja

Cel: Zapoznanie z klasami przestrzeni nazw System.IO służącymi do eksploracji systemu plików oraz implementacja bezpiecznego algorytmu rekurencyjnego odpornego na błędy uprawnień.

Platforma .NET udostępnia w przestrzeni System.IO dwie grupy klas do pracy z systemem plików:

1. Podstawowe operacje na DirectoryInfo oraz FileInfo

using System.IO;

// Inicjalizacja obiektu reprezentującego katalog
var dir = new DirectoryInfo(@"C:\Projekty");

if (dir.Exists)
{
    // Przeglądanie bezpośrednich podkatalogów
    foreach (DirectoryInfo subDir in dir.GetDirectories())
    {
        Console.WriteLine($"[KATALOG] {subDir.Name} | Atrybuty: {subDir.Attributes}");
    }

    // Przeglądanie plików w bieżącym katalogu
    foreach (FileInfo file in dir.GetFiles())
    {
        string ext = file.Extension;       // Rozszerzenie pliku (np. ".cs", ".xlsx")
        long sizeBytes = file.Length;      // Rozmiar pliku w bajtach
        FileAttributes attr = file.Attributes; // Flagi bitowe (np. ReadOnly, Hidden, Archive)

        Console.WriteLine($"[PLIK] {file.Name} ({ext}) - {sizeBytes} B [{attr}]");
    }
}

⚠️ Pułapka SearchOption.AllDirectories w systemach produkcyjnych:
Metoda dir.GetFiles("*", SearchOption.AllDirectories) wydaje się najprostszym sposobem na pobranie wszystkich plików z podkatalogów. Posiada jednak krytyczną wadę: jeśli natrafi na choćby jeden katalog chroniony przez system operacyjny (np. C:\$RECYCLE.BIN lub C:\System Volume Information), rzuca wyjątek UnauthorizedAccessException i natychmiast przerywa całe przeszukiwanie, nie zwracając żadnych wyników!
Ponadto nie pozwala na kontrolowanie maksymalnej głębokości zagnieżdżenia (maxDepth).

2. Wzorzec odpornej rekurencji z kontrolą głębokości

Aby aplikacja działała niezawodnie, należy zaimplementować własną rekurencję (lub iterację ze stosem), przechwytując wyjątki UnauthorizedAccessException oraz IOException na poziomie pojedynczego podkatalogu:

public void ScanDirectorySafe(DirectoryInfo currentDir, int currentDepth, int maxDepth, IList<string> errorLog)
{
    if (currentDepth > maxDepth)
        return;

    FileInfo[] files = Array.Empty<FileInfo>();
    try
    {
        files = currentDir.GetFiles();
    }
    catch (UnauthorizedAccessException ex)
    {
        errorLog.Add($"Brak uprawnień do odczytu plików w '{currentDir.FullName}': {ex.Message}");
        return; // Pomijamy niedostępny katalog, ale kontynuujemy działanie programu!
    }
    catch (IOException ex)
    {
        errorLog.Add($"Błąd We/Wy podczas odczytu '{currentDir.FullName}': {ex.Message}");
        return;
    }

    foreach (FileInfo file in files)
    {
        // Przetwarzanie metadanych pliku (file.Name, file.Extension, file.Length, file.Attributes)...
    }

    if (currentDepth == maxDepth)
        return;

    DirectoryInfo[] subDirs = Array.Empty<DirectoryInfo>();
    try
    {
        subDirs = currentDir.GetDirectories();
    }
    catch (UnauthorizedAccessException ex)
    {
        errorLog.Add($"Brak dostępu do podkatalogów '{currentDir.FullName}': {ex.Message}");
        return;
    }
    catch (IOException ex)
    {
        errorLog.Add($"Błąd We/Wy katalogu '{currentDir.FullName}': {ex.Message}");
        return;
    }

    foreach (DirectoryInfo subDir in subDirs)
    {
        ScanDirectorySafe(subDir, currentDepth + 1, maxDepth, errorLog);
    }
}

4. Biblioteka EPPlus: Skoroszyty, Arkusze, Adresowanie i Konspekty Danych

Cel: Opanowanie API biblioteki EPPlus (OfficeOpenXml) do tworzenia skoroszytów, manipulacji komórkami, definiowania formuł oraz grupowania wierszy w konspekty (Outlines).

1. Konfiguracja licencji i inicjalizacja pakietu ExcelPackage

Wszystkie główne klasy biblioteki znajdują się w przestrzeni nazw OfficeOpenXml:

using OfficeOpenXml;

// 1. Deklaracja użycia w kontekście niekomercyjnym (PolyForm Noncommercial) - wymagane w EPPlus 8+
ExcelPackage.License.SetNonCommercialPersonal("Jan Kowalski");

// 2. Określenie ścieżki docelowej pliku (jeśli plik istnieje, usuwamy go przed wygenerowaniem nowego)
var file = new FileInfo("labEpp.xlsx");
if (file.Exists)
{
    file.Delete();
}

// 3. Utworzenie pakietu ExcelPackage (implementuje IDisposable)
using var ep = new ExcelPackage(file);

// 4. Ustawienie metadanych dokumentu (właściwości skoroszytu)
ep.Workbook.Properties.Title = "Raport struktury katalogu";
ep.Workbook.Properties.Author = "Jan Kowalski";
ep.Workbook.Properties.Comments = "Wygenerowano automatycznie w .NET";
ep.Workbook.Properties.Company = "Politechnika Gdańska";
ep.Workbook.Properties.SetCustomPropertyValue("WersjaGeneratora", "1.2");

2. Zarządzanie arkuszami (ExcelWorksheet) i zapis pliku

// Dodanie nowego arkusza do skoroszytu (nazwa arkusza musi być unikalna)
ExcelWorksheet ws = ep.Workbook.Worksheets.Add("Struktura katalogu");

// Pobranie istniejącego arkusza po nazwie lub po indeksie (indeksowanie od 0 w .NET Core / .NET 8+!)
ExcelWorksheet wsByName = ep.Workbook.Worksheets["Struktura katalogu"];
ExcelWorksheet wsByIndex = ep.Workbook.Worksheets[0];

// Zapisanie pakietu na dysk (wymaga obecności przynajmniej jednego arkusza w skoroszycie)
ep.Save();

3. Adresowanie komórek, zakresy i formuły

⚠️ KRYTYCZNA ZASADA – Indeksowanie komórek od 1 (1-based):
W przeciwieństwie do tablic w języku C#, współrzędne komórek ws.Cells[wiersz, kolumna], wierszy ws.Row(wiersz) oraz kolumn ws.Column(kolumna) w EPPlus są indeksowane od 1!

// Dostęp do pojedynczej komórki po współrzędnych [wiersz, kolumna] oraz po adresie tekstowym:
ws.Cells[1, 2].Value = 14.5;              // Komórka B1
ws.Cells["A2"].Formula = "$E5*G$12";      // Komórka A2 z formułą w notacji A1 (bez znaku '=' na początku!)

// Dostęp do prostokątnego zakresu komórek [OdWiersza, OdKolumny, DoWiersza, DoKolumny]:
ws.Cells[5, 7, 9, 10].Value = 14.5;       // Zakres G5:J9
ws.Cells["B1:D14"].Formula = "$E$5*G$12"; // Formuła w klasycznej notacji A1 (odwołania bezwzględne/mieszane z '$')

// Formuła w notacji względnej R1C1 (np. wartość z wiersza powyżej pomnożona przez kolumnę po lewej):
ws.Cells["E2:E4"].FormulaR1C1 = "R[-1]C*RC[-1]";

// Wykorzystanie klasy pomocniczej ExcelAddress do dynamicznego budowania formuł:
string adr = new ExcelAddress(4, 4, 6, 8).Address; // Zwróci ciąg "D4:H6"
using (ExcelRange range = ws.Cells[1, 1, 2, 1])
{
    range.Formula = $"AVERAGE({adr})";
}

4. Automatyczne wczytywanie danych z kolekcji (LoadFromCollection)

Metoda LoadFromCollection<T> pozwala błyskawicznie wypełnić arkusz danymi z kolekcji obiektów lub rekordów. Kolejne kolumny odpowiadają publicznym właściwościom klasy, a wiersze – kolejnym elementom kolekcji. Ustawienie parametru PrintHeaders: true automatycznie wygeneruje wiersz nagłówkowy z nazwami właściwości:

public record FileStatItem(string Name, string Extension, long SizeBytes);

var topFiles = new List<FileStatItem>
{
    new("baza.bak", ".bak", 104857600),
    new("wideo.mp4", ".mp4", 52428800)
};

// Wpisanie tabeli począwszy od komórki A2 wraz z nagłówkami kolumn:
ws.Cells["A2"].LoadFromCollection(topFiles, PrintHeaders: true);

5. Konspekt danych (Grupowanie / Outline), kolumny i scalanie komórek

Mechanizm konspektu (Outline) pozwala na tworzenie zwijanych i rozwijanych grup wierszy lub kolumn (widocznych w Excelu jako przyciski [+] oraz [-] na lewym/górnym marginesie):

// WAŻNE dla drzewa katalogów: Domyślnie Excel zakłada, że wiersz podsumowujący znajduje się POD grupą.
// Aby nagłówek katalogu nadrzędnego znajdował się NAD jego zawartością, ustawiamy:
ws.OutLineSummaryBelow = false;
ws.OutLineSummaryRight = false;

// Przypisanie poziomu zagnieżdżenia konspektu (wartości od 1 do maksymalnie 7):
ws.Row(2).OutlineLevel = 1; // Poziom 1 (np. zawartość katalogu głównego)
ws.Row(3).OutlineLevel = 2; // Poziom 2 (np. zawartość podkatalogu)
ws.Column(4).OutlineLevel = 1;

// Domyślne zwinięcie/ukrycie zgrupowanej kolumny lub wiersza:
ws.Column(1).Collapsed = true;

// Ustalanie szerokości kolumn i autodopasowanie:
ws.Column(1).Width = 25.0;
ws.Column(1).AutoFit();           // Dopasowanie pojedynczej kolumny do zawartości
ws.Cells.AutoFitColumns(0);       // Autodopasowanie wszystkich kolumn w arkuszu (z podaniem szerokości min.)

// Wyłączenie wyświetlania linii siatki i scalanie komórek:
ws.View.ShowGridLines = false;
ws.Cells[5, 6, 5, 9].Merge = true;
ws.Cells["A5:E5"].Merge = true;

⚠️ Ograniczenie formatu Excel dla OutlineLevel:
Specyfikacja Microsoft Excel pozwala na maksymalnie 7 poziomów grupowania konspektu (OutlineLevel z przedziału 0..7). Jeśli głębokość rekurencji katalogów przekroczy 7, przed przypisaniem wartości należy ją ograniczyć: ws.Row(r).OutlineLevel = Math.Min(depth, 7);.


5. Stylizacja Komórek, Nagłówki Wydruku oraz Wykresy Kołowe

Cel: Formatowanie wizualne arkuszy, stosowanie masek formatowania liczb i dat oraz osadzanie wykresów 3D opartych na seriach danych.

1. Formatowanie wyglądu komórek (Style) oraz maski liczbowe (Numberformat)

using System.Drawing;
using OfficeOpenXml.Style;

using (ExcelRange range = ws.Cells[1, 1, 1, 4])
{
    range.Style.Font.Bold = true;
    range.Style.Font.Italic = true;
    range.Style.Font.Color.SetColor(Color.FromArgb(0, 0, 255));

    range.Style.Fill.PatternType = ExcelFillStyle.Solid; // lub np. ExcelFillStyle.DarkUp
    range.Style.Fill.BackgroundColor.SetColor(Color.Aquamarine);

    range.Style.Border.Top.Style = ExcelBorderStyle.Medium;
    range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Right;
}

// Przykłady określania formatu liczbowego oraz daty (Numberformat.Format):
ws.Cells["B2:B10"].Style.Numberformat.Format = "#####";      // 123 -> 123
ws.Cells["B2:B10"].Style.Numberformat.Format = "00000";      // 123 -> 00123
ws.Cells["B2:B10"].Style.Numberformat.Format = "#,#";        // 1234567890 -> 1,234,567,890
ws.Cells["B2:B10"].Style.Numberformat.Format = "00.00";      // 1.2 -> 01.20
ws.Cells["B2:B10"].Style.Numberformat.Format = "#,##0.00";   // 1234567890 -> 1,234,567,890.00
ws.Cells["C2:C10"].Style.Numberformat.Format = "#0.##%";     // 0.092 -> 9.2%
ws.Cells["D2:D10"].Style.Numberformat.Format = "m/d/yyyy";   // Format daty
ws.Cells["A2:A10"].Style.Numberformat.Format = "@";          // Format tekstowy

2. Nagłówki i stopki strony (HeaderFooter)

// Nagłówek dla stron nieparzystych (czcionka Times New Roman, rozmiar 22, podkreślona):
ws.HeaderFooter.OddHeader.CenteredText = "&22&U&\"Times New Roman,Regular Bold\" Struktura Katalogu";

// Stopka: numer bieżącej strony i całkowita liczba stron (po prawej), nazwa arkusza (w środku), ścieżka (po lewej):
ws.HeaderFooter.OddFooter.RightAlignedText = $"Page {ExcelHeaderFooter.PageNumber} of {ExcelHeaderFooter.NumberOfPages}";
ws.HeaderFooter.OddFooter.CenteredText = ExcelHeaderFooter.SheetName;
ws.HeaderFooter.OddFooter.LeftAlignedText = ExcelHeaderFooter.FilePath + ExcelHeaderFooter.FileName;

3. Tworzenie i konfiguracja wykresów kołowych (ExcelPieChart)

Aby utworzyć wykres w przestrzeni OfficeOpenXml.Drawing.Chart, w arkuszu muszą najpierw znajdować się komórki zawierające etykiety kategorii (np. nazwy rozszerzeń w E2:E6) oraz odpowiadające im wartości liczbowe (np. liczba plików w F2:F6):

using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Chart;

// 1. Dodanie wykresu kołowego 3D do kolekcji rysunków arkusza (nazwa wykresu musi być unikalna):
var chart = ws.Drawings.AddChart("PieChartExtensionsCount", eChartType.Pie3D) as ExcelPieChart;
chart!.Title.Text = "Udział ilościowy rozszerzeń plików";

// 2. Ustalenie pozycji wykresu: SetPosition(WierszOd0, OffsetWierszaPx, KolumnaOd0, OffsetKolumnyPx)
// UWAGA: W metodzie SetPosition wiersz i kolumna są indeksowane od 0! (0 = wiersz 1, 5 = kolumna F)
chart.SetPosition(Row: 0, RowOffsetPixels: 5, Column: 7, ColumnOffsetPixels: 5);
chart.SetSize(PixelWidth: 600, PixelHeight: 350);

// 3. Podpięcie serii danych: Series.Add(AdresWartości, AdresKategorii)
ExcelAddress valAddr = new ExcelAddress(fromRow: 2, fromCol: 6, toRow: 6, toCol: 6); // F2:F6 (Wartości)
ExcelAddress catAddr = new ExcelAddress(fromRow: 2, fromCol: 5, toRow: 6, toCol: 5); // E2:E6 (Etykiety rozszerzeń)
var serie = chart.Series.Add(valAddr.Address, catAddr.Address) as ExcelPieChartSerie;

// 4. Włączenie wyświetlania nazw kategorii oraz wartości procentowych na wycinkach koła:
chart.DataLabel.ShowCategory = true;
chart.DataLabel.ShowPercent = true;

// 5. Formatowanie obramowania i tła legendy:
chart.Legend.Border.LineStyle = eLineStyle.Solid;
chart.Legend.Border.Fill.Style = eFillStyle.SolidFill;
chart.Legend.Border.Fill.Color = Color.DarkBlue;

6. Czysta Architektura (Clean Architecture), Kontener DI i Interfejs Terminal.Gui

Cel: Zastosowanie zasady Odwrócenia Zależności (DIP) oraz wbudowanego kontenera Microsoft.Extensions.DependencyInjection do odseparowania logiki analizy plików od biblioteki EPPlus.

W dobrze zaprojektowanej aplikacji zgodnej z Clean Architecture logika biznesowa (skanowanie drzewa plików, obliczanie rozmiarów katalogów, wyznaczanie 10 największych plików oraz agregacja statystyk rozszerzeń) nie powinna bezpośrednio zależeć od klas konkretnej biblioteki raportującej (ExcelPackage).

[ Warstwa Prezentacji (Program.cs / Terminal.Gui) ]
                        │
                        ▼ (Wstrzyknięcie przez DI)
[ Usługa Aplikacji: ReportService(IDirectoryScanner, IExcelReportGenerator) ]
         │                                               │
         ▼ zależy od abstrakcji                          ▼ zależy od abstrakcji
[ Interfejs: IDirectoryScanner ]                [ Interfejs: IExcelReportGenerator ]
         ▲                                               ▲
         │ implementuje (System.IO)                      │ implementuje (EPPlus)
[ FileSystemDirectoryScanner ]                  [ EpplusReportGenerator ]

1. Definiowanie kontraktów i rejestracja w Microsoft.Extensions.DependencyInjection

using Microsoft.Extensions.DependencyInjection;

// Kontrakty (Interfejsy warstwy domeny/aplikacji):
public interface IDirectoryScanner
{
    DirectoryScanResult Scan(string rootPath, int maxDepth);
}

public interface IExcelReportGenerator
{
    void GenerateReport(DirectoryScanResult scanResult, string outputFilePath);
}

// Główna usługa aplikacyjna (programuje wyłącznie do interfejsów!):
public class ReportService(IDirectoryScanner scanner, IExcelReportGenerator reportGenerator)
{
    public DirectoryScanResult CreateDirectoryReport(string sourceDir, int maxDepth, string targetExcelPath)
    {
        var result = scanner.Scan(sourceDir, maxDepth);
        reportGenerator.GenerateReport(result, targetExcelPath);
        return result;
    }
}

Konfiguracja kontenera DI w Program.cs (z użyciem Top-level statements):

var services = new ServiceCollection();

services.AddTransient<IDirectoryScanner, FileSystemDirectoryScanner>();
services.AddTransient<IExcelReportGenerator, EpplusReportGenerator>();
services.AddTransient<ReportService>();

using ServiceProvider serviceProvider = services.BuildServiceProvider();

var reportService = serviceProvider.GetRequiredService<ReportService>();
reportService.CreateDirectoryReport(@"D:\", maxDepth: 3, @"D:\scanDir.xlsx");
💡 Tworzenie tekstowego interfejsu użytkownika (TUI) z pakietem Terminal.Gui (Kliknij, aby rozwinąć/zwinąć)

Biblioteka Terminal.Gui pozwala na tworzenie okienkowych interfejsów użytkownika bezpośrednio w konsoli tekstowej (obsługujących mysz, skróty klawiszowe, pola tekstowe i przyciski).

Instalacja pakietu (wersja stabilna gałęzi 1.x):

dotnet add package Terminal.Gui --version 1.17.1

Przykładowa implementacja formularza parametryzującego skanowanie:

using Terminal.Gui;

Application.Init();
try
{
    var top = Application.Top;
    var win = new Window("Pobierz katalog plików - naciśnij Ctrl+Q, aby zamknąć")
    {
        X = 0, Y = 0,
        Width = Dim.Fill(), Height = Dim.Fill()
    };
    top.Add(win);

    var lblOut = new Label("Ścieżka zapisu:") { X = 2, Y = 2 };
    var txtOut = new TextField(@"d:\scanDir.xlsx") { X = 28, Y = 2, Width = 40 };

    var lblDir = new Label("Ścieżka do przeszukania:") { X = 2, Y = 4 };
    var txtDir = new TextField(@"d:\") { X = 28, Y = 4, Width = 40 };

    var lblDepth = new Label("Głębokość przeszukiwania:") { X = 2, Y = 6 };
    var txtDepth = new TextField("3") { X = 28, Y = 6, Width = 40 };

    var btnRun = new Button("Generuj raport") { X = 28, Y = 8 };
    var txtLog = new TextView()
    {
        X = 2, Y = 10,
        Width = Dim.Fill() - 4,
        Height = Dim.Fill() - 2,
        ReadOnly = true
    };

    btnRun.Clicked += () =>
    {
        string outPath = txtOut.Text.ToString() ?? "";
        string dirPath = txtDir.Text.ToString() ?? "";

        if (!int.TryParse(txtDepth.Text.ToString(), out int depth) || depth < 0)
        {
            MessageBox.ErrorQuery("Błąd walidacji", "Głębokość musi być liczbą nieujemną!", "OK");
            return;
        }

        try
        {
            var result = reportService.CreateDirectoryReport(dirPath, depth, outPath);
            txtLog.Text = string.Join(Environment.NewLine, result.Errors)
                          + $"{Environment.NewLine}Pomyślnie utworzono plik!{Environment.NewLine}Znajduje się on w pliku: {outPath}";
        }
        catch (Exception ex)
        {
            txtLog.Text = $"Błąd krytyczny: {ex.Message}";
        }
    };

    win.Add(lblOut, txtOut, lblDir, txtDir, lblDepth, txtDepth, btnRun, txtLog);
    Application.Run();
}
finally
{
    Application.Shutdown();
}

Zadanie Laboratoryjne do wykonania (100 pkt)

Cel zadania: Stworzenie aplikacji konsolowej w architekturze Clean Architecture z wykorzystaniem lokalnie skompilowanej biblioteki EPPlus, która skanuje wskazaną strukturę katalogów do zadanej głębokości i generuje wieloarkuszowy raport Excel (.xlsx) zawierający drzewo plików ze zwijanym konspektem (Outline), ranking 10 największych plików oraz dwa wykresy kołowe 3D prezentujące statystyki rozszerzeń.


Część 1: Pobranie, Kompilacja i Uruchomienie Testów Biblioteki EPPlus (10 pkt)

  1. Pobierz kod źródłowy biblioteki EPPlus za pomocą git clone z oficjalnego repozytorium:
    git clone [https://github.com/EPPlusSoftware/EPPlus.git](https://github.com/EPPlusSoftware/EPPlus.git)
  2. Otwórz solucję src\EPPlus.sln, dostosuj w razie potrzeby <TargetFrameworks> do zainstalowanego środowiska (.NET 8 lub .NET 9), skompiluj bibliotekę i uruchom wybrane testy jednostkowe (zademonstruj możliwość ich debugowania).
  3. Utwórz nową solucję wraz z projektem konsolowym (np. ExcelDirScan) – nie dodawaj własnego projektu do solucji biblioteki src\EPPlus.sln!
  4. Dodaj w swoim projekcie bezpośrednią referencję binarną do zbudowanych w podpunkcie 2 bibliotek EPPlus.dll oraz EPPlus.Interfaces.dll (nie instaluj pakietu EPPlus przez NuGet!).
  5. Doinstaluj za pomocą menedżera pakietów NuGet wymagane zależności przechodnie (np. Microsoft.IO.RecyclableMemoryStream, Microsoft.Extensions.Configuration.Json), jeśli środowisko zgłosi ich brak podczas uruchomienia.

Część 2: Arkusz „Struktura katalogu”, Metadane i Konspekt Danych (45 pkt)

  1. (20 pkt) Eksploracja katalogu z limitem głębokości:
    • Skonfiguruj licencję niekomercyjną EPPlus (ExcelPackage.License.SetNonCommercialPersonal(...)) oraz właściwości dokumentu (Title, Author, Company).
    • Utwórz nowy dokument Excela i dodaj w nim arkusz o nazwie „Struktura katalogu”.
    • Wpisz do kolejnych wierszy zawartość katalogu (zarówno pliki, jak i podkatalogi) przekazanego jako pierwszy parametr wywołania metody. Drugim parametrem metody jest liczba całkowita określająca maksymalną głębokość rekurencyjnego przeszukiwania (maxDepth).
  2. (15 pkt) Metadane elementów systemu plików:
    • W kolejnych kolumnach arkusza wyświetl szczegółowe informacje o elementach: Rozszerzenie, Rozmiar (w bajtach, z odpowiednim formatowaniem liczbowym) oraz Atrybuty (FileAttributes).
  3. (10 pkt) Grupowanie danych (Konspekt / Outline):
    • Dodaj możliwość zwijania i rozwijania zawartości poszczególnych podkatalogów za pomocą mechanizmu konspektu danych (ws.Row(r).OutlineLevel).
    • Pamiętaj o ustawieniu ws.OutLineSummaryBelow = false;, aby podkatalogi zwijały się do swojego wiersza nagłówkowego, oraz o dopasowaniu szerokości kolumn (AutoFitColumns).

Część 3: Arkusz „Statystyki” – Top 10 Plików oraz Wykresy Kołowe (25 pkt)

  1. (15 pkt) Ranking największych plików:
    • Dodaj do tego samego skoroszytu drugi arkusz o nazwie „Statystyki”.
    • Umieść w nim sformatowaną tabelę zawierającą listę 10 największych plików znalezionych podczas skanowania, posortowaną malejąco według rozmiaru (możesz wykorzystać LINQ: .OrderByDescending(f => f.Size).Take(10) oraz metodę LoadFromCollection).
  2. (10 pkt) Wizualizacja udziału rozszerzeń na wykresach kołowych:
    • Wygeneruj w arkuszu „Statystyki” zestawienie danych pogrupowanych według rozszerzeń plików (liczba wystąpień każdego rozszerzenia oraz łączna zajmowana powierzchnia).
    • Dodaj do arkusza „Statystyki” dwa wykresy kołowe (eChartType.Pie3D lub Pie) prezentujące:
      1. Procentowy udział plików o danym rozszerzeniu pod względem ilościowym (liczby plików).
      2. Procentowy udział plików o danym rozszerzeniu pod względem zajmowanej powierzchni (rozmiaru na dysku).
    • Skonfiguruj na obu wykresach wyświetlanie kategorii oraz wartości procentowych (ShowCategory = true, ShowPercent = true).
    • (Wskazówka: Jeśli wykresy 3D są niewidoczne w pakiecie LibreOffice Calc, można zweryfikować ich poprawne wygenerowanie w przeglądarce online: https://onlinedocumentviewer.com/Viewer/).

Część 4: Czysta Architektura i Wstrzykiwanie Zależności (DI) (10 pkt)

  1. Zaprojektuj aplikację zgodnie z zasadami Clean Architecture oraz zasadą Odwrócenia Zależności (DIP):
    • Wykorzystaj kontener DI z pakietu NuGet Microsoft.Extensions.DependencyInjection.
    • Wydziel interfejsy abstrakcji dla operacji wejścia/wyjścia i generowania raportu (np. IDirectoryScanner oraz IExcelReportGenerator / IWorkbookBuilder).
    • Zamknij główną logikę orkiestracji w usłudze (np. ReportService), która przyjmuje wymagane abstrakcje przez konstruktor, oddzielając logikę domenową i wyliczanie statystyk od bezpośrednich wywołań klas implementacyjnych EPPlus.

Część 5: Odporność na Błędy i Interfejs Użytkownika (10 pkt)

  1. Odporność na błędy systemu plików:
    • Aplikacja musi być odporna na niepoprawne ścieżki wejściowe oraz brak uprawnień do odczytu katalogów systemowych (np. $RECYCLE.BIN, System Volume Information).
    • Sytuacje wyjątkowe należy obsłużyć w dedykowanych blokach catch (UnauthorizedAccessException, DirectoryNotFoundException, IOException), wypisując czytelny komunikat diagnostyczny.
    • Brak dostępu do pojedynczego podfolderu nie może przerywać skanowania pozostałej części drzewa katalogów!
  2. Punkt wejścia i interfejs użytkownika:
    • Punkt startowy aplikacji (Program.cs) zaimplementuj z wykorzystaniem instrukcji najwyższego poziomu (Top-level statements).
    • Opcjonalnie (rekomendowane): Wykorzystaj bibliotekę Terminal.Gui do stworzenia tekstowego menu pozwalającego na podanie ścieżki zapisu pliku .xlsx, ścieżki przeszukiwanego katalogu oraz głębokości skanowania, wraz z walidacją parametrów i wyświetleniem logu pominiętych katalogów.

Zestawienie Oceny (Suma: 100 pkt)

Nr Zakres modułu Weryfikacja Punkty
1 Kompilacja EPPlus ze źródeł i referencje Sklonowanie repozytorium, kompilacja, uruchomienie testów, referencja do EPPlus.dll i EPPlus.Interfaces.dll bez NuGet. 10 pkt
2 Arkusz „Struktura katalogu” i rekurencja Skanowanie plików i podkatalogów do zadanej głębokości, zapis w arkuszu. 20 pkt
3 Metadane plików i formatowanie Wyświetlenie rozszerzenia, rozmiaru i atrybutów (FileAttributes), stylizacja komórek. 15 pkt
4 Konspekt danych (Outline) Poprawne grupowanie wierszy podkatalogów (OutlineLevel, OutLineSummaryBelow = false). 10 pkt
5 Arkusz „Statystyki” – Top 10 plików Lista 10 największych plików posortowana malejąco w drugim arkuszu. 15 pkt
6 Wykresy kołowe rozszerzeń Dwa wykresy kołowe (ilościowy i rozmiarowy) z etykietami kategorii i procentów. 10 pkt
7 Clean Architecture i Kontener DI Podział na interfejsy, usługa ReportService, Microsoft.Extensions.DependencyInjection. 10 pkt
8 Odporność na błędy i UI (Terminal.Gui) Osobna obsługa UnauthorizedAccessException i IOException bez przerywania skanowania, Top-level statements / Terminal.Gui. 10 pkt
SUMA 100 pkt

Przykładowe pytania na obronie ustnej:

  1. Dlaczego standardowa metoda DirectoryInfo.GetFiles("*", SearchOption.AllDirectories) nie sprawdza się podczas skanowania dysku systemowego i jak należy zaimplementować bezpieczną eksplorację drzewa katalogów?
  2. Jakie indeksowanie (od 0 czy od 1) stosuje biblioteka EPPlus w przypadku kolekcji arkuszy Worksheets[i], komórek Cells[row, col] oraz metody pozycjonowania wykresu chart.SetPosition(...)?
  3. Dlaczego podczas tworzenia konspektu danych (OutlineLevel) dla drzewa katalogów konieczne jest ustawienie właściwości ws.OutLineSummaryBelow = false?
  4. Czym różni się dodanie do projektu bezpośredniej referencji binarnej (<Reference Include="EPPlus.dll">) od referencji pakietu NuGet (<PackageReference>) w kontekście rozwiązywania zależności przechodnich i debugowania kodu?
  5. W jaki sposób oddzielenie interfejsów IDirectoryScanner oraz IExcelReportGenerator od usługi ReportService ułatwia napisanie testów jednostkowych logiki raportowania bez konieczności fizycznego tworzenia plików na dysku?