はじめに
プリザンターの 1.5.7.0 がリリースされました。バージョン番号の下位が 1 つ上がっただけに見えますが、中身を開けてみるとクラウドでの運用を前提とした機能がまとまって入った回になっています。
新規追加されたファイルだけで 60 本以上あり、リリースノートを眺めるだけでは全体像がつかみにくいところです。この記事ではソースコードを追いかけながら、1.5.7.0 で何が増えたのかを機能単位で整理してみます。
バージョン 1.5.7.0 を対象にしています
追加機能の全体像
1.5.6.0 と 1.5.7.0 のソースを比較すると、追加された機能は大きく 8 つに分類できます。
| 分類 | 追加された主なもの | ひとことで言うと | ライセンス |
|---|---|---|---|
| マルチテナント管理 API |
api/tenants 系エンドポイント |
テナントの払い出し・停止・削除を API 化 | 必要(トライアル不可) |
| バイナリストレージ |
AzureBlob プロバイダ |
添付ファイルを Blob Storage に直接置ける | 不要 |
| 起動ウォームアップ | WarmupGateMiddleware |
初期化完了までリクエストを受け付けない | 不要 |
| 分散ロック | DistributedLock |
複数インスタンスでの起動処理の競合を防ぐ | 不要 |
| ヘルスチェック | ウォームアップ状態のチェック | 初期化中はロードバランサから外れる | 不要 |
| データ保護キー | Redis への保存 | インスタンス間で鍵を共有できる | 不要 |
| バックグラウンドジョブ | ImportJobHandler |
インポートをジョブとして裏で流せる | 必要(Queue) |
| 一覧のエラー処理 | GridDataTimeoutException |
一覧のタイムアウトを識別して返す | 不要 |
このほかに、スタイル・スクリプト・HTML の下書き出力(DraftOutputMode)も追加されています。こちらはライセンス不要です。
「ライセンス」列が「必要」の機能は、上位ライセンスを当てていない環境では動きません。設定ファイルで有効にしても無視されるか、API がエラーを返します。各機能の節で判定箇所を示します。
ライセンス判定の実装そのものは 1.5.7.0 でオプションが増えているため、別記事「プリザンターのライセンス判定の実装を詳しくみてみる」にまとめました。
並べてみると分かるとおり、「複数インスタンスで動かす」「クラウドのマネージドサービスに寄せる」 という方向に寄った構成です。オンプレの単一サーバ構成では触らない機能も多いので、必要なものから見ていくのがよさそうです。
この記事では 1.5.6.0 との差分だけを扱います。/healthz エンドポイントやサイト設定キャッシュ(Sites.EnableSsCache)のように 1.5.6.0 以前から存在する機能は、1.5.7.0 で変わった部分だけを取り上げます。
マルチテナント管理 API
追加されたエンドポイント
Implem.Pleasanter/Controllers/Api/TenantsController.cs が新設され、テナントの操作が API で行えるようになりました。
[Route("api/[controller]")]
public class TenantsController : ControllerBase
{
[HttpPost("{id}/Delete")]
public ContentResult Delete(int id)
[HttpPost("Get")]
public ContentResult Get()
[HttpPost("Create")]
public ContentResult Create()
[HttpPost("{id}/Suspend")]
public ContentResult Suspend(int id)
[HttpPost("{id}/Resume")]
public ContentResult Resume(int id)
}
| メソッド | パス | 用途 |
|---|---|---|
| POST | api/tenants/Get |
テナント一覧の取得 |
| POST | api/tenants/Create |
テナントと初期ユーザーの作成 |
| POST | api/tenants/{id}/Suspend |
テナントの利用停止 |
| POST | api/tenants/{id}/Resume |
停止したテナントの再開 |
| POST | api/tenants/{id}/Delete |
テナントの削除依頼 |
呼び出しの前提条件
すべてのメソッドの先頭に、共通のガードが入っています。
if (!Parameters.AllowMultiTenants())
{
return ApiResults.Error(
context: context,
errorData: new ErrorData(type: Error.Types.HasNotPermission));
}
if (!context.HasPrivilege)
{
return ApiResults.Error(
context: context,
errorData: new ErrorData(type: Error.Types.HasNotPermission));
}
Parameters.AllowMultiTenants() はライセンスチェックを含むため、マルチテナント対応のライセンスがない環境では使えません。加えて context.HasPrivilege、つまり Security.json の PrivilegedUsers に含まれる特権ユーザーであることが必要です。
この機能は上位ライセンス(MultiTenants オプション)が必要です。 しかも AllowMultiTenants() は、他のライセンス判定と違ってトライアルライセンスによる代替が効きません。
public static bool AllowQueue()
{
return TrialLicense?.Check() ?? License.Check() && HasQueue();
}
public static bool AllowMultiTenants()
{
return License.Check() && HasMultiTenants(); // トライアルのフォールバックなし
}
他の Allow*() は TrialLicense?.Check() ?? ... の形になっていて、トライアルライセンスがあればオプションフラグを見ずに通ります。AllowMultiTenants() だけがこの形をとっていないため、トライアル環境ではマルチテナント API を試せません。
さらに、ライセンスの有無は API だけでなく認証そのものにも影響します。1.5.7.0 で Context に次の判定が追加されました。
if (!Parameters.MultiTenant.IsProtectedTenant(TenantId) && !Parameters.AllowMultiTenants())
{
Authenticated = false;
}
MultiTenant.json の DefaultTenantId で指定した保護テナント以外は、マルチテナントライセンスがないとログイン自体が通らなくなります。ライセンスなしの環境で 2 つ目以降のテナントを作っていた場合、1.5.7.0 へ上げた時点でそのテナントのユーザーがログインできなくなるため、バージョンアップ前に確認が必要です。
テナントの作成
Create のリクエストボディは CreateTenantApiModel にデシリアライズされます。
public class CreateTenantApiModel
{
public string ApiKey { get; set; }
public string TenantName { get; set; }
public string Title { get; set; }
public string LoginId { get; set; }
public string Name { get; set; }
public string NotifyMailAddress { get; set; }
public string Language { get; set; }
}
TenantName・LoginId・Name・NotifyMailAddress は必須で、未指定だと PleaseInputData エラーになります。NotifyMailAddress はメールアドレスとしての形式チェックも通ります。
{
"ApiKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"TenantName": "サンプル商事",
"Title": "サンプル商事 プリザンター",
"LoginId": "sample-admin",
"Name": "サンプル管理者",
"NotifyMailAddress": "admin@example.com",
"Language": "ja"
}
処理としては、テナントの作成と同時に LoginId / Name のユーザーが 1 人作られます。このユーザーはテナント管理者(TenantManager = true)で、パスワードは Service.json の DefaultPassword が設定されます。Security.json の PasswordExpirationPeriod が有効なら、そこから算出した有効期限も入ります。
作成が完了すると NotifyMailAddress 宛に案内メールが送られます。
if (!apiModel.NotifyMailAddress.IsNullOrEmpty()
&& !Parameters.Mail.SupportFrom.IsNullOrEmpty())
{
new OutgoingMailModel()
{
Title = new Title(Displays.Get(
context: context,
id: "TenantCreated",
data: apiModel.TenantName)),
Body = Displays.Get(
context: context,
id: "TenantCreatedMailBody",
data: new string[]
{
apiModel.TenantName,
tenantModel.TenantId.ToString(),
apiModel.LoginId,
Locations.ApprovaUri(context: context)
}),
From = MimeKit.MailboxAddress.Parse(Parameters.Mail.SupportFrom),
To = apiModel.NotifyMailAddress
}.Send(
context: context,
ss: new SiteSettings());
}
送信元には Mail.json の SupportFrom が使われます。SupportFrom が未設定だとメールは送られません(テナント自体は作成されます)。本文のテンプレートは App_Data/Displays/TenantCreatedMailBody.json にあるため、多言語定義と同じ仕組みで差し替えられます。
停止と再開
Suspend は Tenants テーブルの ContractDeadline(契約期限)に日付を書き込むだけの実装です。
var suspendDate = apiModel?.SuspendDate ?? DateTime.Today;
Repository.ExecuteNonQuery(
context: context,
transactional: true,
statements: Rds.UpdateTenants(
where: Rds.TenantsWhere().TenantId(tenantId),
param: Rds.TenantsParam().ContractDeadline(suspendDate)));
SuspendDate を省略すると当日付が入ります。将来日を指定しておけば「予約停止」もできます。Resume は逆に ContractDeadline を null に戻すだけです。
既存の契約期限の仕組みをそのまま使っていますが、「期限切れで実際にログインを止める」判定も 1.5.7.0 で追加されています。
if (Authenticated
&& !Parameters.MultiTenant.IsProtectedTenant(TenantId)
&& ContractSettings.OverDeadline(context: this))
{
Authenticated = false;
}
保護テナントは対象外なので、基盤テナントが巻き添えで止まることはありません。停止されたテナントのユーザーは認証が通らなくなります。
削除は 2 段階
Delete は物理削除ではありません。1.5.7.0 で Tenants テーブルに追加された DeleteRequestTime 列に削除依頼日時を書き込むだけで、実際のレコード削除は後追いのバックグラウンドサービスが行います。
削除対象の抽出は次のとおりです。
private static List<int> GetTargetTenantIds(Context context)
{
return Repository.ExecuteTable(
context: context,
statements: Rds.SelectTenants(
column: Rds.TenantsColumn().TenantId(),
where: Rds.TenantsWhere()
.DeleteRequestTime(
DateTime.Now.AddDays(
Parameters.BackgroundService.DeleteTenantRetentionPeriod * -1),
_operator: "<")))
.AsEnumerable()
.Select(o => o.Int("TenantId"))
.ToList();
}
動作は BackgroundService.json で制御します。
{
"DeleteTenant": true,
"DeleteTenantTime": [ "04:00" ],
"DeleteTenantRetentionPeriod": 30,
"DeleteTenantChunkSize": 1000,
"DeleteTenantBinariesChunkSize": 10
}
ChunkSize 系が用意されているのは、テナント配下のレコードや添付ファイルを一括で消すと DB に負荷がかかるためです。分割しながら削除していきます。
保護テナント
MultiTenant.json という新しいパラメータファイルが追加されました。
{
"DefaultTenantId": 1
}
public class MultiTenant
{
public int DefaultTenantId { get; set; } = 1;
public bool IsProtectedTenant(int tenantId)
{
return tenantId == DefaultTenantId;
}
}
DefaultTenantId に指定したテナントは Suspend / Resume / Delete のいずれも受け付けません。バックグラウンドの物理削除でもスキップされ、SysLogs に警告が残ります。API の操作ミスで基盤テナントごと消えてしまう事故を防ぐためのガードです。
添付ファイルを Azure Blob Storage に置けるようになった
プロバイダの抽象化
これまで添付ファイルの格納方式は Rds(DB)と Local(ファイルシステム)の 2 択で、処理も各所に直接書かれていました。1.5.7.0 でインターフェースが切られ、プロバイダを差し替えられる構造になっています。
public static IBinaryStorageProvider Create(string provider)
{
return provider switch
{
BinaryStorageProviderNames.LocalFolder => new LocalFolderBinaryStorageProvider(),
BinaryStorageProviderNames.Local => new LocalFolderBinaryStorageProvider(),
BinaryStorageProviderNames.AzureBlob => new AzureBlobBinaryStorageProvider(),
_ => null
};
}
Rds はプロバイダが null になり、従来どおり Binaries テーブルの Bin 列を読み書きします。プロバイダが返るのは外部ストレージを使う場合だけ、という切り分けです。
IBinaryStorageProvider は Upload / Download / OpenRead / Delete / Exists / LastWriteTime の各操作を、同期版と Async サフィックス付きの非同期版の両方で定義しています。独自のストレージに対応させたい場合は、このインターフェースを実装してファクトリに 1 行足すだけで済みます。
AzureBlob の認証方式
BinaryStorage.json には AzureBlobStorageAccountUri と AzureBlobContainerName の 2 つのキーが増えました。どちらも環境変数で上書きできます。
設定手順(ストレージアカウントとコンテナの作成、マネージド ID へのロール付与、環境変数の指定)は「プリザンターの添付ファイルをクラウドPaaS環境で永続化する方法」にまとめています。既存の Azure Files マウント構成から移す場合は「プリザンターの添付ファイルをAzure FilesマウントからBlob Storageへ移行する」を見てみてください。
この記事で注目したいのは認証方式です。
var service = new BlobServiceClient(
new Uri(uri),
new DefaultAzureCredential(credentialOptions),
options);
return service.GetBlobContainerClient(containerName);
DefaultAzureCredential を使っているため、接続文字列やアカウントキーを設定ファイルに書く必要がありません。App Service や Container Apps のマネージド ID にストレージの BLOB データ共同作成者ロールを付与しておけば、それだけで動きます。ローカル開発時は Azure CLI のログイン情報が使われます。
添付ファイルは Attachments/{Guid} というオブジェクト名で保存されます。
サーキットブレーカー
ネットワーク越しのストレージになるため、障害時の振る舞いも実装されています。
private static readonly TimeSpan[] CircuitOpenDurations =
{
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(2),
TimeSpan.FromMinutes(10)
};
private const int MaxRetries = 3;
接続や認証に失敗すると回路が開き、30 秒 → 2 分 → 10 分と段階的に遮断時間が延びます。遮断中のリクエストはストレージへ飛ばさず即座に IOException を返すため、障害中にリクエストが詰まって全体が巻き添えになるのを防げます。1 回でも成功すれば失敗カウントはリセットされます。
失敗の内容は SysLogs に記録されるので、Blob 側の権限設定ミスなどはログから追えます。
一時ファイルが外部ストレージへ移送されるようになった
もう 1 つ、地味ですが運用に効く変更があります。
ロードバランサ配下でプリザンターを動かす場合、アップロード用のリクエストとレコード保存のリクエストが別インスタンスに振り分けられることがあります。これを避けるために TemporaryBinaryStorageProvider を Rds にして、一時ファイルだけ DB に置く運用があります。
1.5.6.0 以前はこの構成にすると、レコード保存後もファイルが Binaries.Bin に残り続けていました。1.5.7.0 では保存時に外部ストレージへ移送されます。
if (Parameters.BinaryStorage.TemporaryBinaryStorageProvider == BinaryStorageProviderNames.Rds
&& context.Api != true)
{
if (provider != null)
{
MoveBinFromRdsToExternal(
context: context,
provider: provider);
}
statements.Add(Rds.UpdateBinaries(
param: Rds.BinariesParam()
// ...
.Bin(raw: "NULL", _using: provider != null)
// ...
where: Rds.BinariesWhere().Guid(Guid)));
}
移送に失敗した場合は SysLogs に記録したうえで例外を投げるため、DB 側だけ NULL になって実体を見失うことはありません。
起動ウォームアップとスケールアウト対応
ウォームアップゲート
プリザンターは起動時にテナント情報や拡張機能、サイト情報などの初期化を行います。従来はこの初期化中でもリクエストを受け付けてしまうため、起動直後のアクセスがエラーになることがありました。
1.5.7.0 では初期化がホステッドサービスとして切り出され、完了するまでリクエストをゲートで止めるようになりました。
var warmupSteps = new List<(string Name, Action Step)>
{
("TenantInitializer.Initialize", () => TenantInitializer.Initialize()),
("ExtensionInitializer.Initialize", () => ExtensionInitializer.Initialize(context: context)),
("UsersInitializer.Initialize", () => UsersInitializer.Initialize(context: context)),
("ItemsInitializer.Initialize", () => ItemsInitializer.Initialize(context: context)),
(StartupMigrationsStepName, () => { /* ... */ }),
("StatusesInitializer.Initialize", () => StatusesInitializer.Initialize(context: context)),
("NotificationInitializer.Initialize", () => NotificationInitializer.Initialize()),
("SiteInfo.Refresh", () => SiteInfo.Refresh(context: context))
};
状態は WarmupStatus で管理されます。
| 値 | 意味 |
|---|---|
NotStarted |
未開始 |
InProgress |
初期化中 |
Completed |
完了(通常運用) |
Failed |
例外で失敗 |
Canceled |
キャンセル |
TimedOut |
タイムアウト |
Completed 以外の状態でリクエストが来ると、WarmupGateMiddleware がリクエストの種類ごとに応答を振り分けます。
静的ファイルとエラーページを通しているのは、リダイレクト先の /errors/warmup 自体が表示できなくなるのを避けるためです。IIS の Application Initialization からのリクエストだけは待ち合わせる作りになっており、ウォームアップが終わってから 200 を返します。
初期化に失敗した場合はアプリケーション自体が停止します。中途半端な状態で動き続けないという判断です。
タイムアウトと停止までの猶予は BackgroundService.json で調整できます。こちらも 1.5.7.0 での追加分です。
{
"WarmupTimeoutSeconds": 180,
"WarmupFailureShutdownDelaySeconds": 60
}
サイト数やユーザー数が多い環境では初期化に時間がかかるため、既定の 180 秒で足りるかは実測して確認しておきたいところです。
分散ロック
複数インスタンスを同時に起動すると、DB のマイグレーションが並行して走ってしまいます。これを防ぐために DistributedLock が追加されました。
private bool TryAcquire(int timeoutSeconds)
{
return Parameters.Rds.Dbms switch
{
"SQLServer" => TryAcquireSqlServer(timeoutSeconds: timeoutSeconds),
"PostgreSQL" => TryAcquirePostgreSql(timeoutSeconds: timeoutSeconds),
"MySQL" => TryAcquireMySql(timeoutSeconds: timeoutSeconds),
_ => true, // 未対応の DBMS では、従来どおり排他制御を行わずに処理を続ける。
};
}
Redis などの外部ミドルウェアではなく、DB が持つロック機構をそのまま使うのがポイントです。追加のインフラを用意しなくてもスケールアウトできます。
| DBMS | 使用する仕組み |
|---|---|
| SQL Server |
sp_getapplock(@LockOwner = 'Session') |
| PostgreSQL |
pg_advisory_lock() + lock_timeout
|
| MySQL | get_lock() |
起動時のマイグレーションは startup-migrations という名前のロックで保護されます。ロックを取れなかったインスタンスは、先行インスタンスのマイグレーション完了を待ってから次のステップに進みます。
ヘルスチェックがウォームアップ状態を返すようになった
/healthz エンドポイント自体は 1.5.6.0 以前から存在します。1.5.7.0 で変わったのは次の 3 点です。
-
Startup.csに直接書かれていた登録処理がHealthCheckExtensions.csに切り出された - ウォームアップ状態を返すチェックが追加された
-
Security.jsonのHealthCheck.Enabledの既定値がfalseからtrueに変わった
追加されたのはこのチェックです。
public static IHealthChecksBuilder AddWarmupHealthCheck(this IHealthChecksBuilder builder)
{
return builder.AddCheck("warmup", () =>
{
var status = ApplicationWarmupHostedService.CurrentStatus;
return status switch
{
WarmupStatus.NotStarted => HealthCheckResult.Degraded("Warmup not started"),
WarmupStatus.InProgress => HealthCheckResult.Degraded("Warmup in progress"),
WarmupStatus.Completed => HealthCheckResult.Healthy("Warmup completed"),
WarmupStatus.Failed => HealthCheckResult.Unhealthy("Warmup failed"),
WarmupStatus.Canceled => HealthCheckResult.Unhealthy("Warmup canceled"),
WarmupStatus.TimedOut => HealthCheckResult.Unhealthy("Warmup timed out"),
_ => HealthCheckResult.Unhealthy($"Warmup status: {status}")
};
});
}
Healthy は 200、Degraded(ウォームアップ中)と Unhealthy は 503 を返します。ウォームアップ中は 503 になるため、ロードバランサの振り分け対象から自動的に外れます。前述のウォームアップゲートと組み合わさって、ローリングデプロイと素直に噛み合う設計です。
既定値の変更も合わせて見ておきます。
"HealthCheck": {
- "Enabled": false,
+ "Enabled": true,
"EnableDatabaseCheck": false,
"HealthQuery": null,
"RequireHosts": null,
"EnableDetailedResponse": false
},
これまで /healthz は明示的に有効化しないと使えませんでしたが、1.5.7.0 からは既定で応答します。RequireHosts が null のままだとホスト制限なしで公開される点には注意してください。監視元を限定したい場合は設定しておきます。
データ保護キーを Redis に置けるようになった
複数インスタンスで動かす場合、ASP.NET Core のデータ保護キーをインスタンス間で共有しないと、Cookie の復号に失敗してログインが維持できません。これまでは Azure Blob Storage への保存に対応していましたが、1.5.7.0 で Redis が選択肢に加わりました。
"AspNetCoreDataProtection": {
"BlobContainerUri": null,
"KeyIdentifier": null,
"KeyFileName": "Keys.xml",
- "XmlAesKey": null
+ "XmlAesKey": null,
+ "KeyValueStoreConnectionString": null,
+ "KeyValueStoreKeyName": null
},
KeyValueStoreConnectionString を指定すると PersistKeysToStackExchangeRedis() が使われます。優先順位は Blob(BlobContainerUri と KeyIdentifier の両方が設定されている場合)→ Redis → ローカル(既定)の順です。
else if (!keyValueStoreConnectionString.IsNullOrEmpty())
{
ValidateAspNetCoreDataProtectionKeyValueStoreParameters(keyValueStoreConnectionString);
services
.AddDataProtection()
.SetApplicationName(Implem.Libraries.Utilities.Environments.ServiceName)
.PersistKeysToStackExchangeRedis(
DataProtectionForRedisConnection.Connection,
GetAspNetCoreDataProtectionKeyValueStoreKeyName());
AddAspNetCoreDataProtectionXmlEncryptor(services);
}
セッションの共有(Kvs.json の ConnectionStringForSession)で既に Redis を使っている環境なら、同じインスタンスにキーも寄せられます。
バックグラウンドジョブでインポートが流せるようになった
バックグラウンドジョブは以前からエクスポートに対応していましたが、1.5.7.0 でインポートが追加されました。
public static class BackgroundJobTypes
{
public const string Export = "Export";
public const string Import = "Import";
}
BackgroundJobs.json にも入力ファイルのパス設定が増えています。
{
"BackgroundQueue": false,
"BackgroundJobDispatcherInterval": 60,
"BackgroundJobTimeout": 3600,
"FallbackLanguage": "ja",
"RecoverAction": "Failed",
"OutputFilePath": null,
"InputFilePath": null
}
ImportJobHandler はジョブに紐づく CSV ファイルを読み込み、通常のインポートと同じ処理を裏で実行します。結果は ImportResultData にまとめられ、件数やエラー内容がジョブの結果として残ります。
大量データのインポートは画面がタイムアウトしがちな処理なので、ジョブに逃がせるようになったのは実運用で効きます。BackgroundQueue を true にしたうえで、InputFilePath に読み込み元のディレクトリを指定して使います。
バックグラウンドジョブ機能そのものに上位ライセンス(Queue オプション)が必要です。 BackgroundQueue を true にしても、ライセンスがなければキューは有効になりません。
public static bool BackgroundQueueEnabled()
{
return Parameters.BackgroundJobs?.BackgroundQueue == true
&& Parameters.AllowQueue();
}
こちらは AllowQueue() なのでトライアルライセンスでも有効になります。1.5.7.0 で新しいのはインポート対応の部分だけで、ライセンスの条件自体は 1.5.6.0 から変わっていません。
一覧のタイムアウトが識別できるようになった
一覧のデータ取得で例外が起きたとき、従来は原因によらず「ソートできません」というメッセージになっていました。1.5.7.0 では 3 種類に分かれます。
catch (DbException e)
when (context.SqlErrors.IsTimeout(e))
{
var message = Messages.GridDataTimeout(context: context);
context.Messages.Add(message);
throw new GridDataTimeoutException(
message: message.Text,
innerException: e);
}
catch (DbException e)
{
// ビューをリセットしてソートエラーを返す
throw new CanNotGridSortException(/* ... */);
}
catch (System.Exception e)
{
var message = Messages.GridDataUnexpectedError(context: context);
context.Messages.Add(message);
throw new GridDataException(
message: message.Text,
innerException: e);
}
タイムアウトの判定は DBMS ごとの実装に委ねられています。
public bool IsTimeout(DbException dbException)
{
return dbException is SqlException sqlException
&& sqlException.Number == -2;
}
重要なのは、タイムアウトのときはビューをリセットしない点です。従来はソートエラー扱いでビューが初期化されていたため、「重いクエリでタイムアウト → 絞り込み条件も消える」という体験になっていました。1.5.7.0 ではフィルタ条件を保ったままタイムアウトのメッセージだけが表示されます。
スタイル・スクリプト・HTML の下書き出力
管理画面のスタイル・スクリプト・HTML に、下書きモードが追加されました。設定項目に DraftOutputMode と DraftKey が増えています。
public bool? Disabled;
public int? DraftOutputMode;
public string DraftKey;
DraftOutputMode |
挙動 |
|---|---|
0(既定) |
常に出力する |
1 |
下書き時のみ出力する |
2 |
下書き時は出力しない |
「下書きかどうか」の判定はクエリパラメータで行います。
public bool DraftOutputEnabled(Context context, int? draftOutputMode, string draftKey)
{
var validKey = draftKey.IsNullOrEmpty()
? "1"
: draftKey;
var matched = string.Equals(
context.QueryStrings.Data("Draft"),
validKey,
StringComparison.Ordinal);
switch (draftOutputMode)
{
case 1: return matched;
case 2: return !matched;
default: return true;
}
}
URL に ?Draft=<キー> を付けてアクセスしたときだけ、DraftOutputMode が 1 のスクリプトが出力されます。DraftKey を指定しなければ既定値の 1 が使われるため、?Draft=1 で切り替わります。
/items/123?Draft=newfeature
DraftKey に newfeature を設定したスクリプトは、この URL でアクセスしたときだけ動きます。逆に DraftOutputMode を 2 にしておけば、既存のスクリプトを下書き確認時だけ止められます。
本番サイトを触りながら新しいスクリプトを検証するという使い方を想定した機能です。これまでは検証用サイトを別に用意するか、有効/無効を切り替えながら確認するしかありませんでした。
DraftKey は URL に平文で載るため、秘匿情報ではありません。あくまで「一般利用者には見せずに動作確認する」ための仕組みとして使ってください。
まとめ
1.5.6.0 から 1.5.7.0 で追加された機能を見てきました。
-
マルチテナント管理 API: テナントの作成・停止・再開・削除を API 化。削除は
DeleteRequestTimeを立てる論理削除で、既定 30 日の猶予後にバックグラウンドで物理削除 -
AzureBlob プロバイダ: 添付ファイルを Blob Storage に直接格納。
DefaultAzureCredentialによるマネージド ID 認証とサーキットブレーカー付き -
一時ファイルの外部移送:
TemporaryBinaryStorageProviderをRdsにしたときの積み残しが解消 - ウォームアップゲート: 初期化完了までリクエストを止め、Ajax/API には 503 を返す
- 分散ロック: DB のロック機構を使い、複数インスタンス起動時のマイグレーション競合を回避
- ヘルスチェック: ウォームアップ状態を返すチェックが加わり、既定で有効に
- データ保護キーの Redis 保存: インスタンス間で鍵を共有できる選択肢が増えた
- インポートのジョブ化: 大量データのインポートをバックグラウンドで実行
- 一覧のタイムアウト識別: タイムアウト時にビューをリセットしなくなった
-
下書き出力:
?Draft=<キー>でスタイル・スクリプト・HTML を出し分け
全体として、プリザンターを複数インスタンスのクラウド環境で運用するための足回りが一気に揃ったバージョンです。単一サーバのオンプレ構成では恩恵を感じにくいかもしれませんが、Azure App Service や Container Apps に載せている環境では、ウォームアップゲートとヘルスチェックだけでもデプロイ時の安定性が変わってきます。
一方、下書き出力や一覧のタイムアウト識別は構成を問わず効いてくる改善です。バージョンアップの際は、このあたりから触ってみるのがよさそうです。