Skip to content

Commit 91ddda0

Browse files
committed
feat: 实现源生成调用 #17
1 parent 0d56c38 commit 91ddda0

14 files changed

Lines changed: 1065 additions & 26 deletions

File tree

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
12
<Project Sdk="Microsoft.NET.Sdk">
2-
3-
<PropertyGroup>
4-
<TargetFramework>netstandard2.0</TargetFramework>
5-
<LangVersion>latest</LangVersion>
6-
<Nullable>enable</Nullable>
7-
<IncludeBuildOutput>false</IncludeBuildOutput>
8-
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
9-
</PropertyGroup>
10-
11-
<ItemGroup>
12-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="all" />
13-
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
14-
</ItemGroup>
15-
16-
<ItemGroup>
17-
<Analyzer Include="$(OutputPath)$(AssemblyName).dll" />
18-
</ItemGroup>
19-
20-
</Project>
21-
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<LangVersion>latest</LangVersion>
6+
<Nullable>enable</Nullable>
7+
<IncludeBuildOutput>true</IncludeBuildOutput>
8+
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
9+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
10+
</PropertyGroup>
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp"
13+
Version="4.5.0"
14+
PrivateAssets="all" />
15+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers"
16+
Version="3.3.4"
17+
PrivateAssets="all" />
18+
</ItemGroup>
19+
</Project>

Cyaim.WebSocketServer/Cyaim.WebSocketServer.SourceGenerator/EndpointInjectorGenerator.cs

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,22 @@ public void Execute(GeneratorExecutionContext context)
5050
continue;
5151

5252
// 生成注入器代码
53-
var source = GenerateInjectorCode(namespaceName, className, classFullName, httpContextProperty, webSocketProperty);
54-
context.AddSource($"{className}Injector.g.cs", SourceText.From(source, Encoding.UTF8));
53+
var injectorSource = GenerateInjectorCode(namespaceName, className, classFullName, httpContextProperty, webSocketProperty);
54+
context.AddSource($"{className}Injector.g.cs", SourceText.From(injectorSource, Encoding.UTF8));
55+
56+
// 为每个带有 WebSocketAttribute 的方法生成调用器
57+
var methods = classSymbol.GetMembers()
58+
.OfType<IMethodSymbol>()
59+
.Where(m => m.GetAttributes().Any(attr =>
60+
attr.AttributeClass?.Name == "WebSocketAttribute" ||
61+
attr.AttributeClass?.ToDisplayString() == "Cyaim.WebSocketServer.Infrastructure.Attributes.WebSocketAttribute"))
62+
.ToList();
63+
64+
foreach (var method in methods)
65+
{
66+
var methodInvokerSource = GenerateMethodInvokerCode(namespaceName, className, classFullName, method);
67+
context.AddSource($"{className}_{method.Name}Invoker.g.cs", SourceText.From(methodInvokerSource, Encoding.UTF8));
68+
}
5569
}
5670
}
5771

@@ -98,6 +112,114 @@ private string GenerateInjectorCode(string namespaceName, string className, stri
98112

99113
return sb.ToString();
100114
}
115+
116+
private string GenerateMethodInvokerCode(string namespaceName, string className, string classFullName, IMethodSymbol method)
117+
{
118+
var sb = new StringBuilder();
119+
sb.AppendLine("using Cyaim.WebSocketServer.Infrastructure.Injectors;");
120+
sb.AppendLine("using System;");
121+
sb.AppendLine();
122+
sb.AppendLine($"namespace {namespaceName}");
123+
sb.AppendLine("{");
124+
sb.AppendLine($" /// <summary>");
125+
sb.AppendLine($" /// 源代码生成的方法调用器:{className}.{method.Name}");
126+
sb.AppendLine($" /// </summary>");
127+
sb.AppendLine($" public class {className}_{method.Name}Invoker : IMethodInvoker");
128+
sb.AppendLine(" {");
129+
sb.AppendLine(" public object Invoke(object instance, object[] args)");
130+
sb.AppendLine(" {");
131+
sb.AppendLine($" if (instance is {classFullName} target)");
132+
sb.AppendLine(" {");
133+
134+
// 构建方法调用参数
135+
var parameters = method.Parameters;
136+
var hasParameters = parameters.Length > 0;
137+
138+
if (hasParameters)
139+
{
140+
for (int i = 0; i < parameters.Length; i++)
141+
{
142+
var param = parameters[i];
143+
var paramType = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
144+
var paramName = param.Name;
145+
var isValueType = param.Type.IsValueType;
146+
147+
// 检查是否为可空值类型 (Nullable<T>)
148+
var isNullable = param.Type is INamedTypeSymbol nullableNamedType &&
149+
nullableNamedType.IsGenericType &&
150+
nullableNamedType.ConstructedFrom?.ToDisplayString() == "System.Nullable<T>";
151+
152+
sb.AppendLine($" var arg{i} = args.Length > {i} ? args[{i}] : null;");
153+
154+
if (isValueType && !isNullable)
155+
{
156+
// 值类型需要转换
157+
sb.AppendLine($" {paramType} {paramName};");
158+
sb.AppendLine($" if (arg{i} == null)");
159+
sb.AppendLine($" {{");
160+
sb.AppendLine($" {paramName} = default({paramType});");
161+
sb.AppendLine($" }}");
162+
sb.AppendLine($" else if (arg{i} is {paramType} typed{i})");
163+
sb.AppendLine($" {{");
164+
sb.AppendLine($" {paramName} = typed{i};");
165+
sb.AppendLine($" }}");
166+
sb.AppendLine($" else");
167+
sb.AppendLine($" {{");
168+
sb.AppendLine($" {paramName} = ({paramType})Convert.ChangeType(arg{i}, typeof({paramType}));");
169+
sb.AppendLine($" }}");
170+
}
171+
else
172+
{
173+
// 引用类型或可空类型
174+
sb.AppendLine($" var {paramName} = arg{i} as {paramType};");
175+
}
176+
}
177+
}
178+
179+
// 构建方法调用
180+
var methodName = method.Name;
181+
var returnType = method.ReturnType;
182+
var isVoid = returnType.SpecialType == SpecialType.System_Void;
183+
var isTask = returnType.Name == "Task";
184+
var isTaskOfT = returnType is INamedTypeSymbol taskNamedType &&
185+
taskNamedType.IsGenericType &&
186+
taskNamedType.ConstructedFrom?.ToDisplayString() == "System.Threading.Tasks.Task<TResult>";
187+
188+
sb.Append(" ");
189+
if (!isVoid)
190+
{
191+
sb.Append("var result = ");
192+
}
193+
sb.Append($"target.{methodName}(");
194+
195+
if (hasParameters)
196+
{
197+
var paramNames = parameters.Select(p => p.Name);
198+
sb.Append(string.Join(", ", paramNames));
199+
}
200+
201+
sb.AppendLine(");");
202+
203+
// 处理返回值
204+
if (isVoid)
205+
{
206+
sb.AppendLine(" return null;");
207+
}
208+
else
209+
{
210+
sb.AppendLine(" return result;");
211+
}
212+
213+
sb.AppendLine(" }");
214+
sb.AppendLine();
215+
sb.AppendLine(" // 如果类型不匹配,返回 null(兼容性回退)");
216+
sb.AppendLine(" return null;");
217+
sb.AppendLine(" }");
218+
sb.AppendLine(" }");
219+
sb.AppendLine("}");
220+
221+
return sb.ToString();
222+
}
101223
}
102224

103225
/// <summary>

Cyaim.WebSocketServer/Cyaim.WebSocketServer.sln

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Clients", "Clients", "{57A6
3333
EndProject
3434
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cyaim.WebSocketServer.Client", "Clients\Cyaim.WebSocketServer.Client\Cyaim.WebSocketServer.Client.csproj", "{A7806838-171C-4A21-19DD-FF4881BDA87A}"
3535
EndProject
36+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cyaim.WebSocketServer.SourceGenerator", "Cyaim.WebSocketServer.SourceGenerator\Cyaim.WebSocketServer.SourceGenerator.csproj", "{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}"
37+
EndProject
38+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cyaim.WebSocketServer.Tests.Injector", "Tests\Cyaim.WebSocketServer.Tests.Injector\Cyaim.WebSocketServer.Tests.Injector.csproj", "{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}"
39+
EndProject
40+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{81668B5E-0334-4CFF-AEA3-60E037A1008D}"
41+
EndProject
3642
Global
3743
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3844
Debug|Any CPU = Debug|Any CPU
@@ -175,6 +181,30 @@ Global
175181
{A7806838-171C-4A21-19DD-FF4881BDA87A}.Release|x64.Build.0 = Release|Any CPU
176182
{A7806838-171C-4A21-19DD-FF4881BDA87A}.Release|x86.ActiveCfg = Release|Any CPU
177183
{A7806838-171C-4A21-19DD-FF4881BDA87A}.Release|x86.Build.0 = Release|Any CPU
184+
{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
185+
{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
186+
{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}.Debug|x64.ActiveCfg = Debug|Any CPU
187+
{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}.Debug|x64.Build.0 = Debug|Any CPU
188+
{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}.Debug|x86.ActiveCfg = Debug|Any CPU
189+
{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}.Debug|x86.Build.0 = Debug|Any CPU
190+
{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
191+
{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}.Release|Any CPU.Build.0 = Release|Any CPU
192+
{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}.Release|x64.ActiveCfg = Release|Any CPU
193+
{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}.Release|x64.Build.0 = Release|Any CPU
194+
{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}.Release|x86.ActiveCfg = Release|Any CPU
195+
{16DB79F7-D876-3AC6-644B-7F2A53EA72E1}.Release|x86.Build.0 = Release|Any CPU
196+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
197+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}.Debug|Any CPU.Build.0 = Debug|Any CPU
198+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}.Debug|x64.ActiveCfg = Debug|Any CPU
199+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}.Debug|x64.Build.0 = Debug|Any CPU
200+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}.Debug|x86.ActiveCfg = Debug|Any CPU
201+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}.Debug|x86.Build.0 = Debug|Any CPU
202+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}.Release|Any CPU.ActiveCfg = Release|Any CPU
203+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}.Release|Any CPU.Build.0 = Release|Any CPU
204+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}.Release|x64.ActiveCfg = Release|Any CPU
205+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}.Release|x64.Build.0 = Release|Any CPU
206+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}.Release|x86.ActiveCfg = Release|Any CPU
207+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA}.Release|x86.Build.0 = Release|Any CPU
178208
EndGlobalSection
179209
GlobalSection(SolutionProperties) = preSolution
180210
HideSolutionNode = FALSE
@@ -190,6 +220,7 @@ Global
190220
{BB22B7F8-F048-329D-E4E2-1A84DA3DFB87} = {D788FF84-82DF-4509-B556-46D803D8D854}
191221
{D342ADD5-87D9-EDE5-BA69-BFD1288A44AE} = {D788FF84-82DF-4509-B556-46D803D8D854}
192222
{A7806838-171C-4A21-19DD-FF4881BDA87A} = {57A6F6E6-F2AF-4FB1-95A0-B2535480947B}
223+
{D6FABA52-2524-2BA9-62DC-C53CF5F70ACA} = {81668B5E-0334-4CFF-AEA3-60E037A1008D}
193224
EndGlobalSection
194225
GlobalSection(ExtensibilityGlobals) = postSolution
195226
SolutionGuid = {5078C553-A1C2-42BD-9F21-BCDE0A70B225}

Cyaim.WebSocketServer/Cyaim.WebSocketServer/Infrastructure/Configures/WebSocketRouteOption.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ public class WebSocketRouteOption
6363
/// </summary>
6464
internal EndpointInjectorFactory InjectorFactory { get; set; }
6565

66+
/// <summary>
67+
/// 方法调用器工厂(用于优化方法调用性能,支持源代码生成和反射两种方式)
68+
/// </summary>
69+
internal MethodInvokerFactory MethodInvokerFactory { get; set; }
70+
6671
/// <summary>
6772
/// Watch assembly path
6873
/// </summary>

Cyaim.WebSocketServer/Cyaim.WebSocketServer/Infrastructure/Handlers/MvcHandler/MvcChannelHandler.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class MvcChannelHandler : IWebSocketHandler
3737
private BandwidthLimitManager bandwidthLimitManager;
3838
private WebSocketMetricsCollector _metricsCollector;
3939
private EndpointInjectorFactory _injectorFactory;
40+
private MethodInvokerFactory _methodInvokerFactory;
4041

4142
/// <summary>
4243
/// Get instance
@@ -130,6 +131,13 @@ public async Task ConnectionEntry(HttpContext context, ILogger<WebSocketRouteMid
130131
}
131132
_injectorFactory = webSocketOptions.InjectorFactory;
132133

134+
// 初始化方法调用器工厂(如果尚未初始化)
135+
if (webSocketOptions.MethodInvokerFactory == null)
136+
{
137+
webSocketOptions.MethodInvokerFactory = new MethodInvokerFactory();
138+
}
139+
_methodInvokerFactory = webSocketOptions.MethodInvokerFactory;
140+
133141
// 获取指标收集器
134142
if (WebSocketRouteOption.ApplicationServices != null)
135143
{
@@ -931,8 +939,10 @@ public static async Task<MvcResponseScheme> MvcDistributeAsync(WebSocketRouteOpt
931939
// 使用lifetime实现直接结束执行/等待执行完成后再结束
932940
appLifetime.ApplicationStopping.ThrowIfCancellationRequested();
933941

934-
// 调用目标方法
935-
invokeResult = method.Invoke(inst, args);
942+
// 使用方法调用器工厂调用目标方法(支持源代码生成和反射两种方式)
943+
var methodInvokerFactory = webSocketOptions.MethodInvokerFactory ?? new MethodInvokerFactory();
944+
var methodInvoker = methodInvokerFactory.GetOrCreateInvoker(method);
945+
invokeResult = methodInvoker.Invoke(inst, args);
936946

937947
// Async api support
938948
if (invokeResult is Task)

Cyaim.WebSocketServer/Cyaim.WebSocketServer/Infrastructure/Injectors/EndpointInjectorFactory.cs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.AspNetCore.Http;
33
using System;
44
using System.Collections.Concurrent;
5+
using System.Linq;
56
using System.Net.WebSockets;
67
using System.Reflection;
78

@@ -51,10 +52,36 @@ private IEndpointInjector TryCreateGeneratedInjector(Type endpointType)
5152
{
5253
try
5354
{
54-
// 查找生成的注入器类型:{TypeName}Injector
55-
var injectorTypeName = $"{endpointType.FullName}Injector";
55+
// 源生成器生成的注入器类名格式:{ClassName}Injector(在同一个命名空间中)
56+
// 例如:MyController -> MyControllerInjector
57+
var className = endpointType.Name;
58+
var injectorTypeName = $"{endpointType.Namespace}.{className}Injector";
59+
60+
// 首先尝试在同一程序集中查找
5661
var injectorType = endpointType.Assembly.GetType(injectorTypeName);
5762

63+
// 如果找不到,尝试在所有已加载的程序集中查找(处理嵌套类型等情况)
64+
if (injectorType == null)
65+
{
66+
// 尝试使用完整名称查找(处理嵌套类型)
67+
var fullName = endpointType.FullName;
68+
if (fullName != null)
69+
{
70+
var fullInjectorName = $"{fullName}Injector";
71+
injectorType = endpointType.Assembly.GetType(fullInjectorName);
72+
}
73+
}
74+
75+
// 如果还是找不到,尝试在同一个命名空间下查找所有类型
76+
if (injectorType == null)
77+
{
78+
var allTypes = endpointType.Assembly.GetTypes();
79+
injectorType = allTypes.FirstOrDefault(t =>
80+
t.Namespace == endpointType.Namespace &&
81+
t.Name == $"{className}Injector" &&
82+
typeof(IEndpointInjector).IsAssignableFrom(t));
83+
}
84+
5885
if (injectorType != null && typeof(IEndpointInjector).IsAssignableFrom(injectorType))
5986
{
6087
// 使用无参构造函数创建注入器实例
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Threading.Tasks;
2+
3+
namespace Cyaim.WebSocketServer.Infrastructure.Injectors
4+
{
5+
/// <summary>
6+
/// 方法调用器接口,用于调用 endpoint 方法(支持源代码生成和反射两种方式)
7+
/// </summary>
8+
public interface IMethodInvoker
9+
{
10+
/// <summary>
11+
/// 调用方法
12+
/// </summary>
13+
/// <param name="instance">Endpoint 实例</param>
14+
/// <param name="args">方法参数</param>
15+
/// <returns>方法返回值(如果是 Task,则返回 Task.Result)</returns>
16+
object Invoke(object instance, object[] args);
17+
}
18+
}
19+

0 commit comments

Comments
 (0)