Wprowadzenie
W poprzednim wpisie nauczyliśmy się pisać skuteczne instrukcje – trwałe zasady które obowiązują przez cały czas. Skills to coś innego: samowystarczalne zdolności na żądanie. Zamiast mówić Copilotowi "zawsze rób X" – uczysz go "gdy ktoś poprosi o Y, wykonaj to w dokładnie taki oto sposób, korzystając z tych konkretnych zasobów".
Skills ogłoszono w GitHub Changelog 18 grudnia 2025 i od razu zrobiły furorę w społeczności. Powód jest prosty – rozwiązują problem który każdy senior developer zna doskonale: jak sprawić żeby agent generował kod zgodny z naszymi wzorcami, a nie z generycznymi przykładami z internetu? Skills dają na to elegancką odpowiedź.
W tym wpisie pokażę jak działają, jak je tworzyć i – to jest mój główny dodatek do dokumentacji – dam Ci gotowy zestaw trzech Skills dla .NET developera do skopiowania i użycia od razu.
Oryginalna dokumentacja, na której bazuje ten wpis: Creating Effective Skills – Awesome GitHub Copilot Learning Hub. Dodatkowe materiały: VS Code Docs – Agent Skills, GitHub Docs – Creating Agent Skills.
Gdzie skill mieszka?
Skill to folder – nie pojedynczy plik. W środku musi być SKILL.md, opcjonalnie mogą być skrypty, szablony i inne zasoby. Copilot ładuje zasoby z folderu dopiero gdy ich potrzebuje – nie wszystko naraz, dzięki czemu okno kontekstu pozostaje czyste.
🔍 Gdzie tworzyć skills – trzy lokalizacje
| Typ |
Lokalizacja |
Zakres |
| Project skill |
.github/skills/[nazwa-skilla]/ |
Tylko to repozytorium – współdzielony z teamem |
| Personal skill |
~/.copilot/skills/[nazwa-skilla]/ |
Wszystkie projekty na Twojej maszynie |
| Claude Code compat. |
.claude/skills/[nazwa-skilla]/ |
Copilot automatycznie tam też zagląda |
Ważna ciekawostka: jeśli masz już skille dla Claude Code w katalogu .claude/skills/ – Copilot automatycznie je wykrywa i używa. Open standard działa w praktyce.
Przykładowa struktura projektu ze skills:
.github/
└── skills/
├── generate-unit-tests/
│ ├── SKILL.md
│ └── templates/
│ └── test-template.cs
├── create-ef-migration/
│ ├── SKILL.md
│ └── scripts/
│ └── verify-migration.sh
└── scaffold-api-endpoint/
├── SKILL.md
└── templates/
├── controller-template.cs
└── validator-template.cs
Anatomia pliku SKILL.md
Plik SKILL.md to Markdown z YAML frontmatter. Frontmatter zawiera metadane które Copilot czyta zawsze i od razu – nawet jeśli nie załadował jeszcze treści skilla. Na tej podstawie decyduje czy skill jest odpowiedni dla aktualnego zadania.
🔍 Pola frontmatter – co i po co
name
WYMAGANE
Unikalny identyfikator skilla. Tylko małe litery i myślniki. Musi być taki sam jak nazwa folderu. Używany jako komenda /nazwa w chatie.
description
WYMAGANE
Opis co skill robi i kiedy Copilot powinien go użyć. To jest najważniejsze pole – na jego podstawie agent decyduje o automatycznym załadowaniu. Maks. 1024 znaki. Bądź konkretny – "Use when asked to write unit tests for C# classes" działa lepiej niż "For testing".
inputHint
OPCJONALNE
Tekst podpowiedzi wyświetlany w polu chatu gdy skill jest wywoływany jako slash command. Np. [nazwa klasy] [opcje]. Pomaga użytkownikom wiedzieć co wpisać.
showInMenu
OPCJONALNE
Domyślnie true. Ustaw false jeśli chcesz żeby skill był dostępny dla agenta automatycznie, ale nie pojawiał się w menu / dla użytkownika.
manualOnly
OPCJONALNE
Domyślnie false. Ustaw true jeśli chcesz żeby skill był wywoływany TYLKO ręcznie przez /slash command – agent nie załaduje go automatycznie.
license
OPCJONALNE
Licencja skilla. Istotne gdy planujesz go udostępnić publicznie (np. MIT, Apache 2.0).
Kompletny przykład minimalnego pliku:
---
name: generate-unit-tests
description: >
Generates unit tests for C# classes using xUnit, FluentAssertions,
and NSubstitute. Use when asked to write tests, create test class,
add unit tests, or cover code with tests.
inputHint: "[class name or paste class code]"
---
# Treść instrukcji skilla...
⚠️ Opis decyduje o wszystkim – poświęć mu czas
Copilot czyta tylko pole description żeby zdecydować czy załadować skilla. Treść SKILL.md jest ładowana dopiero po tej decyzji. Dlatego opis musi zawierać wszystkie frazy którymi użytkownik może poprosić o wykonanie zadania: "write tests", "create test class", "add unit tests", "cover with tests", "test this class" – im więcej synomimów tym lepiej, w granicach 1024 znaków.
Jak używać skilla – trzy sposoby
Gdy agent mode jest aktywny i Copilot uzna że skill jest odpowiedni – załaduje go sam. Piszesz "napisz testy dla klasy OrderService" i Copilot automatycznie sięga po skill generate-unit-tests.
W Copilot Chat wpisujesz /generate-unit-tests i Copilot od razu ładuje skilla. Przydatne gdy chcesz mieć pewność że konkretny skill zostanie użyty, niezależnie od tego jak sformułowałeś pytanie.
// Przykłady ręcznego wywołania:
/generate-unit-tests OrderService
/scaffold-api-endpoint POST /orders/{id}/cancel
/create-ef-migration AddOrderStatusIndex
W VS Code Chat wpisz /skills żeby otworzyć menu zarządzania. Możesz tam przeglądać dostępne skills, włączać i wyłączać poszczególne, sprawdzać z którego pliku pochodzą i czytać ich opisy. Przydatne przy debugowaniu – gdy skill się nie odpala, tu sprawdzisz czy w ogóle jest widoczny.
Gotowe Skills dla .NET developera
To jest sekcja którą chciałem dodać od siebie. Zamiast ogólnych przykładów – trzy kompletne, gotowe do skopiowania Skills dla typowego projektu .NET z Clean Architecture. Każdy możesz wziąć i wrzucić do swojego projektu bez żadnych zmian, albo dostosować do swoich wzorców.
📋 Każdy skill to osobny folder. Stwórz katalog .github/skills/[nazwa-skilla]/ i umieść w nim plik SKILL.md oraz opcjonalne zasoby z podfolderów templates/.
Skill do generowania testów jednostkowych w xUnit z FluentAssertions i NSubstitute, zgodnie z wzorcem AAA.
Struktura:
.github/skills/generate-unit-tests/
├── SKILL.md
└── templates/
└── test-class-template.cs
📄 SKILL.md
---
name: generate-unit-tests
description: >
Generates unit tests for C# classes using xUnit, FluentAssertions,
and NSubstitute. Use when asked to write unit tests, create a test class,
add tests, cover code with tests, test this method, or write specs for
a class. Follows AAA pattern (Arrange, Act, Assert).
inputHint: "[ClassName or paste class content]"
---
# Skill: Generowanie testów jednostkowych (.NET)
## Stack testowy
- **Framework:** xUnit 2
- **Asercje:** FluentAssertions 6
- **Mocki:** NSubstitute 5
- **Wzorzec:** AAA (Arrange / Act / Assert)
## Lokalizacja pliku testowego
Umieść plik testowy w projekcie `[NazwaProjektu].Tests`, w podkatalogu
odpowiadającym testowanej klasie. Np. dla `OrderService` z
`Application/Orders/` → plik idzie do `Tests/Application/Orders/`.
## Konwencja nazewnictwa
- Klasa testowa: `[NazwaKlasy]Tests` (np. `OrderServiceTests`)
- Metoda testowa: `[NazwaMetody]_[Scenariusz]_[OczekiwanyWynik]`
Przykład: `CancelOrder_WhenAlreadyCancelled_ThrowsInvalidOperationException`
## Zasady tworzenia testów
1. **Jeden Assert per test** – jeśli testujesz wiele rzeczy, rozdziel na
osobne metody.
2. **Sekcje AAA z komentarzami:**
```csharp
// Arrange
// Act
// Assert
```
3. **Mocki tylko przez NSubstitute:**
```csharp
var repo = Substitute.For();
repo.GetByIdAsync(orderId).Returns(order);
```
4. **Asercje przez FluentAssertions:**
```csharp
result.Should().NotBeNull();
result.Status.Should().Be(OrderStatus.Cancelled);
await act.Should().ThrowAsync();
```
5. **Nie testuj implementacji – testuj zachowanie:**
- Złe: `service._repository.Received(1).Save(...)` (weryfikacja internals)
- Dobre: `result.Status.Should().Be(OrderStatus.Cancelled)`
6. **Happy path + edge cases + błędy:**
Dla każdej metody wygeneruj przynajmniej:
- Test dla poprawnego scenariusza (happy path)
- Test dla niepoprawnych danych wejściowych (np. null, pusta lista)
- Test dla wyjątku domenowego jeśli metoda go rzuca
## Szablon klasy testowej
Użyj szablonu z pliku [test-class-template.cs](./templates/test-class-template.cs).
📄 templates/test-class-template.cs
using FluentAssertions;
using NSubstitute;
using Xunit;
namespace [Namespace].Tests.[Layer].[Feature];
public class [ClassName]Tests
{
// Zależności (moki)
private readonly I[Dependency] _dependency;
// Testowana klasa
private readonly [ClassName] _sut;
public [ClassName]Tests()
{
_dependency = Substitute.For<I[Dependency]>();
_sut = new [ClassName](_dependency);
}
[Fact]
public async Task [MethodName]_[Scenario]_[ExpectedResult]()
{
// Arrange
var input = [setup input];
_dependency.[Method]([args]).Returns([returnValue]);
// Act
var result = await _sut.[MethodName](input);
// Assert
result.Should().NotBeNull();
result.[Property].Should().Be([expectedValue]);
}
[Fact]
public async Task [MethodName]_WhenNullInput_ThrowsArgumentNullException()
{
// Arrange
// (nothing to set up)
// Act
var act = async () => await _sut.[MethodName](null!);
// Assert
await act.Should().ThrowAsync<ArgumentNullException>()
.WithParameterName("[paramName]");
}
}
Skill do scaffoldowania kompletnego endpointu w minimal API lub kontrolerze – z komendą MediatR, handlerem, walidatorem i testem integracyjnym.
Struktura:
.github/skills/scaffold-api-endpoint/
├── SKILL.md
└── templates/
├── command.cs
├── handler.cs
├── validator.cs
└── endpoint.cs
📄 SKILL.md
---
name: scaffold-api-endpoint
description: >
Scaffolds a complete API endpoint for a .NET Clean Architecture project.
Generates: MediatR command or query, handler, FluentValidation validator,
and minimal API endpoint registration. Use when asked to add an endpoint,
create a new API route, add a controller action, implement a feature,
or scaffold a REST endpoint.
inputHint: "[HTTP method] [route] – e.g. POST /orders/{id}/cancel"
---
# Skill: Scaffolding endpointu API (.NET Clean Architecture)
## Co generujemy
Dla każdego nowego endpointu tworzymy:
1. **Command lub Query** (Application/[Feature]/Commands/ lub /Queries/)
2. **Handler** (w tym samym katalogu co command/query)
3. **Validator** (Application/[Feature]/Validators/)
4. **Endpoint** (API/Endpoints/ lub rejestracja w istniejącym kontrolerze)
## Zasady
### Command vs Query
- **Command** (POST, PUT, PATCH, DELETE) → modyfikuje stan, zwraca `Result` lub `Result`
- **Query** (GET) → tylko czytanie, zwraca `Result`
### Konwencje nazewnictwa
- Command: `[Akcja][Zasób]Command` (np. `CancelOrderCommand`)
- Handler: `[Akcja][Zasób]CommandHandler`
- Query: `Get[Zasób]Query`, `Get[Zasób]ByIdQuery`
- Validator: `[Akcja][Zasób]CommandValidator`
### Obsługa błędów
- Używaj `Result` z Ardalis.Result lub własnego Result type
- Nie rzucaj wyjątków domenowych z handlera – zwracaj `Result.NotFound()`,
`Result.Forbidden()`, `Result.Invalid()`
- W endpoincie mapuj Result na odpowiedni HTTP status code
### Walidacja
- Każdy command ma odpowiadający validator we FluentValidation
- Validator jest rejestrowany przez `AddValidatorsFromAssembly` – nie ręcznie
- Zasada: waliduj tylko to co możesz zwalidować bez dostępu do bazy
## Szablony
- Komenda: [command.cs](./templates/command.cs)
- Handler: [handler.cs](./templates/handler.cs)
- Validator: [validator.cs](./templates/validator.cs)
- Endpoint: [endpoint.cs](./templates/endpoint.cs)
## Po wygenerowaniu
Przypomnij użytkownikowi o:
1. Rejestracji endpointu w `Program.cs` lub `RouteGroupExtensions`
2. Dodaniu migracji EF jeśli zmiana dotyczy encji
3. Napisaniu testu integracyjnego dla nowego endpointu
📄 templates/command.cs
using Ardalis.Result;
using MediatR;
namespace [ProjectName].Application.[Feature].Commands;
/// <summary>
/// [Opis co robi komenda].
/// </summary>
public record [CommandName](
[Type] [Property1],
[Type] [Property2]
) : IRequest<Result<[ReturnType]>>;
📄 templates/handler.cs
using Ardalis.Result;
using MediatR;
namespace [ProjectName].Application.[Feature].Commands;
/// <summary>
/// Handler dla [CommandName].
/// </summary>
public class [CommandName]Handler(
I[Repository] repository
) : IRequestHandler<[CommandName], Result<[ReturnType]>>
{
public async Task<Result<[ReturnType]>> Handle(
[CommandName] request,
CancellationToken cancellationToken)
{
var entity = await repository.GetByIdAsync(
request.[Id], cancellationToken);
if (entity is null)
return Result.NotFound();
// logika biznesowa
await repository.SaveChangesAsync(cancellationToken);
return Result.Success([returnValue]);
}
}
📄 templates/validator.cs
using FluentValidation;
namespace [ProjectName].Application.[Feature].Commands;
public class [CommandName]Validator : AbstractValidator<[CommandName]>
{
public [CommandName]Validator()
{
RuleFor(x => x.[Property1])
.NotEmpty()
.WithMessage("[Property1] jest wymagane.");
RuleFor(x => x.[Property2])
.GreaterThan(0)
.WithMessage("[Property2] musi być większe od zera.");
}
}
📄 templates/endpoint.cs
using Ardalis.Result.AspNetCore;
using MediatR;
namespace [ProjectName].API.Endpoints.[Feature];
public static class [FeatureName]Endpoints
{
public static RouteGroupBuilder Map[FeatureName]Endpoints(
this RouteGroupBuilder group)
{
group.MapPost("/{id}/[action]", async (
[Type] id,
[CommandName] command,
ISender sender,
CancellationToken ct) =>
{
var result = await sender.Send(
command with { Id = id }, ct);
return result.ToMinimalApiResult();
})
.WithName("[EndpointName]")
.WithSummary("[Krótki opis endpointu]")
.Produces<[ReturnType]>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status404NotFound)
.RequireAuthorization();
return group;
}
}
Skill do tworzenia migracji Entity Framework Core – z weryfikacją nazewnictwa, sprawdzaniem czy nie brakuje indeksów i przypomnieniem o rollback.
📄 SKILL.md
---
name: create-ef-migration
description: >
Guides creation of Entity Framework Core migrations for .NET projects.
Use when asked to add a migration, create a database migration, update
the schema, add an index, modify a table, or run EF Core migration.
inputHint: "[migration name describing the change, e.g. AddOrderStatusIndex]"
---
# Skill: Tworzenie migracji EF Core
## Konwencja nazewnictwa migracji
Nazwa migracji powinna opisywać zmianę (nie być datą ani numerem):
- ✅ `AddOrderStatusIndex`
- ✅ `RenameCustomerEmailColumn`
- ✅ `CreateProductsTable`
- ❌ `Migration001`
- ❌ `Update`
- ❌ `FixBug`
## Komendy
### Dodanie nowej migracji
```bash
dotnet ef migrations add [NazwaMigracji] \
--project src/[ProjectName].Infrastructure \
--startup-project src/[ProjectName].API \
--output-dir Migrations
```
### Zastosowanie migracji
```bash
dotnet ef database update \
--project src/[ProjectName].Infrastructure \
--startup-project src/[ProjectName].API
```
### Rollback do poprzedniej migracji
```bash
dotnet ef database update [NazwaPoprzedniej] \
--project src/[ProjectName].Infrastructure \
--startup-project src/[ProjectName].API
```
### Usunięcie ostatniej migracji (jeśli niezastosowana)
```bash
dotnet ef migrations remove \
--project src/[ProjectName].Infrastructure \
--startup-project src/[ProjectName].API
```
## Checklist przed commitem migracji
Przed commitem sprawdź wygenerowany plik migracji:
1. **Indeksy** – czy dodajesz kolumnę która będzie używana w WHERE lub JOIN?
Jeśli tak, dodaj indeks:
```csharp
migrationBuilder.CreateIndex(
name: "IX_Orders_CustomerId",
table: "Orders",
column: "CustomerId");
```
2. **Nullable vs NOT NULL** – czy nowa kolumna w istniejącej tabeli jest
nullable lub ma wartość domyślną? Inaczej migracja nie przejdzie na
niepustej tabeli produkcyjnej.
3. **Down() jest kompletne** – upewnij się że metoda `Down()` poprawnie
cofa wszystkie zmiany z `Up()`. EF generuje to automatycznie, ale
warto sprawdzić przy złożonych migracjach.
4. **Nie edytuj istniejących migracji** – jeśli migracja jest już
zastosowana w jakimkolwiek środowisku, stwórz nową zamiast edytować.
## Ostrzeżenia
- Migracje które zmieniają nazwę kolumny przez `DropColumn` + `AddColumn`
**tracą dane**. Użyj `RenameColumn` zamiast tego.
- Przy dodawaniu NOT NULL do istniejącej tabeli zawsze podaj
`defaultValue` lub użyj migracji dwuetapowej.
Jak weryfikować że skill działa?
Po stworzeniu skilla warto sprawdzić czy Copilot go widzi i czy odpala się w odpowiednich sytuacjach. Jest na to kilka sposobów:
W Copilot Chat wpisz /skills list – zobaczysz wszystkie dostępne skills z ich opisami. Jeśli Twój skill tam jest – Copilot go indeksuje.
Po uzyskaniu odpowiedzi od Copilota rozwiń sekcję References nad odpowiedzią. Jeśli skill był użyty, zobaczysz tam ścieżkę do pliku SKILL.md.
Wywołaj skill przez /generate-unit-tests OrderService i sprawdź czy wygenerowany kod odpowiada instrukcjom ze skilla. Porównaj z tym co Copilot generuje bez skilla.
⚠️ Skill nie odpala? Najczęstsze przyczyny
- Zła lokalizacja pliku – sprawdź czy folder skilla jest w .github/skills/[nazwa]/ a nie np. .github/[nazwa]/.
- Niezgodność nazwy folderu i pola name – nazwa w frontmatter musi być identyczna jak nazwa folderu.
- Nowe pliki nie są jeszcze zaindeksowane – po dodaniu skilla odczekaj 5-10 minut i przeładuj VS Code.
- Agent mode wyłączony – automatyczne wykrywanie działa tylko w agent mode. W zwykłym chatie używaj slash commandu.
💪 Zadanie dla Ciebie – wdróż pierwszy skill
Weź skill generate-unit-tests z tego wpisu i zainstaluj go w swoim projekcie:
- Stwórz folder .github/skills/generate-unit-tests/
- Wklej SKILL.md i dostosuj stack (zmień NSubstitute na Moq jeśli używasz Moq, dodaj własny wzorzec nazewnictwa)
- Stwórz folder templates/ i wklej test-class-template.cs – dostosuj namespace do swojego projektu
- Scommituj i odczekaj chwilę na indeksowanie
- Wpisz w chatie:
/generate-unit-tests OrderService i porównaj wynik z tym co Copilot generował wcześniej bez skilla