如何使用C#中的Lambda表达式操作Redis Hash结构,简化缓存中对象属性的读写操作
Redis是一个开源的、高性能的、基于内存的键值数据库,它支持多种数据结构,如字符串、列表、集合、散列、有序集合等。其中,Redis的散列(Hash)结构是一个常用的结构,今天跟大家分享一个我的日常操作,如何使用Redis的散列(Hash)结构来缓存和查询对象的属性值,以及如何用Lambda表达式树来简化这个过程。
一、什么是Redis Hash结构
Redis Hash结构是一种键值对的集合,它可以存储一个对象的多个字段和值。例如,我们可以用一个Hash结构来存储一个人的信息,如下所示:
HSET person:1 id 1
HSET person:1 name Alice
HSET person:1 age 20
上面的命令将一个人的信息存储到了一个名为person:1的Hash结构中,其中每个字段都有一个名称和一个值。我们可以使用HGET命令来获取某个字段的值,例如:
HGET person:1 name#Alice
我们也可以使用HGETALL命令来获取所有字段的值,例如:
HGETALL person:1id 1name Aliceage 20
二、如何使用C#来操作Redis Hash结构
为了在C#中操作Redis Hash结构,我们需要使用一个第三方库:StackExchange.Redis。这个库提供了一个ConnectionMultiplexer类,用于创建和管理与Redis服务器的连接,以及一个IDatabase接口,用于执行各种命令。例如,我们可以使用以下代码来创建一个连接对象和一个数据库对象:
// 连接Redis服务器
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost");
// 获取数据库对象IDatabase db = redis.GetDatabase();
然后,我们可以使用db对象的HashSet方法和HashGet方法来存储和获取Hash结构中的字段值。
// 创建一个HashEntry数组,存放要缓存的对象属性
HashEntry[] hashfield = new HashEntry[3];
hashfield[0] = new HashEntry("id", "1");
hashfield[1] = new HashEntry("name", "Alice");
hashfield[2] = new HashEntry("age", "20"); // 使用HashSet方法将对象属性缓存到Redis的散列(Hash)结构中
db.HashSet("person:1", hashfield);
// 使用HashGetAll方法从Redis的散列(Hash)结构中查询对象属性
HashEntry[] result = db.HashGetAll("person:1");
// 遍历结果数组,打印对象属性
foreach (var item in result)
{
Console.WriteLine(item.Name + ": " + item.Value);
}
但是,这种方式有一些缺点:
- 首先,我们需要手动将对象的属性名和值转换为HashEntry数组,并且保持一致性。
- 其次,我们需要使用字符串来指定要存储或获取的字段名,并且还要避免拼写错误或重复。
- 最后,我们需要手动将返回的RedisValue类型转换为我们需要的类型。
有没有更优雅的方法来解决这个问题呢?答案是肯定的。
三、如何用Lambda表达式轻松操作Redis Hash结构
Lambda表达式是一种匿名函数,可以用来表示委托或表达式树。在.NET中,我们可以使用Lambda表达式来操作实体类的属性,比如获取属性的值或者更新属性的值。
我们可以利用 Lambda表达式来指定要存储或获取的对象的属性,而不是使用字符串。使用表达式树来遍历Lambda表达式,提取出属性名和属性值,并转换为HashEntry数组或RedisValue数组,使其更易于使用。例如:
Get<Person>(p => new { p.Name, p.Age });
如果我们只想选择一个属性,就可以直接写:
Get<Person>(p => p.Name)
如果要更新对象指定的属性,可以这样写了:
Update<Person>(p => p
.SetProperty(x => x.Name, "Alice")
.SetProperty(x => x.Age, 25));
怎么样,这样是不是优雅多了,这样做有以下好处:
- 代码更加可读和可维护,因为我们可以直接使用对象的属性,而不是使用字符串。
- 代码更加稳定和精确,因为我们可以避免拼写错误或重复,并且可以利用编译器的类型检查和提示。
那么,我们如何实现上面的方法呢?
1、Get方法
这个方法的目的是从缓存中获取对象的一个或多个属性值,使用一个泛型方法和一个Lambda表达式来实现。
private static TResult Get<T, TResult>(IDatabase db, int id, Expression<Func<T, TResult>> selector)
{
if (selector == null)
throw new ArgumentNullException(nameof(selector)); // 使用扩展方法获取要查询的属性名数组
var hashFields = selector.GetMemberNames().Select(m => new RedisValue(m)).ToArray();
// 从缓存中获取对应的属性值数组
var values = db.HashGet($"person:{id}", hashFields);
// 使用扩展方法将HashEntry数组转换为对象
var obj = values.ToObject<T>(hashFields);
// 返回查询结果
return selector.Compile()(obj);
} private static TResult Get<TResult>(IDatabase db, int id, Expression<Func<Person, TResult>> selector)
=> Get<Person, TResult>(db, id, selector);
- 首先,定义一个泛型方法Get<T, TResult>,它接受一个数据库对象db,一个对象id,和一个Lambda表达式selector作为参数。这个Lambda表达式的类型是Expression<Func<T, TResult>>,表示它接受一个T类型的对象,并返回一个TResult类型的结果。这个Lambda表达式的作用是指定要查询的属性。
- 然后,在Get<T, TResult>方法中,首先判断selector是否为空,如果为空,则抛出异常。然后,使用扩展方法GetMemberNames来获取selector中的属性名数组,并转换为RedisValue数组hashFields。这个扩展方法使用了ExpressionVisitor类来遍历表达式树,并重写了VisitMember方法来获取属性名。接下来,使用db.HashGet方法从缓存中获取对应的属性值数组values,使用id作为键。然后,使用扩展方法ToObject来将values数组转换为T类型的对象obj。这个扩展方法使用了反射来获取T类型的属性,并设置对应的属性值和类型转换。最后,返回selector编译后并传入obj作为参数的结果。
- 接下来,定义一个私有方法Get<TResult>,它接受一个数据库对象db,一个对象id,和一个Lambda表达式selector作为参数。这个Lambda表达式的类型是Expression<Func<Person, TResult>>,表示它接受一个Person类型的对象,并返回一个TResult类型的结果。这个Lambda表达式的作用是指定要查询的Person对象的属性。
- 然后,在Get<TResult>方法中,直接调用Get<T, TResult>方法,并传入db,id,selector作为参数,并指定T类型为Person。这样,就可以得到一个TResult类型的结果。
2、MemberExpressionVisitor扩展类
这个类的作用是遍历一个表达式树,收集其中的成员表达式的名称,并存储到一个列表中。
public class MemberExpressionVisitor : ExpressionVisitor
{
private readonly IList<string> _names; public MemberExpressionVisitor(IList<string> list)
{
_names = list;
} protected override Expression VisitMember(MemberExpression node)
{
var name = node.Member.Name;
if (node.Expression is MemberExpression member)
{
Visit(member);
name = member.Member.Name + "." + name;
}
_names.Add(name); return base.VisitMember(node);
}
}
- 首先,定义一个类MemberExpressionVisitor,它继承自ExpressionVisitor类。这个类有一个私有字段_names,用于存储属性名。它还有一个构造函数,接受一个IList<string>类型的参数list,并将其赋值给_names字段。
- 然后,在MemberExpressionVisitor类中,重写了VisitMember方法,这个方法接受一个MemberExpression类型的参数node。这个方法的作用是访问表达式树中的成员表达式节点,并获取其属性名。
- 接下来,在VisitMember方法中,首先获取node节点的属性名,并赋值给name变量。然后判断node节点的表达式是否是另一个成员表达式,如果是,则递归地访问该表达式,并将其属性名和name变量用"."连接起来,形成一个属性路径。然后将name变量添加到_names集合中。最后返回基类的VisitMember方法的结果。
3、Update方法
这个方法目的是将一个对象指定的属性名和值更新到缓存中,使用一个泛型方法和一个委托函数来实现。
public static Dictionary<string, object> Update<TSource>(Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>> setPropertyCalls)
{
if (setPropertyCalls == null)
throw new ArgumentNullException(nameof(setPropertyCalls)); var nameValues = new Dictionary<string, object>(100); // 创建一个字典用于存储属性名和值 var calls = new SetPropertyCalls<TSource>(nameValues); // 创建一个SetPropertyCalls对象 setPropertyCalls(calls); // 调用传入的函数,将属性名和值添加到字典中 return nameValues; // 返回字典
} private static void Update(IDatabase db, int id, Func<SetPropertyCalls<Person>, SetPropertyCalls<Person>> setPropertyCalls)
{
var hashEntries = Update(setPropertyCalls)
.Select(kv => new HashEntry(kv.Key, kv.Value != null ? kv.Value.ToString() : RedisValue.EmptyString))
.ToArray(); // 将HashEntry数组存储到缓存中,使用对象的Id作为键
db.HashSet(id.ToString(), hashEntries);
}}
- 首先,定义一个泛型方法Update<TSource>,它接受一个函数作为参数,这个函数的类型是Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>,表示它接受一个SetPropertyCalls<TSource>对象,并返回一个SetPropertyCalls<TSource>对象。这个函数的作用是设置要更新的属性名和值。
- 然后,在Update<TSource>方法中,创建一个字典nameValues,用于存储属性名和值。创建一个SetPropertyCalls<TSource>对象calls,传入nameValues作为构造参数。调用传入的函数setPropertyCalls,并传入calls作为参数。这样,setPropertyCalls函数就可以通过调用calls的SetProperty方法来添加属性名和值到nameValues字典中。最后,返回nameValues字典。
- 接下来,定义一个私有方法Update,它接受一个数据库对象db,一个对象id,和一个函数setPropertyCalls作为参数。这个函数的类型是Func<SetPropertyCalls<Person>, SetPropertyCalls<Person>>,表示它接受一个SetPropertyCalls<Person>对象,并返回一个SetPropertyCalls<Person>对象。这个函数的作用是设置要更新的Person对象的属性名和值。
- 然后,在Update方法中,调用Update(setPropertyCalls)方法,并传入setPropertyCalls作为参数。这样,就可以得到一个字典nameValues,包含了要更新的Person对象的属性名和值。将nameValues字典转换为HashEntry数组hashEntries,使用属性值的字符串表示作为HashEntry的值。如果属性值为空,则使用RedisValue.EmptyString作为HashEntry的值。最后,使用db.HashSet方法将hashEntries数组存储到缓存中,使用id作为键。
4、SetPropertyCalls泛型类
这个类的作用是收集一个源对象的属性名称和值的对应关系,并提供一个链式调用的方法,用于设置属性的值。
public class SetPropertyCalls<TSource>
{
private readonly Dictionary<string, object> _nameValues; public SetPropertyCalls(Dictionary<string, object> nameValues)
{
_nameValues = nameValues;
} public SetPropertyCalls<TSource> SetProperty<TProperty>(Expression<Func<TSource, TProperty>> propertyExpression, TProperty valueExpression)
{
if (propertyExpression == null)
throw new ArgumentNullException(nameof(propertyExpression)); if (propertyExpression.Body is MemberExpression member && member.Member is PropertyInfo property)
{
if (!_nameValues.TryAdd(property.Name, valueExpression))
{
throw new ArgumentException($"The property '{property.Name}' has already been set.");
}
}
return this;
}
}
- 首先,这个类有一个构造函数,接受一个Dictionary<string, object>类型的参数,作为存储属性名称和值的对应关系的字典,并赋值给一个私有字段_nameValues。
- 然后,这个类有一个泛型方法,叫做SetProperty。这个方法接受两个参数,一个是表示源对象属性的表达式,另一个是表示属性值的表达式。
- 在这个方法中,首先判断第一个参数是否为空,如果为空,则抛出ArgumentNullException异常。
- 然后判断第一个参数的表达式体是否是一个成员表达式,并且该成员表达式的成员是否是一个属性,如果是,则获取该属性的名称,并赋值给一个局部变量property。
- 然后尝试将该属性名称和第二个参数的值添加到_nameValues字典中,如果添加失败,则说明该属性已经被设置过了,抛出ArgumentException异常。
- 最后,返回当前对象的引用,实现链式调用的效果。
这样,我们就可以得到一个包含所有要更新的属性名和值的字典,然后我们就可以根据这些属性名和值来更新实体类的属性了。
Demo示例
让我们来看一下代码示例,为了方便演示和阅读,这是临时码的,实际中大家可以根据自己习惯来进行封装,简化调用,同时也可以使用静态字典来缓存编译好的委托及对象属性,提高性能。
感谢阅读,点赞+分享+收藏+关注
如何使用C#中的Lambda表达式操作Redis Hash结构,简化缓存中对象属性的读写操作的更多相关文章
- C#中的Lambda表达式和表达式树
在C# 2.0中,通过方法组转换和匿名方法,使委托的实现得到了极大的简化.但是,匿名方法仍然有些臃肿,而且当代码中充满了匿名方法的时候,可读性可能就会受到影响.C# 3.0中出现的Lambda表达式在 ...
- Lambda 表达式,Java中应用Lambda 表达式
一.Lambda 表达式 简单来说,编程中提到的 lambda 表达式,通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数. 链接:知乎 先举一个普通的 Python 例 ...
- Qt5中的lambda表达式和使用lambda来写connect
c11新特性中加入了lambda表达式,所以Qt 也支持 需在.pro文件中加入 CONFIG += c++11 例子: QString program = "C:/Windows/Syst ...
- Android中使用Lambda表达式开发
参考文章:ImportNew 要在Android开发中使用lambda表达式,首先需要在 Module 的build.gradle中加入: compileOptions { targetCompati ...
- 编写高质量代码改善C#程序的157个建议——建议27:在查询中使用Lambda表达式
建议27:在查询中使用Lambda表达式 LINQ实际上是基于扩展方法和Lambda表达式的.任何LINQ查询都能通过扩展方法的方式来代替. var personWithCompanyList = f ...
- 一篇文章教会你使用Java8中的Lambda表达式
简介 Java 8为开发者带来了许多重量级的新特性,包括Lambda表达式,流式数据处理,新的Optional类,新的日期和时间API等.这些新特性给Java开发者带来了福音,特别是Lambda表达式 ...
- 你知道C#中的Lambda表达式的演化过程吗
你知道C#中的Lambda表达式的演化过程吗? 阅读目录 委托的使用 匿名方法 Func和Action Lambda的诞生 那得从很久很久以前说起了,记得那个时候... 懵懂的记得从前有个叫委托的东西 ...
- C++11中的Lambda表达式
原文地址:C++中的Lambda表达式 作者:果冻想 一直都在提醒自己,我是搞C++的:但是当C++11出来这么长时间了,我却没有跟着队伍走,发现很对不起自己的身份,也还好,发现自己也有段时间没有写C ...
- Qt5中使用lambda表达式
c11新特性中加入了lambda表达式,所以Qt 也支持 需在.pro文件中加入 CONFIG += c++11 例子: QString program = "C:/Windows/Syst ...
- 在Linq to sql 和 Entity framework 中使用lambda表达式实现left join
在Linq to sql 和 Entity framework 中使用lambda表达式实现left join 我们知道lambda表达式在Linq to sql 和 Entity framework ...
随机推荐
- PHPCMSV9 单文件上传功能代码
后台有"多文件上传"功能,但是对于有些情况,我们只需要上传一个文件,而使用多文件上传功能上传一个文件,而调用时调用一个文件太麻烦了. 所以我就自己动手,参考其他字段类型的网站,研究 ...
- 05-打包样式资源(编写webpack配置文件)
/** * webpack.config.js webpack的配置文件 * 作用:指示 webpack 干哪些活(当你运行 webpack 指令时,会加载里面的配置) * * 所有构件工具都是基于n ...
- PostgreSQL插件那么多,怎样管理最高效?
摘要:华为云RDS for PostgreSQL通过插件管理功能,很好地解决了PostgreSQL版本与插件耦合的问题,帮助用户更直观.更快速地安装管理数据库插件. 本文分享自华为云社区<Pos ...
- elSelect点击空白处无法收起下拉框(失去焦点并隐藏)
学习记录,为了以后有同样的问题,省得再百度了,方便自己也方便你们element 中多选的select 有个问题,就是点击空白或者关闭弹窗,下拉还会一直展示出来百度了好一会,觉得下面两位大佬说的最合理, ...
- 【漏洞分析】ReflectionToken BEVO代币攻击事件分析
前言 BEVO代币是一种Reflection Token(反射型代币),并且拥有通缩的特性.关于Reflection Token更为详细的说明可参考这篇文章.然后目前浏览到的很多分析报告没有指出其漏洞 ...
- 2023-03-03:请用go语言调用ffmpeg,摄像头捕获并编码为h264文件,不管音频。
2023-03-03:请用go语言调用ffmpeg,摄像头捕获并编码为h264文件,不管音频. 答案2023-03-03: 使用 github.com/moonfdd/ffmpeg-go 库. 先用如 ...
- 2020-08-25:BloomFilter的原理以及Zset的实现原理。
福哥答案2020-08-25: 布隆过滤器:哈希+位图.布隆过滤器重要的三个公式1.假设数据量为n,预期的失误率为p(布隆过滤器大小和每个样本的大小无关).2.根据n和p,算出BloomFilter一 ...
- 2020-12-19:系统load过高,你怎么去查?
福哥答案2020-12-20:[答案来自此链接:](http://bbs.xiangxueketang.cn/question/800)1.top命令查看该机器的负载状况.2.cd /proc/pid ...
- 2021-07-16:三个无重叠子数组的最大和。给定数组 nums 由正整数组成,找到三个互不重叠的子数组的最大和。每个子数组的长度为k,我们要使这3*k个项的和最大化。返回每个区间起始索引的列表(索
2021-07-16:三个无重叠子数组的最大和.给定数组 nums 由正整数组成,找到三个互不重叠的子数组的最大和.每个子数组的长度为k,我们要使这3*k个项的和最大化.返回每个区间起始索引的列表(索 ...
- Alist云盘视频加密助手:支持云盘视频文件加密与在线播放,不用再担心视频文件被和谐了!
在当前娱乐资源丰富的时代,人们每天都在接触各种视频资源.然而,网盘限速.版权审核.视频分级.少儿不宜等问题经常让人感到困扰.如何在保护隐私的前提下,让视频存储和分享变得更加便捷.安全呢?分享一款实用的 ...