百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

如何使用C#中的Lambda表达式操作Redis Hash结构,简化缓存中对象属性的读写操作

wptr33 2025-06-10 18:37 7 浏览

Redis是一个开源的、高性能的、基于内存的键值数据库,它支持多种数据结构,如字符串、列表、集合、散列、有序集合等。其中,Redis的散列(Hash)结构是一个常用的结构,今天跟大家分享一个我的日常操作,如何使用Redis的散列(Hash)结构来缓存和查询对象的属性值,以及如何用Lambda表达式树来简化这个过程。

一、什么是Redis Hash结构

Redis Hash结构是一种键值对的集合,它可以存储一个对象的多个字段和值。例如,我们可以用一个Hash结构来存储一个人的信息,如下所示:

HSET person:1 id 1HSET person:1 name AliceHSET 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 == ) throw new ArgumentException(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 == ) throw new ArgumentException(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 != ? 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 == ) throw new ArgumentException(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。这个方法接受两个参数,一个是表示源对象属性的表达式,另一个是表示属性值的表达式。

  • 在这个方法中,首先判断第一个参数是否为空,如果为空,则抛出ArgumentException异常。

  • 然后判断第一个参数的表达式体是否是一个成员表达式,并且该成员表达式的成员是否是一个属性,如果是,则获取该属性的名称,并赋值给一个局部变量property。

  • 然后尝试将该属性名称和第二个参数的值添加到_nameValues字典中,如果添加失败,则说明该属性已经被设置过了,抛出ArgumentException异常。

  • 最后,返回当前对象的引用,实现链式调用的效果。


这样,我们就可以得到一个包含所有要更新的属性名和值的字典,然后我们就可以根据这些属性名和值来更新实体类的属性了

Demo完整示例

让我们来看一下代码示例,为了方便演示和阅读,这是临时码的,实际中大家可以根据自己习惯来进行封装,简化调用,同时也可以使用静态字典来缓存编译好的委托及对象属性,提高性能。

using StackExchange.Redis; // 引入StackExchange.Redis库
namespace RedisHashExample{ public class Program { // 定义一个连接字符串常量 private const string ConnectionString = "localhost";
public static void Main(string[] args) { // 创建一个连接对象 var connection = ConnectionMultiplexer.Connect(ConnectionString); // 获取一个数据库对象 var db = connection.GetDatabase();
// 创建一个测试对象 var person = new Person { Id = 1, Name = "Alice", Age = 20 };
//缓存整个对象 Set(db, person.Id, person);
// 使用泛型方法设置缓存中的对象 Update(db, person.Id, p => p .SetProperty(x => x.Name, person.Name) .SetProperty(x => x.Age, 35));
// 查询缓存中的对象的属性值,使用Lambda表达式指定要查询的属性 var model = Get(db, person.Id, p => new { p.Name, p.Age });
Console.WriteLine(model.Name); Console.WriteLine(model.Age); }
// 定义一个泛型方法来设置缓存中的对象 private static void Set<T>(IDatabase db, int id, T obj) { // 使用扩展方法将对象转换为HashEntry数组 var hashEntries = obj.ToHashEntries(); // 将HashEntry数组存储到缓存中,使用对象的Id作为键 db.HashSet($"person:{id}", hashEntries); }
public static Dictionary<string, object> Update<TSource>(Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>> setPropertyCalls) { if (setPropertyCalls == ) throw new ArgumentException(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 != ? kv.Value.ToString() : RedisValue.EmptyString)) .ToArray();
// 将HashEntry数组存储到缓存中,使用对象的Id作为键 db.HashSet(id.ToString(), hashEntries); }

private static TResult Get<T, TResult>(IDatabase db, int id, Expression<Func<T, TResult>> selector) { if (selector == ) throw new ArgumentException(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);
}
public class Person { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } }
public static class Extensions { // 定义一个扩展方法来将对象转换为HashEntry数组 public static HashEntry[] ToHashEntries<T>(this T obj) { return typeof(T).GetProperties() .Select(p => new HashEntry(p.Name, p.GetValue(obj)?.ToString() ?? RedisValue.EmptyString)) .ToArray(); }
// 定义一个扩展方法来将HashEntry数组转换为对象 public static T ToObject<T>(this RedisValue[] values, RedisValue[] fields) { var obj = Activator.CreateInstance<T>(); var properties = typeof(T).GetProperties(); for (int i = 0; i < fields.Length; i++) { var prop = properties.FirstOrDefault(m => m.Name == fields[i]); prop?.SetValue(obj, values[i].Is ? default : Convert.ChangeType(values[i].ToString(), prop.PropertyType)); } return obj; }
// 定义一个扩展方法来获取Lambda表达式中的属性名数组,使用ExpressionVisitor类来遍历表达式树 public static string[] GetMemberNames<T, TResult>(this Expression<Func<T, TResult>> expression) { var list = new List<string>(100); var visitor = new MemberExpressionVisitor(list); visitor.Visit(expression.Body); return list.ToArray(); } }
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); } }
public class SetPropertyCalls<TSource> { private readonly IDictionary<string, object> _nameValues;
public SetPropertyCalls(IDictionary<string, object> nameValues) { _nameValues = nameValues; }
public SetPropertyCalls<TSource> SetProperty<TProperty>(Expression<Func<TSource, TProperty>> propertyExpression, TProperty valueExpression) { if (propertyExpression == ) throw new ArgumentException(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; } }}

相关推荐

开发者必看的八大Material Design开源项目

MaterialDesign是介于拟物和扁平之间的一种设计风格,自从它发布以来,便引起了很多开发者的关注,在这里小编介绍在Android开发者当中里最受青睐的八个MaterialDesign开源项...

另类插这么可爱,一定是…(另类t恤)

IT之家(www.ithome.com):另类插图:这么可爱,一定是…OSXMavericks和Yosemite打破了苹果对Mac操作系统传统的命名方式,使用加州的某些标志性景点来替换猫...

Android常用ADB命令(安卓adb工具是什么)

杀死应用①根据包名获取APP的PIDadbshellps|grep应用包名②执行kill命令...

微软Mac版PowerPoint测试Reading Order Pane功能

IT之家5月20日消息,微软公司昨日(5月19日)发布博文,邀请Microsoft365Insiders成员,测试macOS新版PowerPoint演示文稿应用,重点引入...

Visual Studio跨平台开发实战(4):Xamarin Android控制项介绍

前言不同于iOS,Xamarin在VisualStudio中针对Android,可以直接设计使用者界面.在本篇教学文章中,笔者会针对Android的专案目录结构以及基本控制项进行介绍,包...

用云存储30分钟快速搭建APP,你信吗?

背景不管你承认与否,移动互联的时代已经到来,这是一个移动互联的时代,手机已经是当今世界上引领潮流的趋势,大型的全球化企业和中小企业都把APP程序开发纳入到他们的企业发展策略当中。但随着手机APP上传的...

谷歌P图神器来了!不用学不用教,输入一句话,分分钟给结果

Pine发自凹非寺量子位|公众号QbitAI当你拍照片时,“模特不好好配合”怎么办?...

iOS文本编辑控件UITextField和UITextVie

记录一个菜鸟的IOS学习之旅,如能帮助正在学习的你,亦枫不胜荣幸;如路过的大神如指教几句,亦枫感激涕淋!细心的朋友可能已经注意到了,IOS学习之旅系列教程在本篇公众号的文章中,封面已经换成美女图片了,...

Android入门图文教程集锦(android 入门教程)

Android入门视频教程集锦AndroidStudio错误gradientandroid:endXattributenotfound...

如何使用Android自定义复合视图(如何使用android自定义复合视图)

在最近的一个客户应用中,我遇到了一个需求,根据选定的值来生成指定数量的编辑框字段,这样用户可以输入人物信息。最初我的想法是把这些逻辑放到Fragment中,只是根据选中值的变化来向线性布局容器中增加编...

原生安卓开发app的框架frida常用关键代码定位

前言有时候可能会对APP进行字符串加密等操作,这样的话你的变量名等一些都被混淆了,看代码就可能无从下手...

教程10 | 三分钟搞定一个智能输入法程序

一案例描述1、考核知识点网格布局线性布局样式和主题Toast2、练习目标掌握网格布局的使用掌握Toast的使用掌握线性布局的使用...

(Android 8.1) 功能与新特性(android的功能)

和你一起终身学习,这里是程序员AndroidAndroid8.1(API级别27)为用户和开发人员引入了各种新特性和功能。本文档重点介绍了开发人员的新功能。通过本章阅读,您将获取到以下内容:Andr...

怎样设置EditText内部文字被锁定不可删除和修改

在做项目的时候,我曾经遇到过这样的要求,就是跟百度贴吧客户端上的一样,在回复帖子的时候,在EditText中显示回复人的名字,而且这个名字不可以修改和删除,说白了就是不可操作,只能在后面输入内容。在E...

如何阻止 Android 活动启动时 EditText 获得焦点

技术背景在Android开发中,当活动启动时,EditText有时会自动获得焦点并弹出虚拟键盘,这可能不是用户期望的行为。为了提升用户体验,我们需要阻止...