├── .gitignore ├── Directory.Build.props ├── NuGet.config ├── README.md ├── SatelliteRpc.sln ├── SatelliteRpc.sln.DotSettings.user ├── assets ├── 240225164316071.png └── 240225164318938.png ├── samples ├── Client │ ├── Client.csproj │ ├── DemoHostedService.cs │ ├── Program.cs │ └── Rpc │ │ ├── DemoLoginClient.cs │ │ └── ILoginClient.cs └── Server │ ├── Program.cs │ ├── Server.csproj │ └── Services │ ├── ILoginService.cs │ └── LoginService.cs ├── src ├── SatelliteRpc.Client.SourceGenerator │ ├── GeneratorExecutionContextExtensions.cs │ ├── SatelliteRpc.Client.SourceGenerator.csproj │ └── SatelliteRpcClientGenerator.cs ├── SatelliteRpc.Client │ ├── Configuration │ │ └── SatelliteRpcClientOptions.cs │ ├── Extensions │ │ ├── HostBuilderExtensions.cs │ │ └── ServiceCollectionExtensions.cs │ ├── Middleware │ │ ├── IRpcClientMiddleware.cs │ │ ├── IRpcClientMiddlewareBuilder.cs │ │ └── RpcClientMiddlewareBuilder.cs │ ├── SatelliteRpc.Client.csproj │ ├── SatelliteRpcAttribute.cs │ └── Transport │ │ ├── CallContext.cs │ │ ├── DefaultSatelliteRpcClient.cs │ │ ├── IRpcConnection.cs │ │ ├── ISatelliteRpcClient.cs │ │ └── RpcConnection.cs ├── SatelliteRpc.Protocol │ ├── BasicProtocolParser.cs │ ├── PayloadConverters │ │ ├── IPayloadConverter.cs │ │ ├── PayloadConverterSource.cs │ │ └── ProtocolBufferPayloadConverter.cs │ ├── Protocol │ │ ├── AppRequest.cs │ │ ├── AppResponse.cs │ │ ├── Empty.cs │ │ ├── Login.proto │ │ ├── PayloadType.cs │ │ ├── PayloadWriter.cs │ │ └── ResponseStatus.cs │ └── SatelliteRpc.Protocol.csproj ├── SatelliteRpc.Server │ ├── Configuration │ │ ├── IRpcServerBuilder.cs │ │ ├── RpcServerBuilder.cs │ │ └── SatelliteRpcServerOptions.cs │ ├── Exceptions │ │ ├── NotFoundException.cs │ │ └── ParametersBindException.cs │ ├── Extensions │ │ ├── HostBuilderExtensions.cs │ │ ├── RpcServerBuilderExtensions.cs │ │ ├── ServiceCollectionExtensions.cs │ │ └── WebHostBuilderExtensions.cs │ ├── Observability │ │ └── ObservabilityMiddleware.cs │ ├── RpcService │ │ ├── DataExchange │ │ │ ├── DefaultRpcDataExchange.cs │ │ │ └── IRpcDataExchange.cs │ │ ├── Endpoint │ │ │ ├── DefaultRpcEndPointResolver.cs │ │ │ ├── EndpointInvokeMiddleware.cs │ │ │ ├── IEndpointResolver.cs │ │ │ ├── RpcServiceEndpoint.cs │ │ │ └── RpcServiceEndpointDataSource.cs │ │ ├── IRpcService.cs │ │ ├── Middleware │ │ │ ├── IRpcServiceMiddleware.cs │ │ │ ├── IRpcServiceMiddlewareBuilder.cs │ │ │ └── RpcServiceMiddlewareBuilder.cs │ │ ├── RpcServiceHandler.cs │ │ └── ServiceContext.cs │ ├── SatelliteRpc.Server.csproj │ └── Transport │ │ ├── IRpcConnectionApplicationHandlerBuilder.cs │ │ ├── RpcConnectionApplicationHandlerBuilder.cs │ │ ├── RpcConnectionHandler.cs │ │ └── RpcRawContext.cs └── SatelliteRpc.Shared │ ├── Application │ ├── ApplicationBuilder.cs │ ├── ApplicationDelegate.cs │ └── IApplicationMiddleware.cs │ ├── Collections │ ├── PooledArray.cs │ ├── PooledArrayExtensions.cs │ └── PooledList.cs │ ├── DisposeManager.cs │ ├── MethodInvoker.cs │ └── SatelliteRpc.Shared.csproj └── tests ├── SatelliteRpc.Protocol.Tests ├── AppRequestTests.cs ├── AppResponseTests.cs ├── GlobalUsings.cs ├── PayloadConverterTests │ └── ProtocolBufferPayloadConverterTests.cs └── SatelliteRpc.Protocol.Tests.csproj ├── SatelliteRpc.Server.Tests ├── GlobalUsings.cs ├── SatelliteRpc.Server.Tests.csproj └── Transport │ ├── RpcConnectionHandlerTest.RunResponseHandler.cs │ └── RpcConnectionHandlerTests.OnConnectedAsync.cs └── SatelliteRpc.Shared.Tests ├── GlobalUsings.cs ├── MethodInvokerTests.cs ├── PooledArrayTests.cs ├── PooledListTests.cs └── SatelliteRpc.Shared.Tests.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | /.vs 7 | /.idea 8 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | NU1803;CS1591;CS0108; 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 背景 2 | 之前在.NET 性能优化群内交流时,我们发现很多朋友对于高性能网络框架有需求,需要创建自己的消息服务器、游戏服务器或者物联网网关。但是大多数小伙伴只知道 DotNetty,虽然 DotNetty 是一个非常优秀的网络框架,广泛应用于各种网络服务器中,不过因为各种原因它已经不再有新的特性支持和更新,很多小伙伴都在寻找替代品。 3 | 4 | 这一切都不用担心,在.NET Core 以后的时代,我们有了更快、更强、更好的 Kestrel 网络框架,正如其名,Kestrel 中文翻译为**红隼(hóng sǔn)**封面就是红隼的样子,是一种飞行速度极快的猛禽。Kestrel 是 ASPNET Core 成为.NET 平台性能最强 Web 服务框架的原因之一,但是很多人还觉得 Kestrel 只是用于 ASPNET Core 的网络框架,但是其实它是一个高性能的通用网络框架。 5 | 6 | 我和拥有多个.NET 千星开源项目作者九哥一拍即合,为了让更多的人了解 Kestrel,计划写一系列的文章来介绍它,九哥已经写了一系列的文章来介绍如何使用Kestrel来创建网络服务,我觉得他写的已经很深入和详细了,于是没有编写的计划。 7 | 8 | 不过最近发现还是有很多朋友在群里面问这样的问题,还有群友提到如何使用Kestrel来实现一个RPC框架,刚好笔者在前面一段时间研究了一下这个,所以这一篇文章也作为Kestrel的应用篇写给大家,目前来说想分为几篇文章来发布,大体的脉络如下所示,后续看自己的时间和读者们感兴趣的点再调整内容。 9 | 10 | * 整体设计 11 | * Kestrel服务端实现 12 | * 请求、响应序列化及反序列化 13 | * 单链接多路复用实现 14 | * 性能优化 15 | * Client实现 16 | * 代码生成技术 17 | * 待定…… 18 | 19 | ## 项目 20 | 21 | 本文对应的项目源码已经开源在Github上,由于时间仓促,笔者只花了几天时间设计和实现这个RPC框架,所以里面肯定有一些设计不合理或者存在BUG的地方,还需要大家帮忙查缺补漏。 22 | 23 | SatelliteRpc: https://github.com/InCerryGit/SatelliteRpc 24 | 25 | **如果对您有帮助,欢迎点个star~** 26 | **再次提醒注意:该项目只作为学习、演示使用,没有经过生产环境的检验。** 27 | 28 | ## 项目信息 29 | 30 | ### 编译环境 31 | 32 | 要求 .NET 7.0 SDK 版本,Visual Studio 和 Rider 对应版本都可以。 33 | 34 | ### 目录结构 35 | ``` 36 | ├─samples // 示例项目 37 | │ ├─Client // 客户端示例 38 | │ │ └─Rpc // RPC客户端服务 39 | │ └─Server // 服务端示例 40 | │ └─Services // RPC服务端服务 41 | ├─src // 源代码 42 | │ ├─SatelliteRpc.Client // 客户端 43 | │ │ ├─Configuration // 客户端配置信息 44 | │ │ ├─Extensions // 针对HostBuilder和ServiceCollection的扩展 45 | │ │ ├─Middleware // 客户端中间件,包含客户端中间件的构造器 46 | │ │ └─Transport // 客户端传输层,包含请求上下文,默认的客户端和Rpc链接的实现 47 | │ ├─SatelliteRpc.Client.SourceGenerator // 客户端代码生成器,用于生成客户端的调用代码 48 | │ ├─SatelliteRpc.Protocol // 协议层,包含协议的定义,协议的序列化和反序列化,协议的转换器 49 | │ │ ├─PayloadConverters // 承载数据的序列化和反序列化,包含ProtoBuf 50 | │ │ └─Protocol // 协议定义,请求、响应、状态和给出的Login.proto 51 | │ ├─SatelliteRpc.Server // 服务端 52 | │ │ ├─Configuration // 服务端配置信息,还有RpcServer的构造器 53 | │ │ ├─Exceptions // 服务端一些异常 54 | │ │ ├─Extensions // 针对HostBuilder、ServiceCollection、WebHostBuilder的扩展 55 | │ │ ├─Observability // 服务端的可观测性支持,目前实现了中间件 56 | │ │ ├─RpcService // 服务端的具体Rpc服务的实现 57 | │ │ │ ├─DataExchange // 数据交换,包含Rpc服务的数据序列化 58 | │ │ │ ├─Endpoint // Rpc服务的端点,包含Rpc服务的端点,寻址,管理 59 | │ │ │ └─Middleware // 包含Rpc服务的中间件的构造器 60 | │ │ └─Transport // 服务端传输层,包含请求上下文,服务端的默认实现,Rpc链接的实现,链接层中间件构建器 61 | │ └─SatelliteRpc.Shared // 共享层,包含一些共享的类 62 | │ ├─Application // 应用层中间件构建基类,客户端和服务端中间件构建都依赖它 63 | │ └─Collections // 一些集合类 64 | └─tests // 测试项目 65 | ├─SatelliteRpc.Protocol.Tests 66 | ├─SatelliteRpc.Server.Tests 67 | └─SatelliteRpc.Shared.Tests 68 | ``` 69 | ## 演示 70 | 71 | 安装好SDK和下载项目以后,`samples`目录是对应的演示项目,这个项目就是通过我们的RPC框架调用Server端创建的一些服务,先启动Server然后再启动Client就可以得到如下的运行结果: 72 | 73 | 74 | 75 | 76 | 77 | ## 设计方案 78 | 下面简单的介绍一下总体的设计方案: 79 | 80 | ### 传输协议设计 81 | 传输协议的主要代码在`SatelliteRpc.Protocol`项目中,协议的定义在`Protocol`目录下。针对RPC的请求和响应创建了两个类,一个是`AppRequest`另一个是`AppResponse`。 82 | 83 | 在代码注释中,描述了协议的具体内容,这里简单的介绍一下,请求协议定义如下: 84 | ```text 85 | [请求总长度][请求Id][请求的路径(字符串)]['\0'分隔符][请求数据序列化类型][请求体] 86 | ``` 87 | 响应协议定义如下: 88 | ```text 89 | [响应总长度][请求Id][响应状态][响应数据序列化类型][响应体] 90 | ``` 91 | 其中主要的参数和数据在各自请求响应体中,请求体和响应体的序列化类型是通过`PayloadConverters`中的序列化器进行序列化和反序列化的。 92 | 93 | 在响应时使用了**请求Id**,这个请求Id是ulong类型,是一个**链接唯一的**自增的值,每次请求都会自增,这样就可以保证每次请求的Id都是唯一的,这样就可以在客户端和服务端进行匹配,从而找到对应的请求,从而实现多路复用的请求和响应匹配功能。 94 | 95 | 当ulong类型的值超过最大值时,会从0开始重新计数,由于ulong类型的值是64位的,值域非常大,所以在正常的情况下,同一连接下不可能出现请求Id重复的情况。 96 | 97 | ### 客户端设计 98 | 99 | 客户端的层次结构如下所示,最底层是传输层的中间件,它由`RpcConnection`生成,它用于TCP网络连接和最终的发送接受请求,中间件构建器保证了它是整个中间件序列的最后的中间件,然后上层就是用户自定义的中间件。 100 | 101 | 默认的客户端实现`DefaultSatelliteRpcClient`,目前只提供了几个Invoke方法,用于不同传参和返参的服务,在这里会执行中间件序列,最后就是具体的`LoginClient`实现,这里方法定义和`ILoginClient`一致,也和服务端定义一致。 102 | 103 | 最后就是调用的代码,现在有一个`DemoHostedService`的后台服务,会调用一下方法,输出日志信息。 104 | 105 | 下面是一个层次结构图: 106 | ```text 107 | [用户层代码] 108 | | 109 | [LoginClient] 110 | | 111 | [DefaultSatelliteRpcClient] 112 | | 113 | [用户自定义中间件] 114 | | 115 | [RpcConnection] 116 | | 117 | [TCP Client] 118 | ``` 119 | 所以整个RCP Client的关键实体的转换如下图所示: 120 | ```text 121 | 请求:[用户PRC 请求响应契约][CallContext - AppRequest&AppResponse][字节流] 122 | 响应:[字节流][CallContext - AppRequest&AppResponse][用户PRC 请求响应契约] 123 | ``` 124 | #### 多路复用 125 | 126 | 上文提到,多路复用主要是使用ulong类型的Id来匹配Request和Response,主要代码在`RpcConnection`,它不仅提供了一个最终用于发送请求的方法, 127 | 在里面声明了一个`TaskCompletionSource`的字典,用于存储请求Id和`TaskCompletionSource`的对应关系,这样就可以在收到响应时,通过请求Id找到对应的`TaskCompletionSource`,从而完成请求和响应的匹配。 128 | 129 | 由于请求可能是并发的,所以在`RpcConnection`中声明了`Channel`,将并发的请求放入到Channel中,然后在`RpcConnection`中有一个后台线程,用于从Channel单线程的中取出请求,然后发送请求,避免并发调用远程接口时,底层字节流的混乱。 130 | 131 | #### 扩展性 132 | 133 | 客户端不仅仅支持`ILoginClient`这一个契约,用户可以自行添加其他契约,只要保障服务端有相同的接口实现即可。也支持增加其它proto文件,Protobuf.Tools会自动生成对应的实体类。 134 | 135 | ##### 中间件 136 | 该项目的扩展性类似ASP.NET Core的中间件,可以自行加入中间件处理请求和响应,中间件支持Delegate形式,也支持自定义中间件类的形式,如下代码所示: 137 | ```csharp 138 | public class MyMiddleware : IRpcClientMiddleware 139 | { 140 | public async Task InvokeAsync(ApplicationDelegate next, CallContext next) 141 | { 142 | // do something 143 | await next(context); 144 | // do something 145 | } 146 | } 147 | ``` 148 | 在客户端中间件中,可以通过`CallContext`获取到请求和响应的数据,然后可以对数据进行处理,然后调用`next`方法,这样就可以实现中间件的链式调用。 149 | 150 | 同样也可以进行阻断操作,比如在某个中间件中,直接返回响应,这样就不会继续调用后面的中间件;或者记录请求响应日志,或者进行一些其他的操作,类似于ASP.NET Core中间件都可以实现。 151 | 152 | ##### 序列化 153 | 序列化的扩展性主要是通过`PayloadConverters`来实现的,内部实现了抽象了一个接口`IPayloadConverter`,只要实现对应PayloadType的序列化和反序列化方法即可,然后注册到DI容器中,便可以使用。 154 | 155 | 由于时间关系,只列出了Protobuf和Json两种序列化器,实际上可以支持用户自定义序列化器,只需要在请求响应协议中添加标识,然后由用户注入到DI容器即可。 156 | 157 | ##### 其它 158 | 159 | 其它一些类的实现基本都是通过接口和依赖注入的方式实现,用户可以很方便的进行扩展,在DI容器中替换默认实现即可。如:`IRpcClientMiddlewareBuilder`、 160 | `IRpcConnection`、`ISatelliteRpcClient`等。 161 | 162 | 另外也可以自行添加其他的服务,因为代码生成器会自动扫描接口,然后生成对应的调用代码,所以只需要在接口上添加`SatelliteRpcAttribute`,声明好方法契约,就能实现。 163 | 164 | ### 服务端设计 165 | 166 | 服务端的设计总体和客户端设计差不多,中间唯一有一点区别的地方就是服务端的中间件有两种: 167 | - 一种是针对连接层的`RpcConnectionApplicationHandler`中间件,设计它的目的主要是为了灵活处理链接请求,由于可以直接访问原始数据,还没有做路由和参数绑定,后续可观测性指标和一些性能优化在这里做会比较方便。 168 | - 比如为了应对RPC调用,定义了一个名为`RpcServiceHandler`的`RpcConnectionApplicationHandler`中间件,放在整个连接层中间件的最后,这样可以保证最后执行的是RPC Service层的逻辑。 169 | - 另外一种是针对业务逻辑层的`RpcServiceMiddleware`,这里就是类似ASP.NET Core的中间件,此时上下文中已经有了路由信息和参数绑定,可以在这做一些AOP编程,也能直接调用对应的服务方法。 170 | - 在RPC层,我们需要完成路由,参数绑定,执行目标方法等功能,这里就是定义了一个名为`EndpointInvokeMiddleware`的中间件,放在整个RPC Service层中间件的最后,这样可以保证最后执行的是RPC Service层的逻辑。 171 | 172 | 下面是一个层次结构图: 173 | ```text 174 | [用户层代码] 175 | | 176 | [LoginService] 177 | | 178 | [用户自定义的RpcServiceMiddleware] 179 | | 180 | [RpcServiceHandler] 181 | | 182 | [用户自定义的RpcConnectionApplicationHandler] 183 | | 184 | [RpcConnectionHandler] 185 | | 186 | [Kestrel] 187 | ``` 188 | 整个RPC Server的关键实体的转换如下图所示: 189 | ```text 190 | 请求:[字节流][RpcRawContext - AppRequest&AppResponse][ServiceContext][用户PRC Service 请求契约] 191 | 响应:[用户PRC Service 响应契约][ServiceContext][AppRequest&AppResponse][字节流] 192 | ``` 193 | #### 多路复用 194 | 195 | 服务端对于多路复用的支持就简单的很多,这里是在读取到一个完整的请求以后,直接使用Task.Run执行后续的逻辑,所以能做到同一链接多个请求并发执行, 196 | 对于响应为了避免混乱,使用了`Channel`,将响应放入到Channel中,然后在后台线程中单线程的从Channel中取出响应,然后返回响应。 197 | 198 | #### 终结点 199 | 200 | 在服务端中有一个终结点的概念,这个概念和ASP.NET Core中的概念类似,它具体的实现类是`RpcServiceEndpoint`;在程序开始启动以后; 201 | 便会扫描入口程序集(当然这块可以优化),然后找到所有的`RpcServiceEndpoint`,然后注册到DI容器中,然后由`RpcServiceEndpointDataSource`统一管理, 202 | 最后在进行路由时有`IEndpointResolver`根据路径进行路由,这只提供了默认实现,用户也可以自定义实现,只需要实现`IEndpointResolver`接口,然后替换DI容器中的默认实现即可。 203 | 204 | #### 扩展性 205 | 206 | 服务端的扩展性也是在**中间件**、**序列化**、**其它接口**上,可以通过DI容器很方便的替换默认实现,增加AOP切面等功能,也可以直接添加新的Service服务,因为会默认去扫描入口程序集中的`RpcServiceEndpoint`,然后注册到DI容器中。 207 | 208 | ## 优化 209 | 210 | 现阶段做的性能优化主要是以下几个方面: 211 | - Pipelines 212 | - 在客户端的请求和服务端处理(Kestrel底层使用)中都使用了Pipelines,这样不仅可以降低编程的复杂性,而且由于直接读写Buffer,可以减少内存拷贝,提高性能。 213 | - 表达式树 214 | - 在动态调用目标服务的方法时,使用了表达式树,这样可以减少反射的性能损耗,在实际场景中可以设置一个快慢阈值,当方法调用次数超过阈值时,就可以使用表达式树来调用方法,这样可以提高性能。 215 | - 代码生成 216 | - 在客户端中,使用了代码生成技术,这个可以让用户使用起来更加简单,无需理解RPC的底层实现,只需要定义好接口,然后使用代码生成器生成对应的调用代码即可;另外实现了客户端自动注入,避免运行时反射注入的性能损耗。 217 | - 内存复用 218 | - 对于RPC框架来说,最大的内存开销基本就在请求和响应体上,创建了PooledArray和PooledList,两个池化的底层都是使用的ArrayPool,请求和响应的Payload都是使用的池化的空间。 219 | - 减少内存拷贝 220 | - RPC框架消耗CPU的地方是内存拷贝,上文提到了客户端和服务端均使用Pipelines,在读取响应和请求的时候直接使用`ReadOnlySequence`读取网络层数据,避免拷贝。 221 | - 客户端请求和服务端响应创建了PayloadWriter类,通过`IBufferWriter`直接将序列化的结果写入网络Buffer中,减少内存拷贝,虽然会引入闭包开销,但是相对于内存拷贝来说,几乎可以忽略。 222 | - 对于这个优化实际应该设置一个阈值,当序列化的数据超过阈值时,才使用PayloadWriter,否则使用内存拷贝的方式,需要Benchmark测试支撑阈值设置。 223 | 224 | 其它更多的性能优化需要Benchmark的数据支持,由于时间比较紧,没有做更多的优化。 225 | 226 | ## 待办 227 | 228 | 计划做,但是没有时间去实现的: 229 | 230 | - 服务端代码生成 231 | - 现阶段服务端的路由是通过字典匹配实现,方法调用使用的表达式树,实际上这一块可以使用代码生成来实现,这样可以提高性能。 232 | - 另外一个地方就是Endpoint注册是通过反射扫描入口程序集实现的,实际上这一步可以放在编译阶段处理,在编译时就可以读取到所有的服务,然后生成代码,这样可以减少运行时的反射。 233 | - 客户端取消请求 234 | - 目前客户端的请求取消只是在客户端本身,取消并不会传递到服务端,这一块可以通过协议来实现,在请求协议中添加一个标识,传递Cancel请求,然后在服务端进行判断,如果是取消请求,则服务端也根据ID取消对应的请求。 235 | - Context 和 AppRequest\AppResponse 池化 236 | - 目前的Context和AppRequest\AppResponse都是每次请求都会创建,对于这些小对象可以使用池化的方式来实现复用,其中AppRequest、AppResponse已经实现了复用的功能,但是没有时间去实现池化,Context也可以实现池化,但是目前没有实现。 237 | - 堆外内存、FOH管理 238 | - 目前的内存管理都是使用的堆内存,对于那些有明显作用域的对象和缓存空间可以使用堆外内存或FOH来实现,这样可以减少GC在扫描时的压力。 239 | - AsyncTask的内存优化 240 | - 目前是有一些地方使用的ValueTask,对于这些地方也是内存分配的优化方向,可以使用`PoolingAsyncValueTaskMethodBuilder`来池化ValueTask,这样可以减少内存分配。 241 | - TaskCompletionSource也是可以优化的,后续可以使用`AwaitableCompletionSource`来降低分配。 242 | - 客户端连接池化 243 | - 目前客户端的连接还是单链接,实际上可以使用连接池来实现,这样可以减少TCP链接的创建和销毁,提高性能。 244 | - 异常场景处理 245 | - 目前对于服务端和客户端来说,没有详细的测试,针对TCP链接断开,数据包错误,服务器异常等场景的重试,熔断等策略都没有实现。 -------------------------------------------------------------------------------- /SatelliteRpc.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Server", "src\SatelliteRpc.Server\SatelliteRpc.Server.csproj", "{146A605B-09A6-4E9B-9A1E-C030F7BC0627}" 4 | EndProject 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B891FBEA-1F02-49C1-A570-1BB76E24DD1C}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "samples\Server\Server.csproj", "{50235CF8-82BA-411A-8E08-6358AF60FEFF}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "samples\Client\Client.csproj", "{7ABE96C5-C451-4260-936D-818C348512CB}" 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{594072AE-D250-4FE5-BAC2-36E09A8F0EF0}" 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1019D4BE-C564-41A2-9A54-64A8E166BFA2}" 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Server.Tests", "tests\SatelliteRpc.Server.Tests\SatelliteRpc.Server.Tests.csproj", "{0D87520E-912F-40B7-A2D3-6C53EF19C718}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Protocol", "src\SatelliteRpc.Protocol\SatelliteRpc.Protocol.csproj", "{B8021DD8-8871-41C4-9494-BDF28CAACA94}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Client", "src\SatelliteRpc.Client\SatelliteRpc.Client.csproj", "{76C4CA40-3FAC-44A0-808F-9C01AC42B4C3}" 20 | EndProject 21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Client.SourceGenerator", "src\SatelliteRpc.Client.SourceGenerator\SatelliteRpc.Client.SourceGenerator.csproj", "{1DE03048-151C-4F15-ACA5-B075820C632C}" 22 | EndProject 23 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Shared", "src\SatelliteRpc.Shared\SatelliteRpc.Shared.csproj", "{A740BF50-4F01-434F-B4CD-BAA81B2E3005}" 24 | EndProject 25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Shared.Tests", "tests\SatelliteRpc.Shared.Tests\SatelliteRpc.Shared.Tests.csproj", "{D6295CC4-A592-46DC-B1CD-B785BFACBACF}" 26 | EndProject 27 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Protocol.Tests", "tests\SatelliteRpc.Protocol.Tests\SatelliteRpc.Protocol.Tests.csproj", "{19040F1D-C753-4FD5-9D78-D9AB2EFE6891}" 28 | EndProject 29 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "items", "items", "{B05A28E4-0DCC-448A-A401-533146FA5BB8}" 30 | ProjectSection(SolutionItems) = preProject 31 | README.md = README.md 32 | EndProjectSection 33 | EndProject 34 | Global 35 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 36 | Debug|Any CPU = Debug|Any CPU 37 | Release|Any CPU = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 40 | {146A605B-09A6-4E9B-9A1E-C030F7BC0627}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {146A605B-09A6-4E9B-9A1E-C030F7BC0627}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {146A605B-09A6-4E9B-9A1E-C030F7BC0627}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {146A605B-09A6-4E9B-9A1E-C030F7BC0627}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {50235CF8-82BA-411A-8E08-6358AF60FEFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {50235CF8-82BA-411A-8E08-6358AF60FEFF}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {50235CF8-82BA-411A-8E08-6358AF60FEFF}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {50235CF8-82BA-411A-8E08-6358AF60FEFF}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {7ABE96C5-C451-4260-936D-818C348512CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {7ABE96C5-C451-4260-936D-818C348512CB}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {7ABE96C5-C451-4260-936D-818C348512CB}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {7ABE96C5-C451-4260-936D-818C348512CB}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {0D87520E-912F-40B7-A2D3-6C53EF19C718}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {0D87520E-912F-40B7-A2D3-6C53EF19C718}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {0D87520E-912F-40B7-A2D3-6C53EF19C718}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {0D87520E-912F-40B7-A2D3-6C53EF19C718}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {B8021DD8-8871-41C4-9494-BDF28CAACA94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {B8021DD8-8871-41C4-9494-BDF28CAACA94}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {B8021DD8-8871-41C4-9494-BDF28CAACA94}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {B8021DD8-8871-41C4-9494-BDF28CAACA94}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {76C4CA40-3FAC-44A0-808F-9C01AC42B4C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {76C4CA40-3FAC-44A0-808F-9C01AC42B4C3}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {76C4CA40-3FAC-44A0-808F-9C01AC42B4C3}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {76C4CA40-3FAC-44A0-808F-9C01AC42B4C3}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {1DE03048-151C-4F15-ACA5-B075820C632C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {1DE03048-151C-4F15-ACA5-B075820C632C}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {1DE03048-151C-4F15-ACA5-B075820C632C}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {1DE03048-151C-4F15-ACA5-B075820C632C}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {A740BF50-4F01-434F-B4CD-BAA81B2E3005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {A740BF50-4F01-434F-B4CD-BAA81B2E3005}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {A740BF50-4F01-434F-B4CD-BAA81B2E3005}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {A740BF50-4F01-434F-B4CD-BAA81B2E3005}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {D6295CC4-A592-46DC-B1CD-B785BFACBACF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 73 | {D6295CC4-A592-46DC-B1CD-B785BFACBACF}.Debug|Any CPU.Build.0 = Debug|Any CPU 74 | {D6295CC4-A592-46DC-B1CD-B785BFACBACF}.Release|Any CPU.ActiveCfg = Release|Any CPU 75 | {D6295CC4-A592-46DC-B1CD-B785BFACBACF}.Release|Any CPU.Build.0 = Release|Any CPU 76 | {19040F1D-C753-4FD5-9D78-D9AB2EFE6891}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 77 | {19040F1D-C753-4FD5-9D78-D9AB2EFE6891}.Debug|Any CPU.Build.0 = Debug|Any CPU 78 | {19040F1D-C753-4FD5-9D78-D9AB2EFE6891}.Release|Any CPU.ActiveCfg = Release|Any CPU 79 | {19040F1D-C753-4FD5-9D78-D9AB2EFE6891}.Release|Any CPU.Build.0 = Release|Any CPU 80 | EndGlobalSection 81 | GlobalSection(NestedProjects) = preSolution 82 | {50235CF8-82BA-411A-8E08-6358AF60FEFF} = {B891FBEA-1F02-49C1-A570-1BB76E24DD1C} 83 | {7ABE96C5-C451-4260-936D-818C348512CB} = {B891FBEA-1F02-49C1-A570-1BB76E24DD1C} 84 | {146A605B-09A6-4E9B-9A1E-C030F7BC0627} = {1019D4BE-C564-41A2-9A54-64A8E166BFA2} 85 | {0D87520E-912F-40B7-A2D3-6C53EF19C718} = {594072AE-D250-4FE5-BAC2-36E09A8F0EF0} 86 | {B8021DD8-8871-41C4-9494-BDF28CAACA94} = {1019D4BE-C564-41A2-9A54-64A8E166BFA2} 87 | {76C4CA40-3FAC-44A0-808F-9C01AC42B4C3} = {1019D4BE-C564-41A2-9A54-64A8E166BFA2} 88 | {1DE03048-151C-4F15-ACA5-B075820C632C} = {1019D4BE-C564-41A2-9A54-64A8E166BFA2} 89 | {A740BF50-4F01-434F-B4CD-BAA81B2E3005} = {1019D4BE-C564-41A2-9A54-64A8E166BFA2} 90 | {D6295CC4-A592-46DC-B1CD-B785BFACBACF} = {594072AE-D250-4FE5-BAC2-36E09A8F0EF0} 91 | {19040F1D-C753-4FD5-9D78-D9AB2EFE6891} = {594072AE-D250-4FE5-BAC2-36E09A8F0EF0} 92 | EndGlobalSection 93 | EndGlobal 94 | -------------------------------------------------------------------------------- /SatelliteRpc.sln.DotSettings.user: -------------------------------------------------------------------------------- 1 |  2 | <SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 3 | <Solution /> 4 | </SessionState> 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/240225164316071.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InCerryGit/SatelliteRpc/7b4a56d76f3418cb3d8fab00c8412082bbba7161/assets/240225164316071.png -------------------------------------------------------------------------------- /assets/240225164318938.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InCerryGit/SatelliteRpc/7b4a56d76f3418cb3d8fab00c8412082bbba7161/assets/240225164318938.png -------------------------------------------------------------------------------- /samples/Client/Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/Client/DemoHostedService.cs: -------------------------------------------------------------------------------- 1 | using Client.Rpc; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using ServerProto; 5 | 6 | namespace Client; 7 | 8 | public class DemoHostedService : BackgroundService 9 | { 10 | private readonly ILogger _logger; 11 | private readonly ILoginClient _client; 12 | 13 | public DemoHostedService(ILogger logger, ILoginClient client) 14 | { 15 | _logger = logger; 16 | _client = client; 17 | } 18 | 19 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 20 | { 21 | _logger.LogInformation("DemoHostedService is starting"); 22 | 23 | var rp = await _client.Login(new LoginReqProto 24 | { 25 | User = "admin", 26 | Password = "123456", 27 | Sn = 1024 28 | }, stoppingToken); 29 | _logger.LogInformation("Received1: {IsOk},{ErrMsg},{Sn}", rp!.IsOk, rp.ErrMsg, rp.Sn); 30 | 31 | rp = await _client.Login(new LoginReqProto 32 | { 33 | User = "guest", 34 | Password = "654321", 35 | Sn = 2048 36 | }, stoppingToken); 37 | _logger.LogInformation("Received2: {IsOk},{ErrMsg},{Sn}", rp!.IsOk, rp.ErrMsg, rp.Sn); 38 | 39 | await _client.LogOut(stoppingToken); 40 | _logger.LogInformation("DemoHostedService is stopping"); 41 | } 42 | } -------------------------------------------------------------------------------- /samples/Client/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Client; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using SatelliteRpc.Client; 6 | using SatelliteRpc.Client.Extensions; 7 | 8 | await Host 9 | .CreateDefaultBuilder() 10 | .UseRpcClient(options => 11 | { 12 | options.ServerAddress = IPAddress.Parse("127.0.0.1"); 13 | options.ServerPort = 58886; 14 | }, middlewareBuilder => 15 | { 16 | middlewareBuilder.Use(async (next, context) => 17 | { 18 | Console.WriteLine($"Before {context.Request.Id}"); 19 | await next(context); 20 | Console.WriteLine($"After {context.Request.Id}"); 21 | }); 22 | }) 23 | .ConfigureServices(services => 24 | { 25 | services.AddAutoGeneratedClients(); 26 | services.AddHostedService(); 27 | }) 28 | .Build() 29 | .RunAsync(); -------------------------------------------------------------------------------- /samples/Client/Rpc/DemoLoginClient.cs: -------------------------------------------------------------------------------- 1 | // 自动化生成的客户端实例如下类似 2 | //using SatelliteRpc.Client; 3 | //using ServerProto; 4 | 5 | //namespace Client.Rpc; 6 | 7 | //public class DemoLoginClient : ILoginClient 8 | //{ 9 | // private readonly ISatelliteRpcClient _client; 10 | // 11 | // public DemoLoginClient(ISatelliteRpcClient client) 12 | // { 13 | // _client = client; 14 | // } 15 | // 16 | // public async Task Login(LoginReqProto req, CancellationToken? cancellationToken = null) 17 | // { 18 | // return await _client.InvokeAsync("LoginService/Login", req, cancellationToken); 19 | // } 20 | // 21 | // public Task LogOut(CancellationToken? cancellationToken = null) 22 | // { 23 | // return _client.InvokeAsync("LoginService/LogOut", cancellationToken); 24 | // } 25 | //} -------------------------------------------------------------------------------- /samples/Client/Rpc/ILoginClient.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Client; 2 | using ServerProto; 3 | 4 | namespace Client.Rpc; 5 | 6 | [SatelliteRpc("LoginService")] 7 | public interface ILoginClient 8 | { 9 | Task Login(LoginReqProto req, CancellationToken? cancellationToken = null); 10 | 11 | Task LogOut(CancellationToken? cancellationToken = null); 12 | } -------------------------------------------------------------------------------- /samples/Server/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Microsoft.Extensions.Hosting; 3 | using SatelliteRpc.Server.Extensions; 4 | 5 | await Host 6 | .CreateDefaultBuilder(args) 7 | .UseSatelliteRpcServer(builder => 8 | { 9 | // define your rpc server 10 | builder.ConfigureSatelliteRpcServer(options => 11 | { 12 | // define your options here 13 | options.Host = IPAddress.Parse("127.0.0.1"); 14 | options.Port = 58886; 15 | }); 16 | 17 | builder.AddRpcConnectionHandler(connMidBuilder => 18 | { 19 | connMidBuilder.Use(async (next, context) => 20 | { 21 | Console.WriteLine($"Before Connection Middleware {context.Request.Id}"); 22 | await next(context); 23 | Console.WriteLine($"After Connection Middleware {context.Request.Id}"); 24 | }); 25 | }); 26 | 27 | builder.AddRpcServiceMiddleware(serviceMidBuilder => 28 | { 29 | serviceMidBuilder.Use(async (next, context) => 30 | { 31 | Console.WriteLine($"Before Service Middleware {context.RawContext.Request.Id}"); 32 | await next(context); 33 | Console.WriteLine($"After Service Middleware {context.RawContext.Request.Id}"); 34 | }); 35 | }); 36 | }) 37 | .Build() 38 | .RunAsync(); -------------------------------------------------------------------------------- /samples/Server/Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/Server/Services/ILoginService.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Server.RpcService; 2 | using ServerProto; 3 | 4 | namespace Server.Services; 5 | 6 | public interface ILoginService : IRpcService 7 | { 8 | Task Login(LoginReqProto req, CancellationToken? cancellationToken); 9 | 10 | Task LogOut(CancellationToken? cancellationToken = null); 11 | } -------------------------------------------------------------------------------- /samples/Server/Services/LoginService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using ServerProto; 3 | 4 | namespace Server.Services; 5 | 6 | public class LoginService : ILoginService 7 | { 8 | private readonly ILogger _logger; 9 | 10 | public LoginService(ILogger logger) 11 | { 12 | _logger = logger; 13 | } 14 | 15 | public Task Login(LoginReqProto req, CancellationToken? cancellationToken) 16 | { 17 | _logger.LogInformation("Login: {UserName} {Password}", req.User, req.Password); 18 | return Task.FromResult(new LoginRespProto 19 | { 20 | IsOk = true, 21 | ErrMsg = "Not Implemented", 22 | Sn = req.Sn 23 | }); 24 | } 25 | 26 | public Task LogOut(CancellationToken? cancellationToken = null) 27 | { 28 | _logger.LogInformation("LogOut"); 29 | return Task.CompletedTask; 30 | } 31 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Client.SourceGenerator/GeneratorExecutionContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace SatelliteRpc.Client.SourceGenerator; 4 | 5 | /// 6 | /// The extension of GeneratorExecutionContext 7 | /// Use this class to log message, warning and error 8 | /// 9 | public static class GeneratorExecutionContextExtensions 10 | { 11 | public static void LogMessage(this GeneratorExecutionContext context, string message) 12 | { 13 | context.ReportDiagnostic(Diagnostic.Create( 14 | new DiagnosticDescriptor( 15 | id: "GEN001", 16 | title: "Generator Message", 17 | messageFormat: message, 18 | category: "SatelliteRpc.Generator", 19 | DiagnosticSeverity.Info, 20 | isEnabledByDefault: true), 21 | location: null)); 22 | } 23 | 24 | public static void LogWarning(this GeneratorExecutionContext context, string message) 25 | { 26 | context.ReportDiagnostic(Diagnostic.Create( 27 | new DiagnosticDescriptor( 28 | id: "GEN002", 29 | title: "Generator Warning", 30 | messageFormat: message, 31 | category: "SatelliteRpc.Generator", 32 | DiagnosticSeverity.Warning, 33 | isEnabledByDefault: true), 34 | location: null)); 35 | } 36 | 37 | public static void LogError(this GeneratorExecutionContext context, string message) 38 | { 39 | context.ReportDiagnostic(Diagnostic.Create( 40 | new DiagnosticDescriptor( 41 | id: "GEN003", 42 | title: "Generator Error", 43 | messageFormat: message, 44 | category: "SatelliteRpc.Generator", 45 | DiagnosticSeverity.Error, 46 | isEnabledByDefault: true), 47 | location: null)); 48 | } 49 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Client.SourceGenerator/SatelliteRpc.Client.SourceGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | enable 6 | enable 7 | true 8 | 11.0 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Client.SourceGenerator/SatelliteRpcClientGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using Microsoft.CodeAnalysis.Text; 5 | 6 | namespace SatelliteRpc.Client.SourceGenerator; 7 | 8 | /// 9 | /// The generator of SatelliteRpc client 10 | /// 11 | [Generator(LanguageNames.CSharp)] 12 | public class SatelliteRpcClientGenerator : ISourceGenerator 13 | { 14 | public void Initialize(GeneratorInitializationContext context) 15 | { 16 | context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); 17 | } 18 | 19 | public void Execute(GeneratorExecutionContext context) 20 | { 21 | 22 | context.LogMessage("Client Generator started"); 23 | 24 | if (context.SyntaxReceiver is not SyntaxReceiver receiver) 25 | return; 26 | 27 | var namespaceNameSet = new HashSet(); 28 | var clientClasses = new List(); 29 | const string namespaceName = "SatelliteRpc.Client"; 30 | 31 | foreach (var @interface in receiver.CandidateInterfaces) 32 | { 33 | // first get the semantic model for the interface, and make sure it's annotated 34 | var model = context.Compilation.GetSemanticModel(@interface.SyntaxTree); 35 | if (model.GetDeclaredSymbol(@interface) is not INamedTypeSymbol symbol) continue; 36 | if (!symbol.GetAttributes().Any(ad => 37 | ad.AttributeClass?.ToDisplayString() == "SatelliteRpc.Client.SatelliteRpcAttribute")) continue; 38 | 39 | var rpcAttribute = symbol.GetAttributes().FirstOrDefault(ad => 40 | ad.AttributeClass?.ToDisplayString() == "SatelliteRpc.Client.SatelliteRpcAttribute"); 41 | 42 | if (rpcAttribute == null) continue; 43 | 44 | var generateClient = (bool?)rpcAttribute 45 | .NamedArguments 46 | .FirstOrDefault(kvp => kvp.Key == "GenerateClient").Value.Value ?? true; 47 | var generateDependencyInjection = (bool?)rpcAttribute 48 | .NamedArguments 49 | .FirstOrDefault(kvp => kvp.Key == "GenerateDependencyInjection").Value.Value ?? true; 50 | 51 | // Skip if both are false 52 | if (!generateClient && !generateDependencyInjection) continue; 53 | 54 | // below is the code to generate the client 55 | var serviceName = (string?)rpcAttribute.ConstructorArguments.FirstOrDefault().Value; 56 | 57 | context.LogMessage(serviceName ?? "Null"); 58 | 59 | var className = symbol.Name.Substring(1) + "Client"; // Changed the class name 60 | var interfaceName = symbol.Name; 61 | 62 | if (generateDependencyInjection) 63 | { 64 | clientClasses.Add($"{interfaceName},{className}"); 65 | } 66 | 67 | var symbolNamespace = symbol.ContainingNamespace.ToDisplayString(); 68 | 69 | namespaceNameSet.Add(symbolNamespace); 70 | 71 | var stringBuilder = new StringBuilder(); 72 | 73 | stringBuilder.AppendLine("// "); 74 | stringBuilder.AppendLine("using SatelliteRpc.Client;"); 75 | stringBuilder.AppendLine("using SatelliteRpc.Client.Transport;"); 76 | stringBuilder.AppendLine("using ServerProto;"); 77 | stringBuilder.AppendLine("using System.Threading.Tasks;"); 78 | stringBuilder.AppendLine("using System.Threading;"); 79 | stringBuilder.AppendLine($"using {symbolNamespace};"); 80 | stringBuilder.AppendLine(); 81 | stringBuilder.AppendLine($"namespace {symbolNamespace}"); 82 | stringBuilder.AppendLine("{"); 83 | stringBuilder.AppendLine($" public class {className} : {interfaceName}"); 84 | stringBuilder.AppendLine(" {"); 85 | stringBuilder.AppendLine(" private readonly ISatelliteRpcClient _client;"); 86 | stringBuilder.AppendLine(); 87 | stringBuilder.AppendLine($" public {className}(ISatelliteRpcClient client)"); 88 | stringBuilder.AppendLine(" {"); 89 | stringBuilder.AppendLine(" _client = client;"); 90 | stringBuilder.AppendLine(" }"); 91 | 92 | // Assume all methods return Task or Task 93 | foreach (var member in symbol.GetMembers().OfType()) 94 | { 95 | 96 | var parameters = string.Join(", ", member.Parameters.Select(p => $"{p.Type} {p.Name}")); 97 | var callParameters = string.Join(",", member.Parameters.Select(p => p.Name)); 98 | var methodName = member.Name; 99 | 100 | var returnType = member.ReturnType.ToString() == "System.Threading.Tasks.Task" 101 | ? "Task" 102 | : $"Task<{((INamedTypeSymbol)member.ReturnType).TypeArguments[0].Name}>"; // Changed the return type 103 | 104 | var invokeAsync = member.Parameters.Length > 1 105 | ? $"await _client.InvokeAsync<{member.Parameters[0].Type.Name}, {((INamedTypeSymbol)member.ReturnType).TypeArguments[0].Name}>" 106 | : "await _client.InvokeAsync"; 107 | 108 | 109 | stringBuilder.AppendLine(); 110 | stringBuilder.AppendLine($" public async {returnType} {methodName}({parameters})"); 111 | stringBuilder.AppendLine(" {"); 112 | if (returnType.StartsWith("Task<")) 113 | stringBuilder.AppendLine($" return {invokeAsync}(\"{serviceName}/{methodName}\", {callParameters});"); 114 | else 115 | stringBuilder.AppendLine($" {invokeAsync}(\"{serviceName}/{methodName}\", {callParameters});"); 116 | stringBuilder.AppendLine(" }"); 117 | 118 | context.LogMessage($"Generator method: {methodName} success"); 119 | } 120 | 121 | stringBuilder.AppendLine(" }"); 122 | stringBuilder.AppendLine("}"); 123 | 124 | context.AddSource($"{className}.g.cs", SourceText.From(stringBuilder.ToString(), Encoding.UTF8)); 125 | 126 | context.LogMessage("Client Generator finished"); 127 | 128 | context.LogMessage("DI Generator started"); 129 | 130 | // Generate a new class to register clients to DI container 131 | var registrationClassBuilder = new StringBuilder(); 132 | registrationClassBuilder.AppendLine("// "); 133 | registrationClassBuilder.AppendLine("using Microsoft.Extensions.DependencyInjection;"); 134 | foreach (var name in namespaceNameSet) 135 | { 136 | registrationClassBuilder.AppendLine($"using {name};"); 137 | } 138 | registrationClassBuilder.AppendLine(); 139 | registrationClassBuilder.AppendLine($"namespace {namespaceName}"); 140 | registrationClassBuilder.AppendLine("{"); 141 | registrationClassBuilder.AppendLine(" public static class RpcClientServiceCollectionExtensions"); 142 | registrationClassBuilder.AppendLine(" {"); 143 | registrationClassBuilder.AppendLine(" public static IServiceCollection AddAutoGeneratedClients(this IServiceCollection services)"); 144 | registrationClassBuilder.AppendLine(" {"); 145 | 146 | for (int i = 0; i < clientClasses.Count; i++) 147 | { 148 | registrationClassBuilder.AppendLine($" services.AddSingleton<{clientClasses[i]}>();"); 149 | } 150 | 151 | registrationClassBuilder.AppendLine(" return services;"); 152 | registrationClassBuilder.AppendLine(" }"); 153 | registrationClassBuilder.AppendLine(" }"); 154 | registrationClassBuilder.AppendLine("}"); 155 | 156 | context.AddSource("RpcClientServiceCollectionExtensions.g.cs", SourceText.From(registrationClassBuilder.ToString(), Encoding.UTF8)); 157 | 158 | context.LogMessage("DI Generator finished"); 159 | } 160 | } 161 | 162 | private class SyntaxReceiver : ISyntaxReceiver 163 | { 164 | public List CandidateInterfaces { get; } = new(); 165 | 166 | public void OnVisitSyntaxNode(SyntaxNode syntaxNode) 167 | { 168 | // We only care about interface declarations 169 | if (syntaxNode is InterfaceDeclarationSyntax { AttributeLists.Count: > 0 } @interface) 170 | { 171 | CandidateInterfaces.Add(@interface); 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/Configuration/SatelliteRpcClientOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using SatelliteRpc.Protocol.Protocol; 3 | 4 | namespace SatelliteRpc.Client.Configuration; 5 | 6 | public class SatelliteRpcClientOptions 7 | { 8 | /// 9 | /// Rpc remote server address 10 | /// 11 | public IPAddress ServerAddress { get; set; } = IPAddress.Parse("127.0.0.1"); 12 | 13 | /// 14 | /// Rpc remote server port 15 | /// 16 | public int ServerPort { get; set; } = 58888; 17 | 18 | /// 19 | /// Request channel max count 20 | /// 21 | public int RequestChannelMaxCount { get; set; } = 1024; 22 | 23 | /// 24 | /// The request default serializer type 25 | /// 26 | public PayloadType PayloadType { get; set; } = PayloadType.Protobuf; 27 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/Extensions/HostBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using SatelliteRpc.Client.Configuration; 4 | using SatelliteRpc.Client.Transport; 5 | using SatelliteRpc.Shared.Application; 6 | 7 | namespace SatelliteRpc.Client.Extensions; 8 | 9 | public static class HostBuilderExtensions 10 | { 11 | /// 12 | /// Use Rpc Client 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | public static IHostBuilder UseRpcClient( 19 | this IHostBuilder builder, 20 | Action? configure = null, 21 | Action>? configureMiddleware = null) 22 | { 23 | builder.ConfigureServices(services => { services.Configure(configure ?? (_ => { })); }); 24 | return builder.ConfigureServices(services => { services.AddRpcClient(configureMiddleware); }); 25 | } 26 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using SatelliteRpc.Client.Middleware; 4 | using SatelliteRpc.Client.Transport; 5 | using SatelliteRpc.Protocol.PayloadConverters; 6 | using SatelliteRpc.Shared.Application; 7 | 8 | namespace SatelliteRpc.Client.Extensions; 9 | 10 | public static class ServiceCollectionExtensions 11 | { 12 | /// 13 | /// Add Rpc Client 14 | /// Register and 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static IServiceCollection AddRpcClient(this IServiceCollection services, 20 | Action>? configureMiddleware) 21 | { 22 | services.TryAddTransient(); 23 | services.TryAddTransient(); 24 | 25 | services.TryAddSingleton(); 26 | services.TryAddSingleton(); 27 | services.TryAddSingleton(provider => 28 | new RpcClientMiddlewareBuilder(provider, configureMiddleware)); 29 | return services; 30 | } 31 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/Middleware/IRpcClientMiddleware.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Client.Transport; 2 | using SatelliteRpc.Shared.Application; 3 | 4 | namespace SatelliteRpc.Client.Middleware; 5 | 6 | /// 7 | /// The middleware interface of Rpc Client 8 | /// 9 | public interface IRpcClientMiddleware : IApplicationMiddleware 10 | { 11 | 12 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/Middleware/IRpcClientMiddlewareBuilder.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Client.Transport; 2 | using SatelliteRpc.Shared.Application; 3 | 4 | namespace SatelliteRpc.Client.Middleware; 5 | 6 | /// 7 | /// The middleware builder interface of Rpc Client 8 | /// 9 | public interface IRpcClientMiddlewareBuilder 10 | { 11 | /// 12 | /// Build the middleware 13 | /// 14 | /// 15 | ApplicationDelegate Build(); 16 | 17 | /// 18 | /// The application middleware builder 19 | /// 20 | ApplicationBuilder Builder { get; } 21 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/Middleware/RpcClientMiddlewareBuilder.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Client.Transport; 2 | using SatelliteRpc.Shared.Application; 3 | 4 | namespace SatelliteRpc.Client.Middleware; 5 | 6 | public class RpcClientMiddlewareBuilder : IRpcClientMiddlewareBuilder 7 | { 8 | /// 9 | /// The application middleware builder 10 | /// 11 | public ApplicationBuilder Builder { get; } 12 | 13 | /// 14 | /// Constructor 15 | /// 16 | /// service provider 17 | /// configure middleware action 18 | public RpcClientMiddlewareBuilder( 19 | IServiceProvider services, 20 | Action>? configure = null) 21 | { 22 | Builder = new ApplicationBuilder(services); 23 | configure?.Invoke(Builder); 24 | } 25 | 26 | /// 27 | /// Build the middleware 28 | /// 29 | /// 30 | public ApplicationDelegate Build() 31 | { 32 | return Builder.Build(); 33 | } 34 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/SatelliteRpc.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/SatelliteRpcAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Client; 2 | 3 | /// 4 | /// Rpc service attribute, used to mark the interface as a rpc service 5 | /// Generate the client and dependency injection by default 6 | /// 7 | [AttributeUsage(AttributeTargets.Interface)] 8 | public class SatelliteRpcAttribute : Attribute 9 | { 10 | public SatelliteRpcAttribute(string serviceName) 11 | { 12 | ServiceName = serviceName; 13 | } 14 | 15 | public string ServiceName { get; set; } 16 | 17 | public bool GenerateClient { get; set; } = true; 18 | 19 | public bool GenerateDependencyInjection { get; set; } = true; 20 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/Transport/CallContext.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Protocol.Protocol; 2 | 3 | namespace SatelliteRpc.Client.Transport; 4 | 5 | /// 6 | /// The call context of Rpc Client 7 | /// Use this class to pass data between middleware 8 | /// 9 | public class CallContext : IDisposable 10 | { 11 | /// 12 | /// Request cancellation token 13 | /// Pass client call cancellation token to middleware 14 | /// 15 | public CancellationToken Cancel { get; set; } 16 | 17 | /// 18 | /// Rpc request 19 | /// 20 | public AppRequest Request { get; set; } 21 | 22 | /// 23 | /// Rpc response 24 | /// 25 | public AppResponse? Response { get; set; } 26 | 27 | /// 28 | /// Constructor 29 | /// 30 | /// 31 | /// 32 | public CallContext(AppRequest request, CancellationToken cancel) 33 | { 34 | Request = request; 35 | Cancel = cancel; 36 | } 37 | 38 | /// 39 | /// When the context is disposed, the request and response will be disposed 40 | /// 41 | public void Dispose() 42 | { 43 | Request.Dispose(); 44 | Response?.Dispose(); 45 | } 46 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/Transport/DefaultSatelliteRpcClient.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Microsoft.Extensions.Options; 3 | using SatelliteRpc.Client.Configuration; 4 | using SatelliteRpc.Client.Middleware; 5 | using SatelliteRpc.Protocol.PayloadConverters; 6 | using SatelliteRpc.Protocol.Protocol; 7 | using SatelliteRpc.Shared.Application; 8 | 9 | namespace SatelliteRpc.Client.Transport; 10 | 11 | /// 12 | /// Default implementation of the ISatelliteRpcClient interface. 13 | /// This class is responsible for invoking remote methods. 14 | /// 15 | public class DefaultSatelliteRpcClient : ISatelliteRpcClient 16 | { 17 | private ulong _id; 18 | 19 | private readonly IPayloadConverter _payloadConverter; 20 | private readonly ApplicationDelegate _clientMiddleware; 21 | 22 | /// 23 | /// Initializes a new instance of the class. 24 | /// 25 | /// Provides access to the SatelliteRpcClientOptions. 26 | /// The RPC connection to use. 27 | /// The source for payload converters. 28 | /// The middleware builder. 29 | public DefaultSatelliteRpcClient( 30 | IOptions optionsAccessor, 31 | IRpcConnection connection, 32 | PayloadConverterSource converterSource, 33 | IRpcClientMiddlewareBuilder builder) 34 | { 35 | var options = optionsAccessor.Value; 36 | 37 | _payloadConverter = converterSource.GetConverter(options.PayloadType); 38 | 39 | // The last middleware, used to send network requests 40 | builder.Builder.Use(async (_, context) => { await connection.RequestMiddlewareAsync(context); }); 41 | _clientMiddleware = builder.Build(); 42 | } 43 | 44 | /// 45 | /// Invoke a remote method without a payload. 46 | /// 47 | /// The remote method path. 48 | /// A cancellation token that can be used to cancel the operation. 49 | public async Task InvokeAsync(string path, CancellationToken? cancellationToken = null) 50 | { 51 | // because the request payload is empty, so we use the Empty type 52 | await InvokeAsync(path, PayloadWriter.Empty, cancellationToken); 53 | } 54 | 55 | /// 56 | /// Invoke a remote method with a specified request and response type. 57 | /// 58 | /// The remote method path. 59 | /// The request payload. 60 | /// A cancellation token that can be used to cancel the operation. 61 | /// The type of the request payload. 62 | /// The type of the response payload. 63 | /// A task that represents the asynchronous operation. The task result contains the response payload. 64 | public async Task InvokeAsync( 65 | string path, 66 | TRequest rawRequest, 67 | CancellationToken? cancellationToken = null) 68 | { 69 | var payload = _payloadConverter.CreatePayloadWriter(rawRequest); 70 | return await InvokeAsync(path, payload, cancellationToken); 71 | } 72 | 73 | /// 74 | /// Private helper method to invoke a remote method with a specified response type. 75 | /// 76 | /// The remote method path. 77 | /// The request payload writer. 78 | /// A cancellation token that can be used to cancel the operation. 79 | /// The type of the response payload. 80 | /// A task that represents the asynchronous operation. The task result contains the response payload. 81 | private async ValueTask InvokeAsync( 82 | string path, 83 | PayloadWriter payload, 84 | CancellationToken? cancellationToken = null) 85 | { 86 | var request = new AppRequest 87 | { 88 | Id = Interlocked.Increment(ref _id), 89 | Path = path, 90 | PayloadWriter = payload 91 | }; 92 | 93 | // Create a new call context 94 | // The call context will be disposed after the request is completed 95 | using var callContext = new CallContext(request, cancellationToken ?? CancellationToken.None); 96 | await _clientMiddleware(callContext); 97 | 98 | // if the response status is not success, the payload is the error message 99 | // this is a convention 100 | if (callContext.Response!.Status != ResponseStatus.Success) 101 | { 102 | throw new Exception(Encoding.UTF8.GetString(callContext.Response.Payload.Span)); 103 | } 104 | 105 | return typeof(TResponse) == typeof(Empty) 106 | ? default 107 | : (TResponse?)_payloadConverter.Convert(callContext.Response.Payload, typeof(TResponse)); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/Transport/IRpcConnection.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Client.Transport; 2 | 3 | public interface IRpcConnection 4 | { 5 | /// 6 | /// Send request to server middleware 7 | /// 8 | /// 9 | /// 10 | ValueTask RequestMiddlewareAsync(CallContext callContext); 11 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/Transport/ISatelliteRpcClient.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Client.Transport; 2 | 3 | public interface ISatelliteRpcClient 4 | { 5 | /// 6 | /// Invoke the remote method 7 | /// 8 | /// 9 | /// 10 | /// 11 | Task InvokeAsync(string path, CancellationToken? cancellationToken = null); 12 | 13 | /// 14 | /// Invoke the remote method 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | Task InvokeAsync( 23 | string path, 24 | TRequest rawRequest, 25 | CancellationToken? cancellationToken = null); 26 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Client/Transport/RpcConnection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.IO.Pipelines; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | using System.Threading.Channels; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Extensions.Options; 9 | using SatelliteRpc.Client.Configuration; 10 | using SatelliteRpc.Protocol.Protocol; 11 | 12 | namespace SatelliteRpc.Client.Transport; 13 | 14 | /// 15 | /// Represents a connection for executing remote procedure calls (RPCs). 16 | /// This class implements both and . 17 | /// 18 | public class RpcConnection : IRpcConnection, IDisposable 19 | { 20 | private readonly ILogger _logger; 21 | private readonly PipeReader _pipeReader; 22 | private readonly PipeWriter _pipeWriter; 23 | 24 | // Channel for sending requests. 25 | private readonly Channel _requestChannel; 26 | 27 | // Table for tracking response tasks. 28 | private readonly ConcurrentDictionary> _responseTable = new(); 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | public RpcConnection( 34 | ILogger logger, 35 | IOptions optionsAccessor, 36 | IHostApplicationLifetime lifetime) 37 | { 38 | _logger = logger; 39 | var options = optionsAccessor.Value; 40 | 41 | _requestChannel = Channel.CreateBounded( 42 | new BoundedChannelOptions(options.RequestChannelMaxCount) 43 | { 44 | SingleReader = true, 45 | SingleWriter = false, 46 | FullMode = BoundedChannelFullMode.Wait, 47 | }); 48 | 49 | var tcpClient = new TcpClient(); 50 | tcpClient.Connect(new IPEndPoint(options.ServerAddress, options.ServerPort)); 51 | var stream = tcpClient.GetStream(); 52 | _pipeReader = PipeReader.Create(stream); 53 | _pipeWriter = PipeWriter.Create(stream); 54 | 55 | // Start write and read tasks. 56 | _ = WriteAsync(lifetime.ApplicationStopping); 57 | _ = ReadAsync(lifetime.ApplicationStopping); 58 | 59 | // Register to dispose when application is stopping. 60 | lifetime.ApplicationStopping.Register(Dispose); 61 | } 62 | 63 | /// 64 | /// Middleware for handling RPC requests. 65 | /// 66 | public async ValueTask RequestMiddlewareAsync(CallContext callContext) 67 | { 68 | var request = callContext.Request; 69 | var cancellationToken = callContext.Cancel; 70 | var tcs = new TaskCompletionSource(); 71 | _responseTable.TryAdd(request.Id, tcs); 72 | try 73 | { 74 | await _requestChannel.Writer.WriteAsync(request, cancellationToken); 75 | 76 | // When cancel, remove TaskCompletionSource from table, and throw exception. 77 | cancellationToken.Register(() => 78 | { 79 | if (_responseTable.TryRemove(request.Id, out var resTcs)) 80 | { 81 | resTcs.TrySetCanceled(cancellationToken); 82 | } 83 | }); 84 | 85 | callContext.Response = await tcs.Task; 86 | } 87 | finally 88 | { 89 | _responseTable.TryRemove(request.Id, out _); 90 | } 91 | } 92 | 93 | /// 94 | /// This class is responsible for writing RPC requests asynchronously to a pipe writer. 95 | /// 96 | private async Task WriteAsync(CancellationToken cancellationToken) 97 | { 98 | try 99 | { 100 | // Continuously write to the pipe writer while there are requests to be read 101 | while (await _requestChannel.Reader.WaitToReadAsync(cancellationToken)) 102 | { 103 | while (_requestChannel.Reader.TryRead(out var request)) 104 | { 105 | try 106 | { 107 | // Serialize the request and write it to the pipe writer 108 | request.Serialize(_pipeWriter); 109 | await _pipeWriter.FlushAsync(cancellationToken); 110 | } 111 | catch (Exception ex) 112 | { 113 | _logger.LogError(ex, "[{Id}]Write request error", request.Id); 114 | 115 | // If an error occurs, set the response status to InternalError 116 | SetResponse(new AppResponse 117 | { 118 | Id = request.Id, 119 | Status = ResponseStatus.InternalError 120 | }); 121 | } 122 | } 123 | } 124 | } 125 | catch (Exception ex) 126 | { 127 | _logger.LogError(ex, "WriteAsync error"); 128 | } 129 | } 130 | 131 | /// 132 | /// This class is responsible for reading RPC responses asynchronously from a pipe reader. 133 | /// 134 | private async Task ReadAsync(CancellationToken cancellationToken) 135 | { 136 | try 137 | { 138 | // Continuously read from the pipe reader until a cancellation is requested 139 | while (cancellationToken.IsCancellationRequested == false) 140 | { 141 | var result = await _pipeReader.ReadAsync(cancellationToken); 142 | if (result.IsCanceled) 143 | { 144 | break; 145 | } 146 | 147 | // Attempt to deserialize the read buffer into an AppResponse 148 | var (success, response) = AppResponse.TryDeserialize(result.Buffer, out var consumed); 149 | 150 | // If the deserialization failed, continue to the next iteration 151 | if (success == false) 152 | { 153 | continue; 154 | } 155 | 156 | // If the deserialization succeeded, set the response and advance the pipe reader 157 | SetResponse(response!); 158 | _pipeReader.AdvanceTo(consumed); 159 | 160 | // If the read operation completed, break the loop 161 | if (result.IsCompleted) 162 | { 163 | break; 164 | } 165 | } 166 | } 167 | catch (Exception ex) 168 | { 169 | _logger.LogError(ex, "ReadAsync error"); 170 | } 171 | } 172 | 173 | 174 | /// 175 | /// Sets the response for a request. 176 | /// 177 | private void SetResponse(AppResponse response) 178 | { 179 | if (_responseTable.TryGetValue(response.Id, out var tcs)) 180 | { 181 | tcs.SetResult(response); 182 | } 183 | } 184 | 185 | /// 186 | /// Disposes resources used by the connection. 187 | /// 188 | public void Dispose() 189 | { 190 | _pipeWriter.CancelPendingFlush(); 191 | _pipeWriter.Complete(); 192 | 193 | _pipeReader.CancelPendingRead(); 194 | _pipeReader.Complete(); 195 | } 196 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Protocol/BasicProtocolParser.cs: -------------------------------------------------------------------------------- 1 | // using System.Buffers; 2 | // using System.Buffers.Binary; 3 | // using System.Diagnostics.CodeAnalysis; 4 | // using System.Runtime.CompilerServices; 5 | // using Google.Protobuf; 6 | // 7 | // namespace SatelliteRpc.Protocol; 8 | // 9 | // public static class BasicProtocolParser 10 | // { 11 | // [MethodImpl(MethodImplOptions.AggressiveInlining)] 12 | // public static bool TryParserRequest( 13 | // ReadOnlySequence sequence, 14 | // out SequencePosition consumed, 15 | // [MaybeNullWhen(returnValue: false)] out AppRequest request) 16 | // { 17 | // // Frame format is defined here: 18 | // // 8 bytes for length of payload 19 | // // N bytes of payload 20 | // request = null; 21 | // consumed = default; 22 | // 23 | // if (sequence.Length < 8) 24 | // return false; 25 | // 26 | // var length = GetLength(sequence); 27 | // if (length > sequence.Length) 28 | // return false; 29 | // 30 | // var payload = sequence.Slice(8, length); 31 | // request = AppRequest.Parser.ParseFrom(payload); 32 | // 33 | // consumed = sequence.GetPosition(8 + length); 34 | // return true; 35 | // } 36 | // 37 | // [MethodImpl(MethodImplOptions.AggressiveInlining)] 38 | // private static long GetLength(in ReadOnlySequence buffer) 39 | // { 40 | // if (buffer.First.Length >= 8) 41 | // { 42 | // return BinaryPrimitives.ReadInt64LittleEndian(buffer.First.Span[..8]); 43 | // } 44 | // 45 | // Span lengthBuffer = stackalloc byte[8]; 46 | // buffer.Slice(0, 8).CopyTo(lengthBuffer); 47 | // return BinaryPrimitives.ReadInt64LittleEndian(lengthBuffer); 48 | // } 49 | // 50 | // [MethodImpl(MethodImplOptions.AggressiveInlining)] 51 | // public static bool TryParserResponse( 52 | // ReadOnlySequence sequence, 53 | // out SequencePosition consumed, 54 | // [MaybeNullWhen(returnValue: false)] out AppResponse response) 55 | // { 56 | // // Frame format is defined here: 57 | // // 8 bytes for length of payload 58 | // // N bytes of payload 59 | // response = null; 60 | // consumed = default; 61 | // 62 | // if (sequence.Length < 8) 63 | // return false; 64 | // 65 | // var length = GetLength(sequence); 66 | // if (length > sequence.Length) 67 | // return false; 68 | // 69 | // var payload = sequence.Slice(8, length); 70 | // response = AppResponse.Parser.ParseFrom(payload); 71 | // 72 | // consumed = sequence.GetPosition(8 + length); 73 | // return true; 74 | // } 75 | // 76 | // [MethodImpl(MethodImplOptions.AggressiveInlining)] 77 | // public static void WriteResponseHeader(IBufferWriter pipeWriter, AppResponse response) 78 | // { 79 | // var length = response.CalculateSize(); 80 | // Span lengthBuffer = stackalloc byte[8]; 81 | // BinaryPrimitives.WriteInt64LittleEndian(lengthBuffer, length); 82 | // pipeWriter.Write(lengthBuffer); 83 | // response.WriteTo(pipeWriter); 84 | // } 85 | // 86 | // [MethodImpl(MethodImplOptions.AggressiveInlining)] 87 | // public static void WriteRequestHeader(IBufferWriter pipeWriter, AppRequest request) 88 | // { 89 | // var length = request.CalculateSize(); 90 | // Span lengthBuffer = stackalloc byte[8]; 91 | // BinaryPrimitives.WriteInt64LittleEndian(lengthBuffer, length); 92 | // pipeWriter.Write(lengthBuffer); 93 | // request.WriteTo(pipeWriter); 94 | // } 95 | // } -------------------------------------------------------------------------------- /src/SatelliteRpc.Protocol/PayloadConverters/IPayloadConverter.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Protocol.Protocol; 2 | using SatelliteRpc.Shared.Collections; 3 | 4 | namespace SatelliteRpc.Protocol.PayloadConverters; 5 | 6 | public interface IPayloadConverter 7 | { 8 | /// 9 | /// Payload type 10 | /// 11 | /// 12 | PayloadType PayloadType { get; } 13 | 14 | /// 15 | /// Convert payload to bytes 16 | /// 17 | /// 18 | /// 19 | /// 20 | object? Convert(PooledArray payload, Type type); 21 | 22 | /// 23 | /// Create payload writer 24 | /// for performance reasons, we use the PayloadWriter to write the payload to the network buffer 25 | /// 26 | /// 27 | /// 28 | PayloadWriter CreatePayloadWriter(object? payload); 29 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Protocol/PayloadConverters/PayloadConverterSource.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Protocol.Protocol; 2 | 3 | namespace SatelliteRpc.Protocol.PayloadConverters; 4 | 5 | /// 6 | /// Manage payload converters 7 | /// 8 | public class PayloadConverterSource 9 | { 10 | private readonly IReadOnlyDictionary _converters; 11 | 12 | public PayloadConverterSource(IEnumerable converters) 13 | { 14 | _converters = converters.ToDictionary(c => c.PayloadType); 15 | } 16 | 17 | /// 18 | /// Get payload converter by payload type 19 | /// 20 | /// 21 | /// 22 | /// 23 | public IPayloadConverter GetConverter(PayloadType payloadType) 24 | { 25 | if (_converters.TryGetValue(payloadType, out var converter)) 26 | { 27 | return converter; 28 | } 29 | 30 | throw new Exception($"No converter found for type {payloadType}"); 31 | } 32 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Protocol/PayloadConverters/ProtocolBufferPayloadConverter.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using SatelliteRpc.Protocol.Protocol; 3 | using SatelliteRpc.Shared.Collections; 4 | 5 | namespace SatelliteRpc.Protocol.PayloadConverters; 6 | 7 | /// 8 | /// Payload converter for protobuf 9 | /// 10 | public class ProtocolBufferPayloadConverter : IPayloadConverter 11 | { 12 | public PayloadType PayloadType => PayloadType.Protobuf; 13 | 14 | /// 15 | /// Create payload writer 16 | /// for performance reasons, we use the PayloadWriter to write the payload to the network buffer 17 | /// 18 | /// 19 | /// 20 | public PayloadWriter CreatePayloadWriter(object? payload) 21 | { 22 | return payload switch 23 | { 24 | null => PayloadWriter.Empty, 25 | IMessage message => new PayloadWriter 26 | { 27 | GetPayloadSize = () => message.CalculateSize(), 28 | PayloadWriteTo = (buffer) => message.WriteTo(buffer) 29 | }, 30 | _ => throw new Exception("Invalid response type") 31 | }; 32 | } 33 | 34 | /// 35 | /// Convert payload to bytes 36 | /// 37 | /// 38 | /// 39 | /// 40 | public object? Convert(PooledArray payload, Type type) 41 | { 42 | if (!typeof(IMessage).IsAssignableFrom(type)) 43 | { 44 | throw new ArgumentException("Type must be protobuf message", nameof(type)); 45 | } 46 | 47 | if (payload.Length == 0) 48 | { 49 | return null; 50 | } 51 | 52 | var message = Activator.CreateInstance(type) as IMessage; 53 | message!.MergeFrom(payload.Span); 54 | return message!; 55 | } 56 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Protocol/Protocol/AppRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Buffers.Binary; 3 | using System.Text; 4 | using SatelliteRpc.Shared; 5 | using SatelliteRpc.Shared.Collections; 6 | 7 | namespace SatelliteRpc.Protocol.Protocol; 8 | 9 | /// 10 | /// AppRequest 11 | /// 12 | public class AppRequest : DisposeManager 13 | { 14 | /// 15 | /// Total length of the request 16 | /// 17 | public int TotalLength { get; set; } 18 | 19 | /// 20 | /// Request Id, used to match the response 21 | /// for multiplexing 22 | /// 23 | public ulong Id { get; set; } 24 | 25 | /// 26 | /// Request path 27 | /// 28 | public string Path { get; set; } 29 | 30 | /// 31 | /// Payload type 32 | /// 33 | public PayloadType PayloadType { get; set; } 34 | 35 | /// 36 | /// Payload 37 | /// if the payload is empty, then the PayloadWriter is used 38 | /// note: because performance reasons, we use the PayloadWriter to write the payload to the network buffer. 39 | /// 40 | public PooledArray Payload { get; set; } 41 | 42 | /// 43 | /// Payload writer 44 | /// 45 | public PayloadWriter PayloadWriter { get; set; } = default; 46 | 47 | /// 48 | /// Get this request size 49 | /// 50 | /// 51 | public (int HeadLength, int PayloadLength) GetSize() 52 | { 53 | // sum every field length 54 | return PayloadWriter.HasPayload 55 | ? (sizeof(ulong) + Encoding.UTF8.GetByteCount(Path) + 1 + sizeof(int) + sizeof(int), PayloadWriter.GetPayloadSize()) 56 | : (sizeof(ulong) + Encoding.UTF8.GetByteCount(Path) + 1 + sizeof(int) + sizeof(int), Payload.Length); 57 | } 58 | 59 | public void Serialize(IBufferWriter writer) 60 | { 61 | // The data format is as follows: 62 | // 4 bytes total length 63 | // N bytes header 64 | // N bytes payload 65 | 66 | var length = GetSize(); 67 | var totalLength = length.HeadLength + length.PayloadLength + 4; 68 | scoped var span = writer.GetSpan(totalLength); 69 | 70 | // write total length 71 | BinaryPrimitives.WriteInt32LittleEndian(span, length.HeadLength + length.PayloadLength); 72 | span = span[sizeof(int)..]; 73 | 74 | // write id 75 | BinaryPrimitives.WriteUInt64LittleEndian(span, Id); 76 | span = span[sizeof(ulong)..]; 77 | 78 | // write path, and add null terminator 79 | var pathBytes = Encoding.UTF8.GetBytes(Path); 80 | pathBytes.CopyTo(span); 81 | span[pathBytes.Length] = 0; // Null terminator 82 | span = span[(pathBytes.Length + 1)..]; 83 | 84 | // write payload type 85 | BinaryPrimitives.WriteInt32LittleEndian(span, (int)PayloadType); 86 | span = span[sizeof(int)..]; 87 | 88 | // write payload length 89 | BinaryPrimitives.WriteInt32LittleEndian(span, length.PayloadLength); 90 | span = span[sizeof(int)..]; 91 | 92 | // for performance reasons, we use the PayloadWriter to write the payload to the network buffer 93 | // reduced memory copying 94 | if (!PayloadWriter.HasPayload) 95 | { 96 | Payload.Span.CopyTo(span); 97 | writer.Advance(totalLength); 98 | } 99 | else 100 | { 101 | writer.Advance(length.HeadLength + 4); 102 | PayloadWriter.PayloadWriteTo(writer); 103 | } 104 | } 105 | 106 | /// 107 | /// Try deserialize 108 | /// 109 | /// 110 | /// 111 | /// 112 | /// 113 | public static (bool Success, AppRequest? Request) TryDeserialize( 114 | ReadOnlySequence sequence, 115 | out SequencePosition consumed, 116 | AppRequest? reuse = null) 117 | { 118 | // The data format is as follows: 119 | // 4 bytes total length 120 | // N bytes header 121 | // N bytes payload 122 | 123 | consumed = default; 124 | 125 | // if the sequence length is less than 4, it means that the request is not complete 126 | if (sequence.Length < 4) 127 | { 128 | return (false, null); 129 | } 130 | 131 | // read the total length of the request, 132 | // if the sequence length is less than the total length, 133 | // it means that the request is not complete 134 | var reader = new SequenceReader(sequence); 135 | reader.TryReadLittleEndian(out int totalLength); 136 | if (sequence.Length < totalLength + 4) 137 | { 138 | return (false, null); 139 | } 140 | 141 | // Deserialize the request 142 | var request = reuse ?? new AppRequest(); 143 | request.TotalLength = totalLength; 144 | 145 | // read id 146 | reader.TryReadLittleEndian(out long id); 147 | request.Id = (ulong)id; 148 | 149 | // read path 150 | if (reader.TryReadTo(out ReadOnlySequence pathSequence, (byte)'\0')) 151 | { 152 | request.Path = Encoding.UTF8.GetString(pathSequence.ToArray()); 153 | } 154 | 155 | // read payload type 156 | reader.TryReadLittleEndian(out int payloadType); 157 | request.PayloadType = (PayloadType)payloadType; 158 | 159 | // read payload length 160 | reader.TryReadLittleEndian(out int payloadLength); 161 | 162 | // for performance reasons, we use the PooledArray to store the payload 163 | // PooledArray use memory pool 164 | var payload = new PooledArray(payloadLength); 165 | reader.Sequence.Slice(reader.Position, payloadLength).CopyTo(payload.Span); 166 | 167 | // The payload is stored in the PooledArray, 168 | // so we need to register the PooledArray to the DisposeManager, 169 | // when the AppRequest is disposed, the PooledArray will be automatically returned to the memory pool 170 | request.RegisterForDispose(payload); 171 | request.Payload = payload; 172 | reader.Advance(payloadLength); 173 | 174 | // consumed 175 | consumed = reader.Position; 176 | 177 | return (true, request); 178 | } 179 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Protocol/Protocol/AppResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Buffers.Binary; 3 | using SatelliteRpc.Shared; 4 | using SatelliteRpc.Shared.Collections; 5 | 6 | namespace SatelliteRpc.Protocol.Protocol; 7 | 8 | /// 9 | /// AppResponse 10 | /// 11 | public class AppResponse : DisposeManager 12 | { 13 | /// 14 | /// Total length of the request 15 | /// 16 | public int TotalLength { get; set; } 17 | 18 | /// 19 | /// Request Id, used to match the request 20 | /// for multiplexing 21 | /// 22 | public ulong Id { get; set; } 23 | 24 | /// 25 | /// Response status 26 | /// 27 | /// 28 | public ResponseStatus Status { get; set; } 29 | 30 | /// 31 | /// Payload type 32 | /// 33 | public PayloadType PayloadType { get; set; } 34 | 35 | /// 36 | /// Payload 37 | /// if the payload is empty, then the PayloadWriter is used 38 | /// note: because performance reasons, we use the PayloadWriter to write the payload to the network buffer. 39 | /// 40 | public PooledArray Payload { get; set; } 41 | 42 | /// 43 | /// Payload writer 44 | /// 45 | public PayloadWriter PayloadWriter { get; set; } = default; 46 | 47 | /// 48 | /// Get this request size 49 | /// except for the first 4 bytes 50 | /// 51 | /// 52 | public (int HeadLength, int PayloadLength) GetSize() 53 | { 54 | // sum every field length 55 | return PayloadWriter.HasPayload 56 | ? (sizeof(ulong) + sizeof(int) + sizeof(int) + sizeof(int), PayloadWriter.GetPayloadSize()) 57 | : (sizeof(ulong) + sizeof(int) + sizeof(int) + sizeof(int), Payload.Length); 58 | } 59 | 60 | public void Serialize(IBufferWriter writer) 61 | { 62 | // The data format is as follows: 63 | // 4 bytes total length 64 | // N bytes header 65 | // N bytes payload 66 | 67 | var length = GetSize(); 68 | var totalLength = length.HeadLength + length.PayloadLength + 4; 69 | 70 | scoped var span = writer.GetSpan(totalLength); 71 | 72 | // write total length 73 | BinaryPrimitives.WriteInt32LittleEndian(span, length.HeadLength + length.PayloadLength); 74 | span = span[sizeof(int)..]; 75 | 76 | // write id 77 | BinaryPrimitives.WriteUInt64LittleEndian(span, Id); 78 | span = span[sizeof(ulong)..]; 79 | 80 | // write status 81 | BinaryPrimitives.WriteInt32LittleEndian(span, (int)Status); 82 | span = span[sizeof(int)..]; 83 | 84 | // write payload type 85 | BinaryPrimitives.WriteInt32LittleEndian(span, (int)PayloadType); 86 | span = span[sizeof(int)..]; 87 | 88 | // write payload length 89 | BinaryPrimitives.WriteInt32LittleEndian(span, length.PayloadLength); 90 | span = span[sizeof(int)..]; 91 | 92 | // for performance reasons, we use the PayloadWriter to write the payload to the network buffer 93 | // reduced memory copying 94 | if (!PayloadWriter.HasPayload) 95 | { 96 | Payload.Span.CopyTo(span); 97 | writer.Advance(totalLength); 98 | } 99 | else 100 | { 101 | writer.Advance(length.HeadLength + 4); 102 | PayloadWriter.PayloadWriteTo(writer); 103 | } 104 | } 105 | 106 | /// 107 | /// Try deserialize 108 | /// 109 | /// 110 | /// 111 | /// 112 | /// 113 | public static (bool Success, AppResponse? Request) TryDeserialize( 114 | ReadOnlySequence sequence, 115 | out SequencePosition consumed, 116 | AppResponse? reuse = null) 117 | { 118 | // The data format is as follows: 119 | // 4 bytes total length 120 | // N bytes header 121 | // N bytes payload 122 | 123 | consumed = default; 124 | 125 | // if the sequence length is less than 4, it means that the request is not complete 126 | if (sequence.Length < 4) 127 | { 128 | return (false, null); 129 | } 130 | 131 | // read the total length of the request, 132 | // if the sequence length is less than the total length, 133 | // it means that the request is not complete 134 | var reader = new SequenceReader(sequence); 135 | reader.TryReadLittleEndian(out int totalLength); 136 | if (sequence.Length < totalLength + 4) 137 | { 138 | return (false, null); 139 | } 140 | 141 | // Deserialize the response 142 | var response = reuse ?? new AppResponse(); 143 | response.TotalLength = totalLength; 144 | 145 | // read id 146 | reader.TryReadLittleEndian(out long id); 147 | response.Id = (ulong)id; 148 | 149 | // read status 150 | reader.TryReadLittleEndian(out int status); 151 | response.Status = (ResponseStatus)status; 152 | 153 | // read payload type 154 | reader.TryReadLittleEndian(out int payloadType); 155 | response.PayloadType = (PayloadType)payloadType; 156 | 157 | // read payload length 158 | reader.TryReadLittleEndian(out int payloadLength); 159 | 160 | // for performance reasons, we use the PooledArray to store the payload 161 | // PooledArray use memory pool 162 | var payload = new PooledArray(payloadLength); 163 | reader.Sequence.Slice(reader.Position, payloadLength).CopyTo(payload.Span); 164 | 165 | // The payload is stored in the PooledArray, 166 | // so we need to register the PooledArray to the DisposeManager, 167 | // when the AppRequest is disposed, the PooledArray will be automatically returned to the memory pool 168 | response.RegisterForDispose(payload); 169 | response.Payload = payload; 170 | 171 | reader.Advance(payloadLength); 172 | 173 | consumed = reader.Position; 174 | 175 | return (true, response); 176 | } 177 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Protocol/Protocol/Empty.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Protocol.Protocol; 2 | 3 | /// 4 | /// Is a flag, means no payload 5 | /// 6 | public record Empty; -------------------------------------------------------------------------------- /src/SatelliteRpc.Protocol/Protocol/Login.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package ServerProto; 4 | 5 | message LoginReqProto 6 | { 7 | string user = 1; 8 | string password = 2; 9 | optional int32 Sn = 6;//请求标识 10 | } 11 | 12 | message LoginRespProto 13 | { 14 | bool isOk = 1; 15 | optional string errMsg = 2; 16 | optional int32 Sn = 4;//请求标识 17 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Protocol/Protocol/PayloadType.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Protocol.Protocol; 2 | 3 | /// 4 | /// Payload type 5 | /// 6 | public enum PayloadType 7 | { 8 | /// 9 | /// Protobuf 10 | /// 11 | Protobuf = 0, 12 | 13 | /// 14 | /// Json 15 | /// 16 | Json = 1 17 | 18 | // add more payload type here, like custom payload type 19 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Protocol/Protocol/PayloadWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace SatelliteRpc.Protocol.Protocol; 5 | 6 | public struct PayloadWriter 7 | { 8 | /// 9 | /// Empty payload writer 10 | /// 11 | public static readonly PayloadWriter Empty = new PayloadWriter 12 | { 13 | GetPayloadSize = () => 0, 14 | PayloadWriteTo = _ => { } 15 | }; 16 | 17 | /// 18 | /// Has payload 19 | /// 20 | [MemberNotNullWhen(true, nameof(GetPayloadSize), nameof(PayloadWriteTo))] 21 | public bool HasPayload => PayloadWriteTo is not null && GetPayloadSize is not null; 22 | 23 | /// 24 | /// Get or set calculate payload size method 25 | /// 26 | public Func? GetPayloadSize { get; set; } 27 | 28 | /// 29 | /// Get or set payload write to method 30 | /// 31 | public Action>? PayloadWriteTo { get; set; } 32 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Protocol/Protocol/ResponseStatus.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Protocol.Protocol; 2 | 3 | /// 4 | /// Response status 5 | /// 6 | public enum ResponseStatus 7 | { 8 | /// 9 | /// Success 10 | /// 11 | Success = 0, 12 | 13 | /// 14 | /// Not found 15 | /// it means the request is not found service or method 16 | /// 17 | NotFound = 1, 18 | 19 | /// 20 | /// Bad request 21 | /// it means the request is not valid 22 | /// 23 | BadRequest = 2, 24 | 25 | /// 26 | /// Internal error 27 | /// it means the request is valid, but the server has an internal error 28 | /// 29 | InternalError = 3 30 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Protocol/SatelliteRpc.Protocol.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Configuration/IRpcServerBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace SatelliteRpc.Server.Configuration; 4 | 5 | /// 6 | /// Builder for rpc server 7 | /// 8 | public interface IRpcServerBuilder 9 | { 10 | /// 11 | /// Service collection 12 | /// 13 | IServiceCollection Services { get; } 14 | 15 | /// 16 | /// Configure satellite rpc server 17 | /// 18 | /// 19 | /// 20 | IRpcServerBuilder ConfigureSatelliteRpcServer(Action configure); 21 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Configuration/RpcServerBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace SatelliteRpc.Server.Configuration; 4 | 5 | /// 6 | /// Class that provides the functionality to construct an RPC server. 7 | /// This class implements the IRpcServerBuilder interface. 8 | /// 9 | public class RpcServerBuilder : IRpcServerBuilder 10 | { 11 | /// 12 | /// Gets the service collection that this RPC server will use. 13 | /// 14 | public IServiceCollection Services { get; } 15 | 16 | /// 17 | /// Constructs a new instance of RpcServerBuilder with the provided service collection. 18 | /// 19 | /// The service collection to use for this RPC server. 20 | public RpcServerBuilder(IServiceCollection services) 21 | { 22 | Services = services; 23 | } 24 | 25 | /// 26 | /// Configures the SatelliteRpcServer with the provided options. 27 | /// 28 | /// An action that configures the options for the SatelliteRpcServer. 29 | /// Returns the IRpcServerBuilder instance after it has been configured. 30 | public IRpcServerBuilder ConfigureSatelliteRpcServer(Action configure) 31 | { 32 | Services.Configure(configure); 33 | return this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Configuration/SatelliteRpcServerOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace SatelliteRpc.Server.Configuration; 4 | 5 | public class SatelliteRpcServerOptions 6 | { 7 | /// 8 | /// Rpc server listen port 9 | /// 10 | public int Port { get; set; } = 58888; 11 | 12 | /// 13 | /// Rpc server listen address 14 | /// 15 | public IPAddress Host { get; set; } = IPAddress.Parse("127.0.0.1"); 16 | 17 | /// 18 | /// Response write channel max count 19 | /// 20 | public int WriteChannelMaxCount { get; set; } = 1024; 21 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Exceptions/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Server.Exceptions; 2 | 3 | /// 4 | /// Not found 5 | /// it means the request is not found service or method 6 | /// 7 | public class NotFoundException : ApplicationException 8 | { 9 | public NotFoundException(string? message) : base(message) 10 | { 11 | } 12 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Exceptions/ParametersBindException.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Server.Exceptions; 2 | 3 | /// 4 | /// Parameters bind exception 5 | /// it means the request parameters bind error, maybe the request parameters is not valid 6 | /// 7 | public class ParametersBindException : ApplicationException 8 | { 9 | public ParametersBindException(string? message, Exception? innerException) : base(message, innerException) 10 | { 11 | } 12 | 13 | public ParametersBindException(string? message) : base(message) 14 | { 15 | } 16 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Extensions/HostBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using SatelliteRpc.Server.Configuration; 3 | 4 | namespace SatelliteRpc.Server.Extensions; 5 | 6 | /// 7 | /// The IHostBuilder extensions 8 | /// 9 | public static class HostBuilderExtensions 10 | { 11 | /// 12 | /// Use satellite rpc server 13 | /// 14 | /// 15 | /// 16 | /// 17 | public static IHostBuilder UseSatelliteRpcServer( 18 | this IHostBuilder builder, 19 | Action? configure = null) 20 | { 21 | // we use kestrel server, so we need to use web host 22 | builder.ConfigureServices(services => 23 | { 24 | var serverBuilder = new RpcServerBuilder(services); 25 | configure?.Invoke(serverBuilder); 26 | serverBuilder.AddRpcService(); 27 | serverBuilder.AddRpcConnectionHandler(); 28 | serverBuilder.AddRpcServiceMiddleware(); 29 | serverBuilder.ConfigureSatelliteRpcServer(_ => { }); 30 | }).ConfigureWebHostDefaults(webBuilder => { webBuilder.UseRpcKestrelServer(); }); 31 | 32 | 33 | return builder; 34 | } 35 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Extensions/RpcServerBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Server.Configuration; 2 | using SatelliteRpc.Server.RpcService; 3 | using SatelliteRpc.Server.Transport; 4 | using SatelliteRpc.Shared.Application; 5 | 6 | namespace SatelliteRpc.Server.Extensions; 7 | 8 | /// 9 | /// The IRpcServerBuilder extensions 10 | /// 11 | public static class RpcServerBuilderExtensions 12 | { 13 | /// 14 | /// Adds a RPC connection handler to the RPC server builder. 15 | /// 16 | /// The RPC server builder. 17 | /// An optional action to configure the application builder. 18 | /// The RPC server builder with the added connection handler. 19 | public static IRpcServerBuilder AddRpcConnectionHandler( 20 | this IRpcServerBuilder builder, 21 | Action>? configure = null) 22 | { 23 | builder.Services.AddRpcConnectionHandler(configure); 24 | return builder; 25 | } 26 | 27 | /// 28 | /// Adds a RPC service to the RPC server builder. 29 | /// 30 | /// The RPC server builder. 31 | /// The RPC server builder with the added RPC service. 32 | public static IRpcServerBuilder AddRpcService(this IRpcServerBuilder builder) 33 | { 34 | builder.Services.AddRpcService(); 35 | return builder; 36 | } 37 | 38 | /// 39 | /// Adds a RPC service middleware to the RPC server builder. 40 | /// 41 | /// The RPC server builder. 42 | /// An optional action to configure the application builder. 43 | /// The RPC server builder with the added service middleware. 44 | public static IRpcServerBuilder AddRpcServiceMiddleware( 45 | this IRpcServerBuilder builder, 46 | Action>? configure = null) 47 | { 48 | builder.Services.AddRpcServiceMiddleware(configure); 49 | return builder; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | using SatelliteRpc.Protocol.PayloadConverters; 5 | using SatelliteRpc.Server.RpcService; 6 | using SatelliteRpc.Server.RpcService.DataExchange; 7 | using SatelliteRpc.Server.RpcService.Endpoint; 8 | using SatelliteRpc.Server.RpcService.Middleware; 9 | using SatelliteRpc.Server.Transport; 10 | using SatelliteRpc.Shared.Application; 11 | 12 | namespace SatelliteRpc.Server.Extensions; 13 | 14 | /// 15 | /// Contains extension methods for IServiceCollection to add RPC-related services. 16 | /// 17 | public static class ServiceCollectionExtensions 18 | { 19 | /// 20 | /// Adds a singleton instance of the service to the specified . 21 | /// 22 | /// The to add the service to. 23 | /// An optional configuration action to apply to the . 24 | /// The same service collection so that multiple calls can be chained. 25 | public static IServiceCollection AddRpcConnectionHandler( 26 | this IServiceCollection services, 27 | Action>? configure = null) 28 | { 29 | services.TryAddSingleton( 30 | provider => new RpcConnectionApplicationHandlerBuilder(provider, configure)); 31 | return services; 32 | } 33 | 34 | /// 35 | /// Adds a singleton instance of the service to the specified . 36 | /// 37 | /// The to add the service to. 38 | /// An optional configuration action to apply to the . 39 | /// The same service collection so that multiple calls can be chained. 40 | public static IServiceCollection AddRpcServiceMiddleware( 41 | this IServiceCollection services, 42 | Action>? configure = null) 43 | { 44 | services.TryAddSingleton( 45 | provider => new RpcServiceMiddlewareBuilder(provider, configure)); 46 | return services; 47 | } 48 | 49 | /// 50 | /// Adds various RPC services to the specified . 51 | /// 52 | /// The to add the services to. 53 | /// The same service collection so that multiple calls can be chained. 54 | public static IServiceCollection AddRpcService(this IServiceCollection services) 55 | { 56 | services.TryAddSingleton(); 57 | services.TryAddSingleton(); 58 | services.TryAddSingleton(); 59 | services.TryAddSingleton(); 60 | services.TryAddSingleton(provider => 61 | { 62 | // Search the entry assembly, find RPC services that implement IRpcService to build RpcServiceEndpointDataSource 63 | var rpcServiceType = typeof(IRpcService); 64 | var rpcServiceTypes = Assembly 65 | .GetEntryAssembly()! 66 | .GetTypes() 67 | .Where(type => rpcServiceType.IsAssignableFrom(type) && !type.IsAbstract); 68 | 69 | var dataSource = ActivatorUtilities.CreateInstance(provider); 70 | foreach (var serviceType in rpcServiceTypes) 71 | { 72 | // Get all public methods of the RPC service 73 | var methods = 74 | serviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); 75 | 76 | foreach (var method in methods) 77 | { 78 | // Add endpoint to the data source 79 | dataSource.AddEndpoint(RpcServiceEndpoint.FromMethodInfo(serviceType, method)); 80 | } 81 | } 82 | 83 | return dataSource; 84 | }); 85 | 86 | return services; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Extensions/WebHostBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Connections; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Options; 5 | using SatelliteRpc.Server.Configuration; 6 | using SatelliteRpc.Server.Transport; 7 | 8 | namespace SatelliteRpc.Server.Extensions; 9 | 10 | /// 11 | /// Provides extension methods for the IWebHostBuilder interface to configure RPC services. 12 | /// 13 | public static class WebHostBuilderExtensions 14 | { 15 | /// 16 | /// Configures the specified IWebHostBuilder to use the Kestrel server and RPC services. 17 | /// 18 | /// The IWebHostBuilder to configure. 19 | /// The IWebHostBuilder that was passed into the method, with the configuration applied. 20 | public static IWebHostBuilder UseRpcKestrelServer( 21 | this IWebHostBuilder builder) 22 | { 23 | builder.UseKestrel(options => 24 | { 25 | var serverOptions = options.ApplicationServices.GetService>(); 26 | 27 | // Configure Kestrel to listen on the specified host and port, 28 | // and to use the RpcConnectionHandler for handling connections 29 | options.Listen( 30 | serverOptions!.Value.Host, 31 | serverOptions.Value.Port, 32 | listenOptions => { listenOptions.UseConnectionHandler(); }); 33 | }); 34 | 35 | // Configure the application to use the RpcMvcMiddleware 36 | // and ignore the default middleware 37 | builder.Configure(_ => { }); 38 | 39 | return builder; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Observability/ObservabilityMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using SatelliteRpc.Server.RpcService; 3 | using SatelliteRpc.Server.RpcService.Middleware; 4 | using SatelliteRpc.Shared.Application; 5 | 6 | namespace SatelliteRpc.Server.Observability; 7 | 8 | /// 9 | /// Middleware for providing observability support to service requests. 10 | /// 11 | public class ObservabilityMiddleware : IRpcServiceMiddleware 12 | { 13 | /// 14 | /// Invokes the middleware with the specified context. 15 | /// 16 | /// The delegate representing the remaining middleware in the request pipeline. 17 | /// The context of the current service request. 18 | /// A task representing the asynchronous operation. 19 | public async ValueTask InvokeAsync(ApplicationDelegate next, ServiceContext context) 20 | { 21 | // Create a new Activity for the RPC request, adding relevant information as tags. 22 | var activity = new Activity("RPC request") 23 | .AddTag("requestId", context.RawContext.Request.Id) 24 | .AddTag("method", context.RawContext.Request.Path) 25 | .AddTag("service", context.Endpoint.ServiceName); 26 | 27 | try 28 | { 29 | activity.Start(); 30 | await next(context); 31 | } 32 | catch (Exception ex) 33 | { 34 | // If an exception occurs, add it as an event to the activity 35 | activity.AddEvent(new ActivityEvent("Exception occurred", 36 | tags: new ActivityTagsCollection(new[] 37 | { 38 | new KeyValuePair("exception", ex) 39 | }!))); 40 | throw; 41 | } 42 | finally 43 | { 44 | activity.Stop(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/DataExchange/DefaultRpcDataExchange.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Protocol.PayloadConverters; 2 | using SatelliteRpc.Protocol.Protocol; 3 | using SatelliteRpc.Server.Exceptions; 4 | using SatelliteRpc.Server.RpcService.Endpoint; 5 | using SatelliteRpc.Server.Transport; 6 | 7 | namespace SatelliteRpc.Server.RpcService.DataExchange; 8 | 9 | /// 10 | /// Default implementation of the IRpcDataExchange interface. 11 | /// This class is responsible for converting payloads to parameters and vice versa for RPC calls. 12 | /// 13 | public class DefaultRpcDataExchange : IRpcDataExchange 14 | { 15 | /// 16 | /// Source of payload converters. Used to convert payloads to parameters and vice versa. 17 | /// 18 | private readonly PayloadConverterSource _converterSource; 19 | 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | /// The source of payload converters. 24 | public DefaultRpcDataExchange(PayloadConverterSource converterSource) 25 | { 26 | _converterSource = converterSource; 27 | } 28 | 29 | /// 30 | /// Binds the parameters for an RPC service endpoint. 31 | /// Converts the payload in the raw context to the types expected by the endpoint. 32 | /// 33 | /// The RPC service endpoint. 34 | /// The raw RPC context. 35 | /// An array of parameters to be used in the RPC call. 36 | public object?[] BindParameters(RpcServiceEndpoint endpoint, RpcRawContext rawContext) 37 | { 38 | try 39 | { 40 | var parameters = new object?[endpoint.ParameterTypes.Length]; 41 | for (var i = 0; i < endpoint.ParameterTypes.Length; i++) 42 | { 43 | var parameterType = endpoint.ParameterTypes[i]; 44 | 45 | // Special case for CancellationToken, use the cancellation token from the raw context 46 | if (parameterType == typeof(CancellationToken) || parameterType == typeof(CancellationToken?)) 47 | { 48 | parameters[i] = rawContext.Cancel; 49 | } 50 | else 51 | { 52 | var converter = _converterSource.GetConverter(rawContext.Request.PayloadType); 53 | parameters[i] = converter.Convert(rawContext.Request.Payload, parameterType); 54 | } 55 | } 56 | 57 | return parameters; 58 | } 59 | catch (Exception ex) 60 | { 61 | throw new ParametersBindException("Failed to bind parameters", ex); 62 | } 63 | } 64 | 65 | /// 66 | /// Gets a payload writer for the given result and raw context. 67 | /// The payload writer is used to convert the result of an RPC call to a payload. 68 | /// 69 | /// The result of the RPC call. 70 | /// The raw RPC context. 71 | /// A payload writer for the result. 72 | public PayloadWriter GetPayloadWriter(object? result, RpcRawContext rawContext) 73 | { 74 | var converter = _converterSource.GetConverter(rawContext.Request.PayloadType); 75 | return converter.CreatePayloadWriter(result); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/DataExchange/IRpcDataExchange.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Protocol.Protocol; 2 | using SatelliteRpc.Server.RpcService.Endpoint; 3 | using SatelliteRpc.Server.Transport; 4 | 5 | namespace SatelliteRpc.Server.RpcService.DataExchange; 6 | 7 | /// 8 | /// The IRpcDataExchange interface defines the methods needed to bind parameters for RPC calls and to 9 | /// get a payload writer for the results of those calls. 10 | /// 11 | public interface IRpcDataExchange 12 | { 13 | /// 14 | /// Binds the parameters for an RPC service call. 15 | /// 16 | /// The endpoint of the RPC service. 17 | /// The raw context of the RPC call, which contains the raw payload. 18 | /// An array of objects representing the parameters to be used for the RPC call. 19 | object?[] BindParameters(RpcServiceEndpoint endpoint, RpcRawContext rawContext); 20 | 21 | /// 22 | /// Gets a PayloadWriter to write the result of an RPC call to a payload. 23 | /// 24 | /// The result of the RPC call. 25 | /// The raw context of the RPC call, which contains the raw payload. 26 | /// A PayloadWriter that can be used to write the result to a payload. 27 | PayloadWriter GetPayloadWriter(object? result, RpcRawContext rawContext); 28 | } 29 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/Endpoint/DefaultRpcEndPointResolver.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Server.Exceptions; 2 | 3 | namespace SatelliteRpc.Server.RpcService.Endpoint; 4 | 5 | /// 6 | /// The DefaultRpcEndPointResolver class is an implementation of the IEndpointResolver interface. 7 | /// It resolves RPC service endpoints using a data source. 8 | /// 9 | public class DefaultRpcEndPointResolver : IEndpointResolver 10 | { 11 | // The data source used to resolve endpoints. 12 | private readonly RpcServiceEndpointDataSource _dataSource; 13 | 14 | /// 15 | /// Initializes a new instance of the DefaultRpcEndPointResolver class. 16 | /// 17 | /// The data source used to resolve endpoints. 18 | public DefaultRpcEndPointResolver(RpcServiceEndpointDataSource dataSource) 19 | { 20 | _dataSource = dataSource; 21 | } 22 | 23 | /// 24 | /// Gets an RPC service endpoint for a given path. 25 | /// 26 | /// The path for which to get the endpoint. 27 | /// The RPC service endpoint for the given path. 28 | /// Thrown when no endpoint is found for the given path. 29 | public RpcServiceEndpoint GetEndpoint(string path) 30 | { 31 | var endPoint = _dataSource.GetEndpoint(path); 32 | if (endPoint is null) 33 | { 34 | throw new NotFoundException($"No endpoint found for path: {path}"); 35 | } 36 | 37 | return endPoint; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/Endpoint/EndpointInvokeMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using SatelliteRpc.Server.RpcService.Middleware; 3 | using SatelliteRpc.Shared.Application; 4 | 5 | namespace SatelliteRpc.Server.RpcService.Endpoint; 6 | 7 | /// 8 | /// Middleware for invoking an endpoint in a RPC service. 9 | /// This class is responsible for retrieving the service instance associated with a given endpoint and invoking it. 10 | /// 11 | public class EndpointInvokeMiddleware : IRpcServiceMiddleware 12 | { 13 | /// 14 | /// The service provider used to resolve service instances. 15 | /// 16 | private readonly IServiceProvider _serviceProvider; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The service provider used to resolve service instances. 22 | public EndpointInvokeMiddleware(IServiceProvider serviceProvider) 23 | { 24 | _serviceProvider = serviceProvider; 25 | } 26 | 27 | /// 28 | /// Invokes the middleware in the RPC service pipeline. 29 | /// 30 | /// The next middleware delegate in the pipeline. Currently not used in this middleware. 31 | /// The context for the current service call. 32 | /// A that represents the asynchronous operation. 33 | public async ValueTask InvokeAsync(ApplicationDelegate _, ServiceContext context) 34 | { 35 | var endpoint = context.Endpoint; 36 | var endpointService = ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, endpoint.ServiceType); 37 | 38 | // Check if the endpoint's return type is Task 39 | if (endpoint.ReturnIsTask) 40 | { 41 | // If it is, just invoke the endpoint and await the result 42 | await endpoint.InvokeAsync(endpointService, context.Arguments); 43 | } 44 | else 45 | { 46 | // If it's not, invoke the endpoint, await the result, and store it in the context 47 | var result = await endpoint.InvokeAsync(endpointService, context.Arguments); 48 | context.Result = result; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/Endpoint/IEndpointResolver.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Server.RpcService.Endpoint; 2 | 3 | /// 4 | /// Defines a mechanism for resolving RPC service endpoints. 5 | /// 6 | public interface IEndpointResolver 7 | { 8 | /// 9 | /// Resolves the for a given path. 10 | /// 11 | /// The path for which to resolve the endpoint. 12 | /// The resolved . 13 | RpcServiceEndpoint GetEndpoint(string path); 14 | } 15 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/Endpoint/RpcServiceEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace SatelliteRpc.Server.RpcService.Endpoint; 4 | 5 | /// 6 | /// Represents a remote procedure call (RPC) service endpoint. 7 | /// 8 | public class RpcServiceEndpoint 9 | { 10 | /// 11 | /// Gets the name of the service. 12 | /// 13 | public string ServiceName { get; } 14 | 15 | /// 16 | /// Gets the name of the method. 17 | /// 18 | public string MethodName { get; } 19 | 20 | /// 21 | /// Gets the type of the service. 22 | /// 23 | public Type ServiceType { get; } 24 | 25 | /// 26 | /// Gets the types of the parameters of the method. 27 | /// 28 | public Type[] ParameterTypes { get; } 29 | 30 | /// 31 | /// Gets the return type of the method. 32 | /// 33 | public Type ReturnType { get; } 34 | 35 | /// 36 | /// Gets a value indicating whether the return type is a Task. 37 | /// 38 | public bool ReturnIsTask => ReturnType == typeof(Task); 39 | 40 | /// 41 | /// Gets a value indicating whether the return type is a generic Task. 42 | /// 43 | public bool ReturnIsTaskT => ReturnType.IsGenericType && ReturnType.GetGenericTypeDefinition() == typeof(Task<>); 44 | 45 | /// 46 | /// Gets the method invoker function. 47 | /// 48 | public Func MethodInvoker { get; } 49 | 50 | /// 51 | /// Gets the path of the RPC service endpoint. 52 | /// 53 | public string Path => $"{ServiceName}/{MethodName}"; 54 | 55 | /// 56 | /// Initializes a new instance of the class. 57 | /// 58 | public RpcServiceEndpoint( 59 | string serviceName, 60 | string methodName, 61 | Type[] parameterTypes, 62 | Type returnType, 63 | Type serviceType, 64 | Func methodInvoker) 65 | { 66 | ServiceName = serviceName; 67 | MethodName = methodName; 68 | ParameterTypes = parameterTypes; 69 | ReturnType = returnType; 70 | ServiceType = serviceType; 71 | MethodInvoker = methodInvoker; 72 | } 73 | 74 | /// 75 | /// Creates a new instance of the class from a MethodInfo object. 76 | /// 77 | public static RpcServiceEndpoint FromMethodInfo(Type type, MethodInfo methodInfo) 78 | { 79 | var serviceName = type.Name; 80 | var methodName = methodInfo.Name; 81 | var parameterTypes = methodInfo.GetParameters().Select(p => p.ParameterType).ToArray(); 82 | var returnType = methodInfo.ReturnType; 83 | return new RpcServiceEndpoint( 84 | serviceName, 85 | methodName, 86 | parameterTypes, 87 | returnType, 88 | type, 89 | Shared.MethodInvoker.CreateInvoker(type, methodInfo)); 90 | } 91 | 92 | /// 93 | /// Invokes the method of this RPC service endpoint asynchronously. 94 | /// 95 | public async Task InvokeAsync(object instance, object?[] parameters) 96 | { 97 | // invoke method, if method is async task or Task, await for task complete 98 | var result = MethodInvoker(instance, parameters); 99 | if (result is not Task task) return result; 100 | 101 | await task; 102 | 103 | return ReturnIsTaskT ? Shared.MethodInvoker.GetTaskResult(task) : null!; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/Endpoint/RpcServiceEndpointDataSource.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace SatelliteRpc.Server.RpcService.Endpoint; 4 | 5 | /// 6 | /// Represents a data source for RpcServiceEndpoint instances. 7 | /// This class is thread-safe and allows for concurrent access to the underlying data. 8 | /// 9 | public class RpcServiceEndpointDataSource 10 | { 11 | /// 12 | /// A concurrent dictionary used to store RpcServiceEndpoint instances. 13 | /// The key is the path of the endpoint. 14 | /// 15 | private readonly ConcurrentDictionary _endpoints = new(); 16 | 17 | /// 18 | /// Adds a new RpcServiceEndpoint to the data source. 19 | /// 20 | /// The RpcServiceEndpoint to add. 21 | public void AddEndpoint(RpcServiceEndpoint endpoint) 22 | { 23 | _endpoints.TryAdd(endpoint.Path, endpoint); 24 | } 25 | 26 | /// 27 | /// Retrieves an RpcServiceEndpoint from the data source by its path. 28 | /// 29 | /// The path of the RpcServiceEndpoint to retrieve. 30 | /// The RpcServiceEndpoint if found, null otherwise. 31 | public RpcServiceEndpoint? GetEndpoint(string path) 32 | { 33 | _endpoints.TryGetValue(path, out var endpoint); 34 | return endpoint; 35 | } 36 | 37 | /// 38 | /// Retrieves all RpcServiceEndpoint instances from the data source. 39 | /// 40 | /// An array of all RpcServiceEndpoint instances. 41 | public RpcServiceEndpoint[] GetEndpoints() 42 | { 43 | return _endpoints.Values.ToArray(); 44 | } 45 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/IRpcService.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Server.RpcService; 2 | 3 | /// 4 | /// Defines the contract for RPC (Remote Procedure Call) services. 5 | /// 6 | public interface IRpcService 7 | { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/Middleware/IRpcServiceMiddleware.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Shared.Application; 2 | 3 | namespace SatelliteRpc.Server.RpcService.Middleware; 4 | 5 | /// 6 | /// Defines a mechanism for adding middleware to the RPC service pipeline. 7 | /// 8 | public interface IRpcServiceMiddleware : IApplicationMiddleware 9 | { 10 | 11 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/Middleware/IRpcServiceMiddlewareBuilder.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Shared.Application; 2 | 3 | namespace SatelliteRpc.Server.RpcService.Middleware; 4 | 5 | /// 6 | /// Defines a mechanism for adding middleware to the RPC service pipeline. 7 | /// 8 | public interface IRpcServiceMiddlewareBuilder 9 | { 10 | ApplicationDelegate Build(); 11 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/Middleware/RpcServiceMiddlewareBuilder.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Server.RpcService.Endpoint; 2 | using SatelliteRpc.Shared.Application; 3 | 4 | namespace SatelliteRpc.Server.RpcService.Middleware; 5 | 6 | /// 7 | /// Represents a builder for constructing the middleware pipeline for RPC services. 8 | /// Implements the IRpcServiceMiddlewareBuilder interface. 9 | /// 10 | public class RpcServiceMiddlewareBuilder : IRpcServiceMiddlewareBuilder 11 | { 12 | /// 13 | /// Optional configuration action for the ApplicationBuilder. 14 | /// 15 | private readonly Action>? _configure; 16 | 17 | /// 18 | /// The ApplicationBuilder that is used to construct the middleware pipeline. 19 | /// 20 | private readonly ApplicationBuilder _builder; 21 | 22 | /// 23 | /// Initializes a new instance of the RpcServiceMiddlewareBuilder class. 24 | /// 25 | /// The IServiceProvider that provides access to the application's service container. 26 | /// An optional configuration action for the ApplicationBuilder. 27 | public RpcServiceMiddlewareBuilder( 28 | IServiceProvider services, 29 | Action>? configure = null) 30 | { 31 | _configure = configure; 32 | _builder = new ApplicationBuilder(services); 33 | } 34 | 35 | /// 36 | /// Builds the middleware pipeline using the ApplicationBuilder. 37 | /// 38 | /// The delegate that represents the middleware pipeline. 39 | public ApplicationDelegate Build() 40 | { 41 | // Apply the configuration action to the ApplicationBuilder, if it exists. 42 | _configure?.Invoke(_builder); 43 | 44 | // Add the EndpointInvokeMiddleware to the pipeline and build the pipeline. 45 | return _builder.Use().Build(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/RpcServiceHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using SatelliteRpc.Protocol.Protocol; 3 | using SatelliteRpc.Server.Exceptions; 4 | using SatelliteRpc.Server.RpcService.DataExchange; 5 | using SatelliteRpc.Server.RpcService.Endpoint; 6 | using SatelliteRpc.Server.RpcService.Middleware; 7 | using SatelliteRpc.Server.Transport; 8 | using SatelliteRpc.Shared.Application; 9 | 10 | namespace SatelliteRpc.Server.RpcService; 11 | 12 | /// 13 | /// Handles the processing of Rpc services. 14 | /// 15 | public class RpcServiceHandler : IApplicationMiddleware 16 | { 17 | private readonly ILogger _logger; 18 | private readonly IEndpointResolver _endpointResolver; 19 | private readonly IRpcDataExchange _rpcDataExchange; 20 | private readonly ApplicationDelegate _middleware; 21 | 22 | /// 23 | /// Initializes a new instance of the class. 24 | /// 25 | /// The logger. 26 | /// The endpoint resolver. 27 | /// The RPC data exchange. 28 | /// The middleware builder. 29 | public RpcServiceHandler( 30 | ILogger logger, 31 | IEndpointResolver endpointResolver, 32 | IRpcDataExchange rpcDataExchange, 33 | IRpcServiceMiddlewareBuilder middlewareBuilder) 34 | { 35 | _logger = logger; 36 | _endpointResolver = endpointResolver; 37 | _rpcDataExchange = rpcDataExchange; 38 | _middleware = middlewareBuilder.Build(); 39 | } 40 | 41 | /// 42 | /// Processes the request asynchronously. 43 | /// 44 | /// The application delegate to be ignored. 45 | /// The raw RPC context. 46 | /// A representing the asynchronous operation. 47 | public async ValueTask InvokeAsync(ApplicationDelegate _, RpcRawContext rawContext) 48 | { 49 | try 50 | { 51 | // Resolve the endpoint from the request path 52 | var endpoint = _endpointResolver.GetEndpoint(rawContext.Request.Path); 53 | 54 | // Bind the parameters from the endpoint and raw context 55 | var parameters = _rpcDataExchange.BindParameters(endpoint, rawContext); 56 | var serviceContext = new ServiceContext(rawContext, endpoint, parameters); 57 | 58 | await _middleware(serviceContext); 59 | 60 | // Set the response payload writer with the result and raw context 61 | rawContext.Response.PayloadWriter = _rpcDataExchange.GetPayloadWriter(serviceContext.Result, rawContext); 62 | } 63 | catch (NotFoundException ex) 64 | { 65 | _logger.LogError(ex, "Not found endpoint"); 66 | rawContext.Response.Status = ResponseStatus.NotFound; 67 | } 68 | catch (ParametersBindException ex) 69 | { 70 | _logger.LogError(ex, "Parameters bind error"); 71 | rawContext.Response.Status = ResponseStatus.BadRequest; 72 | } 73 | catch (Exception ex) 74 | { 75 | _logger.LogError(ex, "Internal server error"); 76 | rawContext.Response.Status = ResponseStatus.InternalError; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/RpcService/ServiceContext.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Server.RpcService.Endpoint; 2 | using SatelliteRpc.Server.Transport; 3 | 4 | namespace SatelliteRpc.Server.RpcService; 5 | 6 | /// 7 | /// Represents the context for a single RPC service invocation. 8 | /// 9 | public class ServiceContext 10 | { 11 | /// 12 | /// Gets the raw context of the RPC request. This includes the original request message and metadata. 13 | /// 14 | public RpcRawContext RawContext { get; } 15 | 16 | /// 17 | /// Gets the endpoint information for the RPC service that will handle this request. 18 | /// 19 | public RpcServiceEndpoint Endpoint { get; } 20 | 21 | /// 22 | /// Gets the arguments that were passed in the RPC request. 23 | /// 24 | public object?[] Arguments { get; } 25 | 26 | /// 27 | /// Gets or sets the result of the RPC service invocation. This will be null before the service has been invoked. 28 | /// 29 | public object? Result { get; set; } 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The raw context of the RPC request. 35 | /// The endpoint information for the RPC service that will handle this request. 36 | /// The arguments that were passed in the RPC request. 37 | public ServiceContext(RpcRawContext rawContext, RpcServiceEndpoint endpoint, object?[] arguments) 38 | { 39 | RawContext = rawContext; 40 | Endpoint = endpoint; 41 | Arguments = arguments; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/SatelliteRpc.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Transport/IRpcConnectionApplicationHandlerBuilder.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Shared.Application; 2 | 3 | namespace SatelliteRpc.Server.Transport; 4 | 5 | /// 6 | /// Defines a mechanism for adding middleware to the RPC connection pipeline. 7 | /// 8 | public interface IRpcConnectionApplicationHandlerBuilder 9 | { 10 | ApplicationDelegate Build(); 11 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Transport/RpcConnectionApplicationHandlerBuilder.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Server.RpcService; 2 | using SatelliteRpc.Shared.Application; 3 | 4 | namespace SatelliteRpc.Server.Transport; 5 | 6 | /// 7 | /// Builder for constructing the application handler for an RPC connection. 8 | /// 9 | public class RpcConnectionApplicationHandlerBuilder : IRpcConnectionApplicationHandlerBuilder 10 | { 11 | /// 12 | /// An optional function to configure the application builder. 13 | /// 14 | private readonly Action>? _configure; 15 | 16 | /// 17 | /// The application builder which will be used to build the application handler. 18 | /// 19 | private readonly ApplicationBuilder _builder; 20 | 21 | /// 22 | /// Initializes a new instance of the class. 23 | /// 24 | /// The service provider to be used by the application builder. 25 | /// An optional function to configure the application builder. 26 | public RpcConnectionApplicationHandlerBuilder( 27 | IServiceProvider services, 28 | Action>? configure = null) 29 | { 30 | _configure = configure; 31 | _builder = new ApplicationBuilder(services); 32 | } 33 | 34 | /// 35 | /// Builds the application handler for the RPC connection. 36 | /// 37 | /// The built application handler. 38 | public ApplicationDelegate Build() 39 | { 40 | // Invoke the configuration function if it is provided. 41 | _configure?.Invoke(_builder); 42 | 43 | // Use the RpcServiceHandler middleware and build the application delegate. 44 | return _builder.Use().Build(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Transport/RpcConnectionHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.IO.Pipelines; 3 | using System.Threading.Channels; 4 | using Microsoft.AspNetCore.Connections; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Options; 7 | using SatelliteRpc.Protocol.Protocol; 8 | using SatelliteRpc.Server.Configuration; 9 | using SatelliteRpc.Shared.Application; 10 | 11 | namespace SatelliteRpc.Server.Transport; 12 | 13 | /// 14 | /// Handles RPC connections, including receiving requests, processing them and sending responses. 15 | /// 16 | public class RpcConnectionHandler : ConnectionHandler 17 | { 18 | private readonly ILogger _logger; 19 | private readonly SatelliteRpcServerOptions _satelliteRpcServerOptions; 20 | private readonly ApplicationDelegate _handler; 21 | 22 | /// 23 | /// Initializes a new instance of the class. 24 | /// 25 | /// The logger used for logging. 26 | /// The options accessor for the RPC server. 27 | /// The builder for the application handler. 28 | public RpcConnectionHandler( 29 | ILogger logger, 30 | IOptions? rpcOptionsAccessor, 31 | IRpcConnectionApplicationHandlerBuilder handlerBuilder) 32 | { 33 | _logger = logger; 34 | _satelliteRpcServerOptions = rpcOptionsAccessor?.Value ?? new SatelliteRpcServerOptions(); 35 | _handler = handlerBuilder.Build(); 36 | } 37 | 38 | /// 39 | /// This method is called when a new connection is established. It reads requests from the input, 40 | /// deserializes them, and handles them asynchronously. If an error occurs, it logs the error and aborts the connection. 41 | /// 42 | /// The context of the connection, which includes information such as the connection ID and the input and output. 43 | /// A task that represents the asynchronous operation. 44 | public override async Task OnConnectedAsync(ConnectionContext context) 45 | { 46 | try 47 | { 48 | 49 | _logger.LogInformation("[{ConnectionId}]Connection established", context.ConnectionId); 50 | 51 | // Create a channel for sending responses and start it. 52 | var responseChannel = CreateAndRunResponseChannel(context); 53 | 54 | _logger.LogInformation("[{ConnectionId}]Start reading", context.ConnectionId); 55 | 56 | var input = context.Transport.Input; 57 | // Continuously read from the input as long as the connection is not closed. 58 | while (context.ConnectionClosed.IsCancellationRequested == false) 59 | { 60 | // Read from the input asynchronously, waiting for more data if necessary. 61 | var result = await input.ReadAsync(context.ConnectionClosed); 62 | if (result.IsCanceled) 63 | { 64 | // If the read operation was cancelled, break out of the loop. 65 | break; 66 | } 67 | 68 | // Try to deserialize the received data into a request. 69 | var (success, request) = AppRequest.TryDeserialize(result.Buffer, out var consumed); 70 | if (success == false) 71 | { 72 | // If the deserialization failed, continue with the next iteration of the loop. 73 | continue; 74 | } 75 | 76 | // Create a context for the RPC, including the request and a new response with the same ID as the request. 77 | var rpcContext = new RpcRawContext(request!, new AppResponse { Id = request!.Id }, 78 | context.ConnectionClosed); 79 | // Handle the request asynchronously, sending the response through the response channel. 80 | AsyncRunRequestHandler(responseChannel, rpcContext); 81 | // Advance the input to the position after the consumed data. 82 | input.AdvanceTo(consumed); 83 | 84 | if (result.IsCompleted) 85 | { 86 | // If all data has been read, break out of the loop. 87 | break; 88 | } 89 | } 90 | 91 | _logger.LogInformation("[{ConnectionId}]Connection closed", context.ConnectionId); 92 | } 93 | catch (Exception ex) 94 | { 95 | _logger.LogError(ex, "[{ConnectionId}]Connection error", context.ConnectionId); 96 | context.Abort(); 97 | } 98 | } 99 | 100 | 101 | /// 102 | /// Creates a response channel and runs the response handler. 103 | /// 104 | /// The connection context. 105 | /// The response channel. 106 | private Channel CreateAndRunResponseChannel(ConnectionContext context) 107 | { 108 | var responseChannel = Channel.CreateBounded( 109 | new BoundedChannelOptions(_satelliteRpcServerOptions.WriteChannelMaxCount) 110 | { 111 | FullMode = BoundedChannelFullMode.Wait, 112 | SingleReader = true, 113 | SingleWriter = false 114 | }); 115 | _ = RunResponseHandler(responseChannel.Reader, context.Transport.Output, context.ConnectionClosed); 116 | return responseChannel; 117 | } 118 | 119 | /// 120 | /// Runs the request handler asynchronously. 121 | /// 122 | /// The writer of the channel. 123 | /// The RPC context. 124 | private void AsyncRunRequestHandler( 125 | Channel writer, 126 | RpcRawContext context) 127 | { 128 | _ = Task.Run(async () => 129 | { 130 | try 131 | { 132 | await _handler(context); 133 | } 134 | catch (Exception ex) 135 | { 136 | // Logs the error if an exception occurs 137 | _logger.LogError(ex, "[{Id}]Async run request handler error", context.Request.Id); 138 | context.Response.Status = ResponseStatus.InternalError; 139 | context.Response.PayloadWriter = new PayloadWriter 140 | { 141 | GetPayloadSize = () => "System Exception".Length, 142 | PayloadWriteTo = (bw) => bw.Write("System Exception"u8) 143 | }; 144 | } 145 | 146 | await writer.Writer.WriteAsync(context, context.Cancel); 147 | }); 148 | } 149 | 150 | /// 151 | /// Runs the response handler. 152 | /// 153 | /// The reader of the channel. 154 | /// The pipe writer for writing the response. 155 | /// The cancellation token. 156 | /// A task that represents the asynchronous operation. 157 | internal async ValueTask RunResponseHandler( 158 | ChannelReader reader, 159 | PipeWriter pipeWriter, 160 | CancellationToken cancellationToken) 161 | { 162 | try 163 | { 164 | while (await reader.WaitToReadAsync(cancellationToken)) 165 | { 166 | while (reader.TryRead(out var context)) 167 | { 168 | try 169 | { 170 | context.Response.Serialize(pipeWriter); 171 | await pipeWriter.FlushAsync(cancellationToken); 172 | // Disposes the context after the request is handled 173 | context.Dispose(); 174 | } 175 | catch (Exception ex) 176 | { 177 | _logger.LogError(ex, "[{Id}]Write response error", context.Request.Id); 178 | } 179 | } 180 | } 181 | } 182 | catch (Exception ex) 183 | { 184 | _logger.LogError(ex, "Run response handler error"); 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Server/Transport/RpcRawContext.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Protocol.Protocol; 2 | 3 | namespace SatelliteRpc.Server.Transport; 4 | 5 | /// 6 | /// Represents the context for a raw RPC (Remote Procedure Call) operation. 7 | /// This class provides access to the request, response, and cancellation token associated with the RPC operation. 8 | /// Implements the IDisposable interface to properly dispose of the request and response when the context is no longer needed. 9 | /// 10 | public class RpcRawContext : IDisposable 11 | { 12 | /// 13 | /// Gets the cancellation token for the RPC operation. 14 | /// This token can be used to signal a cancellation request to the operation. 15 | /// 16 | public CancellationToken Cancel { get; } 17 | 18 | /// 19 | /// Gets the request associated with the RPC operation. 20 | /// This request contains the data sent by the client to the server. 21 | /// 22 | public AppRequest Request { get; } 23 | 24 | /// 25 | /// Gets the response associated with the RPC operation. 26 | /// This response will contain the data to be sent from the server to the client after the operation is completed. 27 | /// 28 | public AppResponse Response { get; } 29 | 30 | /// 31 | /// Initializes a new instance of the class with the specified request, response, and cancellation token. 32 | /// 33 | /// The request associated with the RPC operation. 34 | /// The response associated with the RPC operation. 35 | /// The cancellation token for the RPC operation. 36 | public RpcRawContext(AppRequest request, AppResponse response, CancellationToken cancel) 37 | { 38 | Request = request; 39 | Response = response; 40 | Cancel = cancel; 41 | } 42 | 43 | /// 44 | /// Disposes of the resources used by the instance. 45 | /// This includes disposing of the request and response associated with the RPC operation. 46 | /// 47 | public void Dispose() 48 | { 49 | Request.Dispose(); 50 | Response.Dispose(); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Shared/Application/ApplicationBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace SatelliteRpc.Shared.Application; 4 | 5 | /// 6 | /// Represents a builder for creating an application with a specific context. 7 | /// 8 | public class ApplicationBuilder 9 | { 10 | private readonly ApplicationDelegate _fallbackHandler; 11 | private readonly List, ApplicationDelegate>> _middlewares = new(); 12 | 13 | /// 14 | /// Gets the service provider for the application. 15 | /// 16 | public IServiceProvider ApplicationServices { get; } 17 | 18 | /// 19 | /// Initializes a new instance of the class with the specified service provider. 20 | /// 21 | /// The service provider for the application. 22 | public ApplicationBuilder(IServiceProvider appServices) 23 | : this(appServices, _ => ValueTask.CompletedTask) 24 | { 25 | } 26 | 27 | /// 28 | /// Initializes a new instance of the class with the specified service provider and fallback handler. 29 | /// 30 | /// The service provider for the application. 31 | /// The fallback handler for the application. 32 | public ApplicationBuilder(IServiceProvider appServices, ApplicationDelegate fallbackHandler) 33 | { 34 | ApplicationServices = appServices; 35 | _fallbackHandler = fallbackHandler; 36 | } 37 | 38 | /// 39 | /// Builds the delegate that will handle the application's requests. 40 | /// 41 | /// The delegate to handle the application's requests. 42 | public ApplicationDelegate Build() 43 | { 44 | var handler = _fallbackHandler; 45 | for (var i = _middlewares.Count - 1; i >= 0; i--) 46 | { 47 | handler = _middlewares[i](handler); 48 | } 49 | return handler; 50 | } 51 | 52 | /// 53 | /// Creates a new with the default configuration. 54 | /// 55 | /// A new with the default configuration. 56 | public ApplicationBuilder New() 57 | { 58 | return new ApplicationBuilder(this.ApplicationServices, this._fallbackHandler); 59 | } 60 | 61 | /// 62 | /// Adds a conditional middleware to the application. 63 | /// 64 | /// The condition under which the middleware will be used. 65 | /// The delegate to handle the middleware. 66 | /// The so that additional calls can be chained. 67 | public ApplicationBuilder When(Func predicate, ApplicationDelegate handler) 68 | { 69 | return Use(next => async context => 70 | { 71 | if (predicate(context)) 72 | { 73 | await handler(context); 74 | } 75 | else 76 | { 77 | await next(context); 78 | } 79 | }); 80 | } 81 | 82 | /// 83 | /// Adds a conditional middleware to the application. 84 | /// 85 | /// The condition under which the middleware will be used. 86 | /// The action to configure the middleware. 87 | /// The so that additional calls can be chained. 88 | public ApplicationBuilder When(Func predicate, Action> configureAction) 89 | { 90 | return Use(next => async context => 91 | { 92 | if (predicate(context)) 93 | { 94 | var branchBuilder = this.New(); 95 | configureAction(branchBuilder); 96 | await branchBuilder.Build().Invoke(context); 97 | } 98 | else 99 | { 100 | await next(context); 101 | } 102 | }); 103 | } 104 | 105 | /// 106 | /// Adds a middleware of the specified type to the application. 107 | /// 108 | /// The type of the middleware to add. 109 | /// The so that additional calls can be chained. 110 | public ApplicationBuilder Use() 111 | where TMiddleware : IApplicationMiddleware 112 | { 113 | var middleware = ActivatorUtilities.GetServiceOrCreateInstance(this.ApplicationServices); 114 | return Use(middleware); 115 | } 116 | 117 | /// 118 | /// Adds the specified middleware to the application. 119 | /// 120 | /// The type of the middleware to add. 121 | /// The middleware to add. 122 | /// The so that additional calls can be chained. 123 | public ApplicationBuilder Use(TMiddleware middleware) 124 | where TMiddleware : IApplicationMiddleware 125 | { 126 | return Use(middleware.InvokeAsync); 127 | } 128 | 129 | /// 130 | /// Adds the specified middleware to the application. 131 | /// 132 | /// The middleware to add. 133 | /// The so that additional calls can be chained. 134 | public ApplicationBuilder Use(Func, TContext, ValueTask> middleware) 135 | { 136 | return Use(next => context => middleware(next, context)); 137 | } 138 | 139 | /// 140 | /// Adds the specified middleware to the application. 141 | /// 142 | /// The middleware to add. 143 | /// The so that additional calls can be chained. 144 | public ApplicationBuilder Use(Func, ApplicationDelegate> middleware) 145 | { 146 | _middlewares.Add(middleware); 147 | return this; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Shared/Application/ApplicationDelegate.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Shared.Application; 2 | 3 | /// 4 | /// Represents a delegate that can handle application requests. 5 | /// 6 | /// The type of the middleware context. 7 | /// The middleware context. 8 | /// A task that represents the asynchronous operation. 9 | public delegate ValueTask ApplicationDelegate(TContext context); 10 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Shared/Application/IApplicationMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Shared.Application; 2 | 3 | /// 4 | /// Interface for application middleware. 5 | /// 6 | /// The type of the middleware context. 7 | public interface IApplicationMiddleware 8 | { 9 | /// 10 | /// Executes the middleware. 11 | /// 12 | /// The next middleware in the pipeline. 13 | /// The context for the middleware. 14 | /// A task that represents the asynchronous operation. 15 | ValueTask InvokeAsync(ApplicationDelegate next, TContext context); 16 | } 17 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Shared/Collections/PooledArray.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace SatelliteRpc.Shared.Collections; 4 | 5 | /// 6 | /// Represents a disposable array object that uses an array pool for better performance and less memory usage. 7 | /// 8 | /// The type of the elements in the array. 9 | public class PooledArray : IDisposable 10 | { 11 | private readonly T[] _array; 12 | private readonly int _length; 13 | private bool _disposed; 14 | 15 | /// 16 | /// Initializes a new instance of the PooledArray class with a specified length. 17 | /// 18 | /// The length of the array. 19 | public PooledArray(int length) 20 | { 21 | _length = length; 22 | _array = ArrayPool.Shared.Rent(length); 23 | } 24 | 25 | /// 26 | /// Gets a span that represents the array. 27 | /// 28 | public Span Span 29 | { 30 | get 31 | { 32 | ThrowIfDisposed(); 33 | return _array.AsSpan(0, _length); 34 | } 35 | } 36 | 37 | /// 38 | /// Gets a memory that represents the array. 39 | /// 40 | public Memory Memory 41 | { 42 | get 43 | { 44 | ThrowIfDisposed(); 45 | return _array.AsMemory(0, _length); 46 | } 47 | } 48 | 49 | /// 50 | /// Gets the raw array as an ArraySegment. 51 | /// 52 | public ArraySegment RawArray 53 | { 54 | get 55 | { 56 | ThrowIfDisposed(); 57 | return new ArraySegment(_array, 0, _length); 58 | } 59 | } 60 | 61 | /// 62 | /// Gets the length of the array. 63 | /// 64 | public int Length => _length; 65 | 66 | /// 67 | /// Releases the array back to the pool and sets the PooledArray instance as disposed. 68 | /// 69 | public void Dispose() 70 | { 71 | if (!_disposed) 72 | { 73 | try 74 | { 75 | ArrayPool.Shared.Return(_array); 76 | } 77 | catch (Exception) 78 | { 79 | // Catch exceptions because ArrayPool doesn't always accept returned arrays 80 | } 81 | _disposed = true; 82 | } 83 | } 84 | 85 | /// 86 | /// Throws an ObjectDisposedException if the PooledArray instance has been disposed. 87 | /// 88 | private void ThrowIfDisposed() 89 | { 90 | if (_disposed) 91 | { 92 | throw new ObjectDisposedException("PooledArray is disposed"); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Shared/Collections/PooledArrayExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Shared.Collections; 2 | 3 | /// 4 | /// Contains extension methods for PooledArray and Array classes. 5 | /// 6 | public static class PooledArrayExtensions 7 | { 8 | /// 9 | /// Converts a regular array to a PooledArray. 10 | /// 11 | /// The array to be converted. 12 | /// The type of the elements in the array. 13 | /// A new instance of PooledArray containing the same elements as the input array. 14 | public static PooledArray ToPooledArray(this T[] array) 15 | { 16 | var pooledArray = new PooledArray(array.Length); 17 | array.CopyTo(pooledArray.Span); 18 | return pooledArray; 19 | } 20 | 21 | /// 22 | /// Converts a PooledArray to a regular array. 23 | /// 24 | /// The PooledArray to be converted. 25 | /// The type of the elements in the PooledArray. 26 | /// A new array containing the same elements as the input PooledArray. 27 | public static T[] ToArray(this PooledArray pooledArray) 28 | { 29 | return pooledArray.Span.ToArray(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Shared/Collections/PooledList.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | using System.Buffers; 3 | using System.Collections; 4 | 5 | namespace SatelliteRpc.Shared.Collections; 6 | 7 | /// 8 | /// A list that uses array pooling for improved performance and reduced GC pressure. 9 | /// 10 | /// The type of elements in the list. 11 | public class PooledList : IList, IDisposable 12 | { 13 | internal T[] Buffer; 14 | private int _count; 15 | 16 | /// 17 | /// Initializes a new instance of the PooledList class with a specified initial capacity. 18 | /// 19 | /// The number of elements that the new list can initially store. 20 | public PooledList(int initialCapacity = 8) 21 | { 22 | if (initialCapacity < 0) throw new ArgumentOutOfRangeException(nameof(initialCapacity)); 23 | Buffer = ArrayPool.Shared.Rent(initialCapacity); 24 | } 25 | 26 | /// 27 | /// Gets or sets the element at the specified index. 28 | /// 29 | /// The zero-based index of the element to get or set. 30 | public T this[int index] 31 | { 32 | get 33 | { 34 | if (index < 0 || index >= _count) throw new IndexOutOfRangeException(); 35 | return Buffer[index]; 36 | } 37 | set 38 | { 39 | if (index < 0 || index >= _count) throw new IndexOutOfRangeException(); 40 | Buffer[index] = value; 41 | } 42 | } 43 | 44 | /// 45 | /// Gets a span that includes all elements of the list. 46 | /// 47 | public Span Span => Buffer.AsSpan(0, _count); 48 | 49 | /// 50 | /// Gets the number of elements contained in the list. 51 | /// 52 | public int Count => _count; 53 | 54 | /// 55 | /// Gets a value indicating whether the list is read-only. 56 | /// 57 | public bool IsReadOnly => false; 58 | 59 | /// 60 | /// Adds an item to the list. 61 | /// 62 | /// The object to add to the list. 63 | public void Add(T item) 64 | { 65 | EnsureCapacity(_count + 1); 66 | Buffer[_count++] = item; 67 | } 68 | 69 | /// 70 | /// Removes all items from the list. 71 | /// 72 | public void Clear() 73 | { 74 | Array.Clear(Buffer, 0, _count); // Clear to allow GC to collect 75 | _count = 0; 76 | } 77 | 78 | /// 79 | /// Determines whether the list contains a specific value. 80 | /// 81 | /// The object to locate in the list. 82 | public bool Contains(T item) 83 | { 84 | return Array.IndexOf(Buffer, item, 0, _count) >= 0; 85 | } 86 | 87 | /// 88 | /// Copies the elements of the list to an Array, starting at a particular Array index. 89 | /// 90 | /// The one-dimensional Array that is the destination of the elements copied from list. 91 | /// The zero-based index in array at which copying begins. 92 | public void CopyTo(T[] array, int arrayIndex) 93 | { 94 | Array.Copy(Buffer, 0, array, arrayIndex, _count); 95 | } 96 | 97 | /// 98 | /// Returns an enumerator that iterates through the list. 99 | /// 100 | public IEnumerator GetEnumerator() 101 | { 102 | for (var i = 0; i < _count; i++) 103 | { 104 | yield return Buffer[i]; 105 | } 106 | } 107 | 108 | /// 109 | /// Determines the index of a specific item in the list. 110 | /// 111 | /// The object to locate in the list. 112 | public int IndexOf(T item) 113 | { 114 | return Array.IndexOf(Buffer, item, 0, _count); 115 | } 116 | 117 | /// 118 | /// Inserts an item to the list at the specified index. 119 | /// 120 | /// The zero-based index at which item should be inserted. 121 | /// The object to insert into the list. 122 | public void Insert(int index, T item) 123 | { 124 | if (index < 0 || index > _count) throw new ArgumentOutOfRangeException(nameof(index)); 125 | EnsureCapacity(_count + 1); 126 | Array.Copy(Buffer, index, Buffer, index + 1, _count - index); 127 | Buffer[index] = item; 128 | _count++; 129 | } 130 | 131 | /// 132 | /// Removes the first occurrence of a specific object from the list. 133 | /// 134 | /// The object to remove from the list. 135 | public bool Remove(T item) 136 | { 137 | var index = Array.IndexOf(Buffer, item, 0, _count); 138 | if (index < 0) return false; 139 | RemoveAt(index); 140 | return true; 141 | } 142 | 143 | /// 144 | /// Removes the list item at the specified index. 145 | /// 146 | /// The zero-based index of the item to remove. 147 | public void RemoveAt(int index) 148 | { 149 | if (index < 0 || index >= _count) throw new ArgumentOutOfRangeException(nameof(index)); 150 | _count--; 151 | Array.Copy(Buffer, index + 1, Buffer, index, _count - index); 152 | Buffer[_count] = default; // Clear to allow GC to collect 153 | } 154 | 155 | /// 156 | /// Returns an enumerator that iterates through a collection. 157 | /// 158 | IEnumerator IEnumerable.GetEnumerator() 159 | { 160 | return GetEnumerator(); 161 | } 162 | 163 | /// 164 | /// Releases all resources used by the PooledList. 165 | /// 166 | public void Dispose() 167 | { 168 | if (Buffer != null) 169 | { 170 | try 171 | { 172 | ArrayPool.Shared.Return(Buffer); 173 | } 174 | catch (Exception) 175 | { 176 | // Catch exceptions because ArrayPool doesn't always accept returned arrays 177 | } 178 | Buffer = null; 179 | } 180 | } 181 | 182 | /// 183 | /// Ensures that the capacity of this list is at least the specified minimum value. 184 | /// 185 | /// The new minimum capacity for this list. 186 | private void EnsureCapacity(int min) 187 | { 188 | if (Buffer.Length < min) 189 | { 190 | var newCapacity = Buffer.Length == 0 ? 4 : Buffer.Length * 2; 191 | if (newCapacity < min) newCapacity = min; 192 | var newBuffer = ArrayPool.Shared.Rent(newCapacity); 193 | if (_count > 0) 194 | { 195 | Array.Copy(Buffer, 0, newBuffer, 0, _count); 196 | ArrayPool.Shared.Return(Buffer); 197 | } 198 | Buffer = newBuffer; 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Shared/DisposeManager.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Shared.Collections; 2 | 3 | namespace SatelliteRpc.Shared; 4 | 5 | /// 6 | /// A class to manage the lifecycle of IDisposable resources. 7 | /// It provides a centralized way of disposing of multiple resources. 8 | /// 9 | public class DisposeManager : IDisposable 10 | { 11 | /// 12 | /// A list of resources that implement IDisposable. 13 | /// These resources will be disposed of when the DisposeManager is disposed. 14 | /// 15 | private readonly PooledList _resources = new(); 16 | 17 | /// 18 | /// Register an IDisposable resource to be disposed of when the DisposeManager is disposed. 19 | /// 20 | /// The IDisposable resource to register. 21 | public void RegisterForDispose(IDisposable resource) 22 | { 23 | _resources.Add(resource); 24 | } 25 | 26 | /// 27 | /// Disposes of all registered resources and clears the list. 28 | /// This method should be called when the resources are no longer needed. 29 | /// 30 | public void Dispose() 31 | { 32 | foreach (var resource in _resources) 33 | { 34 | resource.Dispose(); 35 | } 36 | 37 | _resources.Clear(); 38 | _resources.Dispose(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/SatelliteRpc.Shared/MethodInvoker.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | 5 | namespace SatelliteRpc.Shared; 6 | 7 | /// 8 | /// A static class that provides a method to create a method invoker. 9 | /// 10 | public static class MethodInvoker 11 | { 12 | /// 13 | /// Creates a method invoker for a given type and method. 14 | /// 15 | /// The type of the object that contains the method to invoke. 16 | /// The method to invoke. 17 | /// A function that takes an object instance and an array of arguments, and returns the result of the method invocation. 18 | public static Func CreateInvoker(Type type, MethodInfo methodInfo) 19 | { 20 | // Get the parameters of the method 21 | var parameters = methodInfo.GetParameters(); 22 | 23 | // Create the parameters for the lambda expression 24 | var instance = Expression.Parameter(typeof(object), "instance"); 25 | var arguments = Expression.Parameter(typeof(object[]), "arguments"); 26 | 27 | // Create the method call expression 28 | var methodCall = Expression.Call( 29 | Expression.Convert(instance, type), 30 | methodInfo, 31 | CreateParameterExpressions(parameters, arguments)); 32 | 33 | // Create the lambda expression based on the return type of the method 34 | LambdaExpression lambdaExpression; 35 | if (methodInfo.ReturnType == typeof(void)) 36 | { 37 | var voidLambda = Expression.Lambda>(methodCall, instance, arguments); 38 | return (ins, args) => 39 | { 40 | voidLambda.Compile().Invoke(ins, args!); 41 | return null!; 42 | }; 43 | } 44 | 45 | if (methodInfo.ReturnType == typeof(Task)) 46 | { 47 | lambdaExpression = Expression.Lambda>(methodCall, instance, arguments); 48 | } 49 | else if (methodInfo.ReturnType.IsGenericType && 50 | methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) 51 | { 52 | lambdaExpression = 53 | Expression.Lambda( 54 | typeof(Func<,,>).MakeGenericType(typeof(object), typeof(object[]), methodInfo.ReturnType), 55 | methodCall, instance, arguments); 56 | } 57 | else 58 | { 59 | lambdaExpression = Expression.Lambda>(methodCall, instance, arguments); 60 | } 61 | 62 | return (Func)lambdaExpression.Compile(); 63 | } 64 | 65 | /// 66 | /// A ConcurrentDictionary that caches compiled functions for extracting results from different types of Tasks. 67 | /// 68 | private static readonly ConcurrentDictionary> ResultExtractors = new(); 69 | 70 | /// 71 | /// Gets the result of a Task. 72 | /// 73 | /// The Task to extract the result from. 74 | /// The result of the Task. 75 | public static object GetTaskResult(Task task) 76 | { 77 | var taskType = task.GetType(); 78 | if (!ResultExtractors.TryGetValue(taskType, out var extractor)) 79 | { 80 | extractor = CreateResultExtractor(taskType); 81 | ResultExtractors.TryAdd(taskType, extractor); 82 | } 83 | 84 | return extractor(task); 85 | } 86 | 87 | /// 88 | /// Creates a function that can extract the result from a Task of a specific type. 89 | /// 90 | /// The type of the Task to create the extractor for. 91 | /// A function that can extract the result from a Task of the provided type. 92 | private static Func CreateResultExtractor(Type taskType) 93 | { 94 | var parameter = Expression.Parameter(typeof(Task), "task"); 95 | var cast = Expression.Convert(parameter, taskType); 96 | var property = Expression.Property(cast, "Result"); 97 | var castResult = Expression.Convert(property, typeof(object)); 98 | var lambda = Expression.Lambda>(castResult, parameter); 99 | return lambda.Compile(); 100 | } 101 | 102 | /// 103 | /// Creates an array of Expressions that represents the parameters for a method call. 104 | /// 105 | /// The parameters of the method. 106 | /// The Expression that represents the arguments to the method. 107 | /// An array of Expressions that represents the parameters for a method call. 108 | private static Expression[] CreateParameterExpressions(ParameterInfo[] parameters, Expression arguments) 109 | { 110 | var expressions = new Expression[parameters.Length]; 111 | for (var i = 0; i < parameters.Length; i++) 112 | { 113 | var parameter = parameters[i]; 114 | var argument = Expression.ArrayIndex(arguments, Expression.Constant(i)); 115 | expressions[i] = Expression.Convert(argument, parameter.ParameterType); 116 | } 117 | 118 | return expressions; 119 | } 120 | } -------------------------------------------------------------------------------- /src/SatelliteRpc.Shared/SatelliteRpc.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/SatelliteRpc.Protocol.Tests/AppRequestTests.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using SatelliteRpc.Protocol.Protocol; 3 | using SatelliteRpc.Shared.Collections; 4 | 5 | namespace SatelliteRpc.Protocol.Tests; 6 | 7 | public class AppRequestTests 8 | { 9 | [Fact] 10 | public void Can_Serialize_And_Deserialize_Request() 11 | { 12 | var original = new AppRequest 13 | { 14 | Id = 12345, 15 | Path = "/test/path", 16 | PayloadType = PayloadType.Json, 17 | Payload = new byte[] { 1, 2, 3, 4, 5 }.ToPooledArray() 18 | }; 19 | 20 | var writer = new ArrayBufferWriter(); 21 | original.Serialize(writer); 22 | 23 | var sequence = new ReadOnlySequence(writer.WrittenMemory); 24 | var (success,deserialized) = AppRequest.TryDeserialize(sequence, out var consumed); 25 | 26 | Assert.Equal(sequence.Length, consumed.GetInteger()); 27 | Assert.True(success); 28 | Assert.NotNull(deserialized); 29 | Assert.Equal(original.Id, deserialized.Id); 30 | Assert.Equal(original.Path, deserialized.Path); 31 | Assert.Equal(original.PayloadType, deserialized.PayloadType); 32 | Assert.Equal(original.Payload.ToArray(), deserialized.Payload.ToArray()); 33 | } 34 | 35 | [Fact] 36 | public void Can_Serialize_And_Deserialize_Request_With_PayloadWriteTo() 37 | { 38 | var payload = Enumerable.Range(0, 10240).Select(c => (byte)c).ToArray(); 39 | var original = new AppRequest 40 | { 41 | Id = 12345, 42 | Path = "/test/path", 43 | PayloadType = PayloadType.Json, 44 | PayloadWriter = new PayloadWriter 45 | { 46 | GetPayloadSize = () => payload.Length, 47 | PayloadWriteTo = (writer) => 48 | { 49 | writer.Write(payload); 50 | } 51 | } 52 | }; 53 | 54 | var writer = new ArrayBufferWriter(); 55 | original.Serialize(writer); 56 | 57 | var sequence = new ReadOnlySequence(writer.WrittenMemory); 58 | var (success,deserialized) = AppRequest.TryDeserialize(sequence, out var consumed); 59 | 60 | Assert.Equal(sequence.Length, consumed.GetInteger()); 61 | Assert.True(success); 62 | Assert.NotNull(deserialized); 63 | 64 | Assert.Equal(original.Id, deserialized.Id); 65 | Assert.Equal(original.Path, deserialized.Path); 66 | Assert.Equal(original.PayloadType, deserialized.PayloadType); 67 | Assert.Equal(payload.ToArray(), deserialized.Payload.ToArray()); 68 | } 69 | 70 | [Fact] 71 | public void Deserialize_Request_Should_Fail_With_Not_Enough_Length() 72 | { 73 | var original = new AppRequest 74 | { 75 | Id = 12345, 76 | Path = "/test/path", 77 | PayloadType = PayloadType.Json, 78 | Payload = new byte[] { 1, 2, 3, 4, 5 }.ToPooledArray() 79 | }; 80 | 81 | var writer = new ArrayBufferWriter(); 82 | original.Serialize(writer); 83 | 84 | var sequence = new ReadOnlySequence(writer.WrittenMemory[..^1]); 85 | var (success,deserialized) = AppRequest.TryDeserialize(sequence, out _); 86 | 87 | Assert.False(success); 88 | Assert.Null(deserialized); 89 | } 90 | 91 | [Fact] 92 | public void Deserialize_Request_Should_Fail_With_Not_Length() 93 | { 94 | 95 | var sequence = new ReadOnlySequence(new byte[] { 1, 2, 3}); 96 | var (success,deserialized) = AppRequest.TryDeserialize(sequence, out _); 97 | 98 | Assert.False(success); 99 | Assert.Null(deserialized); 100 | } 101 | 102 | [Fact] 103 | public void Can_Reuse_Memory_When_Deserializing_Request() 104 | { 105 | var original = new AppRequest 106 | { 107 | Id = 12345, 108 | Path = "/test/path", 109 | PayloadType = PayloadType.Json, 110 | Payload = new byte[] { 1, 2, 3, 4, 5 }.ToPooledArray() 111 | }; 112 | 113 | var writer = new ArrayBufferWriter(); 114 | original.Serialize(writer); 115 | 116 | var sequence = new ReadOnlySequence(writer.WrittenMemory); 117 | 118 | var reuse = new AppRequest(); 119 | var (_,deserialized) = AppRequest.TryDeserialize(sequence, out _, reuse); 120 | 121 | Assert.Same(reuse, deserialized); 122 | } 123 | } -------------------------------------------------------------------------------- /tests/SatelliteRpc.Protocol.Tests/AppResponseTests.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using SatelliteRpc.Protocol.Protocol; 3 | using SatelliteRpc.Shared.Collections; 4 | 5 | namespace SatelliteRpc.Protocol.Tests; 6 | 7 | public class AppResponseTests 8 | { 9 | [Fact] 10 | public void Can_Serialize_And_Deserialize_Response() 11 | { 12 | var original = new AppResponse 13 | { 14 | Id = 12345, 15 | Status = ResponseStatus.Success, 16 | PayloadType = PayloadType.Json, 17 | Payload = new byte[]{ 1, 2, 3, 4, 5 }.ToPooledArray() 18 | }; 19 | 20 | var writer = new ArrayBufferWriter(); 21 | original.Serialize(writer); 22 | 23 | var sequence = new ReadOnlySequence(writer.WrittenMemory); 24 | var (success,deserialized) = AppResponse.TryDeserialize(sequence, out var consumed); 25 | 26 | Assert.Equal(sequence.Length, consumed.GetInteger()); 27 | Assert.True(success); 28 | Assert.NotNull(deserialized); 29 | 30 | Assert.Equal(original.Id, deserialized.Id); 31 | Assert.Equal(original.Status, deserialized.Status); 32 | Assert.Equal(original.PayloadType, deserialized.PayloadType); 33 | Assert.Equal(original.Payload.ToArray(), deserialized.Payload.ToArray()); 34 | } 35 | 36 | [Fact] 37 | public void Can_Serialize_And_Deserialize_Response_With_PayloadWriteTo() 38 | { 39 | var payload = Enumerable.Range(0, 10240).Select(c => (byte)c).ToArray(); 40 | var original = new AppResponse 41 | { 42 | Id = 12345, 43 | Status = ResponseStatus.Success, 44 | PayloadType = PayloadType.Json, 45 | PayloadWriter = new PayloadWriter 46 | { 47 | GetPayloadSize = () => payload.Length, 48 | PayloadWriteTo = (writer) => 49 | { 50 | writer.Write(payload); 51 | } 52 | } 53 | }; 54 | 55 | var writer = new ArrayBufferWriter(); 56 | original.Serialize(writer); 57 | 58 | var sequence = new ReadOnlySequence(writer.WrittenMemory); 59 | var (success,deserialized) = AppResponse.TryDeserialize(sequence, out var consumed); 60 | 61 | Assert.Equal(sequence.Length, consumed.GetInteger()); 62 | Assert.True(success); 63 | Assert.NotNull(deserialized); 64 | 65 | Assert.Equal(original.Id, deserialized.Id); 66 | Assert.Equal(original.Status, deserialized.Status); 67 | Assert.Equal(original.PayloadType, deserialized.PayloadType); 68 | Assert.Equal(payload.ToArray(), deserialized.Payload.ToArray()); 69 | } 70 | 71 | [Fact] 72 | public void Deserialize_Response_Should_Fail_With_Not_Enough_Length() 73 | { 74 | var original = new AppResponse 75 | { 76 | Id = 12345, 77 | Status = ResponseStatus.Success, 78 | PayloadType = PayloadType.Json, 79 | Payload = new byte[]{ 1, 2, 3, 4, 5 }.ToPooledArray() 80 | }; 81 | 82 | var writer = new ArrayBufferWriter(); 83 | original.Serialize(writer); 84 | 85 | var sequence = new ReadOnlySequence(writer.WrittenMemory[..^1]); 86 | var (success,deserialized) = AppResponse.TryDeserialize(sequence, out _); 87 | 88 | Assert.False(success); 89 | Assert.Null(deserialized); 90 | } 91 | 92 | [Fact] 93 | public void Deserialize_Response_Should_Fail_With_Not_Length() 94 | { 95 | 96 | var sequence = new ReadOnlySequence(new byte[] { 1, 2, 3}); 97 | var (success,deserialized) = AppResponse.TryDeserialize(sequence, out _); 98 | 99 | Assert.False(success); 100 | Assert.Null(deserialized); 101 | } 102 | 103 | [Fact] 104 | public void Can_Reuse_Memory_When_Deserializing_Response() 105 | { 106 | var original = new AppResponse 107 | { 108 | Id = 12345, 109 | Status = ResponseStatus.Success, 110 | PayloadType = PayloadType.Json, 111 | Payload = new byte[]{ 1, 2, 3, 4, 5 }.ToPooledArray() 112 | }; 113 | 114 | var writer = new ArrayBufferWriter(); 115 | original.Serialize(writer); 116 | 117 | var sequence = new ReadOnlySequence(writer.WrittenMemory); 118 | 119 | var reuse = new AppResponse(); 120 | var (_,deserialized) = AppResponse.TryDeserialize(sequence, out _, reuse); 121 | 122 | Assert.Same(reuse, deserialized); 123 | } 124 | } -------------------------------------------------------------------------------- /tests/SatelliteRpc.Protocol.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /tests/SatelliteRpc.Protocol.Tests/PayloadConverterTests/ProtocolBufferPayloadConverterTests.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using SatelliteRpc.Protocol.PayloadConverters; 3 | using SatelliteRpc.Protocol.Protocol; 4 | using SatelliteRpc.Shared.Collections; 5 | using ServerProto; 6 | 7 | namespace SatelliteRpc.Protocol.Tests.PayloadConverterTests; 8 | 9 | public class ProtocolBufferPayloadConverterTests 10 | { 11 | private readonly ProtocolBufferPayloadConverter _converter = new(); 12 | 13 | [Fact] 14 | public void CreatePayloadWriter_NullPayload_ReturnsEmpty() 15 | { 16 | var writer = _converter.CreatePayloadWriter(null); 17 | Assert.Equal(PayloadWriter.Empty, writer); 18 | } 19 | 20 | [Fact] 21 | public void CreatePayloadWriter_ValidIMessage_ReturnsPayloadWriter() 22 | { 23 | var loginReq = new LoginReqProto { User = "test", Password = "password", Sn = 1 }; 24 | var writer = _converter.CreatePayloadWriter(loginReq); 25 | 26 | Assert.NotNull(writer.GetPayloadSize); 27 | Assert.NotNull(writer.PayloadWriteTo); 28 | Assert.Equal(loginReq.CalculateSize(), writer.GetPayloadSize()); 29 | } 30 | 31 | [Fact] 32 | public void CreatePayloadWriter_InvalidPayload_ThrowsException() 33 | { 34 | Assert.Throws(() => _converter.CreatePayloadWriter(new object())); 35 | } 36 | 37 | [Fact] 38 | public void Convert_InvalidType_ThrowsException() 39 | { 40 | Assert.Throws(() => _converter.Convert(new PooledArray(4), typeof(object))); 41 | } 42 | 43 | [Fact] 44 | public void Convert_EmptyPayload_ThrowsException() 45 | { 46 | Assert.Throws(() => _converter.Convert(new PooledArray(4), 47 | typeof(LoginRespProto))); 48 | } 49 | 50 | [Fact] 51 | public void Convert_ValidPayload_ReturnsMessage() 52 | { 53 | var loginResp = new LoginRespProto { IsOk = true, ErrMsg = "Success", Sn = 1 }; 54 | var bytes = loginResp.ToByteArray(); 55 | var pooledArray = new PooledArray(bytes.Length); 56 | bytes.CopyTo(pooledArray.Memory); 57 | 58 | var result = _converter.Convert(pooledArray, typeof(LoginRespProto)) as LoginRespProto; 59 | 60 | Assert.NotNull(result); 61 | Assert.Equal(loginResp.IsOk, result.IsOk); 62 | Assert.Equal(loginResp.ErrMsg, result.ErrMsg); 63 | Assert.Equal(loginResp.Sn, result.Sn); 64 | } 65 | } -------------------------------------------------------------------------------- /tests/SatelliteRpc.Protocol.Tests/SatelliteRpc.Protocol.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/SatelliteRpc.Server.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /tests/SatelliteRpc.Server.Tests/SatelliteRpc.Server.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/SatelliteRpc.Server.Tests/Transport/RpcConnectionHandlerTest.RunResponseHandler.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using System.Threading.Channels; 3 | using Microsoft.Extensions.Logging; 4 | using Moq; 5 | using SatelliteRpc.Server.Transport; 6 | 7 | namespace SatelliteRpc.Server.Tests.Transport; 8 | 9 | public partial class RpcConnectionHandlerTests 10 | { 11 | [Fact] 12 | public async Task RunResponseHandler_ShouldLogError_WhenExceptionOccurs() 13 | { 14 | var rpcConnectionHandler = new RpcConnectionHandler(_mockLogger.Object, _mockRpcOptionsAccessor.Object, 15 | _mockHandlerBuilder.Object); 16 | 17 | var mockChannelReader = new Mock>(); 18 | mockChannelReader.Setup(x => x.WaitToReadAsync(It.IsAny())) 19 | .ReturnsAsync(true); 20 | mockChannelReader.Setup(x => x.TryRead(out It.Ref.IsAny)) 21 | .Returns(true); 22 | 23 | var mockPipeWriter = new Mock(); 24 | mockPipeWriter.Setup(x => x.FlushAsync(It.IsAny())) 25 | .Throws(new Exception()); 26 | 27 | await rpcConnectionHandler.RunResponseHandler(mockChannelReader.Object, mockPipeWriter.Object, 28 | new CancellationToken()); 29 | 30 | _mockLogger.Verify( 31 | x => x.Log( 32 | LogLevel.Error, 33 | It.IsAny(), 34 | It.Is((v, t) => v.ToString()!.Contains("Run response handler error")), 35 | It.IsAny(), 36 | It.Is>((v, t) => true)!)); 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /tests/SatelliteRpc.Server.Tests/Transport/RpcConnectionHandlerTests.OnConnectedAsync.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Connections; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Options; 4 | using Moq; 5 | using SatelliteRpc.Server.Configuration; 6 | using SatelliteRpc.Server.Transport; 7 | 8 | namespace SatelliteRpc.Server.Tests.Transport; 9 | 10 | public partial class RpcConnectionHandlerTests 11 | { 12 | private readonly Mock> _mockLogger = new(); 13 | private readonly Mock> _mockRpcOptionsAccessor = new(); 14 | private readonly Mock _mockHandlerBuilder = new(); 15 | private readonly Mock _mockConnectionContext = new(); 16 | 17 | [Fact] 18 | public async Task OnConnectedAsync_ShouldLogInformation_WhenConnectionIsEstablished() 19 | { 20 | var rpcConnectionHandler = new RpcConnectionHandler(_mockLogger.Object, _mockRpcOptionsAccessor.Object, 21 | _mockHandlerBuilder.Object); 22 | 23 | await rpcConnectionHandler.OnConnectedAsync(_mockConnectionContext.Object); 24 | 25 | _mockLogger.Verify( 26 | x => x.Log( 27 | It.IsAny(), 28 | It.IsAny(), 29 | It.Is((v, t) => true), 30 | It.IsAny(), 31 | It.Is>((v, t) => true)!)); 32 | } 33 | 34 | [Fact] 35 | public async Task OnConnectedAsync_ShouldAbort_WhenExceptionOccurs() 36 | { 37 | var rpcConnectionHandler = new RpcConnectionHandler(_mockLogger.Object, _mockRpcOptionsAccessor.Object, 38 | _mockHandlerBuilder.Object); 39 | 40 | _mockConnectionContext.Setup(x => x.ConnectionClosed).Throws(new Exception()); 41 | 42 | await rpcConnectionHandler.OnConnectedAsync(_mockConnectionContext.Object); 43 | 44 | _mockConnectionContext.Verify(x => x.Abort(), Times.Once); 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /tests/SatelliteRpc.Shared.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /tests/SatelliteRpc.Shared.Tests/MethodInvokerTests.cs: -------------------------------------------------------------------------------- 1 | namespace SatelliteRpc.Shared.Tests; 2 | 3 | public class MethodInvokerTests 4 | { 5 | private class TestService 6 | { 7 | public void VoidMethod() 8 | { 9 | } 10 | 11 | public Task TaskMethod() => Task.CompletedTask; 12 | public Task TaskTMethod() => Task.FromResult(42); 13 | } 14 | 15 | [Fact] 16 | public void Test_Void_Method() 17 | { 18 | var type = typeof(TestService); 19 | var methodInfo = type.GetMethod(nameof(TestService.VoidMethod)); 20 | var invoker = MethodInvoker.CreateInvoker(type, methodInfo!); 21 | 22 | var service = new TestService(); 23 | var result = invoker(service, Array.Empty()); 24 | 25 | Assert.Null(result); 26 | } 27 | 28 | [Fact] 29 | public void Test_Task_Method() 30 | { 31 | var type = typeof(TestService); 32 | var methodInfo = type.GetMethod(nameof(TestService.TaskMethod)); 33 | var invoker = MethodInvoker.CreateInvoker(type, methodInfo!); 34 | 35 | var service = new TestService(); 36 | var result = invoker(service, Array.Empty()); 37 | 38 | Assert.IsAssignableFrom(result); 39 | Assert.True(((Task)result).IsCompleted); 40 | } 41 | 42 | 43 | [Fact] 44 | public async Task Test_Task_TMethod() 45 | { 46 | var type = typeof(TestService); 47 | var methodInfo = type.GetMethod(nameof(TestService.TaskTMethod)); 48 | var invoker = MethodInvoker.CreateInvoker(type, methodInfo!); 49 | 50 | var service = new TestService(); 51 | var result = invoker(service, Array.Empty()); 52 | 53 | await Assert.IsType>(result); 54 | var taskResult = await (Task)result; 55 | Assert.Equal(42, taskResult); 56 | } 57 | } -------------------------------------------------------------------------------- /tests/SatelliteRpc.Shared.Tests/PooledArrayTests.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Shared.Collections; 2 | 3 | namespace SatelliteRpc.Shared.Tests; 4 | 5 | public class PooledArrayTests 6 | { 7 | [Fact] 8 | public void Can_Rent_And_Return_Array() 9 | { 10 | using var pooledArray = new PooledArray(10); 11 | Assert.NotNull(pooledArray.RawArray.Array); 12 | Assert.Equal(10, pooledArray.RawArray.Count); 13 | } 14 | 15 | [Fact] 16 | public void Can_Access_Array_As_Span() 17 | { 18 | using var pooledArray = new PooledArray(10); 19 | Span span = pooledArray.Span; 20 | Assert.Equal(10, span.Length); 21 | } 22 | 23 | [Fact] 24 | public void Can_Access_Array_As_Memory() 25 | { 26 | using var pooledArray = new PooledArray(10); 27 | Memory memory = pooledArray.Memory; 28 | Assert.Equal(10, memory.Length); 29 | } 30 | 31 | [Fact] 32 | public void Throws_When_Disposed() 33 | { 34 | var pooledArray = new PooledArray(10); 35 | pooledArray.Dispose(); 36 | 37 | Assert.Throws(() => pooledArray.RawArray); 38 | Assert.Throws(() => 39 | { 40 | _ = pooledArray.Span; 41 | }); 42 | Assert.Throws(() => pooledArray.Memory); 43 | } 44 | } -------------------------------------------------------------------------------- /tests/SatelliteRpc.Shared.Tests/PooledListTests.cs: -------------------------------------------------------------------------------- 1 | using SatelliteRpc.Shared.Collections; 2 | 3 | namespace SatelliteRpc.Shared.Tests; 4 | 5 | public class PooledListTests 6 | { 7 | [Fact] 8 | public void Add_Adds_Items_Correctly() 9 | { 10 | using var list = new PooledList(); 11 | list.Add(1); 12 | list.Add(2); 13 | list.Add(3); 14 | 15 | Assert.Equal(3, list.Count); 16 | Assert.Equal(1, list[0]); 17 | Assert.Equal(2, list[1]); 18 | Assert.Equal(3, list[2]); 19 | } 20 | 21 | [Fact] 22 | public void Insert_Inserts_Items_Correctly() 23 | { 24 | using var list = new PooledList(); 25 | list.Add(1); 26 | list.Add(2); 27 | list.Insert(1, 3); 28 | 29 | Assert.Equal(3, list.Count); 30 | Assert.Equal(1, list[0]); 31 | Assert.Equal(3, list[1]); 32 | Assert.Equal(2, list[2]); 33 | } 34 | 35 | [Fact] 36 | public void Remove_Removes_Items_Correctly() 37 | { 38 | using var list = new PooledList(); 39 | list.Add(1); 40 | list.Add(2); 41 | list.Add(3); 42 | list.Remove(2); 43 | 44 | Assert.Equal(2, list.Count); 45 | Assert.Equal(1, list[0]); 46 | Assert.Equal(3, list[1]); 47 | } 48 | 49 | [Fact] 50 | public void Clear_Clears_Items_Correctly() 51 | { 52 | using var list = new PooledList(); 53 | list.Add(1); 54 | list.Add(2); 55 | list.Add(3); 56 | list.Clear(); 57 | 58 | Assert.Empty(list); 59 | } 60 | 61 | [Fact] 62 | public void Indexer_Gets_And_Sets_Correctly() 63 | { 64 | using var list = new PooledList(); 65 | list.Add(1); 66 | list.Add(2); 67 | list[1] = 3; 68 | 69 | Assert.Equal(3, list[1]); 70 | } 71 | 72 | [Fact] 73 | public void Dispose_Returns_Buffer_To_Pool() 74 | { 75 | var list = new PooledList(); 76 | list.Add(1); 77 | list.Dispose(); 78 | 79 | Assert.Null(list.Buffer); 80 | } 81 | 82 | [Fact] 83 | public void Enumerator_Enumerates_Correctly() 84 | { 85 | using var list = new PooledList(); 86 | list.Add(1); 87 | list.Add(2); 88 | list.Add(3); 89 | 90 | int expected = 1; 91 | foreach (var item in list) 92 | { 93 | Assert.Equal(expected++, item); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /tests/SatelliteRpc.Shared.Tests/SatelliteRpc.Shared.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------