Skip to main content
Veriqa резолвит настройку одним механизмом: ключ объявляется один раз — имя, тип значения, уровни, на которых он живёт, и адрес на каждом из них, — а любое чтение идёт через единственный резолвер, который применяет порядок уровней и правила гейтов этого объявления. Механизм не зарезервирован за ядром. Расширение, которое поставляете вы — свой канальный адаптер, сток аудита, внешняя проверка допустимости, что угодно, приезжающее отдельной сборкой, — объявляет свои ключи ровно так же, на тех же уровнях и с той же протективной моделью. Эта страница — минимальный рабочий путь.
API объявления живёт в Veriqa.Core.Configuration. Это часть MPL-2.0-ядра, которое ваш хост и так запускает (пакет приезжает с Veriqa.Core.AuthServer и с пакетами канальных адаптеров), а не MIT-пакета контрактов — поэтому ссылайтесь на него из сборки, которая объявляет, либо объявляйте в хосте. Всё, что описано на этой странице, — публичный API.

Уровни приложения и ui_config открыты расширениям

Читатели записей уровней — и тот, который достаёт запись OIDC-клиента, и тот, который достаёт запись ui_config, — internal, но расширению они и не нужны. Владельцу ключа нужен адрес внутри записи, а не сама запись: вы указываете, где в записи лежит ваше значение, а читатель отдаёт запись резолюции, не зная, что из неё прочитали. Обе записи принимают члены, которых заранее никто не объявлял:
  • Уровень приложения — элемент массива Veriqa:OpenIddict:Clients в конфигурации хоста. Он читается как узел, по пути: любой член, который вы впишете в запись клиента, читается по адресу, объявленному вашим ключом, независимо от того, есть ли у OidcClientOptions свойство с таким именем.
  • Уровень ui_config — динамическая запись. Неизвестные ей поля сохраняются при чтении и записи через [JsonExtensionData], поэтому ваше поле переживает каждый цикл чтения-записи записи.
Чего расширение не может — заменить сам способ доставать запись уровня, например положить запись клиента в свой стор. Это решение развёртывания, а не расширения: у контракта резолюции одна реализация, а её заменяемые части перечислены явно точками расширения самой регистрации. Обслуживание уровня из другого стора — одна из них, и заявляет её хост, который этим стором владеет (см. README пакета Veriqa.Core.Configuration), а не библиотека, объявляющая ключи.

1. Свой каталог

Каталог держит объявления одного владельца — общего каталога на все расширения нет. Заполняйте его из статического инициализатора своего класса ключей и выставляйте наружу:
AcmeConfigKeys.cs
Of<T>(name) открывает цепочку для значения любого типа, Text(name) — для текстовой формы, в которой живёт большинство настроек, а Declare() её закрывает: собирает ключ, кладёт объявление в каталог и отдаёт ключ обратно. Незакрытая цепочка не объявляет ничего — такого ключа не существует, и ни один его уровень не привязан.

Адреса по уровням

Одно правило решает здесь больше ошибок, чем все остальные: из корня конфигурации хоста адресуется только уровень ядра. Его запись — это и есть конфигурация приложения, поэтому Acme:Approvals:MaxAttempts там полный путь. Выше ядра адрес относителен записи своего уровня — записи клиента, записи ui_config, — и объявление поэтому не называет никакого стора. Адрес выше ядра, открывающийся секцией конфигурации хоста, останавливает старт сообщением, в котором названы ключ, уровень и адрес.
Ключ составного типа читается из поддерева по своему адресу строгим биндингом: член, которого тип не несёт, — остаток прошлой версии, случайный ключ, «комментарий», написанный полем, — делает нечитаемым всё поддерево, и шаг отрабатывает ровно как значение, отбракованное доменом (выше уровня ядра запись выбрасывается, на уровне ядра ключ с FailStart останавливает хост). Это принятая цена гарантии: без неё элемент набора с опечаткой приезжает как набор поменьше, с виду совершенно корректный. Ключей со скалярным значением это не касается — как и членов, которые вы дописали рядом со своим ключом в чужую запись: запись клиента вашим поддеревом не является.

2. Зарегистрируйте каталог из своей композиции

AcmeServiceCollectionExtensions.cs
В хосте это одна строка рядом с прочими:
Program.cs
Три свойства этого вызова стоит назвать прямо — именно они делают безопасной композицию независимых расширений:
  • Он идемпотентен, покаталожно. Каталог, который контейнер уже держит, пропускается в одиночку. И композиция, вызванная дважды, и две композиции, назвавшие один каталог, оставляют ровно одну регистрацию.
  • Он ничего не подавляет. Ваш вызов регистрирует названное вами и пропускает уже имеющееся — ни один владелец не может выбить каталоги другого, и ни один не регистрируется дважды.
  • Порядок не важен. Каждый регистратор объявляет свои ключи до того, как зарегистрирован первый биндинг, поэтому от того, отработала ваша композиция раньше или позже композиции ядра, не зависит, найдёт ли биндинг свой ключ.
Читатель каталогов приезжает вместе с этой регистрацией, а не ожидается от хоста: каталог, который никто не читает, не объявляет ничего — его ключей нет в схеме. AddVeriqaConfigKeyCatalogs принимает любое число каталогов, AddVeriqaConfigKeyCatalog регистрирует один. Поставляемый код идёт обоими путями: общий пакет канальных адаптеров Veriqa.Core.ChannelAdapter заявляет свои три каталога одним вызовом, а каждый канальный адаптер — свой на собственной регистрации, поэтому развёртывание объявляет креденшелы тех каналов, которые добавило, и ничьи больше.

3. Прочитайте значение

IConfigurationResolver — единственный вход резолюции, и он асинхронный: уровень, обслуживаемый внешним стором, читается асинхронно, а синхронная обёртка над ним была бы sync-over-async. ResolutionContext.ForTenant(tenantId) закрывает случай «только тенант», ResolutionContext.Core — спрашивает один уровень ядра. Резолвите несколько ключей, которые обслуживает одна запись? Откройте ConfigResolutionScope над контекстом и передавайте его вместо контекста — запись тогда читается один раз на всё, что резолвится внутри:

4. Что может заявить объявление

Ваши ключи получают ту же модель, что и ключи ядра, а не урезанную. Семантика сведения уровней. По умолчанию — простое значение: верхний уровень свободно переопределяет нижний. Домен значения и цена значения вне него:
SkipStep (по умолчанию) оставляет провинившийся шаг незаданным, и резолюция уходит на уровень ниже; FailStart отказывает старт на уровне ядра и выбрасывает запись выше него. Текст boundary — это то, что оператор читает в отчёте, а correctAt — где ему сказано это исправить. Само значение ни в одно из сообщений не попадает. Ключ перечисления получает домен своих членов и без этого вызова. Измерения. .Identity("channel") заявляет измерение, без которого вопрос теряет смысл («настройка какого канала»), — оно адресуется на каждом шаге цепочки фолбэка и никогда из неё не выпадает. .Narrowing("theme", "surface") заявляет сужающие атрибуты, самый значимый первым; они выпадают из цепочки по одному, начиная с наименее значимого. .Fallback(...) заменяет сгенерированную цепочку там, где ключ пробует лишь часть комбинаций. Остальная цепочка одним списком:

5. Проверьте, что объявление подхватилось

Старт-отчёт механизма печатает то, что объявляет развёртывание. Одна строка уровня Information называет всю схему:
Поднимите механизм до Debug — и каждое объявление печатается со своими границами; строка ключа выше выглядит так:
Там же названы два расхождения: ключи, объявленные без единого биндинга извлечения в этом развёртывании (Information — из каких частей собрано развёртывание, решает оно само), и Warning на каждую пару «ключ + уровень», где ключ объявил уровень, который обслуживает эта установка, а биндинг на пару не зарегистрирован — такой уровень молча пуст. Ключ, которого в отчёте нет вовсе, не зарегистрирован: обычная причина — незакрытая цепочка (нет Declare()) либо композиция, которая так и не вызвала AddVeriqaConfigKeyCatalogs.

Пример в поставляемом коде

CorePageBrandingConfigKeys и LoginConfirmationConfigKeys живут в Veriqa.Core.ChannelAdapter — сборке, которая не может ссылаться на сервер аутентификации, это была бы обратная зависимость, — и объявляют ключи на уровнях приложения и ui_config, записи которых достаёт как раз сервер аутентификации. Свои каталоги они регистрируют из собственной композиции пакета канальных адаптеров, ровно как описано в §2.

Где этот путь нужен

  • Свой канальный адаптер — его креденшелы, тексты и лимиты по тенантам. Руководство по канальному адаптеру резолвит настройки через IConfigurationResolver; эта страница — про то, откуда берётся резолвимый им ключ.
  • Сток аудита, экспортёр метрик, исходящий шлюз — всё, что поставляется отдельной сборкой и что оператору нужно настраивать по приложениям или по тенантам.
  • Свой движок политик ограничения. Собственные правила допустимости Veriqa — внутренний контракт, и точкой расширения они не публикуются: интегратор, которому нужен другой движок политик — Rego, Cedar, собственный DSL, — поставляет такую проверку сам, и она держит свою конфигурацию на своих ключах, объявленных ровно так, как описано выше.

Дальше

Справочник конфигурации

Все секции и ключи продукта и таблица объявленных ключей.

Свой канальный адаптер

Вторая половина пути расширения — канальный SPI.