Wprowadzenie
Tworząc aplikację, wcześniej czy później będziemy musieli zaimportować lub wyeksportować jakieś dane. Jednym z formatów, który prawdopodobnie będziemy musieli obsłużyć, będzie plik csv. Można taki import lub eksport zrobić ręcznie, korzystając z takich metod klasy string, jak Join lub Split. Z drugiej strony możemy skorzystać z czegoś gotowego. W swoich projektach, gdy mam pracować z plikami csv, wykorzystuję bibliotekę CsvHelper (https://joshclose.github.io/CsvHelper/), którą chcę Ci pokazać w tym wpisie.
Eksport list produktów
Przygotowałem dwie klasy, które będziemy chcieli wyeksportować do pliku csv i później zaimportować z powrotem do aplikacji. Pierwszą klasą będzie Product, która zawiera w sobie właściwość typu Category:
public class Category | |
{ | |
public int Id { get; set; } | |
public string Name { get; set; } | |
} |
public class Product | |
{ | |
public int Id { get; set; } | |
public string Name { get; set; } | |
public Category Category { get; set; } | |
public bool IsActive { get; set; } = true; | |
} |
W testowej aplikacji skorzystałem z biblioteki Bogus, aby wygenerować 5 testowych kategorii oraz 10 produktów:
static IEnumerable<Product> GetProducts() | |
{ | |
int id = 1; | |
var categories = new Faker<Category>() | |
.RuleFor(p => p.Id, (f, p) => id++) | |
.RuleFor(p => p.Name, (f, p) => f.Commerce.Categories(1)[0]) | |
.Generate(5); | |
id = 1; | |
return new Faker<Product>() | |
.RuleFor(p => p.Id, (f, p) => id++) | |
.RuleFor(p => p.Name, (f, p) => f.Commerce.ProductName()) | |
.RuleFor(p => p.Category, (f, p) => f.PickRandom(categories)) | |
.Generate(10); | |
} |
W wynikowym pliku będziemy chcieli zapisać cztery kolumny: ID produktu, nazwa produktu, ID kategorii oraz nazwę kategorii. Zobaczmy, jak można to zrobić z wykorzystaniem CsvHelper.
Po zainstalowaniu pakietu CsvHelper (https://www.nuget.org/packages/CsvHelper/) możemy skorzystać z klasy CsvWritter, którą budujemy na ogół na podstawie jakiejś klasy dziedziczącej po TextWriter. W przykładzie użyjemy klasy StreamWriter, która zapisze dane do pliku. CsvWriter zawiera szereg metod, które możemy wykorzystać i które zapisują pojedyncze obiekty lub całe ich kolekcje:
private static string _fileName = "products.csv"; | |
static void Export(IEnumerable<Product> products) | |
{ | |
using (var writer = new StreamWriter(_fileName, false)) | |
{ | |
using (var csvWriter = new CsvWriter(writer)) | |
{ | |
csvWriter.WriteRecords(products); | |
} | |
} | |
} |
Metoda WriteRecords zapisuje kolekcje przekazanych obiektów, czyli to, czego potrzebujemy w naszym przykładzie.
W wyniku działania powyższej metody w pliku products.csv pojawi się coś takiego (oczywiście z racji użycia biblioteki Bogus dane są losowe podczas każdego uruchomienia testowej aplikacji):
Id;Name;Id;Name;IsActive | |
1;Awesome Fresh Pants;1;Health;True | |
2;Unbranded Frozen Bacon;2;Books;True | |
3;Incredible Rubber Pizza;1;Health;True | |
4;Fantastic Concrete Pants;2;Books;True | |
5;Practical Fresh Cheese;3;Beauty;True | |
6;Gorgeous Metal Chicken;2;Books;True | |
7;Fantastic Steel Hat;2;Books;True | |
8;Generic Steel Shirt;2;Books;True | |
9;Rustic Cotton Keyboard;5;Home;True | |
10;Unbranded Steel Pants;1;Health;True |
Jak widać, wygenerowane dane w tym momencie są dalekie od tego, czego potrzebujemy. Dzieje się tak głównie przez to, że próbujemy wygenerować plik csv dla dwóch powiązanych klas. W takiej sytuacji biblioteka zapisuje w pliku wszystkie właściwości z obu klas pod ich nazwami (bez żadnych przedrostków), w związku z czym dostaliśmy kolumny o takich samych nazwach, ale innym znaczeniu i wartościach.
W przypadku gdybyśmy generowali plik dla płaskiego obiektu, takiego problemu by nie było. Ale z drugiej strony ta sytuacja pozwoli mi pokazać dodatkowe możliwości CsvHelper. 🙂
Konfigurowanie eksportu oraz importu
W CsvHelper możemy konfigurować działanie biblioteki. W takiej sytuacji jak nasza, możemy przekazać informacje dotyczące tego, w jaki sposób należy wygenerować plik z danymi. Robi się to poprzez dodanie klasy mapowań, w której definiujemy to, w jaki sposób należy wyeksportować lub zaimportować dane dla konkretnego typu.
Klasa mapowań w przykładzie wygląda tak:
public class ProductMap : ClassMap<Product> | |
{ | |
public ProductMap() | |
{ | |
AutoMap(); | |
Map(m => m.Category.Id).Name("CategoryId"); | |
Map(m => m.Category.Name).Name("CategoryName"); | |
Map(m => m.IsActive).Ignore(); | |
} | |
} |
Dziedziczy ona po klasie ClassMap z biblioteki. Jest generyczna i jako parametru oczekuje typu, dla którego będziemy konfigurować mapowanie (w przykładzie dla klasy Product). Następnie w konstruktorze klasy definiujemy mapowania.
Metoda AutoMap służy do dodania domyślnej logiki mapowań. W przypadku gdy korzystamy z klas mapowań, musimy określić wszystkie właściwości, które mają zostać wyeksportowane. Na ogół robi się tak, że na początku importujemy domyślne mapowania za pomocą metody AutoMap i później te mapowania zmieniamy.
Użycie metody AutoMap może być istotne w sytuacji, gdy chcemy, aby po dodaniu nowej właściwości do klasy Product również sama ta klasa została wyeksportowana. Gdybyśmy nie skorzystali z metody AutoMap, wtedy ręcznie musielibyśmy dodać mapowanie dla tej nowej właściwości.
Z drugiej strony nie skorzystamy z metody AutoMap, gdy chcemy zawsze mieć te same skonfigurowane właściwości, bez nowo dodanych do klasy.
Za pomocą metody Map definiujemy reguły mapowania dla właściwości przekazanej w formie wyrażenia lambda. W przykładzie użyłem dwóch metod:
- Name – określa nazwę kolumny w pliku – tutaj zmieniłem nazwę dla właściwości z danymi kategorii
- Ignore – służy do ignorowania właściwości podczas eksportu oraz importu – w przykładzie ignorujemy właściwość IsActive.
Biblioteka udostępnia jeszcze kilka innych metod, których możemy użyć podczas definiowania mapowań.
Po dodaniu klasy mapowań trzeba jeszcze przekazać ją do writera, aby wykorzystał ją podczas eksportu danych. Robi się to za pomocą metody RegisterClassMap przed zapisaniem danych do pliku:
private static string _fileName = "products.csv"; | |
static void Export(IEnumerable<Product> products) | |
{ | |
using (var writer = new StreamWriter(_fileName, false)) | |
{ | |
using (var csvWriter = new CsvWriter(writer)) | |
{ | |
csvWriter.Configuration.RegisterClassMap<ProductMap>(); | |
csvWriter.WriteRecords(products); | |
} | |
} | |
} |
Import produktów
Import danych z pliku za pomocą biblioteki CsvHelper jest równie łatwy, jak eksport danych. Proces importu jest bardzo podobny, tylko zamiast klas writer korzystamy z klas reader. Najlepiej od razu zobaczyć, jak wygląda kod importu:
static IEnumerable<Product> Import() | |
{ | |
using (var reader = new StreamReader(_fileName)) | |
{ | |
using (var csvReader = new CsvReader(reader)) | |
{ | |
csvReader.Configuration.RegisterClassMap<ProductMap>(); | |
return csvReader.GetRecords<Product>().ToList(); | |
} | |
} | |
} |
W pierwszej kolejności tworzymy instancję klasy StreamReader, która odczytuje dane z pliku. Następnie CsvReader, który sparsuje dane z pliku do obiektów. Również tutaj możemy korzystać z klas mapowań, tak jak to było przy eksporcie. W tym przypadku import wczyta te same dane, które zostały wcześniej wyeksportowane.
Przykład
Na githubie (https://github.com/danielplawgo/CsvHelperTest) znajduje się przykład do tego wpisu. Jest to prosta aplikacja konsolowa, która po wygenerowaniu danych zapisuje je do pliku csv. Następnie je odczytuje i wyświetla na konsoli.
Podsumowanie
CsvHelper bardzo ułatwia importowanie oraz eksportowanie danych w formacie csv. W dosłownie kliku linijkach kodu możemy obsłużyć kod za to odpowiedzialny. Możliwość konfigurowania sposobu pracy biblioteki powoduje, że w tym momencie jest to mój numer 1, gdy muszę pracować z plikami csv.
A jak Ty sobie z tym radzisz?
1 thought on “CsvHelper – praca z plikami csv”