.NETでのキャッシングと接続処理:アスペクト指向プログラミングチュートリアル

公開: 2022-03-11

ボイラープレートコードが好きな人はいません。 通常、一般的なオブジェクト指向プログラミングパターンを使用して削減しますが、パターンを使用する場合のコードオーバーヘッドは、最初にボイラープレートコードを使用した場合とほぼ同じです(大きくはないにしても)。 どういうわけか、特定の動作を実装する必要があるコードの一部をマークし、別の場所で実装を解決するのは本当に素晴らしいことです。

たとえば、 StudentRepositoryがある場合、Dapperを使用して、リレーショナルデータベースからすべての学生を取得できます。

 public class StudentRepository { public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection) { return connection.GetAllAsync<Student>(); } }

これは、リレーショナルデータベースリポジトリの非常に単純な実装です。 学生リストがあまり変更されず、頻繁に呼び出される場合は、それらのアイテムをキャッシュして、システムの応答時間を最適化できます。 通常、コードには(リレーショナルであるかどうかに関係なく)多くのリポジトリがあるため、キャッシュに関するこの横断的関心事を脇に置いて、次のように非常に簡単に利用できると便利です。

 public class StudentRepository { [Cache] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection) { return connection.GetAllAsync<Student>(); } }

ボーナスは、データベース接続について心配しないことです。 この横断的関心事も脇に置いて、外部接続マネージャーを使用するメソッドに次のようにラベルを付けてください。

 public class StudentRepository { [Cache] [DbConnection] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync<Student>(); } }

この記事では、一般的に使用されるOOPではなく、アスペクト指向パターンの使用法について検討します。 AOPは以前から存在していましたが、開発者は通常、AOPよりもOOPを好みます。 手続き型プログラミングとOOPのように、AOPで行うことはすべてOOPでも行うことができますが、AOPを使用すると、開発者は使用できるパラダイムでより多くの選択肢を得ることができます。 AOPコードは異なる方法で編成されており、特定の側面(しゃれを意図したもの)では、OOPよりも適切に議論される場合があります。 結局、使用するパラダイムの選択は個人的な好みです。

どうすればいいですか

.NETでは、AOPパターンは、 ILウィービングとしてよく知られている中間言語ウィービングを使用して実装できます。 これは、コードのコンパイル後に開始されるプロセスであり、コンパイラーによって生成されたILコードを変更して、コードが期待される動作を実現するようにします。 したがって、すでに述べた例を見ると、このクラスでキャッシュ用のコードを記述していなくても、キャッシュコードを呼び出すために、記述したメソッドが変更(または置換)されます。 説明のために、最終結果は次のようになります。

 // Weaved by PostSharp public class StudentRepository { [DebuggerTargetMethod(100663306)] [DebuggerBindingMethod(100663329)] [DebuggerBindingMethod(100663335)] public async Task<IEnumerable<Student>> GetAllAsync( IDbConnection connection = null) { AsyncMethodInterceptionArgsImpl<IEnumerable<Student>> 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<Student>) interceptionArgsImpl.TypedReturnValue; } [DebuggerSourceMethod(100663300)] private Task<IEnumerable<Student>> <GetAllAsync>z__OriginalMethod( [Optional] IDbConnection connection) { return (Task<IEnumerable<Student>>) SqlMapperExtensions.GetAllAsync<Student>(connection, (IDbTransaction) null, new int?()); } }

必要な工具

アスペクトと統合テストを含むこの記事のすべてのコードは、 notmarkopadjen/dot-net-aspects-postsharpリポジトリにあります。 ILウィービングには、VisualStudioマーケットプレイスのPostSharpを使用します。 これは商用ツールであり、商用目的にはライセンスが必要です。 実験のために、無料のPostSharpEssentialsライセンスを選択できます。

統合テストを実行する場合は、MySQLとRedisサーバーが必要です。 上記のコードでは、MariaDB10.4とRedis5.0を使用してDockerComposeでラボを作成しました。 これを使用するには、Dockerをインストールして、構成の作成を起動する必要があります。

 docker-compose up -d

もちろん、他のサーバーを使用して、 appsettings.jsonの接続文字列を変更することもできます。

基本的なアスペクト指向コーディング

AOPのインターセプトパターンを試してみましょう。 PostSharpでこれを行うには、新しい属性を実装し、 MethodInterceptionAspect属性を継承し、必要なメソッドをオーバーライドする必要があります。

 [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非同期機能を最大限に活用するには、これらを適切に実装することが重要です。 StackExchange.Redisライブラリを使用してRedisから読み取る場合、同期または非同期のコードブランチにあるかどうかに応じて、 StringGetまたはStringGetAsyncメソッド呼び出しを使用します。

コード実行フローは、 MethodInterceptionArgsargsオブジェクトのメソッドの呼び出し、およびオブジェクトのプロパティへの値の設定の影響を受けます。 最も重要なメンバー:

  • ProceedProceedAsync )メソッド-元のメソッドの実行を呼び出します。
  • ReturnValueプロパティ-メソッド呼び出しの戻り値が含まれます。 元のメソッドを実行する前は空で、後は元の戻り値が含まれています。 いつでも交換できます。
  • MethodプロパティSystem.Reflection.MethodBase (通常はSystem.Reflection.MethodInfo )には、ターゲットメソッドのリフレクション情報が含まれています。
  • Instanceプロパティ-ターゲットオブジェクト(メソッドの親インスタンス)。
  • Argumentsプロパティ-引数値が含まれます。 いつでも交換できます。

DbConnectionアスペクト

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<IConfigurationRoot> config; static string connectionString; public static string ConnectionString { get { return connectionString ?? config.Value.GetConnectionString(DefaultConnectionStringName); } set { connectionString = value; } } static DbConnectionAttribute() { config = new Lazy<IConfigurationRoot>(() => 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 < parameters.Length; i++) { var parameter = parameters[i]; if (parameter.ParameterType == typeof(IDbConnection) && parameter.IsOptional && args.Arguments[i] == null) { return i; } } return null; } } }

ここで重要なのは、アスペクトの実行順序を指定することです。 ここでは、アスペクトロールを割り当て、ロールの実行を順序付けています。 とにかく使用されない場合(たとえば、キャッシュから読み取られた値)、 IDbConnectionが作成されることは望ましくありません。 これは、次の属性によって定義されます。

 [ProvideAspectRole(StandardRoles.TransactionHandling)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.After, StandardRoles.Caching)]

PostSharpは、クラスレベルおよびアセンブリレベルですべての側面を実装することもできるため、属性スコープを定義することが重要です。

 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]

接続文字列はappsettings.jsonから読み取られていますが、静的プロパティConnectionStringを使用してオーバーライドできます。

実行フローは次のとおりです。

  1. アスペクトは、値が指定されていないIDbConnectionオプションの引数インデックスを識別します。 見つからない場合はスキップします。
  2. MySqlConnectionは、提供されたConnectionStringに基づいて作成されます。
  3. IDbConnection引数値が設定されます。
  4. 元のメソッドが呼び出されます。

したがって、この側面を使用する場合は、接続が提供されていない状態でリポジトリメソッドを呼び出すことができます。

 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<string> redisServer; public int ExpirySeconds = DefaultExpirySeconds; private TimeSpan? Expiry => ExpirySeconds == -1 ? (TimeSpan?)null : TimeSpan.FromSeconds(ExpirySeconds); static CacheAttribute() { redisServer = new Lazy<string>(() => 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<object> 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<T, TResult>(Expression<Func<T, TResult>> 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 < parameters.Length; i++) { var parameter = parameters[i]; var argument = methodCallExpression.Arguments[i]; object value = null; if (argument is ConstantExpression constantArgument) { value = constantArgument.Value; } else if (argument is MemberExpression memberArgument) { value = Expression.Lambda(memberArgument).Compile().DynamicInvoke(); } else if (argument is MethodCallExpression methodCallArgument) { if (methodCallArgument.Method == anyMethod.MakeGenericMethod(methodCallArgument.Method.GetGenericArguments())) { value = "*"; } } AppendParameterValue(keyBuilder, parameter, value); } if (methodCallExpression.Arguments.Any()) { keyBuilder.Remove(keyBuilder.Length - 2, 2); } keyBuilder.Append(")"); using (var connection = ConnectionMultiplexer.Connect(redisServer.Value)) { connection.GetDatabase().ScriptEvaluate(@" local keys = redis.call('keys', ARGV[1]) for i=1, #keys, 5000 do redis.call('del', unpack(keys, i, math.min(i + 4999, #keys))) end", values: new RedisValue[] { CacheExtensions.EscapeRedisString(keyBuilder.ToString()) }); } } private static StringBuilder GetKeyBuilder(MethodInfo method) { var keyBuilder = new StringBuilder(); keyBuilder.Append(method.ReturnType.FullName); keyBuilder.Append(" {"); keyBuilder.Append(method.ReflectedType.AssemblyQualifiedName); keyBuilder.Append("}."); keyBuilder.Append(method.ReflectedType.FullName); keyBuilder.Append("."); keyBuilder.Append(method.Name); return keyBuilder; } private static void AppendParameterValue(StringBuilder keyBuilder, ParameterInfo parameter, object value) { keyBuilder.Append(parameter.ParameterType.FullName); keyBuilder.Append(" "); if (parameter.ParameterType == typeof(IDbConnection)) { keyBuilder.Append("<IGNORED>"); } else { keyBuilder.Append(value == null ? "<NULL>" : value.ToString()); } keyBuilder.Append(", "); } } }

ここでは、アスペクトの順序も尊重します。 アスペクトの役割は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<IEnumerable<Student>> GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync<Student>(); }

実行フローは次のとおりです。

  1. インスタンスがICacheAwareであるかどうかをアスペクトチェックします。これは、この特定のオブジェクトインスタンスでキャッシュの使用をスキップするフラグを提供できます。
  2. アスペクトは、メソッド呼び出しのキーを生成します。
  3. アスペクトはRedis接続を開きます。
  4. 生成されたキーに値が存在する場合、値が返され、元のメソッドの実行はスキップされます。
  5. 値が存在しない場合は、元のメソッドが呼び出され、生成されたキーとともに戻り値がキャッシュに保存されます。

キーの生成には、ここでいくつかの制限が適用されます。

  1. パラメータとしてのIDbConnectionは、nullであるかどうかに関係なく、常に無視されます。 これは、前の側面の使用に対応するために意図的に行われます。
  2. 文字列値としての特別な値は、 <IGNORED><NULL>値など、キャッシュからの誤った読み取りを引き起こす可能性があります。 これは、値のエンコードで回避できます。
  3. 参照型は考慮されず、その型のみが考慮されます( .ToString()は値の評価に使用されます)。 ほとんどの場合、これは問題なく、複雑さを増すことはありません。

キャッシュを適切に使用するには、エンティティの更新やエンティティの削除など、有効期限が切れる前にキャッシュを無効にする必要がある場合があります。

 public class StudentRepository : ICacheAware { // ... [Cache] [DbConnection] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync<Student>(); } [Cache] [DbConnection] public Task<Student> GetAsync(int id, IDbConnection connection = null) { return connection.GetAsync<Student>(id); } [DbConnection] public async Task<int> InsertAsync(Student student, IDbConnection connection = null) { var result = await connection.InsertAsync(student); this.InvalidateCache(r => r.GetAllAsync(Any<IDbConnection>())); return result; } [DbConnection] public async Task<bool> UpdateAsync(Student student, IDbConnection connection = null) { var result = await connection.UpdateAsync(student); this.InvalidateCache(r => r.GetAllAsync(Any<IDbConnection>())); this.InvalidateCache(r => r.GetAsync(student.Id, Any<IDbConnection>())); return result; } [DbConnection] public async Task<bool> DeleteAsync(Student student, IDbConnection connection = null) { var result = await connection.DeleteAsync(student); this.InvalidateCache(r => r.GetAllAsync(Any<IDbConnection>())); this.InvalidateCache(r => r.GetAsync(student.Id, Any<IDbConnection>())); return result; } }

InvalidateCacheヘルパーメソッドは式を受け入れるため、ワイルドカードを使用できます(Moqフレームワークと同様)。

 this.InvalidateCache(r => r.GetAsync(student.Id, Any<IDbConnection>()));

この側面は特別なパラメーターなしで使用されているため、開発者はコードの制限にのみ注意する必要があります。

すべてを一緒に入れて

最良の方法は、それを試してデバッグすることです。プロジェクト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<IDbConnection>(); 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 <IGNORED>)" 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 <IGNORED>)" "{\"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リポジトリにあります。 ILウィービングには、VisualStudioマーケットプレイスのPostSharpを使用しました。 コードには、MariaDB10.4とRedis5.0を使用してdockercomposeで作成されたラボが含まれています。