1729781866
2024-10-24 12:32:00
で パート 1、 パート 2 そして パート 3アーキテクチャから NuGet パッケージとしてパッケージ化するまで、Optimizely CMS のアドオンを作成するために必要な手順の概要を説明しました。このパートでは、アドオン開発者として成功するのに役立ついくつかのベスト プラクティスについて説明します。このシリーズ全体の例をこのセクション内で表示できます。 Optimizely アドオン テンプレート 私が作り続けてきたもの。
複数のアドオンを管理する個人開発者として、定期的にアップデートをリリースできるかどうかは、広範な単体テストを実施することに大きく依存しています。例えば、 ストットセキュリティ 機能ブランチを開発ブランチにマージするプル リクエストが行われるたびに実行される 1,500 を超える単体テストが含まれています。このレベルのカバレッジにより、リリース間で機能の一貫性が保たれます。
ビジネス ロジックの単体テストを作成するだけでなく、コントローラーのセキュリティを検証する追加の単体テストを作成することもできます。これらのテストは次のことを保証するため、システムのセキュリティを確保するために不可欠な部分として追加することを検討します。
- コントローラーのアクションは、意図した HTTP メソッドのみを明示的に許可し、エンドポイントが正しい動詞のみに応答するようにします。
- コントローラーのアクションは、Authorization 属性で保護されるか、セキュリティが必要ない場合は、AllowAnonymous でマークされます。これにより、各エンドポイントに明確なセキュリティ要件が強制されます。
- コントローラーのアクションは特定のルートで定義され、他のモジュールや使用側アプリケーションのルーティングとの競合を防ぎます。
[TestFixture]
public sealed class ControllerStandardsTests
{
[Test]
[TestCaseSource(typeof(ControllerStandardsTestCases), nameof(ControllerStandardsTestCases.PageControllerActionTestCases))]
public void ControllersShouldHaveHttpMethodAttributes(string controllerName, string methodName, MethodInfo methodInfo)
{
// Act
var hasHttpMethodAttribute = methodInfo.GetCustomAttributes(typeof(HttpMethodAttribute)).Any();
// Assert
// Controllers should only respond in intended Verbs and should respond with method not allowed on unintended verbs.
// This will prevent posting of malicious payloads to methods not intended to retrieve of deal with these payloads.
// First raised by a penetration test where an attempt to post files to a content end point returned a 200 instead of a 405
Assert.That(hasHttpMethodAttribute, Is.True, $"{controllerName}.{methodName} should be decorated with a http method attribute.");
}
[Test]
[TestCaseSource(typeof(ControllerStandardsTestCases), nameof(ControllerStandardsTestCases.PageControllerActionTestCases))]
public void ControllerMethodsShouldEitherHaveAuthorizeOrAllowAnonymousAttributes(string controllerName, string methodName, MethodInfo methodInfo)
{
// Act
var hasAuthorizeAttribute = methodInfo.GetCustomAttributes(typeof(AuthorizeAttribute)).Any();
var hasAllowAnonymousAttribute = methodInfo.GetCustomAttributes(typeof(AllowAnonymousAttribute)).Any();
var controllerHasAuthorizeAttribute = methodInfo.DeclaringType?.GetCustomAttributes(typeof(AuthorizeAttribute)).Any() ?? false;
var hasAttribute = hasAuthorizeAttribute || hasAllowAnonymousAttribute || controllerHasAuthorizeAttribute;
// Assert
// Controller actions should be protected with an Authorization attribute or an intentional AllowAnonymous attribute.
// This will ensure your controllers are secure by default and that you have to explicitly allow anonymous access.
Assert.That(hasAttribute, Is.True, $"{controllerName}.{methodName} should be decorated directly or indirectly with an Authorize or AllowAnonymous attribute.");
}
[Test]
[TestCaseSource(typeof(ControllerStandardsTestCases), nameof(ControllerStandardsTestCases.PageControllerActionTestCases))]
public void ControllerMethodsShouldHaveRouteAttributes(string controllerName, string methodName, MethodInfo methodInfo)
{
// Act
var hasRouteAttribute = methodInfo.GetCustomAttributes(typeof(RouteAttribute)).Any();
var controllerHasRouteAttribute = methodInfo.DeclaringType?.GetCustomAttributes(typeof(RouteAttribute)).Any() ?? false;
var hasAttribute = hasRouteAttribute || controllerHasRouteAttribute;
// Assert
// Controller actions should have a fixed route attribute so as to not have clashes with routes declared by other modules.
Assert.That(hasAttribute, Is.True, $"{controllerName}.{methodName} should be decorated directly with a Route attribute.");
}
}
これらのテストでは、リフレクションを使用してソリューション内のすべてのコントローラーを識別するテスト ケース ソース メソッドを使用します。これは、新しいコントローラーを追加するときに、それらを保護することを忘れないことを意味します。
public static class ControllerStandardsTestCases
{
public static IEnumerable PageControllerActionTestCases
{
get
{
var assembly = Assembly.GetAssembly(typeof(SettingsLandingPageController));
if (assembly == null)
{
yield break;
}
var controllers = assembly.GetTypes()
.Where(t => (t.BaseType?.Name.StartsWith("Controller") ?? false)
|| (t.BaseType?.Name.StartsWith("BaseController") ?? false))
.ToList();
foreach (var controller in controllers)
{
var actions = controller.GetMethods()
.Where(x => x.DeclaringType == controller && x.IsPublic)
.ToList();
foreach (var methodInfo in actions)
{
if (methodInfo.ReturnType == typeof(IActionResult) || methodInfo.ReturnType == typeof(Task))
{
yield return new TestCaseData(controller.Name, methodInfo.Name, methodInfo);
}
}
}
}
}
}
このシリーズに従ってアドオンを作成した場合は、アドオン開発用のサンプル サイトがアドオン コードの使用に nuget を使用せず、代わりにプロジェクト参照を使用していることに気づくでしょう。これにより、効率的に開発できるようになりますが、本番環境のような方法でコードをテストしていないことを意味します。
独自の Optimizely CMS ソリューションを備えた別のリポジトリを作成します。アドオンを NuGet パッケージとしてこのソリューションに直接インポートします。これにより、他の開発者がアドオンを初めて体験するのと同じ方法でアドオンをテストできるようになります。開発者クラウド ライセンスを生成できる場合は、 EPiServer ライセンス センター次に、.NET 6.0 または 8.0 を搭載した Linux 上で実行されている Azure WebApp にこのテスト システムをデプロイして、デプロイされた環境内でアドオンを検証できるようにすることをお勧めします。
本番稼働サイクルの一環として、次のことを実行します。
- nuget パッケージのベータ版を作成する
- ベータ版パッケージを nuget.org にアップロードする
- ベータ パッケージを使用するようにテスト システムを更新する
- テスト システムを Azure にデプロイする
- nuget.org でベータ版のリストを削除する
- テスト システムでアドオンをテストする
- nuget パッケージの運用バージョンを作成する
- 運用パッケージを nuget.optimizely.com にアップロードします。
- パッケージが Optimizely の QA チームによって承認されるまで待ちます
- 実稼働パッケージを nuget.org にアップロードする
- リリースを発表する
Azure SQL Server の Optimizely の既定の設定では、通常、約 120 の同時データベース接続がサポートされており、運用環境には S2 SQL Database (または同等) の使用が推奨されています。この構成と Optimizely CMS コードによる効率的なキャッシュを組み合わせることで、トラフィックの多い Web サイトに対して小規模なデータベースが効率的に実行できるようになります。
Microsoft Entity Framework を使用している場合は、それぞれの点に注意してください。 DbContext インスタンスは新しいデータベース接続を開きます。これらの接続を管理しないと、接続制限によりサーバーが不安定になる可能性があります。したがって、キャッシュを広範囲に使用するという Optimizely のアプローチに従うことをお勧めします。これを実装するには、次の手順を検討してください。
- Optimizely のキャッシュを消費するカスタム キャッシュ ラッパーを使用する
ISynchronizedObjectInstanceCache独自のマスターキーを使用して、キャッシュを効果的に削除できるようにします。 - DbContext をスコープ付きオブジェクトとして挿入して、リクエストごとのインスタンス数を 1 に制限します。
- DB コンテキストを必要とする遅延読み込み依存関係。消費されない場合はインスタンス化されません。
- データのロードは次の順序で処理します。
- 最初にキャッシュからデータを取得して返すことを試みます。
- 2 番目にデータベースからデータを取得して返してみます。
- データを返す前に、データをキャッシュにプッシュします。
以下は、 ISynchronizedObjectInstanceCache。
public sealed class CacheWrapper : ICacheWrapper
{
private readonly ISynchronizedObjectInstanceCache _cache;
private const string MasterKey = "My-OptimizelyAddOn-MasterKey";
public CacheWrapper(ISynchronizedObjectInstanceCache cache)
{
_cache = cache;
}
public void Add(string cacheKey, T? objectToCache)
where T : class
{
if (string.IsNullOrWhiteSpace(cacheKey) || objectToCache == null)
{
return;
}
try
{
var evictionPolicy = new CacheEvictionPolicy(
TimeSpan.FromHours(12),
CacheTimeoutType.Absolute,
Enumerable.Empty(),
new[] { MasterKey });
_cache.Insert(cacheKey, objectToCache, evictionPolicy);
}
catch (Exception exception)
{
// Add logging here
}
}
public T? Get(string cacheKey)
where T : class
{
return _cache.TryGet(cacheKey, ReadStrategy.Wait, out var cachedObject) ? cachedObject : default;
}
public void RemoveAll()
{
try
{
_cache.Remove(MasterKey);
}
catch (Exception exception)
{
// Add logging here
}
}
}
Lazy Loaded 依存関係を使用するには、まずサービス拡張メソッド内で Lazy バリアントを定義する必要があります。
services.AddScoped();
services.AddScoped>(provider => new Lazy(() => provider.GetRequiredService()));
次に、以下の例のように、コンストラクターで依存関係を Lazy として宣言し、それらを使用できます。このシナリオでは、 IMyDataContext は一度インスタンス化されますが、それは、によって使用されるまで延期されます。 GetData() 方法:
internal sealed class MyRepository : IMyRepository
{
private readonly Lazy _context;
public MyRepository(Lazy context)
{
_context = context;
}
public async Task> GetData()
{
return await _context.Value.MyData.ToListAsync();
}
}
その後、サービスは両方を消費できるようになります。 IMyRepositoryそして ICacheWrapper パフォーマンス呼び出しを実行して、変更されていないデータを取得します。次の例では、最初にキャッシュからデータを取得しようとします。次に、データが null または空の場合は、リポジトリからデータをロードし、そのデータを返す前にキャッシュにプッシュしようとします。もし Delete メソッドはサービス内で呼び出されます。 RemoveAll() キャッシュ ラッパーでマスター キーに基づいてキャッシュ エントリを無効にします。
internal sealed class MyService : IMyService
{
private readonly IMyRepository _repository;
private readonly ICacheWrapper _cache;
private const string CacheKey = "Unique.Cache.Key";
public MyService(IMyRepository repository, ICacheWrapper cache)
{
_repository = repository;
_cache = cache;
}
public async Task> GetDate()
{
var data = _cache.Get>(CacheKey);
if (data is not { Count: >0 })
{
data = await _repository.GetData();
_cache.Add(CacheKey, data);
}
return data;
}
public async Task Delete(string data)
{
await _repository.Delete(data);
_cache.RemoveAll();
}
}
データベース コンテキストはリポジトリ自体によってすでに遅延ロードされているため、リポジトリを遅延ロードする必要がないことに注意してください。ただし、キャッシュが設定されている場合はリポジトリが使用されないため、リポジトリを遅延ロードすることを選択することもできます。
- すべての主要なビジネス ロジックが単体テストでカバーされていることを確認します。
- 単体テストを使用して、次のような標準を強制します。
- コントローラーのアクションには正しい HTTP メソッド属性が含まれています。
- コントローラーには、承認または明示的な匿名アクセスによる安全なアクションがあります。
- コントローラーは競合を避けるためにルートを定義しています。
- 別の実稼働環境に似た環境でアドオンをテストします。
- キャッシュを使用してデータベース アクセスを最適化します。
- 遅延読み込みとスコープ付き依存関係を実装します。
2024 年 10 月 24 日
#Optimizely #アドオンの作成 #ベスト #プラクティス