# Низкоуровневое API

Прямая работа с репозиторием Evos: продукты, экземпляры, семейства, транзакции, история версий.
Берите его, когда нужен полный контроль над моделью — пройти по всем продуктам, поправить свойство,
посмотреть историю, собрать воздуховод повторителем.

Точки входа собраны в сборке `Evos.Api.Repository` (пространство имён `Evos.Api.Repository`):
всё остальное — типы самого репозитория из `Evos.ModelsRepository.*`.

| Тип | Что делает |
|---|---|
| `ApiConcurrentRepositoryFactory` | Создаёт и открывает репозиторий |
| `ApiRepositoryVcsControllerFactory` | Даёт контроллер VCS поверх открытого репозитория |
| `ApiRepeaterFactory` | Создаёт повторители — тиражирование геометрии вдоль оси |
| `ApiConcurrentRepositoryTypeSystem` | Система типов репозитория (схема типов и компараторы) |

Рабочие примеры в `Evos.Api.Samples`: `RepositoryLifecycleSample` (открыть репозиторий),
`RepositoryEditSample` (свойства и присоединяемые семейства), `VcsSample`, `RepeaterSample`.

Открытый здесь `IConcurrentRepository` принимает и высокоуровневое API — публикация обменной
модели идёт в него же (`PublishToRepositorySample`).

## Открыть репозиторий

Репозиторий живёт в папке на диске. Путь задаёт `ProjectLocation` — директория плюс имя файла проекта
(либо полный путь к файлу одним аргументом).

```csharp
var location = new ProjectLocation(directory, projectName);
var factory = new ApiConcurrentRepositoryFactory();

using var repository = factory.OpenNewRepository(location);  // создать новый
using var repository = factory.OpenRepository(location);     // открыть существующий
```

Обе перегрузки принимают и `string projectFilePath`. Репозиторий — `IConcurrentRepository`,
он `IDisposable`: закрыли — данные остались на диске, открывать заново нужно тем же `OpenRepository`.

## Читать и писать

Это два разных соединения, и **совмещать их нельзя**: репозиторий не даёт взять write-блокировку,
пока открыто read-соединение. Прочитали нужное, закрыли соединение — потом пишите.

Чтение — короткоживущее read-соединение:

```csharp
using var readConnection = repository.ReadConnection();

var names = readConnection.GetProducts()
    .Select(product => product.GetProductName())
    .ToList();
```

Порядок продуктов в `GetProducts()` не оговорён — не полагайтесь на него, ищите по свойствам.

Запись — только внутри транзакции. `TryExecuteTransaction` сам открывает write-соединение
и коммитит его на выходе; возвращает `false`, если транзакция не прошла.

```csharp
repository.TryExecuteTransaction(
    writeAccess =>
    {
        var pump = writeAccess.NewNamedProduct("Насос", ProductKind.Designed);
        var valve = writeAccess.NewNamedProduct("Клапан", ProductKind.Designed);

        pump.NewProductInstance(valve);
    },
    new TransactionInfo(TransactionDescription.AddingProduct, TransactionMode.Default));
```

`TransactionInfo` описывает, что за операция идёт (`TransactionDescription.AddingProduct`,
`TransactionDescription.Default` и т. д.) — это попадёт в историю и в undo/redo. Есть перегрузка
с `out int transactionNumber`, если номер транзакции нужен вызывающему.

Транзакция — это набор элементарных операций над моделями и объектами репозитория, и она
атомарна: незавершённая транзакция (исключение внутри, падение процесса) не сериализуется,
репозиторий остаётся в состоянии до её начала. Операций там больше, чем кажется: одно только
создание продукта разворачивается в полтора десятка операций, поэтому не дробите одно
пользовательское действие на несколько транзакций — держите его в одной.

## Соединения и блокировки

Внутри — обычный `ReaderWriterLockSlim`, один на открытый репозиторий. Синхронизация
**внутрипроцессная**: на совместную работу двух процессов с одной папкой она не рассчитана.

| | Сколько одновременно | Чем берётся |
|---|---|---|
| Читатели | **сколько угодно**, параллельно | `ReadConnection()`, `VCSReadConnection()`, `TransactionalConnection()` |
| Писатель | **один, эксклюзивно**: ни читателей, ни второго писателя | `TryExecuteTransaction`, операции VCS |

### Три запрета — и это исключения, а не ожидание

1. **Нельзя открыть `ReadConnection()` внутри своей же транзакции.**
2. **Нельзя начать запись, держа своё read-соединение** — в том числе `VCSReadConnection`: операции
   VCS берут полноценную write-блокировку. Прочитали, закрыли соединение — потом пишите.
3. **Нельзя вложить транзакцию в транзакцию.**

Во всех трёх случаях вы получите `InvalidOperationException` сразу, а не зависание.

Важно: проверки эти — **по текущему потоку**. Если read-соединение держит *другой* поток, запись
не бросит исключение, а **будет ждать** его освобождения, причём без таймаута. Несколько
read-соединений на одном потоке одновременно — законны.

### Как читать внутри записи

Через ту же write-грань: `writeAccess` умеет и читать, второго соединения открывать не надо.
Именно поэтому присоединяемые семейства (уровень, система) читают внутри транзакции — открыть
рядом read-соединение нельзя.

### Почитал → решил → записал

Если решение о записи зависит от прочитанного, нужен `TransactionalConnection()`: читаете под
read-блокировкой, копите действия и применяете их одним вызовом.

```csharp
using var connection = repository.TransactionalConnection();

// ... читаем, решаем ...

var status = connection.Transaction
    .Do(writeAccess => writeAccess.NewNamedProduct("Насос", ProductKind.Designed))
    .Apply(new TransactionInfo(TransactionDescription.AddingProduct, TransactionMode.Default));
```

`Apply` вернёт `RepositoryTransactionApplyStatus.RepositoryIsBusy`, если между чтением и записью
кто-то успел записать: прочитанное устарело, надо перечитать и повторить. Это оптимистичная
блокировка, а не атомарное повышение — рассчитывайте на повтор.

### Долгий читатель обязан уступать

Писатель встаёт в очередь и ждёт всех читателей. Чтобы долгий обход репозитория не морозил запись,
соединение отдаёт флаг `InterruptionRequired` — проверяйте его в длинных циклах и сворачивайтесь,
освобождая соединение. Принудительно читателя никто не выселит.

### Ловушка: `Dispose` репозитория ждёт соединений

`repository.Dispose()` не вернётся, пока не закрыты все соединения, — и ждёт он **без таймаута**.
Забытое read-соединение подвесит закрытие репозитория навсегда. Держите соединения в `using`.

## Модель данных: продукты, экземпляры, семейства

Тут легко ошибиться, поэтому сначала о том, что чем является.

**Продукт** (`IProduct`) — это **тип, а не деталь на чертеже**. Он описывает класс изделия или сборки:
«Лампа», «Насос UPS-25», «Люстра». В терминах предметной области его называют компонентом.
Продукт может содержать экземпляры других продуктов, семейства и виды.

**Экземпляр продукта** (`IProductInstance`) — размещение продукта в пространстве другого продукта.
Четыре лампы в люстре — это один продукт «Лампа» и четыре его экземпляра в продукте «Люстра».
Отсюда главное следствие: **правка свойства продукта меняет все его экземпляры разом**, а
индивидуально экземпляр настраивают переопределением (см. ниже).

**Семейство** — логически связанная группа свойств, которую навешивают на продукт (в терминах
предметной области — опция). Семейство «Шар» со свойствами «центр» и «радиус», добавленное
в продукт «Лампа», даёт продукту эти свойства. Семейства бывают предопределённые (описаны
в программе, фасады в `Evos.ModelsRepository.PredefinedFamilies`) и пользовательские
(создаются в проекте — `NewCustomFamilyDescription`).

```csharp
var lamp = writeAccess.NewNamedProduct("Лампа", ProductKind.Designed);    // продукт-тип
var chandelier = writeAccess.NewNamedProduct("Люстра", ProductKind.Designed);

for (var i = 0; i < 4; i++)
{
    chandelier.NewProductInstance(lamp);                                  // четыре лампы в люстре
}
```

`NewNamedProduct` — расширение (`Evos.ModelsRepository.SpaceModel`), которое поверх `NewProduct()`
добавляет продукту семейства имени и категоризации. `ProductKind` — вид компонента:
`Designed`, `Cataloged`, `Imported`, `Graphic`.

### Свойства продукта

Работают через фасад семейства: `Facade()` — чтение, `EditableFacade()` — запись
(только в транзакции).

```csharp
product.AddFamilyOrThrow<IStaticGraphic>();            // добавить (бросит, если уже есть)

var name = product.GetPredefinedFamilyOrThrow<IProductName>().Facade().Name;   // прочитать

product.GetPredefinedFamilyOrThrow<IProductName>()
    .EditableFacade()
    .SetProperty(family => family.Name, "Насос");                              // записать
```

Если не уверены, что семейство есть, берите `TryGetPredefinedFamily<T>(out …)` вместо `…OrThrow`.

### Переопределение свойств в экземплярах

Значение, заданное в продукте, — значение по умолчанию для всех его экземпляров. Изменить его
у конкретного экземпляра можно, только если свойство **открыто**; закрытое свойство одинаково
у всех экземпляров. Открытость переключают тем же фасадом:

```csharp
product.GetPredefinedFamilyOrThrow<IProductName>()
    .EditableFacade()
    .SetUnlock(family => family.Name, true);       // разрешили переопределять имя в экземплярах
```

Дальше значение переопределяют уже на семействе экземпляра — `productInstance` даёт те же
`GetPredefinedFamilyOrThrow<T>()` / `EditableFacade()`. Прочитать текущее состояние можно
через `IsUnlocked`.

Переопределение действует **в контексте**: экземпляр лампы живёт внутри люстры, люстра — внутри
комнаты, комната — внутри здания. Значение, заданное для лампы на уровне люстры, наследуется
глубже (в комнате и здании лампа останется той же), а поменять его на следующем уровне вложенности
можно, только если на предыдущем свойство оставили открытым. Цепочка экземпляров, через которые
мы «смотрим» на объект, и есть его контекст; сам объект при этом не копируется — в репозитории он
хранится в единственном экземпляре, а переопределённые значения лежат во внешних объектах контекста.

### Присоединяемые семейства

Отдельный случай — семейства, которые живут не на продукте, а на **конкретном экземпляре**:
ими помечают вхождение, а не тип. Так, например, экземпляр привязывают к уровню:

```csharp
var levelId = levelInstance.GetPredefinedFamilyOrThrow<ILevel>().Facade().FamilyId.AsUntyped();

pumpInstance.GetOrAttachTransientFamily<IProductCurrentLevel>()
    .AsEditable()
    .SetProperty(family => family.LevelId, levelId);
```

`GetOrAttachTransientFamily<T>()` присоединит семейство, если его ещё нет. Рядом —
`TryAttachTransientFamily`, `RemoveTransientFamily<T>()`, `ClearTransientFamilies()`.
Рабочий пример — `RepositoryEditSample`.

Две вещи, на которых спотыкаются.

**Уровень адресуется идентификатором семейства, а не экземпляра.** В `LevelId` кладут не id
экземпляра уровня, а id его семейства `ILevel` — `FamilyId.AsUntyped()`, как в примере выше.

**Присоединяемые семейства не читаются с read-соединения.** `GetOrAttachTransientFamily` живёт
на `IProductInstance` (write-грань), а `ReadConnection()` отдаёт `IReadOnlyProductInstance`.
Поэтому даже чтобы **прочитать**, на каком уровне стоит экземпляр или к какой системе он относится,
приходится открывать транзакцию — ничего в ней не меняя. Так же устроено и штатное чтение
структуры проекта.

### Идентификаторы

Понадобятся, как только вы начнёте адресовать объекты между вызовами или между системами.

| Идентификатор | Что это |
|---|---|
| `ModelId` | Модель продукта. У продукта его отдаёт `product.ProductId` |
| `ModelObjectId` | Объект внутри модели. Переживает импорт: при импорте компонента `ModelObjectId` сохраняется, а `ModelId` генерируется новый |
| `RepositoryObjectId` | `{ModelId, ModelObjectId}` — объект в пределах проекта |
| `InstanceId` | `RepositoryObjectId` плюс контекст (`InstanceContextId`) — объект, рассматриваемый через конкретную цепочку вложенности |

Именно поэтому у продукта два разных «id»: `product.Id` (`ProductId`) — сам объект,
`product.ProductId` (`ModelId`) — его модель. Высокоуровневое API отдаёт наружу сериализованные
формы этих идентификаторов: `EvosModelId` — это `ModelId`, `EvosInstanceRef` — `InstanceId`.

## История версий (VCS)

История хранится в самом репозитории и работает без сервера: коммиты, ветки, откат.
Контроллер строится поверх уже открытого репозитория.

```csharp
var vcs = ApiRepositoryVcsControllerFactory.Create(repository);
```

`IApiRepositoryVcsController` (`Evos.Api.Repository.VCS`):

| Метод | Что делает |
|---|---|
| `CreateCommitInNewBranch(message, branchName)` | Коммит с созданием ветки |
| `CreateCommit(message)` | Коммит в текущую ветку |
| `RenameBranch(branchId, newName)` | Переименовать ветку |
| `Checkout(commitId)` | Переключиться на коммит |
| `Strip(commitId, out output)` | Удалить коммит вместе с его изменениями |
| `SetServerConfiguration(createNewRemoteProject, serverUrl, remoteProjectName)` | Привязать репозиторий к серверу |
| `Pull(out output)` / `PushAll(out output)` / `PushSelectedCommit(commitId, out output)` | Обмен с сервером |

Все методы возвращают `bool`; там, где есть `out`-параметр, в нём приходят подробности отказа.

**Первый коммит обязан создать ветку** — у нового репозитория её ещё нет:

```csharp
vcs.CreateCommitInNewBranch("Добавил насос", "main");
vcs.CreateCommit("Добавил клапан");
```

Историю читают через отдельное VCS-соединение — и его тоже надо закрыть до записи, потому что
операции VCS открывают свою write-транзакцию:

```csharp
BranchId branchId;
using (var vcsRead = repository.VCSReadConnection())
{
    branchId = vcsRead.GetBranches().Single().Id;
    var commits = vcsRead.GetCommits();
    var current = vcsRead.GetCurrentCommitId();
}

vcs.RenameBranch(branchId, "release");
```

Откат последнего коммита — это `Strip`: коммит удаляется вместе с изменениями, которые он принёс,
репозиторий возвращается к состоянию родителя.

## Повторитель

Повторитель (repeater) — продукт, который тиражирует геометрию другого продукта вдоль оси
с заданным шагом. Так строят воздуховоды, лотки, трубы.

Тиражируемому сегменту нужны два предопределённых семейства: геометрия (`IStaticGraphic`)
и её положение в пространстве (`ISpaceGraphicModel`). Без них повторитель строить не из чего,
и фабрика бросит исключение.

Семейство добавляют пустым — геометрию в него кладут отдельно, фасадом. Повторителю есть что
тиражировать только тогда, когда у сегмента заданы вершины и грани:

```csharp
var segment = writeAccess.NewProduct();

var graphic = segment.AddFamilyOrThrow<IStaticGraphic>();
segment.AddFamilyOrThrow<ISpaceGraphicModel>();

graphic.EditableFacade()
    .SetProperty(family => family.Vertices, vertices)
    .SetProperty(family => family.Faces, faces);

var step = Length.CreateOrThrow(ValueObjectsAccess.Meter.FromMeter(0.5));

var repeater = new ApiRepeaterFactory().FindOrCreateRepeater(
    writeAccess,
    segment,
    step,
    new MeshCuttingAdditionalSettings(),
    out var created);
```

`FindOrCreateRepeater` ищет повторитель **по сегменту**: если у сегмента он уже есть, второй
не создаётся, возвращается существующий, а `out bool` говорит, что именно произошло.
Рядом есть `CreateRepeater` (всегда создаёт новый) и `FindRepeaters` (все повторители сегмента).

**Ловушка: шаг участвует только в создании.** В поиске он не учитывается вовсе, поэтому вызов
с другим шагом вернёт **существующий** повторитель со **старым** шагом — и никто об этом не скажет.
Менять шаг у готового повторителя надо самому, через семейство `IRepeatAlongAxisParameters`.
См. `RepeaterSample`.

Всё это — операции записи, поэтому только внутри `TryExecuteTransaction`.

## Система типов

`ApiConcurrentRepositoryTypeSystem` отдаёт схему типов репозитория (`TypeDefinitionSchema`)
и провайдер компараторов значений (`ComparerProvider`). Нужен, если вы сами сериализуете значения
свойств или сравниваете value-объекты Evos; в обычном сценарии его трогать не надо — высокоуровневое
API берёт схему само (`DtoConvertersProviderAccessor.Default`).

## Частые ошибки

**Открыли read-соединение и пишете.** Транзакция или операция VCS не возьмёт write-блокировку.
Закрывайте read-соединение (`using`-блоком с явными границами) до записи.

**Забыли транзакцию.** Любое изменение — `NewProduct`, `EditableFacade().SetProperty`, повторитель —
живёт только внутри `TryExecuteTransaction`.

**Правка продукта вместо правки экземпляра.** Продукт — это тип: поменяли свойство на нём — поменяли
его у всех экземпляров сразу. Чтобы изменить одну лампу из четырёх, свойство должно быть открыто
(`SetUnlock`), а значение — задано на экземпляре.

**`OpenNewRepository` вместо `OpenRepository`.** Первый создаёт репозиторий заново, второй открывает
существующий. Перепутать легко, а данных на диске это касается напрямую.

**Повторитель на продукте без графики.** Нет `IStaticGraphic` + `ISpaceGraphicModel` — будет исключение.
А если семейства есть, но геометрия в них не задана, повторитель построится и будет тиражировать пустоту.

**Чтение присоединяемого семейства с read-соединения.** Не выйдет: система и уровень экземпляра
доступны только с write-грани, то есть внутри транзакции.

**Опора на порядок `GetProducts()`.** Он ничем не гарантирован; ищите продукты по свойствам.
