.net gRPC CRUD 实战

本文详细介绍了如何使用.NET gRPC技术进行数据库的增删改查操作,包括定义protobuf消息和RPC服务,接口统一扩展,以及利用Entity Services提高效率。作者还讨论了将消息和RPC分开管理的优势和简化代码的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目标

    通过.net gRPC实现对数据库的增删改查。

解决方案结构

  1. Protos 文件夹   存放 proto 文件,分为两组,一组定义message,一组定义rpc;
  2. Models .net standard 2.1 项目,导入message.proto,并通过 public partial class 扩展实体;
  3. Contract  .net standard 2.1 项目,导入rpc.proto,引用Models项目,提供服务端和客户端调用;
  4. Service  gRPC asp.net core3.1项目,引用Contract项目,实现rpc.proto的具体方法;
  5. Client .net core3.1 console项目,引用Contract,调用rpc.proto的方法,完成测试。

Node 实体的message和rpc的proto文件

    Node是一个树形结构的对象,数字 Id 为主键,ParentId 为自耦的外键,实体的proto定义如下,CUDType为修改类型,用于记录并批量向后台提交修改。

message Node {
  int32 id = 1;
  string name = 2;
  string code = 3;
  int32 type_id = 4;
  int32 parent_id = 5;
  int32 sequence=6;
  CUDType change=7;
}

enum CUDType{
  No_Change = 0;
  Create = 1;
  Update = 2;
  Delete = 3;
}

    Node的 增删改查和批量提交rpc如下,其中 CreateNewId 用于为新对象分配 Id,不依赖数据库的自增长id。NodeCondition 可以提供多个查询条件,避免函数重载(proto不支持同名函数重载)。

service NodeRPC {
  rpc CreateNewId(Empty) returns (IntResponse);
  rpc GetAll (NodeCondition) returns (NodeList);
  rpc Get (RequestId) returns (Node);
  rpc Insert (Node) returns (Node);
  rpc Update (Node) returns (Node);
  rpc Remove (RequestId) returns (BoolResponse);
  rpc SaveChanges(NodeList) returns (SaveResult);
}

message NodeCondition{
  oneof value{
    int32 id= 1;
    string name = 2;
	int32 parent_id = 3;
	Empty all=4;
  }
}

通过接口统一和扩展message生成的实体类

    由于proto生成的cs代码是partial class,可以扩展,比较好的方法是定义接口,然后继承接口,在接口上扩展行为,这样Model就可以与原来的业务体系融合了。实体对象接口分为四个层级IEntity->IIdEntity->IIdNameEntity->ITreeNode。IEntity只需要记录修改状态,IIdEntity增加了Id主键属性,IIdNameEntity增加了Name,ITreeNode增加了ParentId等。

    public interface IEntity
    {
        CUDType Change { get; set; }
    }

    public interface IIdEntity : IEntity
    {
        int Id { get; set; }
    }

    public interface IIdNameEntity : IIdEntity
    {
        string Name { get; set; }
    }

    public interface ITreeNode : IIdNameEntity
    {
        int Sequence { get; set; }

        int ParentId { get; set; }

        ITreeNode Parent { get; set; }

        TreeNodeCollection Children { get; }
    }

   例子一:通过 ITreeNode 为 Node 扩展 Parent 和 Children 两个属性。

    public sealed partial class Node : ITreeNode
    {
        public ITreeNode Parent { get; set; }//对应Proto文件中定义的Node的ParentId属性.

        private TreeNodeCollection _Children = null;
        public TreeNodeCollection Children
        { 
            get 
            {
                if (_Children == null)
                { 
                    _Children = new TreeNodeCollection(this); 
                }
                return _Children;
            } 
        }
    }

   例子二:通过扩展 ITreeNode 增加共性行为。

    public static class TreeNodeExtension
    {
        public static int GetDepth(this ITreeNode node)
        {
            if (node.Parent == null)
                return 0;
            else
                return node.Parent.GetDepth() + 1;
        }

        public static string GetFullPath(this ITreeNode node)
        {
                if (node.Parent == null)
                    return node.Name;
                else
                {
                    return $"{node.Parent.GetFullPath()}.{node.Name}";
                }
        }
    }

CRUD 实体服务接口定义

        我把数据-实体放到了Entity Services里,这样在服务层内部调用就没必要通过gRPC了,毕竟gRPC的参数有额外封装,调用起来麻烦且要多消耗内存。

    public interface IEntityService<T> where T:IEntity
    {
        void Reconfigure();

        IEnumerable<T> GetMany();

        IEnumerable<T> GetMany(Func<T, bool> filter);

        T GetOne(Func<T, bool> filter);

        T Insert(T item);

        T Update(T item);

        bool Delete(T item);

        SaveResult SaveChanges(IList<T> changes);
    }

    public interface IIdEntityService<T> : IEntityService<T> where T : IIdEntity
    {
        T GetOne(int id);

        int CreateNewId();

        int CreateNewId(int count);

        bool Delete(int id);

        IEnumerable<T> GetMany(ICollection<int> ids);
    }

    public interface IIdNameEntityService<T> : IIdEntityService<T> where T : IIdNameEntity
    {
        T GetOne(string name);

        IDictionary<string, int> GetIds(ICollection<string> names);
    }

CRUD的gRPC服务实现

    Entity Service的实现可以用EF Core,也可以自己写,无外乎是sql语句。

    然后在Services中再调用Entity Services实现RPC。

    注意:Entity Service和 gRPC Service 用的是同一套 Models,否则要转来转去,这也是扩展Model的原因所在:面向接口(ITreeNode)的Entity Service实现可以子类共享。

   public class NodeService : NodeRPC.NodeRPCBase
    {
        EntityServices.NodeService entityService;
        public NodeService()
        {
            entityService = EntityServices.NodeService.GetInstance();
        }

        public override Task<Node> Get(RequestId request, ServerCallContext context)
        {
            if (request == null) return Task.FromResult((Node)null);
            return Task.FromResult(entityService.GetOne(request.Id));
        }
   
        //...
    }

 测试验证

    我写了客户端循环调用服务器GetOne 58次(=数据库记录数),平均单次调用耗时越6、7ms,速度很快。

    ps: google把对象的ToString做成了JSON结构,如图Client。

后记

    熟练使用gRPC后,感觉 proto 还是分成 ***messages.proto和***rpcs.proto 比较清晰,如果按照 每个实体/每张表 一个message.proto 和 一个rpc.proto,很快就一大堆了,不如按照服务合并好管理,也可以减少相互的import。

    另外,gRPC的生产率要高于自己写实体和接口代码,因为 属性{get;set;} 自动生成完了,大部分时候models, contract工程没几行代码,估计慢慢的不用还不习惯了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值