ボイラープレートコードが好きな人はいません。通常、一般的なオブジェクト指向プログラミングパターンを使用して削減しますが、パターンを使用した場合のコードオーバーヘッドは、最初に定型コードを使用した場合とほぼ同じです。どういうわけか、特定の動作を実装する必要があるコードの一部をマークし、別の場所で実装を解決するのは本当に素晴らしいことです。
たとえば、StudentRepository
がある場合、次を使用できます。 ダッパー リレーショナルデータベースからすべての学生を取得するには:
public class StudentRepository { public Task GetAllAsync(IDbConnection connection) { return connection.GetAllAsync(); } }
これは、リレーショナルデータベースリポジトリの非常に単純な実装です。学生リストがあまり変更されず、頻繁に呼び出される場合は、それらのアイテムをキャッシュして、システムの応答時間を最適化できます。通常、コードには(リレーショナルであるかどうかに関係なく)多くのリポジトリがあるため、キャッシュに関するこの横断的関心事を脇に置いて、次のように非常に簡単に利用できると便利です。
public class StudentRepository { [Cache] public Task GetAllAsync(IDbConnection connection) { return connection.GetAllAsync(); } }
ボーナスは、データベース接続について心配しないことです。この横断的関心事も脇に置いて、外部接続マネージャーを使用するメソッドに次のようにラベルを付けてください。
次の設計原則のうち、ページを統一できるのはどれですか?
public class StudentRepository { [Cache] [DbConnection] public Task GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync(); } }
この記事では、一般的に使用されるOOPではなく、アスペクト指向パターンの使用法について検討します。 AOPは以前から存在していましたが、開発者は通常、AOPよりもOOPを好みます。手続き型プログラミングとOOPのように、AOPで行うことはすべてOOPでも行うことができますが、AOPを使用すると、開発者は使用できるパラダイムの選択肢が増えます。 AOPコードは異なる方法で編成されており、特定の側面(しゃれを意図したもの)ではOOPよりも適切に議論される場合があります。結局、使用するパラダイムの選択は個人的な好みです。
.NETでは、AOPパターンは中間言語ウィービングを使用して実装できます。 織り 。これは、コードのコンパイル後に開始されるプロセスであり、コンパイラーによって生成されたILコードを変更して、コードが期待される動作を実現するようにします。したがって、すでに説明した例を見ると、このクラスでキャッシュ用のコードを記述していなくても、キャッシュコードを呼び出すために、記述したメソッドが変更(または置換)されます。説明のために、最終結果は次のようになります。
// Weaved by PostSharp public class StudentRepository { [DebuggerTargetMethod(100663306)] [DebuggerBindingMethod(100663329)] [DebuggerBindingMethod(100663335)] public async Task GetAllAsync( IDbConnection connection = null) { AsyncMethodInterceptionArgsImpl interceptionArgsImpl; try { // ISSUE: reference to a compiler-generated field await z__a_1.a2.OnInvokeAsync((MethodInterceptionArgs) interceptionArgsImpl); // ISSUE: reference to a compiler-generated field this.1__state = -2; } finally { } return (IEnumerable) interceptionArgsImpl.TypedReturnValue; } [DebuggerSourceMethod(100663300)] private Task z__OriginalMethod( [Optional] IDbConnection connection) { return (Task ) SqlMapperExtensions.GetAllAsync(connection, (IDbTransaction) null, new int?()); } }
アスペクトと統合テストを含む、この記事のすべてのコードは、 notmarkopadjen/dot-net-aspects-postsharp
GitHubリポジトリ。 IL織りには使用します PostSharp VisualStudioマーケットプレイスから。これは商用ツールであり、商用目的にはライセンスが必要です。実験のために、無料のPostSharpEssentialsライセンスを選択できます。
統合テストを実行する場合は、MySQLとRedisサーバーが必要です。上記のコードでは、MariaDB10.4とRedis5.0を使用してDockerComposeでラボを作成しました。使用するには、インストールする必要があります Docker 構成の作成を起動します。
docker-compose up -d
もちろん、他のサーバーを使用して、appsettings.json
の接続文字列を変更することもできます。
AOPのインターセプトパターンを試してみましょう。 PostSharpでこれを行うには、新しい属性を実装し、 MethodInterceptionAspect
属性を設定し、必要なメソッドをオーバーライドします。
nodejsをいつ使用するか
[PSerializable] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class CacheAttribute : MethodInterceptionAspect { // ... public override void OnInvoke(MethodInterceptionArgs args) { // ... var redisValue = db.StringGet(key); // ... } public override async Task OnInvokeAsync(MethodInterceptionArgs args) { // ... var redisValue = await db.StringGetAsync(key); // ... } }
同期呼び出しと非同期呼び出しには2つの異なる方法があることがわかります。 .NET非同期機能を最大限に活用するには、これらを適切に実装することが重要です。を使用してRedisから読み取る場合 StackExchange.Redis
ライブラリでは、StringGet
を使用しますまたはStringGetAsync
同期中か非同期コードブランチかによって、メソッド呼び出し。
コード実行フローは、次のメソッドの呼び出しによって影響を受けます。 MethodInterceptionArgs
、args
オブジェクト、およびオブジェクトのプロパティに値を設定します。最も重要なメンバー:
Proceed
(ProceedAsync
)メソッド-元のメソッドの実行を呼び出します。ReturnValue
property-メソッド呼び出しの戻り値が含まれます。元のメソッドを実行する前は空で、後は元の戻り値が含まれています。いつでも交換できます。Method
プロパティ-System.Reflection.MethodBase
(通常はSystem.Reflection.MethodInfo
)には、ターゲットメソッドのリフレクション情報が含まれます。Instance
プロパティ-ターゲットオブジェクト(メソッドの親インスタンス)。Arguments
プロパティ-引数値が含まれます。いつでも交換できます。IDbConnection
のインスタンスなしでリポジトリメソッドを呼び出し、アスペクトにそれらの接続を作成させ、メソッド呼び出しに提供できるようにする必要があります。とにかく接続を提供したい場合があり(トランザクションなどのため)、その場合、アスペクトは何もしません。
以下の実装では、他のデータベースエンティティリポジトリと同様に、データベース接続管理専用のコードを使用します。この特定のケースでは、MySqlConnection
のインスタンスメソッドの実行に解析され、メソッドの実行が完了した後に破棄されます。
using Microsoft.Extensions.Configuration; using MySql.Data.MySqlClient; using PostSharp.Aspects; using PostSharp.Aspects.Dependencies; using PostSharp.Serialization; using System; using System.Data; using System.Threading.Tasks; namespace Paden.Aspects.Storage.MySQL { [PSerializable] [ProvideAspectRole(StandardRoles.TransactionHandling)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.After, StandardRoles.Caching)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class DbConnectionAttribute : MethodInterceptionAspect { const string DefaultConnectionStringName = 'DefaultConnection'; static Lazy config; static string connectionString; public static string ConnectionString { get { return connectionString ?? config.Value.GetConnectionString(DefaultConnectionStringName); } set { connectionString = value; } } static DbConnectionAttribute() { config = new Lazy(() => new ConfigurationBuilder().AddJsonFile('appsettings.json', false, false).Build()); } public override void OnInvoke(MethodInterceptionArgs args) { var i = GetArgumentIndex(args); if (!i.HasValue) { args.Proceed(); return; } using (IDbConnection db = new MySqlConnection(ConnectionString)) { args.Arguments.SetArgument(i.Value, db); args.Proceed(); } } public override async Task OnInvokeAsync(MethodInterceptionArgs args) { var i = GetArgumentIndex(args); if (!i.HasValue) { await args.ProceedAsync(); return; } using (IDbConnection db = new MySqlConnection(ConnectionString)) { args.Arguments.SetArgument(i.Value, db); await args.ProceedAsync(); } } private int? GetArgumentIndex(MethodInterceptionArgs args) { var parameters = args.Method.GetParameters(); for (int i = 0; i ここで重要なのは、アスペクトの実行順序を指定することです。ここでは、アスペクトロールを割り当て、ロールの実行を順序付けています。欲しくないIDbConnection
とにかく使用されない場合に作成されます(たとえば、キャッシュから読み取られた値)。これは、次の属性によって定義されます。
[ProvideAspectRole(StandardRoles.TransactionHandling)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.After, StandardRoles.Caching)]
PostSharpは、クラスレベルとアセンブリレベルですべての側面を実装することもできるため、属性スコープを定義することが重要です。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
接続文字列はappsettings.json
から読み取られていますが、静的プロパティConnectionString
を使用してオーバーライドできます。
実行フローは次のとおりです。
- アスペクトは
IDbConnection
を識別します値が指定されていないオプションの引数インデックス。見つからない場合はスキップします。 - MySqlConnectionは、提供された
ConnectionString
に基づいて作成されます。 IDbConnection
引数値が設定されます。 - 元のメソッドが呼び出されます。
したがって、この側面を使用する場合は、接続を提供せずにリポジトリメソッドを呼び出すことができます。
await studentRepository.InsertAsync(new Student { Name = 'Not Marko Padjen' }, connection: null);
アスペクトキャッシュ
ここでは、一意のメソッド呼び出しを識別してキャッシュします。同じクラスの同じメソッドが同じパラメーターで呼び出された場合、メソッド呼び出しは一意であると見なされます。
以下の実装では、各メソッドで、呼び出しのインターセプトキーが作成されます。次に、これを使用して、戻り値がキャッシュサーバーに存在するかどうかを確認します。含まれている場合は、元のメソッドを呼び出さずに返されます。そうでない場合は、元のメソッドが呼び出され、戻り値がキャッシュサーバーに保存されてさらに使用されます。
デザインの原則と要素
using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using PostSharp.Aspects; using PostSharp.Aspects.Dependencies; using PostSharp.Serialization; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Paden.Aspects.Caching.Redis { [PSerializable] [ProvideAspectRole(StandardRoles.Caching)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.Before, StandardRoles.TransactionHandling)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class CacheAttribute : MethodInterceptionAspect { const int DefaultExpirySeconds = 5 * 60; static Lazy redisServer; public int ExpirySeconds = DefaultExpirySeconds; private TimeSpan? Expiry => ExpirySeconds == -1 ? (TimeSpan?)null : TimeSpan.FromSeconds(ExpirySeconds); static CacheAttribute() { redisServer = new Lazy(() => new ConfigurationBuilder().AddJsonFile('appsettings.json', false, false).Build()['Redis:Server']); } public override void OnInvoke(MethodInterceptionArgs args) { if (args.Instance is ICacheAware cacheAware && !cacheAware.CacheEnabled) { args.Proceed(); return; } var key = GetKey(args.Method as MethodInfo, args.Arguments); using (var connection = ConnectionMultiplexer.Connect(redisServer.Value)) { var db = connection.GetDatabase(); var redisValue = db.StringGet(key); if (redisValue.IsNullOrEmpty) { args.Proceed(); db.StringSet(key, JsonConvert.SerializeObject(args.ReturnValue), Expiry); } else { args.ReturnValue = JsonConvert.DeserializeObject(redisValue.ToString(), (args.Method as MethodInfo).ReturnType); } } } public override async Task OnInvokeAsync(MethodInterceptionArgs args) { if (args.Instance is ICacheAware cacheAware && !cacheAware.CacheEnabled) { await args.ProceedAsync(); return; } var key = GetKey(args.Method as MethodInfo, args.Arguments); using (var connection = ConnectionMultiplexer.Connect(redisServer.Value)) { var db = connection.GetDatabase(); var redisValue = await db.StringGetAsync(key); if (redisValue.IsNullOrEmpty) { await args.ProceedAsync(); db.StringSet(key, JsonConvert.SerializeObject(args.ReturnValue), Expiry); } else { args.ReturnValue = JsonConvert.DeserializeObject(redisValue.ToString(), (args.Method as MethodInfo).ReturnType.GenericTypeArguments[0]); } } } private string GetKey(MethodInfo method, IList values) { var parameters = method.GetParameters(); var keyBuilder = GetKeyBuilder(method); keyBuilder.Append('('); foreach (var parameter in parameters) { AppendParameterValue(keyBuilder, parameter, values[parameter.Position]); } if (parameters.Any()) { keyBuilder.Remove(keyBuilder.Length - 2, 2); } keyBuilder.Append(')'); return keyBuilder.ToString(); } public static void InvalidateCache(Expression expression) { var methodCallExpression = expression.Body as MethodCallExpression; var keyBuilder = GetKeyBuilder(methodCallExpression.Method); var parameters = methodCallExpression.Method.GetParameters(); var anyMethod = typeof(CacheExtensions).GetMethod(nameof(CacheExtensions.Any)); keyBuilder.Append('('); for (int i = 0; i ここでは、アスペクトの順序も尊重します。アスペクトの役割はCaching
であり、TransactionHandling
の後に続くように定義されています。
[ProvideAspectRole(StandardRoles.Caching)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.Before, StandardRoles.TransactionHandling)]
属性スコープは、DbConnectionアスペクトの場合と同じです。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
キャッシュされたアイテムの有効期限は、パブリックフィールドExpirySeconds
を定義することで各メソッドに設定できます。 (デフォルトは5分です)例:
[Cache(ExpirySeconds = 2 * 60 /* 2 minutes */)] [DbConnection] public Task GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync(); }
実行フローは次のとおりです。
- インスタンスが
ICacheAware
であるかどうかのアスペクトチェックこれは、この特定のオブジェクトインスタンスでのキャッシュの使用をスキップするフラグを提供できます。 - アスペクトは、メソッド呼び出しのキーを生成します。
- アスペクトはRedis接続を開きます。
- 生成されたキーに値が存在する場合、値が返され、元のメソッドの実行はスキップされます。
- 値が存在しない場合は、元のメソッドが呼び出され、生成されたキーとともに戻り値がキャッシュに保存されます。
鍵の生成については、ここでいくつかの制限が適用されます。
IDbConnection
パラメータは常に無視され、nullかどうかは関係ありません。これは、前の側面の使用に対応するために意図的に行われます。 - 文字列値としての特別な値は、likeandvaluesなどのキャッシュからの誤った読み取りを引き起こす可能性があります。これは、値のエンコードで回避できます。
- 参照型は考慮されず、その型のみが考慮されます(
.ToString()
は値の評価に使用されます)。ほとんどの場合、これは問題なく、複雑さを増すことはありません。
キャッシュを適切に使用するために、エンティティの更新やエンティティの削除など、有効期限が切れる前にキャッシュを無効にする必要がある場合があります。
public class StudentRepository : ICacheAware { // ... [Cache] [DbConnection] public Task GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync(); } [Cache] [DbConnection] public Task GetAsync(int id, IDbConnection connection = null) { return connection.GetAsync(id); } [DbConnection] public async Task InsertAsync(Student student, IDbConnection connection = null) { var result = await connection.InsertAsync(student); this.InvalidateCache(r => r.GetAllAsync(Any())); return result; } [DbConnection] public async Task UpdateAsync(Student student, IDbConnection connection = null) { var result = await connection.UpdateAsync(student); this.InvalidateCache(r => r.GetAllAsync(Any())); this.InvalidateCache(r => r.GetAsync(student.Id, Any())); return result; } [DbConnection] public async Task DeleteAsync(Student student, IDbConnection connection = null) { var result = await connection.DeleteAsync(student); this.InvalidateCache(r => r.GetAllAsync(Any())); this.InvalidateCache(r => r.GetAsync(student.Id, Any())); return result; } }
InvalidateCache
ヘルパーメソッドは式を受け入れるため、ワイルドカードを使用できます( Moq フレームワーク):
this.InvalidateCache(r => r.GetAsync(student.Id, Any()));
この側面は特別なパラメーターなしで使用されているため、開発者はコードの制限にのみ注意する必要があります。
jwtが拒否されました:ユーザー認証に失敗しました
すべてを一緒に入れて
最良の方法は、それを試してデバッグすることです。プロジェクトで提供されている統合テストを使用することですPaden.Aspects.DAL.Tests
。
次の統合テスト方法では、実サーバー(リレーショナルデータベースとキャッシュ)を使用します。接続ファサードは、メソッド呼び出しを追跡するためにのみ使用されます。
[Fact] public async Task Get_Should_Call_Database_If_Entity_Not_Dirty_Otherwise_Read_From_Cache() { var student = new Student { Id = studentId, Name = 'Not Marko Padjen' }; var studentUpdated = new Student { Id = studentId, Name = 'Not Marko Padjen UPDATED' }; await systemUnderTest.InsertAsync(student); // Gets entity by id, should save in cache Assert.Equal(student.Name, (await systemUnderTest.GetAsync(studentId)).Name); // Updates entity by id, should invalidate cache await systemUnderTest.UpdateAsync(studentUpdated); var connectionMock = fixture.GetConnectionFacade(); // Gets entity by id, ensures that it is the expected one Assert.Equal(studentUpdated.Name, (await systemUnderTest.GetAsync(studentId, connectionMock)).Name); // Ensures that database was used for the call Mock.Get(connectionMock).Verify(m => m.CreateCommand(), Times.Once); var connectionMockUnused = fixture.GetConnectionFacade(); // Calls again, should read from cache Assert.Equal(studentUpdated.Name, (await systemUnderTest.GetAsync(studentId, connectionMockUnused)).Name); // Ensures that database was not used Mock.Get(connectionMockUnused).Verify(m => m.CreateCommand(), Times.Never); }
データベースは、クラスフィクスチャを使用して自動的に作成および破棄されます。
using Microsoft.Extensions.Configuration; using Moq; using MySql.Data.MySqlClient; using Paden.Aspects.DAL.Entities; using Paden.Aspects.Storage.MySQL; using System; using System.Data; namespace Paden.Aspects.DAL.Tests { public class DatabaseFixture : IDisposable { public MySqlConnection Connection { get; private set; } public readonly string DatabaseName = $'integration_test_{Guid.NewGuid():N}'; public DatabaseFixture() { var config = new ConfigurationBuilder().AddJsonFile('appsettings.json', false, false).Build(); var connectionString = config.GetConnectionString('DefaultConnection'); Connection = new MySqlConnection(connectionString); Connection.Open(); new MySqlCommand($'CREATE DATABASE `{DatabaseName}`;', Connection).ExecuteNonQuery(); Connection.ChangeDatabase(DatabaseName); DbConnectionAttribute.ConnectionString = $'{connectionString};Database={DatabaseName}'; } public void RecreateTables() { new MySqlCommand(Student.ReCreateStatement, Connection).ExecuteNonQuery(); } public IDbConnection GetConnectionFacade() { var connectionMock = Mock.Of(); Mock.Get(connectionMock).Setup(m => m.CreateCommand()).Returns(Connection.CreateCommand()).Verifiable(); Mock.Get(connectionMock).SetupGet(m => m.State).Returns(ConnectionState.Open).Verifiable(); return connectionMock; } public void Dispose() { try { new MySqlCommand($'DROP DATABASE IF EXISTS `{DatabaseName}`;', Connection).ExecuteNonQuery(); } catch (Exception) { // ignored } Connection.Close(); } } }
テストの実行後にデータベースが削除され、キャッシュが手動で無効化されるため、デバッグ中に手動チェックを実行できます。
たとえば、Get_Should_Call_Database_If_Entity_Not_Dirty_Otherwise_Read_From_Cache
の実行中テストでは、Redisデータベースで次の値を見つけることができます。
127.0.0.1:6379> KEYS * 1) 'System.Threading.Tasks.Task`1[[Paden.Aspects.DAL.Entities.Student, Paden.Aspects.DAL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]] {Paden.Aspects.DAL.StudentRepository, Paden.Aspects.DAL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null}.Paden.Aspects.DAL.StudentRepository.GetAsync(System.Int32 1, System.Data.IDbConnection )' 127.0.0.1:6379> GET 'System.Threading.Tasks.Task`1[[Paden.Aspects.DAL.Entities.Student, Paden.Aspects.DAL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]] {Paden.Aspects.DAL.StudentRepository, Paden.Aspects.DAL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null}.Paden.Aspects.DAL.StudentRepository.GetAsync(System.Int32 1, System.Data.IDbConnection )' '{'Id':1,'Name':'Not Marko Padjen'}'
統合テストGetAllAsync_Should_Not_Call_Database_On_Second_Call
また、キャッシュされた呼び出しが元のデータソース呼び出しよりもパフォーマンスが高いことを確認しています。また、各呼び出しの実行にかかった時間を示すトレースも生成されます。
Database run time (ms): 73 Cache run time (ms): 9
本番環境で使用する前の改善
ここで提供されるコードは、教育目的で作成されています。実際のシステムで使用する前に、いくつかの改善が行われる場合があります。
- DbConnectionアスペクト:
- 必要に応じて、接続プールを実装できます。
- 複数の接続文字列を実装できます。これの一般的な使用法は、読み取り専用接続タイプと読み取り/書き込み接続タイプを区別するリレーショナルデータベースクラスターです。
- キャッシュの側面:
- 必要に応じて、接続プールを実装できます。
- ユースケースによっては、参照型の値も生成されたキーの一部と見なすことができます。ほとんどの場合、それらはおそらくパフォーマンスの欠点を提供するだけです。
これらの機能は、使用されているシステムの特定の要件に関連しているため、ここでは実装されていません。適切に実装されていないと、システムのパフォーマンスに影響しません。
結論
からの「単一責任」、「オープンクローズ」、および「依存性逆転」と主張する人もいるかもしれません。 SOLIDの原則 原則は、OOPよりもAOPでより適切に実装される可能性があります。事実は、 .NET開発者 特定の状況に適用できる多くのツール、フレームワーク、およびパターンを使用して実現できる、優れたコード編成である必要があります。
繰り返しになりますが、アスペクトと統合テストを含む、この記事のすべてのコードは、 notmarkopadjen/dot-net-aspects-postsharp
GitHubリポジトリ 。 IL織りには、 PostSharp VisualStudioマーケットプレイスから。コードには、MariaDB10.4とRedis5.0を使用してdockercomposeで作成されたラボが含まれています。
基本を理解する
AOPは何に適していますか?
アスペクト指向プログラミング(AOP)は、横断的関心事の分離を可能にするのに非常に優れています。
ゼロからangularjsアプリを構築する
IL織りとは何ですか?
ILウィービングは、ソースコードがコンパイルされた後にILコードを変更するプロセスです。
AOPを使用する必要がありますか?
コーディングスタイル、最終結果、および得られるメリットが気に入った場合は、そうです。
AOPはOOPよりも優れていますか?
この質問への答えは、関数型プログラミングがオブジェクト指向プログラミングよりも優れていると言われるのと同じように、プロジェクト、コードの再利用性に関する要件、および個人のチームメンバーの好みに大きく関係しています。