Skip to content

Commit 34b83a4

Browse files
committed
feat(http): 添加HttpContentType特性支持内容类型配置
实现接口和方法级别的HttpContentType特性,支持通过特性配置HTTP请求的内容类型 添加内容类型优先级处理逻辑(方法级 > 接口级 > Body参数级 > 默认值) 新增测试用例验证不同场景下的内容类型优先级
1 parent c61c9a5 commit 34b83a4

File tree

6 files changed

+201
-2
lines changed

6 files changed

+201
-2
lines changed

Core/Mud.CodeGenerator/Consts/HttpClientGeneratorConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ internal static class HttpClientGeneratorConstants
4343
public const string HeaderAttribute = "HeaderAttribute";
4444
public const string BodyAttribute = "BodyAttribute";
4545
public const string FilePathAttribute = "FilePathAttribute";
46+
public static readonly string[] HttpContentTypeAttributeNames = ["HttpContentTypeAttribute", "HttpContentType"];
4647

4748

4849
public const string TimeoutProperty = "Timeout";

Core/Mud.ServiceCodeGenerator/HttpInvoke/Generators/RequestBuilder.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,20 @@ public void GenerateBodyParameter(StringBuilder codeBuilder, MethodAnalysisResul
175175

176176
// 检查参数是否明确指定了ContentType
177177
var hasExplicitContentType = bodyAttr.NamedArguments.ContainsKey("ContentType");
178-
var contentTypeExpression = hasExplicitContentType ? $"\"{contentType}\"" : "GetMediaType(_defaultContentType)";
178+
179+
// 获取有效的内容类型(方法级 > 接口级 > Body参数级 > 默认值)
180+
string contentTypeExpression;
181+
if (hasExplicitContentType)
182+
{
183+
contentTypeExpression = $"\"{contentType}\"";
184+
}
185+
else
186+
{
187+
var effectiveContentType = methodInfo.GetEffectiveContentType();
188+
contentTypeExpression = !string.IsNullOrEmpty(effectiveContentType)
189+
? $"\"{effectiveContentType}\""
190+
: "GetMediaType(_defaultContentType)";
191+
}
179192

180193
if (useStringContent)
181194
{

Core/Mud.ServiceCodeGenerator/HttpInvoke/Models/MethodAnalysisResult.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,25 @@ public string InterfaceName
115115
/// </summary>
116116
public List<InterfaceHeaderAttribute> InterfaceHeaderAttributes { get; set; } = [];
117117

118+
/// <summary>
119+
/// 接口级别的内容类型(从接口的[HttpContentType]特性获取)
120+
/// </summary>
121+
public string? InterfaceContentType { get; set; }
122+
123+
/// <summary>
124+
/// 方法级别的内容类型(从方法的[HttpContentType]特性获取)
125+
/// </summary>
126+
public string? MethodContentType { get; set; }
127+
128+
/// <summary>
129+
/// 获取最终的内容类型(方法级优先于接口级)
130+
/// </summary>
131+
/// <returns>内容类型字符串,如果方法级和接口级都未定义则返回null</returns>
132+
public string? GetEffectiveContentType()
133+
{
134+
return MethodContentType ?? InterfaceContentType;
135+
}
136+
118137
/// <summary>
119138
/// 无效的分析结果实例
120139
/// </summary>

Core/Mud.ServiceCodeGenerator/MethodHelper.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public static MethodAnalysisResult AnalyzeMethod(Compilation compilation, IMetho
3535
var httpMethod = httpMethodAttr.Name.ToString();
3636
var urlTemplate = GetAttributeArgumentValue(httpMethodAttr, 0)?.ToString().Trim('"') ?? "";
3737

38+
// 获取方法级别的HttpContentType特性
39+
var methodContentType = GetHttpContentTypeFromSymbol(methodSymbol);
40+
3841
var parameters = methodSymbol.Parameters.Select(p =>
3942
{
4043
var parameterInfo = new ParameterInfo
@@ -64,9 +67,13 @@ public static MethodAnalysisResult AnalyzeMethod(Compilation compilation, IMetho
6467
var interfaceSymbol = compilation.GetSemanticModel(interfaceDecl.SyntaxTree).GetDeclaredSymbol(interfaceDecl) as INamedTypeSymbol;
6568
var interfaceAttributes = new HashSet<string>();
6669
var interfaceHeaderAttributes = new List<InterfaceHeaderAttribute>();
70+
string? interfaceContentType = null;
6771

6872
if (interfaceSymbol != null)
6973
{
74+
// 获取接口级别的HttpContentType特性
75+
interfaceContentType = GetHttpContentTypeFromSymbol(interfaceSymbol);
76+
7077
// 检查并处理所有[Header]特性
7178
var headerAttributes = interfaceSymbol.GetAttributes()
7279
.Where(attr => (attr.AttributeClass?.Name == "HeaderAttribute" || attr.AttributeClass?.Name == "Header"));
@@ -125,7 +132,9 @@ public static MethodAnalysisResult AnalyzeMethod(Compilation compilation, IMetho
125132
IgnoreImplement = HasMethodAttribute(methodSymbol, HttpClientGeneratorConstants.IgnoreImplementAttributeNames),
126133
IgnoreWrapInterface = HasMethodAttribute(methodSymbol, HttpClientGeneratorConstants.IgnoreWrapInterfaceAttributeNames),
127134
InterfaceAttributes = interfaceAttributes,
128-
InterfaceHeaderAttributes = interfaceHeaderAttributes
135+
InterfaceHeaderAttributes = interfaceHeaderAttributes,
136+
InterfaceContentType = interfaceContentType,
137+
MethodContentType = methodContentType
129138
};
130139
}
131140
private static AttributeSyntax? FindHttpMethodAttribute(MethodDeclarationSyntax methodSyntax)
@@ -198,6 +207,36 @@ private static string GetHeaderName(AttributeData headerAttr)
198207
}
199208

200209

210+
/// <summary>
211+
/// 从符号获取HttpContentType特性的ContentType值
212+
/// </summary>
213+
/// <param name="symbol">符号(方法或接口)</param>
214+
/// <returns>Content-Type值,如果未定义则返回null</returns>
215+
private static string? GetHttpContentTypeFromSymbol(ISymbol symbol)
216+
{
217+
if (symbol == null)
218+
return null;
219+
220+
// 查找HttpContentType特性
221+
var httpContentTypeAttr = AttributeDataHelper.GetAttributeDataFromSymbol(
222+
symbol,
223+
HttpClientGeneratorConstants.HttpContentTypeAttributeNames);
224+
225+
if (httpContentTypeAttr == null)
226+
return null;
227+
228+
// 优先从构造函数参数获取
229+
if (httpContentTypeAttr.ConstructorArguments.Length > 0)
230+
{
231+
var constructorArg = httpContentTypeAttr.ConstructorArguments[0].Value?.ToString();
232+
if (!string.IsNullOrEmpty(constructorArg))
233+
return constructorArg;
234+
}
235+
236+
// 从命名参数获取
237+
return AttributeDataHelper.GetStringValueFromAttribute(httpContentTypeAttr, ["ContentType"]);
238+
}
239+
201240
/// <summary>
202241
/// 查询方法的语法对象。
203242
/// </summary>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
namespace CodeGeneratorTest.WebApi;
2+
3+
4+
/// <summary>
5+
/// 用于测试 HttpContentType 特性的优先级功能
6+
/// 测试场景:
7+
/// 1. 接口级特性 vs 方法级特性(方法级优先)
8+
/// 2. 构造函数参数 vs 命名参数
9+
/// 3. 默认值回退
10+
/// </summary>
11+
[HttpClientApi("https://api.example.com")]
12+
[HttpContentType("application/xml")]
13+
public interface IContentTypeTestApi
14+
{
15+
/// <summary>
16+
/// 方法1:接口使用 application/xml,方法覆盖为 application/json
17+
/// 预期结果:使用 application/json
18+
/// </summary>
19+
[Post("/api/test1")]
20+
[HttpContentType("application/json")]
21+
Task<string> TestMethod1Async([Body] string data);
22+
23+
/// <summary>
24+
/// 方法2:接口使用 application/xml,方法覆盖为 text/plain
25+
/// 预期结果:使用 text/plain
26+
/// </summary>
27+
[Post("/api/test2")]
28+
[HttpContentType("text/plain")]
29+
Task<string> TestMethod2Async([Body] string data);
30+
31+
/// <summary>
32+
/// 方法3:接口使用 application/xml,方法未指定
33+
/// 预期结果:使用 application/xml(从接口继承)
34+
/// </summary>
35+
[Post("/api/test3")]
36+
Task<string> TestMethod3Async([Body] string data);
37+
38+
/// <summary>
39+
/// 方法4:使用命名参数方式指定内容类型
40+
/// 预期结果:使用 application/x-www-form-urlencoded
41+
/// </summary>
42+
[Post("/api/test4")]
43+
[HttpContentType(ContentType = "application/x-www-form-urlencoded")]
44+
Task<string> TestMethod4Async([Body] string data);
45+
46+
/// <summary>
47+
/// 方法5:使用构造函数参数方式指定内容类型
48+
/// 预期结果:使用 multipart/form-data
49+
/// </summary>
50+
[Post("/api/test5")]
51+
[HttpContentType("multipart/form-data")]
52+
Task<string> TestMethod5Async([Body] string data);
53+
}
54+
55+
56+
/// <summary>
57+
/// 用于测试 HttpContentType 特性的默认值回退功能
58+
/// 测试场景:
59+
/// 接口和方法均未定义 HttpContentType 特性
60+
/// </summary>
61+
[HttpClientApi("https://api.example2.com")]
62+
public interface IContentTypeDefaultTestApi
63+
{
64+
/// <summary>
65+
/// 方法1:接口和方法均未指定内容类型
66+
/// 预期结果:使用默认值 application/json
67+
/// </summary>
68+
[Post("/api/default")]
69+
Task<string> TestDefaultAsync([Body] string data);
70+
}
71+
72+
73+
/// <summary>
74+
/// 用于测试 HttpContentType 特性与 Body 参数 ContentType 的优先级
75+
/// 测试场景:
76+
/// Body 参数的 ContentType 优先级最高
77+
/// </summary>
78+
[HttpClientApi("https://api.example3.com")]
79+
[HttpContentType("application/xml")]
80+
public interface IContentTypeBodyPriorityTestApi
81+
{
82+
/// <summary>
83+
/// 方法1:接口使用 application/xml,方法使用 application/json,Body 参数使用 text/html
84+
/// 预期结果:使用 text/html(Body 参数优先级最高)
85+
/// </summary>
86+
[Post("/api/priority1")]
87+
[HttpContentType("application/json")]
88+
Task<string> TestPriority1Async([Body(ContentType = "text/html")] string data);
89+
90+
/// <summary>
91+
/// 方法2:接口使用 application/xml,方法未指定,Body 参数使用 application/json
92+
/// 预期结果:使用 application/json(Body 参数优先级最高)
93+
/// </summary>
94+
[Post("/api/priority2")]
95+
Task<string> TestPriority2Async([Body(ContentType = "application/json")] string data);
96+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace Mud.Common.CodeGenerator;
2+
3+
4+
/// <summary>
5+
/// 用于指定HTTP请求的内容类型
6+
/// </summary>
7+
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Method, AllowMultiple = false)]
8+
public class HttpContentTypeAttribute : Attribute
9+
{
10+
/// <summary>
11+
/// HTTP请求的内容类型
12+
/// </summary>
13+
public string ContentType { get; set; } = "application/json";
14+
15+
/// <summary>
16+
/// 使用默认的"application/json"内容类型初始化一个新的实例
17+
/// </summary>
18+
public HttpContentTypeAttribute()
19+
{
20+
21+
}
22+
23+
/// <summary>
24+
/// 使用指定的内容类型初始化一个新的实例
25+
/// </summary>
26+
/// <param name="contentType"></param>
27+
public HttpContentTypeAttribute(string contentType)
28+
{
29+
ContentType = contentType;
30+
}
31+
}

0 commit comments

Comments
 (0)