いがにんのぼやき

若手WEBエンジニアのブログ。IT、WEB、バンド、アニメ。

C#(.Net Framework)でStackExchange.Redisを扱うときの例外を想定する

単純に使うだけ

まずNugetでStackExchange.Redisをインストール。

using StackExchange.Redis;

Azureの記事だけどこれが参考になる。

docs.microsoft.com

IDatabase cache = ConnectionMultiplexer.Connect("localhost").GetDatabase();
cache.StringSet("key", "value");
string result = cache.StringGet("key");

例外

Redisを使う以上、あらゆる例外に向き合わなければいけない。

C# Redis Samples · GitHub

ここではString型にJSONが入っていることを想定し値をオブジェクトにマッピングするときに、想定しうる例外を各々キャッチする処理を書いてみた。
ここに挙げたコードを元に発生しうる例外を挙げてみる。

接続失敗

単純に接続情報が間違えていた時やRedisが落ちた時。
Redisの操作がある場合、一律でRedisConnectionExceptionをcatchしておかないと例外が発生し落ちる可能性がある。

try
{
    cache = Connection.GetDatabase();
}
catch (RedisConnectionException e)
{
    WriteException("接続失敗", e);
    return;
}

キーが存在しない場合

存在しないキーをStringGetメソッドに投げるとRedisValue型でNullを表すような値が返ってくる。
それをDeserializeObjectに投げると当然例外が発生する。
ここではArgumentExceptionになる。

try
{
    var notExist = JsonConvert.DeserializeObject<EmployeeDto>(cache.StringGet(key));
}
catch (ArgumentException e)
{
    WriteException("キーが存在しない場合", e);
}

String型ではない別の型で保存されていた場合

String型が欲しい時にハッシュ型などの違う型で値が格納されている場合は一律でRedisServerExceptionが発生する。

key = "other-type";
HashEntry[] hashEntries = {
    new HashEntry("a", 1),
};
cache.HashSet(key, hashEntries);
try
{
    var hash = JsonConvert.DeserializeObject<EmployeeDto>(cache.StringGet(key));
}
catch (RedisServerException e)
{
    WriteException("String型じゃない場合", e);
}

パースするJSONマッピング対象のクラスが一致しない場合

下の例だとマッピングしたクラスのプロパティはNullで何もマッピングされない。
なのでしっかりNullチェックをしないとNullReferenceExceptionが発生する。

key = "anonymous";
var objDto = new { T = "a" };
cache.StringSet(key, JsonConvert.SerializeObject(objDto));
var eAnonymous = JsonConvert.DeserializeObject<EmployeeDto>(cache.StringGet(key));
try
{
    // そのままオブジェクトのプロパティにアクセスするようなメソッド
    Write(key, eAnonymous);
}
catch (NullReferenceException e)
{
    WriteException("パースするJSONとマッピング対象のクラスが一致しない場合", e);
}

中身がJSONではなくただの文字列の場合

JSON形式で文字列が保存されていない場合Json.Net側でJsonReaderExceptionが発生する。

key = "string";
var stringDto = "test";
cache.StringSet(key, stringDto);
EmployeeDto str = null;
try
{
    str = JsonConvert.DeserializeObject<EmployeeDto>(cache.StringGet(key));
    Write(key, str);
}
catch (JsonReaderException e)
{
    WriteException("中身がJSONではなくただの文字列の場合", e);
}

中身が配列の場合

中身が配列の文字列を格納したString型の場合Json.NetのJsonSerializationExceptionが発生する。

key = "array";
var badJsonDto = "[1, 2, 3]";
cache.StringSet(key, badJsonDto);
try
{
    var arr = JsonConvert.DeserializeObject<EmployeeDto>(cache.StringGet(key));
    Write(key, arr);
}
catch (JsonSerializationException e)
{
    WriteException("中身が配列の場合", e);
}

まとめ

こんな感じで数多の例外が発生する可能性がある。
防御的に各々全ての例外をcatchしてもいいのだが、格納されている型が違うだとかJSONの形式として正しい形式で値が入っていないとかは値を保存する側のプログラムのバグや仕様把握漏れになると思うので取得側では契約的に例外をcatchしなくてもいいんじゃないかと思っている。
でも間違えられた瞬間システムが落ちるので悩みどころ。

とりあえず最低限としては接続確認としてRedisConnectionExceptionと、キーが存在しないとき用に値のNullチェックあたりをして、そこからはシステムの要件と相談という形になりそう。