Tworzenie szkieletów (inżynieria odwrotna)

Inżynieria odwrotna to proces tworzenia szkieletu klas typów jednostek i DbContext klasy opartej na schemacie bazy danych. Można je wykonać przy użyciu polecenia Scaffold-DbContext narzędzi konsoli menedżera pakietów platformy EF Core (PMC) lub polecenia dotnet ef dbcontext scaffold narzędzi interfejsu wiersza polecenia (CLI) platformy .NET.

Uwaga

Szkielet DbContext typów jednostek i opisanych tutaj różni się od szkieletu kontrolerów w ASP.NET Core przy użyciu programu Visual Studio, który nie jest tutaj udokumentowany.

Napiwek

Jeśli używasz programu Visual Studio, wypróbuj rozszerzenie społeczności narzędzi EF Core Power Tools . Te narzędzia udostępniają graficzne narzędzie, które opiera się na narzędziach wiersza polecenia platformy EF Core i oferuje dodatkowe opcje przepływu pracy i dostosowywania.

Wymagania wstępne

  • Przed tworzeniem szkieletów należy zainstalować narzędzia PMC, które działają tylko w programie Visual Studio, lub narzędzia interfejsu wiersza polecenia platformy .NET, które są obsługiwane przez platformę .NET.
  • Zainstaluj pakiet NuGet dla klasy Microsoft.EntityFrameworkCore.Design w projekcie, do którego wykonujesz tworzenie szkieletów.
  • Zainstaluj pakiet NuGet dla dostawcy bazy danych, który jest przeznaczony dla schematu bazy danych, z którego chcesz utworzyć szkielet.

Wymagane argumenty

Polecenia PMC i interfejsu wiersza polecenia platformy .NET mają dwa wymagane argumenty: parametry połączenia do bazy danych i dostawcę bazy danych EF Core do użycia.

Connection string

Pierwszym argumentem polecenia są parametry połączenia z bazą danych. Narzędzia będą używać tych parametrów połączenia do odczytywania schematu bazy danych.

Sposób cytowania i ucieczki parametry połączenia zależy od powłoki używanej do wykonania polecenia. Aby uzyskać więcej informacji, zapoznaj się z dokumentacją powłoki. Na przykład program PowerShell wymaga znaku wyjścia $, ale nie \.

W poniższym przykładzie typy jednostek szkieletów i element DbContext z Chinook bazy danych znajdującej się w wystąpieniu programu SQL Server LocalDB maszyny, który korzysta z Microsoft.EntityFrameworkCore.SqlServer dostawcy bazy danych.

dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook" Microsoft.EntityFrameworkCore.SqlServer

Wpisy tajne użytkownika dla parametry połączenia

Jeśli masz aplikację platformy .NET, która korzysta z modelu hostingu i systemu konfiguracji, takiego jak projekt ASP.NET Core, możesz użyć klasy składni Name=<connection-string>, aby odczytać parametry połączenia z konfiguracji.

Rozważmy na przykład aplikację ASP.NET Core z następującym plikiem konfiguracji:

{
  "ConnectionStrings": {
    "Chinook": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Chinook"
  }
}

Tego parametry połączenia w pliku konfiguracji można użyć do tworzenia szkieletu z bazy danych przy użyciu:

dotnet ef dbcontext scaffold "Name=ConnectionStrings:Chinook" Microsoft.EntityFrameworkCore.SqlServer

Jednak przechowywanie parametry połączenia w plikach konfiguracji nie jest dobrym pomysłem, ponieważ jest zbyt łatwe, aby przypadkowo je uwidocznić, na przykład przez wypchnięcie do kontroli źródła. Zamiast tego parametry połączenia powinny być przechowywane w bezpieczny sposób, na przykład przy użyciu usługi Azure Key Vault lub, podczas pracy lokalnie, narzędzia Secret Manager, czyli "Wpisy tajne użytkownika".

Aby na przykład użyć wpisów tajnych użytkownika, najpierw usuń parametry połączenia z pliku konfiguracji ASP.NET Core. Następnie zainicjuj wpisy tajne użytkownika, wykonując następujące polecenie w tym samym katalogu co projekt ASP.NET Core:

dotnet user-secrets init

To polecenie konfiguruje magazyn na komputerze niezależnie od kodu źródłowego i dodaje klucz dla tego magazynu do projektu.

Następnie zapisz parametry połączenia w wpisach tajnych użytkownika. Na przykład:

dotnet user-secrets set ConnectionStrings:Chinook "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook"

Teraz to samo polecenie, które wcześniej używało nazwanego parametry połączenia z pliku konfiguracji, zamiast tego użyje parametry połączenia przechowywanego w wpisach tajnych użytkownika. Na przykład:

dotnet ef dbcontext scaffold "Name=ConnectionStrings:Chinook" Microsoft.EntityFrameworkCore.SqlServer

ciągi Połączenie ion w kodzie szkieletowym

Domyślnie szkielet będzie zawierać parametry połączenia w kodzie szkieletowym, ale z ostrzeżeniem. Na przykład:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
    => optionsBuilder.UseSqlServer("Data Source=(LocalDb)\\MSSQLLocalDB;Database=AllTogetherNow");

Dzieje się tak, aby wygenerowany kod nie ulegał awarii podczas pierwszego użycia, co byłoby bardzo słabym doświadczeniem w nauce. Jednak jak mówi ostrzeżenie, parametry połączenia nie powinny istnieć w kodzie produkcyjnym. Zobacz DbContext Lifetime, Configuration, and Initialization (Okres istnienia, konfiguracja i inicjowanie bazy danych), aby poznać różne sposoby zarządzania parametry połączenia.

Napiwek

-NoOnConfiguring Można przekazać opcję (Visual Studio PMC) lub --no-onconfiguring (interfejs wiersza polecenia platformy .NET), aby pominąć tworzenie OnConfiguring metody zawierającej parametry połączenia.

Nazwa dostawcy

Drugim argumentem jest nazwa dostawcy. Nazwa dostawcy jest zwykle taka sama jak nazwa pakietu NuGet dostawcy. Na przykład w przypadku programu SQL Server lub usługi Azure SQL użyj polecenia Microsoft.EntityFrameworkCore.SqlServer.

Opcje wiersza polecenia

Proces tworzenia szkieletu może być kontrolowany przez różne opcje wiersza polecenia.

Określanie tabel i widoków

Domyślnie wszystkie tabele i widoki w schemacie bazy danych są podzielone na szkielety typów jednostek. Tabele i widoki można ograniczyć, określając schematy i tabele.

Argument -Schemas (Visual Studio PMC) lub --schema (interfejs wiersza polecenia platformy .NET) określa schematy tabel i widoków, dla których zostaną wygenerowane typy jednostek. Jeśli ten argument zostanie pominięty, zostaną uwzględnione wszystkie schematy. Jeśli ta opcja jest używana, wszystkie tabele i widoki w schematach zostaną uwzględnione w modelu, nawet jeśli nie zostaną jawnie uwzględnione przy użyciu lub -Tables--table.

Argument -Tables (Visual Studio PMC) lub --table (interfejs wiersza polecenia platformy .NET) określił tabele i widoki, dla których zostaną wygenerowane typy jednostek. Tabele lub widoki w określonym schemacie można uwzględnić przy użyciu formatu "schema.table" lub "schema.view". Jeśli ta opcja zostanie pominięta, zostaną uwzględnione wszystkie tabele i widoki. |

Na przykład w celu tworzenia szkieletu Artists tylko tabel i Albums :

dotnet ef dbcontext scaffold ... --table Artist --table Album

Aby skleić wszystkie tabele i widoki ze Customer schematów i :Contractor

dotnet ef dbcontext scaffold ... --schema Customer --schema Contractor

Na przykład w celu tworzenia szkieletu Purchases tabeli ze schematu Customer oraz Accounts tabel i Contracts ze schematu Contractor :

dotnet ef dbcontext scaffold ... --table Customer.Purchases --table Contractor.Accounts --table Contractor.Contracts

Zachowywanie nazw baz danych

Nazwy tabel i kolumn zostały naprawione w celu lepszego domyślnego dopasowania do konwencji nazewnictwa platformy .NET dla typów i właściwości. Określenie -UseDatabaseNames (Visual Studio PMC) lub --use-database-names (interfejs wiersza polecenia platformy .NET) spowoduje wyłączenie tego zachowania, zachowując jak najwięcej oryginalnych nazw baz danych. Nieprawidłowe identyfikatory platformy .NET będą nadal stałe i syntetyzowane nazwy, takie jak właściwości nawigacji, będą nadal zgodne z konwencjami nazewnictwa platformy .NET.

Rozważmy na przykład następujące tabele:

CREATE TABLE [BLOGS] (
    [ID] int NOT NULL IDENTITY,
    [Blog_Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([ID]));

CREATE TABLE [posts] (
    [id] int NOT NULL IDENTITY,
    [postTitle] nvarchar(max) NOT NULL,
    [post content] nvarchar(max) NOT NULL,
    [1 PublishedON] datetime2 NOT NULL,
    [2 DeletedON] datetime2 NULL,
    [BlogID] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogID]) REFERENCES [Blogs] ([ID]) ON DELETE CASCADE);

Domyślnie następujące typy jednostek będą szkieletowe z tych tabel:

public partial class Blog
{
    public int Id { get; set; }
    public string BlogName { get; set; } = null!;
    public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
}

public partial class Post
{
    public int Id { get; set; }
    public string PostTitle { get; set; } = null!;
    public string PostContent { get; set; } = null!;
    public DateTime _1PublishedOn { get; set; }
    public DateTime? _2DeletedOn { get; set; }
    public int BlogId { get; set; }
    public virtual Blog Blog { get; set; } = null!;
    public virtual ICollection<Tag> Tags { get; set; } = new List<Tag>();
}

Jednak użycie lub -UseDatabaseNames--use-database-names powoduje użycie następujących typów jednostek:

public partial class BLOG
{
    public int ID { get; set; }
    public string Blog_Name { get; set; } = null!;
    public virtual ICollection<post> posts { get; set; } = new List<post>();
}

public partial class post
{
    public int id { get; set; }
    public string postTitle { get; set; } = null!;
    public string post_content { get; set; } = null!;
    public DateTime _1_PublishedON { get; set; }
    public DateTime? _2_DeletedON { get; set; }
    public int BlogID { get; set; }
    public virtual BLOG Blog { get; set; } = null!;
}

Używanie atrybutów mapowania (np. adnotacji danych)

Typy jednostek są domyślnie konfigurowane przy użyciu interfejsu ModelBuilder API OnModelCreating . Określ -DataAnnotations (PMC) lub --data-annotations (interfejs wiersza polecenia platformy .NET Core), aby zamiast tego używać atrybutów mapowania, jeśli to możliwe.

Na przykład użycie interfejsu API Fluent spowoduje utworzenie szkieletu:

entity.Property(e => e.Title)
    .IsRequired()
    .HasMaxLength(160);

Podczas korzystania z adnotacji danych będzie to szkielet:

[Required]
[StringLength(160)]
public string Title { get; set; }

Napiwek

Niektórych aspektów modelu nie można skonfigurować przy użyciu atrybutów mapowania. Szkielet będzie nadal używać interfejsu API tworzenia modelu do obsługi tych przypadków.

Nazwa dbContext

Nazwa klasy szkieletowej DbContext będzie domyślnie nazwą sufiksu bazy danych z kontekstem. Aby określić inną, użyj klasy opcji -Context w PMC i --context w interfejsie wiersza polecenia platformy .NET Core.

Katalogi docelowe i przestrzenie nazw

Klasy jednostek i klasa DbContext są szkieletami w katalogu głównym projektu i używają domyślnej przestrzeni nazw projektu.

Można określić katalog, w którym są określane szkielety klas przy użyciu opcji --output-dir, a opcji --context-dir można użyć do tworzenia szkieletu klasy DbContext w oddzielnym katalogu z klas typów jednostek:

dotnet ef dbcontext scaffold ... --context-dir Data --output-dir Models

Domyślnie przestrzeń nazw będzie przestrzenią nazw katalogu głównego plus nazwy wszystkich podkatalogów w katalogu głównym projektu. Można jednak zastąpić przestrzeń nazw dla wszystkich klas wyjściowych przy użyciu polecenia --namespace. Możesz również zastąpić przestrzeń nazw tylko dla klasy DbContext przy użyciu opcji --context-namespace:

dotnet ef dbcontext scaffold ... --namespace Your.Namespace --context-namespace Your.DbContext.Namespace

Kod szkieletowy

Wynikiem tworzenia szkieletu z istniejącej bazy danych jest:

  • Plik zawierający klasę dziedziczącą z DbContext
  • Plik dla każdego typu jednostki

Napiwek

Począwszy od programu EF7, można również użyć szablonów tekstowych T4, aby dostosować wygenerowany kod. Aby uzyskać więcej szczegółów, zobacz Niestandardowe szablony odtwarzania.

Typy referencyjne dopuszczane do wartości null w języku C#

Szkielet może tworzyć typy modeli i jednostek EF, które używają typów odwołań dopuszczających wartość null w języku C# (NRT). Użycie nrT jest automatycznie szkieletowe, gdy obsługa NRT jest włączona w projekcie języka C#, w którym kod jest szkieletowany.

Na przykład poniższa Tags tabela zawiera kolumny ciągów o wartości null, które nie mogą być dopuszczane do wartości null:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

Powoduje to, że odpowiednie właściwości ciągu dopuszczane do wartości null i niepuste w wygenerowanej klasie:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

Podobnie poniższe Posts tabele zawierają wymaganą relację z tabelą Blogs :

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]));

Powoduje to tworzenie szkieletów relacji między blogami bez wartości null (wymagane):

public partial class Blog
{
    public Blog()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public virtual ICollection<Post> Posts { get; set; }
}

Wpisy i wpisy:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

Relacje „wiele do wielu”

Proces tworzenia szkieletu wykrywa proste tabele sprzężenia i automatycznie generuje mapowanie wiele-do-wielu. Rozważmy na przykład tabele dla Posts i Tagsi i tabelę sprzężenia PostTag łączącą je:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]));

CREATE TABLE [PostTag] (
    [PostsId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE);

Po utworzeniu szkieletu powoduje to klasę Post:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

I klasa tagu:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

Ale nie ma klasy dla PostTag tabeli. Zamiast tego konfiguracja relacji wiele-do-wielu jest szkieletem:

entity.HasMany(d => d.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity<Dictionary<string, object>>(
        "PostTag",
        l => l.HasOne<Tag>().WithMany().HasForeignKey("PostsId"),
        r => r.HasOne<Post>().WithMany().HasForeignKey("TagsId"),
        j =>
            {
                j.HasKey("PostsId", "TagsId");
                j.ToTable("PostTag");
                j.HasIndex(new[] { "TagsId" }, "IX_PostTag_TagsId");
            });

Inne języki programowania

Pakiety EF Core opublikowane przez kod C# szkieletu firmy Microsoft. Jednak podstawowy system tworzenia szkieletów obsługuje model wtyczki do tworzenia szkieletów w innych językach. Ten model wtyczki jest używany przez różne projekty uruchamiane przez społeczność, na przykład:

Dostosowywanie kodu

Począwszy od programu EF7, jednym z najlepszych sposobów dostosowywania wygenerowanego kodu jest dostosowanie szablonów T4 używanych do jego generowania.

Kod można również zmienić po jego wygenerowaniu, ale najlepszym sposobem, aby to zrobić, zależy od tego, czy zamierzasz ponownie uruchomić proces tworzenia szkieletu po zmianie modelu bazy danych.

Szkielet tylko raz

Dzięki temu podejściu kod szkieletowy zapewnia punkt wyjścia dla mapowania opartego na kodzie w przyszłości. Wszelkie zmiany w wygenerowanym kodzie mogą zostać wprowadzone zgodnie z potrzebami — staje się normalnym kodem tak samo jak każdy inny kod w projekcie.

Przechowywanie bazy danych i modelu EF w synchronizacji można wykonać na jeden z dwóch sposobów:

  • Przejdź do korzystania z migracji baz danych ef Core i użyj typów jednostek i konfiguracji modelu EF jako źródła prawdy, używając migracji do napędzania schematu.
  • Ręcznie zaktualizuj typy jednostek i konfigurację ef, gdy baza danych ulegnie zmianie. Jeśli na przykład nowa kolumna zostanie dodana do tabeli, dodaj właściwość kolumny do zamapowanego typu jednostki i dodaj dowolną wymaganą konfigurację przy użyciu atrybutów mapowania i/lub kodu w pliku OnModelCreating. Jest to stosunkowo łatwe, a jedynym prawdziwym wyzwaniem jest proces, aby upewnić się, że zmiany bazy danych są rejestrowane lub wykrywane w jakiś sposób, aby deweloperzy odpowiedzialni za kod mogli reagować.

Powtarzające się tworzenie szkieletów

Alternatywne podejście do tworzenia szkieletów raz polega na ponownym utworzeniu szkieletu za każdym razem, gdy zmienia się baza danych. Spowoduje to zastąpienie dowolnego wcześniej szkieletowego kodu, co oznacza, że wszelkie zmiany wprowadzone w typach jednostek lub konfiguracji ef w tym kodzie zostaną utracone.

[PORADA] Domyślnie polecenia ef nie zastąpią żadnego istniejącego kodu, aby chronić przed przypadkową utratą kodu. Argument -Force (Visual Studio PMC) lub --force (interfejs wiersza polecenia platformy .NET) może służyć do wymuszania zastępowania istniejących plików.

Ponieważ kod szkieletowy zostanie zastąpiony, najlepiej nie modyfikować go bezpośrednio, ale zamiast tego polegać na częściowych klasach i metodach oraz mechanizmach w programie EF Core, które umożliwiają zastąpienie konfiguracji. Szczególnie:

  • DbContext Zarówno klasa, jak i klasy jednostek są generowane jako częściowe. Umożliwia to wprowadzenie dodatkowych elementów członkowskich i kodu w osobnym pliku, który nie zostanie zastąpiony podczas uruchamiania szkieletu.
  • Klasa DbContext zawiera metodę częściową o nazwie OnModelCreatingPartial. Implementację tej metody można dodać do klasy częściowej dla klasy DbContext. Następnie zostanie wywołana po OnModelCreating wywołaniu.
  • Konfiguracja modelu wykonana przy użyciu ModelBuilder interfejsów API zastępuje dowolną konfigurację wykonywaną przez konwencje lub atrybuty mapowania, a także wcześniejszą konfigurację wykonywaną w konstruktorze modelu. Oznacza to, że kod w OnModelCreatingPartial programie może służyć do zastąpienia konfiguracji wygenerowanej przez proces tworzenia szkieletów bez konieczności usuwania tej konfiguracji.

Na koniec pamiętaj, że począwszy od ef7 szablony T4 używane do generowania kodu można dostosować. Jest to często bardziej efektywne podejście niż tworzenie szkieletów z wartościami domyślnymi, a następnie modyfikowanie za pomocą klas częściowych i/lub metod.

Jak to działa

Odtwarzanie rozpoczyna się od odczytania schematu bazy danych. Odczytuje ono informacje o tabelach, kolumnach, ograniczeniach i indeksach.

Następnie użyje ono informacji o schemacie do utworzenia modelu EF Core. Tabele są używane do tworzenia typów jednostek, kolumny są używane do tworzenia właściwości a klucze obce są używane do tworzenia relacji.

Na koniec model jest używany do generowania kodu. Odpowiednie klasy typów jednostek, interfejs API Fluent i adnotacje danych służą do utworzenia szkieletu w celu ponownego utworzenia tego samego modelu z poziomu aplikacji.

Ograniczenia

  • Nie wszystkie informacje o modelu mogą być reprezentowane przy użyciu schematu bazy danych. Na przykład informacje o hierarchiach dziedziczenia, typach własności i dzieleniu tabel nie występują w schemacie bazy danych. W związku z tym konstrukcje te nigdy nie będą szkieletowe.
  • Ponadto niektóre typy kolumn mogą nie być obsługiwane przez dostawcę platformy EF Core. Te kolumny nie zostaną uwzględnione w modelu.
  • Tokeny współbieżności można zdefiniować w modelu EF Core, aby uniemożliwić dwóm użytkownikom jednoczesne aktualizowanie tej samej jednostki. Niektóre bazy danych mają specjalny typ reprezentujący ten typ kolumny (na przykład rowversion w programie SQL Server), w którym przypadku możemy odtworzyć te informacje; jednak inne tokeny współbieżności nie będą szkieletowe.