diff --git a/src/AutoWasmApi.Roslyn/AutoWasmApi.Roslyn.csproj b/src/AutoWasmApi.Roslyn/AutoWasmApi.Roslyn.csproj deleted file mode 100644 index 4ad31fb..0000000 --- a/src/AutoWasmApi.Roslyn/AutoWasmApi.Roslyn.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - netstandard2.0 - AutoWasmApiGenerator - latest - 0.1.1 - - - - - - diff --git a/src/AutoWasmApi.Roslyn/DiagnosticDefinitions.cs b/src/AutoWasmApi.Roslyn/DiagnosticDefinitions.cs deleted file mode 100644 index 780651b..0000000 --- a/src/AutoWasmApi.Roslyn/DiagnosticDefinitions.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Microsoft.CodeAnalysis; -using System; -using System.Collections.Generic; -using System.Text; - -namespace AutoWasmApiGenerator -{ - internal class DiagnosticDefinitions - { - /// - /// 继承多个接口需要指定接口标注[WebControllerAttribute] - /// - /// - /// - public static Diagnostic WAG00001(Location? location) => Diagnostic.Create(new DiagnosticDescriptor( - id: "WAG00001", - title: "继承多个接口需要指定接口标注[WebControllerAttribute]", - messageFormat: "继承多个接口需要指定接口标注[WebControllerAttribute]", - category: typeof(ControllerGenerator).FullName!, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true), location); - - /// - /// 无法为该类型生成WebApi调用类,缺少接口 - /// - /// - /// - public static Diagnostic WAG00002(Location? location) => Diagnostic.Create(new DiagnosticDescriptor( - id: "WAG00002", - title: "无法为该类型生成WebApi调用类,缺少接口", - messageFormat: "无法为该类型生成WebApi调用类,缺少接口", - category: typeof(HttpServiceInvokerGenerator).FullName!, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true), location); - - /// - /// 方法参数过多 - /// - /// - /// - public static Diagnostic WAG00003(Location? location) => Diagnostic.Create(new DiagnosticDescriptor( - id: "WAG00003", - title: "方法参数过多", - messageFormat: "方法参数过多", - category: typeof(HttpServiceInvokerGenerator).FullName!, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true), location); - - /// - /// 控制器(controller)不能包含泛型 - /// - /// - /// - public static Diagnostic WAG00004(Location? location) => Diagnostic.Create(new DiagnosticDescriptor( - id: "WAG00004", - title: "控制器(controller)不能包含泛型", - messageFormat: "控制器(controller)不能包含泛型", - category: typeof(HttpServiceInvokerGenerator).FullName!, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true), location); - - /// - /// 仅支持异步方法 - /// - /// - /// - public static Diagnostic WAG00005(Location? location) => Diagnostic.Create(new DiagnosticDescriptor( - id: "WAG00005", - title: "仅支持异步方法", - messageFormat: "仅支持异步方法", - category: typeof(HttpServiceInvokerGenerator).FullName!, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true), location); - - /// - /// 路由参数 - /// - /// - /// - public static Diagnostic WAG00006(Location? location, string? symbolString = null) => Diagnostic.Create(new DiagnosticDescriptor( - id: "WAG00006", - title: "路由中未包含路由参数", - messageFormat: $"路由中未包含路由参数({symbolString})", - category: typeof(HttpServiceInvokerGenerator).FullName!, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true), location); - - /// - /// 不能同时设置[FromBody]和[FromForm] - /// - /// - /// - public static Diagnostic WAG00007(Location? location, string? symbolString = null) => Diagnostic.Create(new DiagnosticDescriptor( - id: "WAG00007", - title: "不能同时设置[FromBody]和[FromForm]", - messageFormat: $"不能同时设置[FromBody]和[FromForm]({symbolString})", - category: typeof(HttpServiceInvokerGenerator).FullName!, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true), location); - - /// - /// 不能设置多个[FromBody] - /// - /// - /// - public static Diagnostic WAG00008(Location? location, string? symbolString = null) => Diagnostic.Create(new DiagnosticDescriptor( - id: "WAG00008", - title: "不能设置多个[FromBody]", - messageFormat: $"不能设置多个[FromBody]({symbolString})", - category: typeof(HttpServiceInvokerGenerator).FullName!, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true), location); - - /// - /// 暂不支持的返回值类型 - /// - /// - /// - public static Diagnostic WAG00009(Location? location, string? symbolString = null) => Diagnostic.Create(new DiagnosticDescriptor( - id: "WAG00009", - title: "暂不支持的返回值类型", - messageFormat: $"暂不支持的返回值类型({symbolString})", - category: typeof(HttpServiceInvokerGenerator).FullName!, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true), location); - } -} diff --git a/src/AutoWasmApi.Roslyn/HttpServiceInvokerGenerator.cs b/src/AutoWasmApi.Roslyn/HttpServiceInvokerGenerator.cs deleted file mode 100644 index 0af5869..0000000 --- a/src/AutoWasmApi.Roslyn/HttpServiceInvokerGenerator.cs +++ /dev/null @@ -1,429 +0,0 @@ -using Generators.Shared; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Collections.Generic; -using System.Linq; -using static AutoWasmApiGenerator.GeneratorHelpers; -using Generators.Shared.Builder; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reflection.Metadata; -using System.Text.RegularExpressions; - -namespace AutoWasmApiGenerator -{ - [Generator(LanguageNames.CSharp)] - public class HttpServiceInvokerGenerator : IIncrementalGenerator - { - public void Initialize(IncrementalGeneratorInitializationContext context) - { - //var list = context.SyntaxProvider.ForAttributeWithMetadataName( - // ApiInvokerAssemblyAttributeFullName, - // static (node, token) => true, - // static (c, t) => c); - - context.RegisterSourceOutput(context.CompilationProvider, static (context, compilation) => - { - try - { - if (!compilation.Assembly.HasAttribute(ApiInvokerAssemblyAttributeFullName)) - { - return; - } - var all = compilation.GetAllSymbols(ApiInvokerAttributeFullName); - foreach (var item in all) - { - if (!item.HasAttribute(WebControllerAttributeFullName)) - { - continue; - } - if (CreateCodeFile(item, context, out var file)) - { -#if DEBUG - var ss = file.ToString(); -#endif - context.AddSource(file); - } - } - } - catch (System.Exception ex) - { - context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor( - id: "ERROR00001", - title: "生成错误", - messageFormat: ex.Message, - category: typeof(HttpServiceInvokerGenerator).FullName!, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true - ), Location.None)); - } - }); - } - - private static bool CreateCodeFile(INamedTypeSymbol interfaceSymbol, SourceProductionContext context, [NotNullWhen(true)] out CodeFile? file) - { - var methods = interfaceSymbol.GetAllMethodWithAttribute(WebMethodAttributeFullName); - List members = new List(); - _ = interfaceSymbol.GetAttribute(WebControllerAttributeFullName, out var controllerAttrData); - var ns = NamespaceBuilder.Default.Namespace(interfaceSymbol.ContainingNamespace.ToDisplayString()); - var invokeClass = CreateHttpClassBuilder(interfaceSymbol); - var scopeName = interfaceSymbol.FormatClassName(); - var route = controllerAttrData.GetNamedValue("Route") as string; - bool needAuth = (bool)(controllerAttrData.GetNamedValue("Authorize") ?? false); - foreach (var method in methods) - { - var result = BuildMethod(method, route, scopeName, needAuth, out var n); - if (n && !needAuth) - { - needAuth = true; - } - if (result.Item2 != null) - { - context.ReportDiagnostic(result.Item2); - file = null; - return false; - } - members.Add(result.Item1!); - } - - var fields = BuildField(needAuth); - var constructor = BuildConstructor(interfaceSymbol, needAuth); - members.AddRange(fields); - members.Add(constructor); - - file = CodeFile.New($"{interfaceSymbol.FormatFileName()}ApiInvoker.g.cs") - .AddUsings("using Microsoft.Extensions.DependencyInjection;") - .AddMembers(ns.AddMembers(invokeClass.AddMembers([.. members]))); - - return true; - } - - private static (MethodBuilder?, Diagnostic?) BuildMethod((IMethodSymbol, AttributeData?) method, string? route, string scopeName, bool controllerAuth, out bool needAuth) - { - var methodSymbol = method.Item1; - var methodAttribute = method.Item2; - - var allowsAnonymous = (bool)(methodAttribute.GetNamedValue("AllowAnonymous") ?? false); - var authorize = (bool)(methodAttribute.GetNamedValue("Authorize") ?? false); - needAuth = !allowsAnonymous && (authorize || controllerAuth); - - if (methodSymbol.HasAttribute(NotSupported)) - { - var b = MethodBuilder.Default - .MethodName(methodSymbol.Name) - .Generic([.. methodSymbol.GetTypeParameters()]) - .ReturnType(methodSymbol.ReturnType.ToDisplayString()) - .AddParameter([.. methodSymbol.Parameters.Select(p => $"{p.Type.ToDisplayString()} {p.Name}")]) - .AddGeneratedCodeAttribute(typeof(HttpServiceInvokerGenerator)) - .Lambda("throw new global::System.NotSupportedException()"); - return (b, null); - } - - // 检查当前返回类型是否是Task或Task - // 如果检查类型不符合要求,说明不是异步方法 - // 返回错误信息 - var returnTypeInfo = methodSymbol.ReturnType; - var isTask = returnTypeInfo.Name == "Task"; - var isGenericTask = returnTypeInfo.OriginalDefinition.ToDisplayString() == "System.Threading.Tasks.Task"; - - if (!isTask && !isGenericTask) - { - return (null, DiagnosticDefinitions.WAG00005(methodSymbol.Locations.FirstOrDefault())); - } - - string webMethod; - if (!methodAttribute.GetNamedValue("Method", out var v)) - { - webMethod = "Post"; - } - else - { - webMethod = WebMethod[(int)v!]; - } - var methodScoped = methodSymbol.Name.Replace("Async", ""); - var customRoute = methodAttribute?.GetNamedValue("Route")?.ToString(); - string methodRoute; - bool useRouteParam = false; - if (string.IsNullOrEmpty(customRoute)) - { - methodRoute = methodScoped; - } - else if (Regex.Match(customRoute, "{.+}").Success) - { - useRouteParam = true; - methodRoute = $"{methodScoped}/{customRoute}"; - } - else - { - methodRoute = customRoute!; - } - //var methodRoute = $"{methodAttribute?.GetNamedValue("Route") ?? methodSymbol.Name.Replace("Async", "")}"; - List statements = - [ - // var url = ""; - // var client = clientFactory.CreateClient(nameof()); - $"var _client_gen = this.clientFactory.CreateClient(\"{scopeName}\")", - // var request = new HttpRequestMessage(); - "var _request_gen = new global::System.Net.Http.HttpRequestMessage()", - // request.Method = HttpMethod. - $"_request_gen.Method = global::System.Net.Http.HttpMethod.{webMethod}", - ]; - if (needAuth) - { - statements.Add("await headerHandler.SetRequestHeaderAsync(_request_gen)"); - } - - // 处理参数标签 - var paramInfos = methodSymbol.Parameters.Select(p => - { - if (p.GetAttribute(WebMethodParameterBindingAttribute, out var ad)) - { - ad!.GetConstructorValue(0, out var bindingType); - var t = (int)bindingType!; - return ((int)bindingType!, p); - } - if (useRouteParam && customRoute!.Contains(p.Name)) - { - return (1, p); - } - return (-1, p); - }); - - // 0 - Query - // 1 - Route - // 2 - Form - // 3 - Body - // 4 - Header - - #region 检查参数配置 - - var routerParameters = paramInfos.Where(t => t.Item1 == 1); - foreach ((int _, IParameterSymbol p) item in routerParameters) - { - if (!methodRoute.Contains($"{{{item.p.Name}}}")) - { - return (null, DiagnosticDefinitions.WAG00006(methodSymbol.Locations.FirstOrDefault(), methodSymbol.ToDisplayString())); - } - } - - if (paramInfos.Any(t => t.Item1 == 2) && paramInfos.Any(t => t.Item1 == 3)) - { - return (null, DiagnosticDefinitions.WAG00007(methodSymbol.Locations.FirstOrDefault(), methodSymbol.ToDisplayString())); - } - #endregion - - var url = $""" - var _url_gen = $"api/{route ?? scopeName}/{methodRoute}" - """; - statements.Add(url); - - #region 处理Query参数 - - var queryParameters = paramInfos.Where(t => (t.Item1 == -1 && webMethod == "Get") || t.Item1 == 0); - - if (queryParameters.Any()) - { - statements.Add("var _queries_gen = new List()"); - foreach (var item in queryParameters) - { - var p = item.p; - if (p.Type is INamedTypeSymbol { TypeKind: TypeKind.Class, SpecialType: not SpecialType.System_String } parameterClassType) - { - var properties = parameterClassType.GetMembers().Where(m => m.Kind == SymbolKind.Property); - foreach (var prop in properties) - { - statements.Add($$"""_queries_gen.Add($"{nameof({{p.Name}}.{{prop.Name}})}={{{p.Name}}.{{prop.Name}}}")"""); - } - } - else - { - statements.Add($$"""_queries_gen.Add($"{nameof({{p.Name}})}={{{p.Name}}}")"""); - } - } - var setUrl = """ - _url_gen = $"{_url_gen}?{string.Join("&", _queries_gen)}" - """; - statements.Add(setUrl); - } - - #endregion - - #region 处理Form参数 - - var formParameters = paramInfos.Where(t => t.Item1 == 2); - - if (formParameters.Any()) - { - statements.Add("var _formDatas_gen = new List>()"); - foreach (var item in formParameters) - { - var p = item.p; - if (p.Type is INamedTypeSymbol { TypeKind: TypeKind.Class, SpecialType: not SpecialType.System_String } parameterClassType) - { - var properties = parameterClassType.GetMembers().Where(m => m.Kind == SymbolKind.Property); - foreach (var prop in properties) - { - statements.Add($$""" - _formDatas_gen.Add(new global::System.Collections.Generic.KeyValuePair(nameof({{p.Name}}.{{prop.Name}}), $"{{{p.Name}}.{{prop.Name}}}")) - """); - } - } - else - { - statements.Add($$"""_formDatas_gen.Add(new global::System.Collections.Generic.KeyValuePair(nameof({{p.Name}}), $"{{{p.Name}}}"))"""); - } - } - statements.Add("var _formContent_gen = new global::System.Net.Http.FormUrlEncodedContent(_formDatas_gen)"); - statements.Add("_formContent_gen.Headers.ContentType = new(\"application/x-www-form-urlencoded\")"); - statements.Add("""_request_gen.Content = _formContent_gen"""); - - } - - #endregion - - #region 处理Body参数 - - var bodyParameters = paramInfos.Where(t => (t.Item1 == -1 && webMethod != "Get") || t.Item1 == 3); - if (bodyParameters.Count() > 1) - { - return (null, DiagnosticDefinitions.WAG00008(methodSymbol.Locations.FirstOrDefault(), methodSymbol.ToDisplayString())); - } - - if (bodyParameters.Any()) - { - var p = bodyParameters.First().p; - statements.Add($"var _json_gen = global::System.Text.Json.JsonSerializer.Serialize({p.Name})"); - statements.Add("""_request_gen.Content = new global::System.Net.Http.StringContent(_json_gen, global::System.Text.Encoding.Default, "application/json")"""); - } - - #endregion - - #region 处理Header参数 - - var headerParameters = paramInfos.Where(t => t.Item1 == 4); - - if (headerParameters.Any()) - { - foreach (var item in headerParameters) - { - var p = item.p; - if (p.Type is INamedTypeSymbol { TypeKind: TypeKind.Class, SpecialType: not SpecialType.System_String } parameterClassType) - { - var properties = parameterClassType.GetMembers().Where(m => m.Kind == SymbolKind.Property); - foreach (var prop in properties) - { - statements.Add($$"""_request_gen.Headers.Add(nameof({{p.Name}}.{{prop.Name}}), $"{{{p.Name}}.{{prop.Name}}}")"""); - } - } - else - { - statements.Add($$"""_request_gen.Headers.Add(nameof({{p.Name}}), $"{{{p.Name}}}")"""); - } - } - } - - #endregion - - statements.Add("_request_gen.RequestUri = new global::System.Uri(_url_gen, UriKind.Relative)"); - var returnType = methodSymbol.ReturnType.GetGenericTypes().FirstOrDefault() ?? methodSymbol.ReturnType; - - if (methodSymbol.ReturnsVoid || returnType.ToDisplayString() == "System.Threading.Tasks.Task") - { - statements.Add("_ = await _client_gen.SendAsync(_request_gen)"); - } - else - { - statements.Add("var _response_gen = await _client_gen.SendAsync(_request_gen)"); - statements.Add("_response_gen.EnsureSuccessStatusCode()"); - // 返回值是复杂类型,使用Json反序列化 - if (returnType is { TypeKind: TypeKind.Class, SpecialType: not SpecialType.System_String }) - { - statements.Add("var _stream_gen = await _response_gen.Content.ReadAsStreamAsync()"); - //return System.Text.Json.JsonSerializer.Deserialize(jsonStream, jsonOptions); - statements.Add($"return global::System.Text.Json.JsonSerializer.Deserialize<{returnType.ToDisplayString()}>(_stream_gen, _JSON_OPTIONS_gen);"); - } - else - { - statements.Add("var _str_gen = await _response_gen.Content.ReadAsStringAsync()"); - if (returnType.SpecialType == SpecialType.System_String) - { - statements.Add("return _str_gen"); - } - else if (returnType.IsValueType) - { - statements.Add($"{returnType.ToDisplayString()}.TryParse(_str_gen, out var val)"); - statements.Add("return val"); - } - else - { - return (null, DiagnosticDefinitions.WAG00009(methodSymbol.Locations.FirstOrDefault())); - } - } - } - - var builder = MethodBuilder.Default - .MethodName(methodSymbol.Name) - .Generic([.. methodSymbol.GetTypeParameters()]) - .Async() - .ReturnType(methodSymbol.ReturnType.ToDisplayString()) - .AddParameter([.. methodSymbol.Parameters.Select(p => $"{p.Type.ToDisplayString()} {p.Name}")]) - .AddGeneratedCodeAttribute(typeof(HttpServiceInvokerGenerator)) - .AddBody([.. statements]); - return (builder, null); - } - - private static IEnumerable BuildField(bool needAuth) - { - // private readonly JsonSerializerOptions jsonOptions; - yield return FieldBuilder.Default.MemberType("global::System.Text.Json.JsonSerializerOptions") - .FieldName("_JSON_OPTIONS_gen"); - // private readonly IHttpClientFactory clientFactory; - yield return FieldBuilder.Default - .MemberType("global::System.Net.Http.IHttpClientFactory") - .FieldName("clientFactory"); - if (needAuth) - { - yield return FieldBuilder.Default - .MemberType("global::AutoWasmApiGenerator.IHttpClientHeaderHandler") - .FieldName("headerHandler"); - } - } - - private static ConstructorBuilder BuildConstructor(INamedTypeSymbol classSymbol, bool needAuth) - { - List parameters = [ - "global::System.Net.Http.IHttpClientFactory factory" - ]; - List body = ["clientFactory = factory;"]; - if (needAuth) - { - //parameters.Add("global::AutoWasmApiGenerator.IHttpClientHeaderHandler handler"); - parameters.Add("global::System.IServiceProvider services"); - body.Add("headerHandler = services.GetService() ?? global::AutoWasmApiGenerator.DefaultHttpClientHeaderHandler.Default"); - } - - return ConstructorBuilder.Default - .MethodName($"{FormatClassName(classSymbol.MetadataName)}ApiInvoker") - .AddParameter([.. parameters]) - .AddBody([.. body]) - .AddBody("_JSON_OPTIONS_gen = new global::System.Text.Json.JsonSerializerOptions() { PropertyNameCaseInsensitive = true };"); - } - - private static ClassBuilder CreateHttpClassBuilder(INamedTypeSymbol interfaceSymbol) - { - IEnumerable additionalAttribute = []; - if (interfaceSymbol.GetAttribute(ApiInvokerAttributeFullName, out var data)) - { - //var o = data.GetAttributeValue(nameof(ApiInvokerGeneraAttribute.Attribute)); - additionalAttribute = interfaceSymbol.GetAttributeInitInfo(ApiInvokerAttributeFullName, data!); - } - - - return ClassBuilder.Default - .ClassName($"{FormatClassName(interfaceSymbol.MetadataName)}ApiInvoker") - .AddGeneratedCodeAttribute(typeof(HttpServiceInvokerGenerator)) - .Attribute([.. additionalAttribute.Select(i => i.ToString())]) - .BaseType(interfaceSymbol.ToDisplayString()); - } - } -} diff --git a/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/AnalyzerReleases.Shipped.md b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..d567f14 --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/AnalyzerReleases.Unshipped.md b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..f7a0679 --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/AnalyzerReleases.Unshipped.md @@ -0,0 +1,17 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +WAG00001 | AutoWasmApiGenerator.ControllerGenerator | Error | 继承多个接口需要指定接口标注[WebControllerAttribute] +WAG00002 | AutoWasmApiGenerator.HttpServiceInvokerGenerator | Error | 无法为该类型生成WebApi调用类,缺少接口 +WAG00003 | AutoWasmApiGenerator.HttpServiceInvokerGenerator | Error | 方法参数过多 +WAG00004 | AutoWasmApiGenerator.ControllerGenerator | Error | 控制器(controller)不能包含泛型 +WAG00005 | AutoWasmApiGenerator.HttpServiceInvokerGenerator | Error | 仅支持异步方法 +WAG00006 | AutoWasmApiGenerator.HttpServiceInvokerGenerator | Error | 路由中未包含路由参数({0}) +WAG00007 | AutoWasmApiGenerator.HttpServiceInvokerGenerator | Error | 不能同时设置[FromBody]和[FromForm]({0}) +WAG00008 | AutoWasmApiGenerator.HttpServiceInvokerGenerator | Error | 不能设置多个[FromBody]({0}) +WAG00009 | AutoWasmApiGenerator.HttpServiceInvokerGenerator | Error | 暂不支持的返回值类型({0}) +WAG00010 | AutoWasmApiGenerator.HttpServiceInvokerGenerator | Error | 生成服务调用器过程中发生错误: {0} +WAG00011 | AutoWasmApiGenerator.HttpServiceInvokerGenerator | Error | AutoWasmApiGenerator.ApiInvokerGenerateAttribute需要同时设置AutoWasmApiGenerator.WebControllerAttribute \ No newline at end of file diff --git a/src/AutoWasmApi.Roslyn/Attributes/NullableAttributes.cs b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/Attributes/NullableAttributes.cs similarity index 100% rename from src/AutoWasmApi.Roslyn/Attributes/NullableAttributes.cs rename to src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/Attributes/NullableAttributes.cs diff --git a/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/AutoWasmApi.Roslyn.csproj b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/AutoWasmApi.Roslyn.csproj new file mode 100644 index 0000000..f8f722b --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/AutoWasmApi.Roslyn.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + + + + + + diff --git a/src/AutoWasmApi.Roslyn/ControllerGenerator.cs b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/ControllerGenerator.cs similarity index 95% rename from src/AutoWasmApi.Roslyn/ControllerGenerator.cs rename to src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/ControllerGenerator.cs index 8db296b..7e07580 100644 --- a/src/AutoWasmApi.Roslyn/ControllerGenerator.cs +++ b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/ControllerGenerator.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading; +using AutoWasmApiGenerator.Options; using Generators.Shared; using Generators.Shared.Builder; using Microsoft.CodeAnalysis; @@ -25,18 +26,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // WebControllerAttributeFullName // , static (node, _) => true // , static (ctx, _) => ctx); -#if DEBUG && false - if (!Debugger.IsAttached) - { - Debugger.Launch(); // This will launch the debugger when the source generator runs. - } -#endif + var globalOptions = context.AnalyzerConfigOptionsProvider.Select(GlobalOptions.Select); - context.RegisterSourceOutput(context.CompilationProvider, static (context, compilation) => + context.RegisterSourceOutput(context.CompilationProvider.Combine(globalOptions), static (context, compilationData) => { //try //{ - if (!compilation.Assembly.HasAttribute(WebControllerAssemblyAttributeFullName)) + var compilation = compilationData.Left; + var globalOptions = compilationData.Right; + var generate = compilation.Assembly.HasAttribute(WebControllerAssemblyAttributeFullName) + || globalOptions.GenerateWebController; + + if (!generate) { return; } @@ -118,7 +119,7 @@ private static MethodBuilder BuildMethod((IMethodSymbol, AttributeData?) data, s { methodRoute = methodScoped; } - else if(Regex.Match(customRoute, "{.+}").Success) + else if (Regex.Match(customRoute, "{.+}").Success) { methodRoute = $"{methodScoped}/{customRoute}"; } diff --git a/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/DiagnosticDefinitions.cs b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/DiagnosticDefinitions.cs new file mode 100644 index 0000000..4a5e442 --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/DiagnosticDefinitions.cs @@ -0,0 +1,219 @@ +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Text; +// ReSharper disable InconsistentNaming + +namespace AutoWasmApiGenerator +{ + internal class DiagnosticDefinitions + { + private static readonly DiagnosticDescriptor Wag00001DiagDescriptor = new DiagnosticDescriptor( + id: "WAG00001", + title: "继承多个接口需要指定接口标注[WebControllerAttribute]", + messageFormat: "继承多个接口需要指定接口标注[WebControllerAttribute]", + category: typeof(ControllerGenerator).FullName!, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor Wag00002DiagDescriptor = new DiagnosticDescriptor( + id: "WAG00002", + title: "无法为该类型生成WebApi调用类,缺少接口", + messageFormat: "无法为该类型生成WebApi调用类,缺少接口", + category: typeof(HttpServiceInvokerGenerator).FullName!, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor Wag00003DiagDescriptor = new DiagnosticDescriptor( + id: "WAG00003", + title: "方法参数过多", + messageFormat: "方法参数过多", + category: typeof(HttpServiceInvokerGenerator).FullName!, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor Wag00004DiagDescriptor = new DiagnosticDescriptor( + id: "WAG00004", + title: "控制器(controller)不能包含泛型", + messageFormat: "控制器(controller)不能包含泛型", + category: typeof(ControllerGenerator).FullName!, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor Wag00005DiagDescriptor = new DiagnosticDescriptor( + id: "WAG00005", + title: "仅支持异步方法", + messageFormat: "仅支持异步方法", + category: typeof(HttpServiceInvokerGenerator).FullName!, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor Wag00006DiagDescriptor = new DiagnosticDescriptor( + id: "WAG00006", + title: "路由中未包含路由参数", + messageFormat: "路由中未包含路由参数({0})", + category: typeof(HttpServiceInvokerGenerator).FullName!, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor Wag00007DiagDescriptor = new DiagnosticDescriptor( + id: "WAG00007", + title: "不能同时设置[FromBody]和[FromForm]", + messageFormat: "不能同时设置[FromBody]和[FromForm]({0})", + category: typeof(HttpServiceInvokerGenerator).FullName!, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor Wag00008DiagDescriptor = new DiagnosticDescriptor( + id: "WAG00008", + title: "不能设置多个[FromBody]", + messageFormat: "不能设置多个[FromBody]({0})", + category: typeof(HttpServiceInvokerGenerator).FullName!, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + + private static readonly DiagnosticDescriptor Wag00009DiagDescriptor = new DiagnosticDescriptor( + id: "WAG00009", + title: "暂不支持的返回值类型", + messageFormat: "暂不支持的返回值类型({0})", + category: typeof(HttpServiceInvokerGenerator).FullName!, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor Wag00010DiagDescriptor = new( + id: "WAG00010", + title: "生成错误", + messageFormat: "生成过程中发生错误: {0}", + category: typeof(HttpServiceInvokerGenerator).FullName!, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + private static readonly DiagnosticDescriptor Wag00011DiagDescriptor = new( + id: "WAG00011", + title: "属性依赖错误", + messageFormat: "AutoWasmApiGenerator.ApiInvokerGenerateAttribute需要同时设置AutoWasmApiGenerator.WebControllerAttribute", + category: typeof(HttpServiceInvokerGenerator).FullName!, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + /// + /// 继承多个接口需要指定接口标注[WebControllerAttribute] + /// + /// + /// + public static Diagnostic WAG00001(Location? location) + { + return Diagnostic.Create(Wag00001DiagDescriptor, location); + } + + /// + /// 无法为该类型生成WebApi调用类,缺少接口 + /// + /// + /// + public static Diagnostic WAG00002(Location? location) + { + return Diagnostic.Create(Wag00002DiagDescriptor, location); + } + + /// + /// 方法参数过多 + /// + /// + /// + public static Diagnostic WAG00003(Location? location) + { + return Diagnostic.Create(Wag00003DiagDescriptor, location); + } + + /// + /// 控制器(controller)不能包含泛型 + /// + /// + /// + /// + public static Diagnostic WAG00004(Location? location, string? symbolString = null) + { + return Diagnostic.Create(Wag00004DiagDescriptor, location, symbolString); + } + + /// + /// 仅支持异步方法 + /// + /// + /// + /// + public static Diagnostic WAG00005(Location? location, string? symbolString = null) + { + return Diagnostic.Create(Wag00005DiagDescriptor, location, symbolString); + } + + /// + /// 路由中未包含路由参数 + /// + /// + /// + /// + public static Diagnostic WAG00006(Location? location, string? symbolString = null) + { + return Diagnostic.Create(Wag00006DiagDescriptor, location, symbolString); + } + + /// + /// 不能同时设置[FromBody]和[FromForm] + /// + /// + /// + /// + public static Diagnostic WAG00007(Location? location, string? symbolString = null) + { + return Diagnostic.Create(Wag00007DiagDescriptor, location, symbolString); + } + + /// + /// 不能设置多个[FromBody] + /// + /// + /// + /// + public static Diagnostic WAG00008(Location? location, string? symbolString = null) + { + return Diagnostic.Create(Wag00008DiagDescriptor, location, symbolString); + } + + /// + /// 暂不支持的返回值类型 + /// + /// + /// + /// + public static Diagnostic WAG00009(Location? location, string? symbolString = null) + { + return Diagnostic.Create(Wag00009DiagDescriptor, location, symbolString); + } + + /// + /// 生成错误 + /// + /// + /// + /// + public static Diagnostic WAG00010(Location? location, string symbolString) + { + return Diagnostic.Create(Wag00010DiagDescriptor, location, symbolString); + } + + /// + /// 属性依赖错误 + /// + /// + /// + public static Diagnostic WAG00011(Location? location) + { + return Diagnostic.Create(Wag00011DiagDescriptor, location); + } + } +} diff --git a/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/Extensions/MethodCheckerExtensions.cs b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/Extensions/MethodCheckerExtensions.cs new file mode 100644 index 0000000..8106e10 --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/Extensions/MethodCheckerExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis; + +namespace AutoWasmApiGenerator.Extensions; + +internal static class MethodCheckerExtensions +{ + public static bool HasTryParseMethod(this ITypeSymbol returnType) + { + foreach (var method in returnType.GetMembers("TryParse").OfType()) + { + var match = method is { Parameters.Length: 2, ReturnType.Name: "Boolean" }; + if (match) return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/AutoWasmApi.Roslyn/GeneratorHelpers.cs b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/GeneratorHelpers.cs similarity index 96% rename from src/AutoWasmApi.Roslyn/GeneratorHelpers.cs rename to src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/GeneratorHelpers.cs index cc829a5..54d4fcb 100644 --- a/src/AutoWasmApi.Roslyn/GeneratorHelpers.cs +++ b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/GeneratorHelpers.cs @@ -8,7 +8,7 @@ public static class GeneratorHelpers { public const string WebControllerAttributeFullName = "AutoWasmApiGenerator.WebControllerAttribute"; public const string WebControllerAssemblyAttributeFullName = "AutoWasmApiGenerator.WebControllerAssemblyAttribute"; - public const string ApiInvokerAttributeFullName = "AutoWasmApiGenerator.ApiInvokerGenerateAttribute"; + public const string ApiInvokerGenerateAttributeFullName = "AutoWasmApiGenerator.ApiInvokerGenerateAttribute"; public const string ApiInvokerAssemblyAttributeFullName = "AutoWasmApiGenerator.ApiInvokerAssemblyAttribute"; public const string WebMethodAttributeFullName = "AutoWasmApiGenerator.WebMethodAttribute"; public const string DisableWebApiGenerator = "DisableWebApiGenerator"; diff --git a/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/HttpServiceInvokerGenerator.cs b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/HttpServiceInvokerGenerator.cs new file mode 100644 index 0000000..333f327 --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/HttpServiceInvokerGenerator.cs @@ -0,0 +1,69 @@ +using System; +using System.Diagnostics; +using System.Linq; +using AutoWasmApiGenerator.Options; +using Generators.Shared; +using Microsoft.CodeAnalysis; +using static AutoWasmApiGenerator.GeneratorHelpers; + +namespace AutoWasmApiGenerator; + +[Generator(LanguageNames.CSharp)] +public class HttpServiceInvokerGenerator : IIncrementalGenerator +{ + private static readonly HttpServiceInvokerGeneratorImpl Generator = new(); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // if (!Debugger.IsAttached) + // { + // Debugger.Launch(); + // } + var globalOptions = context.AnalyzerConfigOptionsProvider.Select(GlobalOptions.Select); + context.RegisterSourceOutput(context.CompilationProvider.Combine(globalOptions), static (context, compilationData) => + { + try + { + var compilation = compilationData.Left; + var globalOptions = compilationData.Right; + var generate = compilation.Assembly.HasAttribute(ApiInvokerAssemblyAttributeFullName) + || globalOptions.GenerateInvoker; + if (!generate) + { + return; + } + + var all = compilation.GetAllSymbols(ApiInvokerGenerateAttributeFullName); + + foreach (var item in all) + { + if (!item.HasAttribute(WebControllerAttributeFullName)) + { + foreach (var location in item.Locations) + { + context.ReportDiagnostic(DiagnosticDefinitions.WAG00011(location)); + } + continue; + } + + var (generatedFileName, sourceCode, errorAndWarnings) = Generator.Generate(item); + if (errorAndWarnings.Any()) + { + foreach (var item1 in errorAndWarnings) + { + context.ReportDiagnostic(item1); + } + } + else + { + context.AddSource(generatedFileName, sourceCode); + } + } + } + catch (Exception ex) + { + context.ReportDiagnostic(DiagnosticDefinitions.WAG00010(Location.None, ex.Message)); + } + }); + } +} \ No newline at end of file diff --git a/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/HttpServiceInvokerGeneratorImpl.cs b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/HttpServiceInvokerGeneratorImpl.cs new file mode 100644 index 0000000..bbc2491 --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/HttpServiceInvokerGeneratorImpl.cs @@ -0,0 +1,492 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using AutoWasmApiGenerator.Extensions; +using Generators.Models; +using Generators.Shared; +using Generators.Shared.Builder; +using Microsoft.CodeAnalysis; +using static AutoWasmApiGenerator.GeneratorHelpers; + +namespace AutoWasmApiGenerator; + +public class HttpServiceInvokerGeneratorImpl : IHttpServiceInvokerGenerator +{ + // 0 - Query + // 1 - Route + // 2 - Form + // 3 - Body + // 4 - Header + private static class WebMethodConstants + { + public const string Get = "Get"; + public const string Post = "Post"; + public const string Put = "Put"; + public const string Delete = "Delete"; + } + private enum ParameterBindingType + { + Ignore = -1, + FromQuery = 0, + FromRoute = 1, + FromForm = 2, + FromBody = 3, + FromHeader = 4 + } + + public (string generatedFileName, string sourceCode, List errorAndWarnings) Generate( + INamedTypeSymbol interfaceSymbol) + { + var errorAndWarnings = new List(); + var ret = CreateCodeFile(interfaceSymbol, errorAndWarnings, out var file); + return ret + ? (file!.FileName, file.ToString(), errorAndWarnings) + : ("", "", errorAndWarnings); + } + + private static bool CreateCodeFile(INamedTypeSymbol interfaceSymbol, List errorAndWarnings, + [NotNullWhen(true)] out CodeFile? file) + { + var methods = interfaceSymbol.GetAllMethodWithAttribute(WebMethodAttributeFullName); + List members = new(); + _ = interfaceSymbol.GetAttribute(WebControllerAttributeFullName, out var controllerAttrData); + var invokeClass = CreateHttpClassBuilder(interfaceSymbol); + var scopeName = interfaceSymbol.FormatClassName(); + var route = controllerAttrData.GetNamedValue("Route") as string; + var needAuth = (bool)(controllerAttrData.GetNamedValue("Authorize") ?? false); + foreach (var method in methods) + { + var methodBuilder = BuildMethod(method, route, scopeName, needAuth, out var n, errorAndWarnings); + if (n && !needAuth) + { + needAuth = true; + } + + if (errorAndWarnings.Count > 0) + { + file = null; + return false; + } + + members.Add(methodBuilder!); + } + + var fields = BuildField(needAuth); + var constructor = BuildConstructor(interfaceSymbol, needAuth); + members.AddRange(fields); + members.Add(constructor); + + var classBuilder = invokeClass.AddMembers([.. members]); + var ns = NamespaceBuilder.Default.Namespace(interfaceSymbol.ContainingNamespace.ToDisplayString()); + var namespaceBuilder = ns.AddMembers(classBuilder); + file = CodeFile.New($"{interfaceSymbol.FormatFileName()}ApiInvoker.g.cs") + .AddUsings("using Microsoft.Extensions.DependencyInjection;") + .AddFileHeader(""" + // + #pragma warning disable + #nullable enable + """) + .AddMembers(namespaceBuilder); + + return true; + } + + private static MethodBuilder? BuildMethod((IMethodSymbol methodSymbol, AttributeData? methodAttribute) method, string? route, + string scopeName, bool controllerAuth, out bool needAuth, List errorAndWarnings) + { + var methodSymbol = method.Item1; + var methodAttribute = method.Item2; + + var allowsAnonymous = (bool)(methodAttribute.GetNamedValue("AllowAnonymous") ?? false); + var authorize = (bool)(methodAttribute.GetNamedValue("Authorize") ?? false); + needAuth = !allowsAnonymous && (authorize || controllerAuth); + var cancellationTokenName = methodSymbol.Parameters.Where(p => p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.Threading.CancellationToken").Select(p => p.Name).FirstOrDefault(); + var hasCancellationToken = cancellationTokenName != null; + + if (methodSymbol.HasAttribute(NotSupported)) + { + TypeParameterInfo[] typeParameterInfos = [.. methodSymbol.GetTypeParameters()]; + var rt = methodSymbol.ReturnType; + var type = rt.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + string[] parameters = [.. methodSymbol.Parameters.Select(p => $"{p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {p.Name}")]; + var b = MethodBuilder.Default + .MethodName(methodSymbol.Name) + .Generic(typeParameterInfos) + .ReturnType(type) + .AddParameter(parameters) + .AddGeneratedCodeAttribute(typeof(HttpServiceInvokerGenerator)) + .Lambda("throw new global::System.NotSupportedException()"); + return b; + } + + // 检查当前返回类型是否是Task或Task + // 如果检查类型不符合要求,说明不是异步方法 + // 返回错误信息 + var returnTypeInfo = methodSymbol.ReturnType; + var isTask = returnTypeInfo.Name == "Task"; + var isGenericTask = returnTypeInfo.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + .StartsWith("global::System.Threading.Tasks.Task<"); + + if (!isTask && !isGenericTask) + { + errorAndWarnings.Add(DiagnosticDefinitions.WAG00005(methodSymbol.Locations.FirstOrDefault())); + return null; + } + + var webMethod = methodAttribute.GetNamedValue("Method", out var v) ? WebMethod[(int)v!] : "Post"; + var methodScoped = methodSymbol.Name.Replace("Async", ""); + var customRoute = methodAttribute?.GetNamedValue("Route")?.ToString(); + string methodRoute; + var useRouteParam = false; + if (string.IsNullOrEmpty(customRoute)) + { + methodRoute = methodScoped; + } + else if (Regex.Match(customRoute, "{.+}").Success) + { + useRouteParam = true; + methodRoute = $"{methodScoped}/{customRoute}"; + } + else + { + methodRoute = customRoute!; + } + + //var methodRoute = $"{methodAttribute?.GetNamedValue("Route") ?? methodSymbol.Name.Replace("Async", "")}"; + List statements = + [ + // var url = ""; + // var client = clientFactory.CreateClient(nameof()); + $"var _client_gen = this.clientFactory.CreateClient(\"{scopeName}\")", + // var request = new HttpRequestMessage(); + "var _request_gen = new global::System.Net.Http.HttpRequestMessage()", + // request.Method = HttpMethod. + $"_request_gen.Method = global::System.Net.Http.HttpMethod.{webMethod}" + ]; + if (needAuth) + { + statements.Add($"await headerHandler.SetRequestHeaderAsync(_request_gen, {cancellationTokenName ?? "global::System.Threading.CancellationToken.None"})"); + } + + // 处理参数标签 + var paramInfos = methodSymbol.Parameters.Select(p => + { + if (p.GetAttribute(WebMethodParameterBindingAttribute, out var ad)) + { + ad!.GetConstructorValue(0, out var bindingType); + var t = (ParameterBindingType)(int)bindingType!; + return (bindingType: t, p); + } + + if (useRouteParam && customRoute!.Contains(p.Name)) + { + return (bindingType: ParameterBindingType.FromRoute, p); + } + + return (bindingType: ParameterBindingType.Ignore, p); + }).ToList(); + + #region 检查参数配置 + + var routerParameters = paramInfos.Where(t => t.bindingType == ParameterBindingType.FromRoute); + foreach (var item in routerParameters) + { + // 如果路由参数中包含方法名,则忽略 + if (methodRoute.Contains($"{{{item.p.Name}}}")) continue; + errorAndWarnings.Add(DiagnosticDefinitions.WAG00006(methodSymbol.Locations.FirstOrDefault(), + methodSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); + return null; + } + + if (paramInfos.Any(t => t.bindingType == ParameterBindingType.FromForm) && paramInfos.Any(t => t.bindingType == ParameterBindingType.FromBody)) + { + // 不能同时存在FromBody和FromForm + errorAndWarnings.Add(DiagnosticDefinitions.WAG00007(methodSymbol.Locations.FirstOrDefault(), + methodSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); + return null; + } + + #endregion + + var url = $""" + var _url_gen = $"api/{route ?? scopeName}/{methodRoute}" + """; + statements.Add(url); + + AddQueryParameters(statements, paramInfos, webMethod, errorAndWarnings); + AddFormParameters(statements, paramInfos); + AddBodyParameters(statements, paramInfos, webMethod, methodSymbol, errorAndWarnings); + AddHeaderParameters(statements, paramInfos); + + statements.Add("_request_gen.RequestUri = new global::System.Uri(_url_gen, UriKind.Relative)"); + var returnType = methodSymbol.ReturnType.GetGenericTypes().FirstOrDefault() ?? methodSymbol.ReturnType; + + if (methodSymbol.ReturnsVoid || (isTask && !isGenericTask)) + { + statements.Add(hasCancellationToken + ? $"_ = await _client_gen.SendAsync(_request_gen, {cancellationTokenName})" + : "_ = await _client_gen.SendAsync(_request_gen)"); + } + else + { + statements.Add(hasCancellationToken + ? $"var _response_gen = await _client_gen.SendAsync(_request_gen, {cancellationTokenName})" + : "var _response_gen = await _client_gen.SendAsync(_request_gen)"); + statements.Add("_response_gen.EnsureSuccessStatusCode()"); + AddResponseHandling(statements, returnType, errorAndWarnings, cancellationTokenName, methodSymbol); + } + + var parameter = methodSymbol.Parameters.Select(p => $"{p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {p.Name}").ToArray(); + var builder = MethodBuilder.Default + .MethodName(methodSymbol.Name) + .Generic(methodSymbol.GetTypeParameters().ToArray()) + .Async() + .ReturnType(methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)) + .AddParameter(parameter) + .AddGeneratedCodeAttribute(typeof(HttpServiceInvokerGenerator)) + .AddBody(statements.ToArray()); + return builder; + } + + private static IEnumerable BuildField(bool needAuth) + { + // private readonly JsonSerializerOptions jsonOptions; + yield return FieldBuilder.Default.MemberType("global::System.Text.Json.JsonSerializerOptions") + .FieldName("_JSON_OPTIONS_gen"); + // private readonly IHttpClientFactory clientFactory; + yield return FieldBuilder.Default + .MemberType("global::System.Net.Http.IHttpClientFactory") + .FieldName("clientFactory"); + if (needAuth) + { + yield return FieldBuilder.Default + .MemberType("global::AutoWasmApiGenerator.IHttpClientHeaderHandler") + .FieldName("headerHandler"); + } + } + + /// + /// 处理Query参数 + /// + /// + /// + /// + /// + private static void AddQueryParameters(List statements, + IEnumerable<(ParameterBindingType i, IParameterSymbol p)> paramInfos, string webMethod, List errorAndWarnings) + { + var queryParameters = paramInfos.Where(t => (t.i == ParameterBindingType.Ignore && webMethod == WebMethodConstants.Get) || t.i == ParameterBindingType.FromQuery).ToList(); + + if (!queryParameters.Any()) return; + statements.Add("var _queries_gen = new global::System.Collections.Generic.List()"); + foreach (var item in queryParameters) + { + var p = item.p; + if (p.Type is INamedTypeSymbol + { + TypeKind: TypeKind.Class, SpecialType: not SpecialType.System_String + } parameterClassType) + { + var properties = parameterClassType.GetMembers().Where(m => m.Kind == SymbolKind.Property); + foreach (var prop in properties) + { + statements.Add( + $$"""_queries_gen.Add($"{nameof({{p.Name}}.{{prop.Name}})}={{{p.Name}}.{{prop.Name}}}")"""); + } + } + else + { + statements.Add($$"""_queries_gen.Add($"{nameof({{p.Name}})}={{{p.Name}}}")"""); + } + } + + var setUrl = """ + _url_gen = $"{_url_gen}?{string.Join("&", _queries_gen)}" + """; + statements.Add(setUrl); + } + + /// + /// 处理Form参数 + /// + /// + /// + private static void AddFormParameters(List statements, IEnumerable<(ParameterBindingType i, IParameterSymbol p)> paramInfos) + { + var formParameters = paramInfos.Where(t => t.i == ParameterBindingType.FromForm).ToList(); + + if (!formParameters.Any()) return; + statements.Add( + "var _formData_gen = new List>()"); + foreach (var item in formParameters) + { + var p = item.p; + if (p.Type is INamedTypeSymbol + { + TypeKind: TypeKind.Class, SpecialType: not SpecialType.System_String + } parameterClassType) + { + var properties = parameterClassType.GetMembers().Where(m => m.Kind == SymbolKind.Property); + foreach (var prop in properties) + { + statements.Add($$""" + _formData_gen.Add(new global::System.Collections.Generic.KeyValuePair(nameof({{p.Name}}.{{prop.Name}}), $"{{{p.Name}}.{{prop.Name}}}")) + """); + } + } + else + { + statements.Add( + $$"""_formData_gen.Add(new global::System.Collections.Generic.KeyValuePair(nameof({{p.Name}}), $"{{{p.Name}}}"))"""); + } + } + + statements.Add("var _formContent_gen = new global::System.Net.Http.FormUrlEncodedContent(_formData_gen)"); + statements.Add("_formContent_gen.Headers.ContentType = new(\"application/x-www-form-urlencoded\")"); + statements.Add("_request_gen.Content = _formContent_gen"); + } + + /// + /// 处理Body参数 + /// + /// + /// + /// + /// + /// + /// + private static bool AddBodyParameters(List statements, IEnumerable<(ParameterBindingType i, IParameterSymbol p)> paramInfos, string webMethod, IMethodSymbol methodSymbol, List errorAndWarnings) + { + // var bodyParameters = paramInfos.Where(t => (t.i == -1 && webMethod != WebMethodConstants.Get) || t.i == WebMethodConstants.Body).ToList(); + var bodyParameters = paramInfos.Where(t => t.i == ParameterBindingType.FromBody).ToList(); + return bodyParameters.Count switch + { + > 1 => DoReturnError(), + 0 => true, + 1 => DoAddBody(), + _ => throw new ArgumentOutOfRangeException() + }; + + bool DoReturnError() + { + errorAndWarnings.Add(DiagnosticDefinitions.WAG00008(methodSymbol.Locations.FirstOrDefault())); + return false; + } + + bool DoAddBody() + { + var p = bodyParameters[0].p; + statements.Add($"var _json_gen = global::System.Text.Json.JsonSerializer.Serialize({p.Name})"); + statements.Add( + """_request_gen.Content = new global::System.Net.Http.StringContent(_json_gen, global::System.Text.Encoding.Default, "application/json")"""); + return true; + } + } + + /// + /// 处理Header参数 + /// + /// + /// + private static bool AddHeaderParameters(List statements, IEnumerable<(ParameterBindingType i, IParameterSymbol p)> paramInfos) + { + var headerParameters = paramInfos.Where(t => t.i == ParameterBindingType.FromHeader).ToList(); + + if (!headerParameters.Any()) return true; + foreach (var item in headerParameters) + { + var p = item.p; + if (p.Type is INamedTypeSymbol + { + TypeKind: TypeKind.Class, SpecialType: not SpecialType.System_String + } parameterClassType) + { + var properties = parameterClassType.GetMembers().Where(m => m.Kind == SymbolKind.Property); + foreach (var prop in properties) + { + statements.Add( + $$"""_request_gen.Headers.Add(nameof({{p.Name}}.{{prop.Name}}), $"{{{p.Name}}.{{prop.Name}}}")"""); + } + } + else + { + statements.Add($$"""_request_gen.Headers.Add(nameof({{p.Name}}), $"{{{p.Name}}}")"""); + } + } + + return true; + } + + private static bool AddResponseHandling(List statements, ITypeSymbol returnType, + List errorAndWarnings, string? cancellationTokenName, IMethodSymbol methodSymbol) + { + cancellationTokenName ??= string.Empty; + if (returnType is { TypeKind: TypeKind.Class, SpecialType: not SpecialType.System_String }) + { + statements.Add($"var _stream_gen = await _response_gen.Content.ReadAsStreamAsync({cancellationTokenName})"); + statements.Add( + $"return global::System.Text.Json.JsonSerializer.Deserialize<{returnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(_stream_gen, _JSON_OPTIONS_gen);"); + } + else + { + statements.Add($"var _str_gen = await _response_gen.Content.ReadAsStringAsync({cancellationTokenName})"); + if (returnType.SpecialType == SpecialType.System_String) + { + statements.Add("return _str_gen"); + } + else if (returnType.HasTryParseMethod()) + { + statements.Add($"{returnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.TryParse(_str_gen, out var val)"); + statements.Add("return val"); + } + else + { + errorAndWarnings.Add(DiagnosticDefinitions.WAG00009(methodSymbol.Locations.FirstOrDefault(), returnType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + return false; + } + } + + return true; + } + + + private static ConstructorBuilder BuildConstructor(INamedTypeSymbol classSymbol, bool needAuth) + { + List parameters = ["global::System.Net.Http.IHttpClientFactory factory"]; + List body = ["clientFactory = factory;"]; + if (needAuth) + { + //parameters.Add("global::AutoWasmApiGenerator.IHttpClientHeaderHandler handler"); + parameters.Add("global::System.IServiceProvider services"); + body.Add( + "headerHandler = services.GetService() ?? global::AutoWasmApiGenerator.DefaultHttpClientHeaderHandler.Default"); + } + + return ConstructorBuilder.Default + .MethodName($"{FormatClassName(classSymbol.MetadataName)}ApiInvoker") + .AddParameter([.. parameters]) + .AddBody([.. body]) + .AddBody( + "_JSON_OPTIONS_gen = new global::System.Text.Json.JsonSerializerOptions() { PropertyNameCaseInsensitive = true };"); + } + + private static ClassBuilder CreateHttpClassBuilder(INamedTypeSymbol interfaceSymbol) + { + IEnumerable additionalAttribute = []; + if (interfaceSymbol.GetAttribute(ApiInvokerGenerateAttributeFullName, out var data)) + { + //var o = data.GetAttributeValue(nameof(ApiInvokerGeneraAttribute.Attribute)); + additionalAttribute = interfaceSymbol.GetAttributeInitInfo(ApiInvokerGenerateAttributeFullName, data!); + } + + return ClassBuilder.Default + .ClassName($"{FormatClassName(interfaceSymbol.MetadataName)}ApiInvoker") + .AddGeneratedCodeAttribute(typeof(HttpServiceInvokerGenerator)) + .Attribute([.. additionalAttribute.Select(i => i.ToString())]) + .BaseType(interfaceSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } +} diff --git a/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/IHttpServiceInvokerGenerator.cs b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/IHttpServiceInvokerGenerator.cs new file mode 100644 index 0000000..bb06939 --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/IHttpServiceInvokerGenerator.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace AutoWasmApiGenerator; + +public interface IHttpServiceInvokerGenerator +{ + (string generatedFileName, string sourceCode, List errorAndWarnings) Generate( + INamedTypeSymbol interfaceSymbol); +} \ No newline at end of file diff --git a/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/Options/GlobalOptions.cs b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/Options/GlobalOptions.cs new file mode 100644 index 0000000..0b3e2d7 --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/Options/GlobalOptions.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace AutoWasmApiGenerator.Options; + +public sealed class GlobalOptions +{ + public GlobalOptions(AnalyzerConfigOptions options) + { + if (options.TryGetValue("build_property.MSBuildProjectFullPath", out var projectFullPath)) + { + ProjectFullPath = projectFullPath; + } + + ProjectFullPath ??= string.Empty; + + GenerateInvoker = + options.TryGetValue("build_property.AutoWasmApiGenerator_GenerateInvoker", out var generateInvoker) && + generateInvoker is { Length: > 0 } && + generateInvoker.Equals("true", StringComparison.OrdinalIgnoreCase); + + GenerateWebController = + options.TryGetValue("build_property.AutoWasmApiGenerator_GenerateWebController", out var generateWebController) && + generateWebController is { Length: > 0 } && + generateWebController.Equals("true", StringComparison.OrdinalIgnoreCase); + } + + /// + /// + /// + public bool GenerateInvoker { get; } + + /// + /// + /// + public bool GenerateWebController { get; } + + /// + /// + /// + public string? ProjectFullPath { get; } + + public static GlobalOptions Select(AnalyzerConfigOptionsProvider provider, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + return new GlobalOptions(provider.GlobalOptions); + } +} \ No newline at end of file diff --git a/src/AutoWasmApi.Roslyn/Properties/launchSettings.json b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/Properties/launchSettings.json similarity index 100% rename from src/AutoWasmApi.Roslyn/Properties/launchSettings.json rename to src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/Properties/launchSettings.json diff --git a/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/build/AutoWasmApiGenerator.props b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/build/AutoWasmApiGenerator.props new file mode 100644 index 0000000..eda792e --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/build/AutoWasmApiGenerator.props @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/AutoWasmApi.Roslyn/usings.cs b/src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/usings.cs similarity index 100% rename from src/AutoWasmApi.Roslyn/usings.cs rename to src/AutoWasmApiGenerator/AutoWasmApi.Roslyn/usings.cs diff --git a/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/AutoWasmApiGenerator.Test.csproj b/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/AutoWasmApiGenerator.Test.csproj new file mode 100644 index 0000000..0a68c0a --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/AutoWasmApiGenerator.Test.csproj @@ -0,0 +1,55 @@ + + + + net8.0 + enable + enable + + false + true + true + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/GlobalOptionsTest.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/GlobalOptionsTest.cs new file mode 100644 index 0000000..a9d518a --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/GlobalOptionsTest.cs @@ -0,0 +1,76 @@ +using System.Diagnostics.CodeAnalysis; +using AutoWasmApiGenerator.Options; +using FluentAssertions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace AutoWasmApiGenerator.Test; + +public class GlobalOptionsTest +{ + private static readonly GlobalOptions LocalGlobalOptionsForTests = CreateOptions(new AnalyzerConfigOptionsDummy()); + + private static GlobalOptions CreateOptions(AnalyzerConfigOptionsDummy dummy) + { + return GlobalOptions.Select(new AnalyzerConfigOptionsProviderDummy(dummy), default); + } + + [Fact] + public void GlobalDefault() + { + var globalOptions = LocalGlobalOptionsForTests; + globalOptions.ProjectFullPath.Should().BeEmpty(); + globalOptions.GenerateInvoker.Should().BeFalse(); + globalOptions.GenerateWebController.Should().BeFalse(); + } + + [Fact] + public void GlobalSettings_CanReadAll() + { + var globalOptions = CreateOptions(new AnalyzerConfigOptionsDummy() + { + MSBuildProjectFullPath = "projectFullPath.csproj", + AutoWasmApiGenerator_GenerateInvoker = "true", + AutoWasmApiGenerator_GenerateWebController = "true" + }); + globalOptions.ProjectFullPath.Should().Be("projectFullPath.csproj"); + globalOptions.GenerateInvoker.Should().BeTrue(); + globalOptions.GenerateWebController.Should().BeTrue(); + } +} + +public class AnalyzerConfigOptionsDummy : AnalyzerConfigOptions +{ + // ReSharper disable InconsistentNaming + public string? MSBuildProjectFullPath { get; init; } + public string? AutoWasmApiGenerator_GenerateInvoker { get; init; } + public string? AutoWasmApiGenerator_GenerateWebController { get; init; } + // ReSharper restore InconsistentNaming + + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + { + value = key switch + { + "build_property.MSBuildProjectFullPath" => MSBuildProjectFullPath, + "build_property.AutoWasmApiGenerator_GenerateInvoker" => AutoWasmApiGenerator_GenerateInvoker, + "build_property.AutoWasmApiGenerator_GenerateWebController" => AutoWasmApiGenerator_GenerateWebController, + _ => null + }; + return value != null; + } +} + +public sealed class AnalyzerConfigOptionsProviderDummy(AnalyzerConfigOptions globalOptions) : AnalyzerConfigOptionsProvider +{ + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) + { + throw new NotImplementedException(); + } + + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) + { + return GlobalOptions; + } + + public override AnalyzerConfigOptions GlobalOptions { get; } = globalOptions; +} \ No newline at end of file diff --git a/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/HttpServiceInvokerGeneratorTest.Constants.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/HttpServiceInvokerGeneratorTest.Constants.cs new file mode 100644 index 0000000..58f8520 --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/HttpServiceInvokerGeneratorTest.Constants.cs @@ -0,0 +1,385 @@ +// ReSharper disable InconsistentNaming +namespace AutoWasmApiGenerator.Test; + +public partial class HttpServiceInvokerGeneratorTest +{ + private const string AssemblyTag = @" [assembly: AutoWasmApiGenerator.ApiInvokerAssembly] + [assembly: AutoWasmApiGenerator.WebControllerAssembly] +"; + + private const string Usings = @"global using AutoWasmApiGenerator; +global using System; +global using System.Threading; +global using System.Threading.Tasks; +global using System.Text.Json; +global using System.Net.Http; +global using System.Collections.Generic; +global using System.Text; +global using System.Linq; +// using Microsoft.Extensions.DependencyInjection; +"; + + #region Wag00005 + + private const string TestWag00005 = @"namespace AutoWasmApiGenerator.Test; + +[WebController(Authorize = true)] +[ApiInvokerGenerate] +public interface ITest +{ + void Log(string message); +}"; + + #endregion + + #region Wag00006 + + private const string TestWag00006 = @"using AutoWasmApiGenerator +namespace AutoWasmApiGenerator.Test; + +[WebController] +[ApiInvokerGenerate] +public interface ITest +{ + [WebMethod(Method = WebMethod.Post)] + Task LogAsync([WebMethodParameterBinding(BindingType.FromRoute)] string model0)] TestModel1 model1); +} +"; + + #endregion + + #region Wag00007 + + private const string TestWag00007 = @"using AutoWasmApiGenerator +namespace AutoWasmApiGenerator.Test; + +public record TestModel0(string Name, int Age); + +public record TestModel1(string Name, double Salary); + +[WebController] +[ApiInvokerGenerate] +public interface ITest +{ + [WebMethod(Method = WebMethod.Post)] + Task LogAsync([WebMethodParameterBinding(BindingType.FromBody)] TestModel0 model0, [WebMethodParameterBinding(BindingType.FromForm)] TestModel1 model1); +} + +"; + + #endregion + + #region Wag00010 + + private const string TestWag00010 = @"using AutoWasmApiGenerator"; + // private const string TestWag00010Result = @"using AutoWasmApiGenerator"; + + #endregion + + #region Wag00011 + + private const string TestWag00011 = @"namespace AutoWasmApiGenerator.Test; + +[ApiInvokerGenerate] +public interface ITest +{ + [WebMethod(Method = WebMethod.Get)] + Task LogAsync(string message); +}"; + + #endregion + + #region Wag00002 + + private const string TestWag00002 = @"using AutoWasmApiGenerator"; + + #endregion + + #region Wag00003 + + private const string TestWag00003 = @"using AutoWasmApiGenerator"; + + #endregion + + #region Success + + private static void UpdateSuccessTestString() + { + TestSuccessResult = + $$""" + // + #pragma warning disable + #nullable enable + + using Microsoft.Extensions.DependencyInjection; + + namespace AutoWasmApiGenerator.Test + { + [global::System.CodeDom.Compiler.GeneratedCode("AutoWasmApiGenerator.HttpServiceInvokerGenerator", "{{GeneratorVersion}}")] + /// + public partial class TestApiInvoker : global::AutoWasmApiGenerator.Test.ITest + { + private readonly global::System.Text.Json.JsonSerializerOptions _JSON_OPTIONS_gen; + + private readonly global::System.Net.Http.IHttpClientFactory clientFactory; + + private readonly global::AutoWasmApiGenerator.IHttpClientHeaderHandler headerHandler; + + public TestApiInvoker(global::System.Net.Http.IHttpClientFactory factory, global::System.IServiceProvider services) + { + clientFactory = factory; + headerHandler = services.GetService() ?? global::AutoWasmApiGenerator.DefaultHttpClientHeaderHandler.Default; + _JSON_OPTIONS_gen = new global::System.Text.Json.JsonSerializerOptions() { PropertyNameCaseInsensitive = true }; + } + + [global::System.CodeDom.Compiler.GeneratedCode("AutoWasmApiGenerator.HttpServiceInvokerGenerator", "{{GeneratorVersion}}")] + public void Log(string message) + => throw new global::System.NotSupportedException(); + + [global::System.CodeDom.Compiler.GeneratedCode("AutoWasmApiGenerator.HttpServiceInvokerGenerator", "{{GeneratorVersion}}")] + public async global::System.Threading.Tasks.Task LogAsync(string message) + { + var _client_gen = this.clientFactory.CreateClient("Test"); + var _request_gen = new global::System.Net.Http.HttpRequestMessage(); + _request_gen.Method = global::System.Net.Http.HttpMethod.Get; + await headerHandler.SetRequestHeaderAsync(_request_gen, global::System.Threading.CancellationToken.None); + var _url_gen = $"api/Test/Log"; + var _queries_gen = new global::System.Collections.Generic.List(); + _queries_gen.Add($"{nameof(message)}={message}"); + _url_gen = $"{_url_gen}?{string.Join("&", _queries_gen)}"; + _request_gen.RequestUri = new global::System.Uri(_url_gen, UriKind.Relative); + var _response_gen = await _client_gen.SendAsync(_request_gen); + _response_gen.EnsureSuccessStatusCode(); + var _str_gen = await _response_gen.Content.ReadAsStringAsync(); + bool.TryParse(_str_gen, out var val); + return val; + } + + [global::System.CodeDom.Compiler.GeneratedCode("AutoWasmApiGenerator.HttpServiceInvokerGenerator", "{{GeneratorVersion}}")] + public async global::System.Threading.Tasks.Task Log2Async(string message, global::System.Threading.CancellationToken token) + { + var _client_gen = this.clientFactory.CreateClient("Test"); + var _request_gen = new global::System.Net.Http.HttpRequestMessage(); + _request_gen.Method = global::System.Net.Http.HttpMethod.Post; + await headerHandler.SetRequestHeaderAsync(_request_gen, token); + var _url_gen = $"api/Test/Log2"; + var _json_gen = global::System.Text.Json.JsonSerializer.Serialize(message); + _request_gen.Content = new global::System.Net.Http.StringContent(_json_gen, global::System.Text.Encoding.Default, "application/json"); + _request_gen.RequestUri = new global::System.Uri(_url_gen, UriKind.Relative); + var _response_gen = await _client_gen.SendAsync(_request_gen, token); + _response_gen.EnsureSuccessStatusCode(); + var _str_gen = await _response_gen.Content.ReadAsStringAsync(token); + bool.TryParse(_str_gen, out var val); + return val; + } + + [global::System.CodeDom.Compiler.GeneratedCode("AutoWasmApiGenerator.HttpServiceInvokerGenerator", "{{GeneratorVersion}}")] + public async global::System.Threading.Tasks.Task Log3Async(string message, string path, global::System.Threading.CancellationToken token) + { + var _client_gen = this.clientFactory.CreateClient("Test"); + var _request_gen = new global::System.Net.Http.HttpRequestMessage(); + _request_gen.Method = global::System.Net.Http.HttpMethod.Delete; + await headerHandler.SetRequestHeaderAsync(_request_gen, token); + var _url_gen = $"api/Test/Log3"; + var _queries_gen = new global::System.Collections.Generic.List(); + _queries_gen.Add($"{nameof(path)}={path}"); + _url_gen = $"{_url_gen}?{string.Join("&", _queries_gen)}"; + var _json_gen = global::System.Text.Json.JsonSerializer.Serialize(message); + _request_gen.Content = new global::System.Net.Http.StringContent(_json_gen, global::System.Text.Encoding.Default, "application/json"); + _request_gen.RequestUri = new global::System.Uri(_url_gen, UriKind.Relative); + var _response_gen = await _client_gen.SendAsync(_request_gen, token); + _response_gen.EnsureSuccessStatusCode(); + var _str_gen = await _response_gen.Content.ReadAsStringAsync(token); + bool.TryParse(_str_gen, out var val); + return val; + } + } + } + """; + } + + private const string TestSuccessCode = @"using AutoWasmApiGenerator; +using System.Threading.Tasks; +namespace AutoWasmApiGenerator.Test; + +[WebController(Authorize = true)] +[ApiInvokerGenerate] +public interface ITest +{ + [ApiInvokeNotSupported] + void Log(string message); + [WebMethod(Method = WebMethod.Get)] + Task LogAsync(string message); + + [WebMethod(Method = WebMethod.Post)] + Task Log2Async([WebMethodParameterBinding(BindingType.FromBody)] string message, [WebMethodParameterBinding(BindingType.Ignore)] CancellationToken token); + + [WebMethod(Method = WebMethod.Delete)] + Task Log3Async([WebMethodParameterBinding(BindingType.FromBody)] string message, [WebMethodParameterBinding(BindingType.FromQuery)] string path, CancellationToken token); +} +"; + + private static string TestSuccessResult = null!; // Updated in UpdateTestString + + #endregion + + #region FromRoute + + private const string TestFromRoute = + """ + using AutoWasmApiGenerator + namespace AutoWasmApiGenerator.Test; + + [WebController] + [ApiInvokerGenerate] + public interface ITest + { + [WebMethod(Method = WebMethod.Post, Route="Log/{model0}")] + Task LogAsync([WebMethodParameterBinding(BindingType.FromRoute)] string model0); + } + """; + + private static string TestFromRouteResult = null!; // Updated in UpdateFromRouteTestString + + private static void UpdateFromRouteTestString() + { + TestFromRouteResult = + $$""" + // + #pragma warning disable + #nullable enable + + using Microsoft.Extensions.DependencyInjection; + + namespace AutoWasmApiGenerator.Test + { + [global::System.CodeDom.Compiler.GeneratedCode("AutoWasmApiGenerator.HttpServiceInvokerGenerator", "{{GeneratorVersion}}")] + /// + public partial class TestApiInvoker : global::AutoWasmApiGenerator.Test.ITest + { + private readonly global::System.Text.Json.JsonSerializerOptions _JSON_OPTIONS_gen; + + private readonly global::System.Net.Http.IHttpClientFactory clientFactory; + + public TestApiInvoker(global::System.Net.Http.IHttpClientFactory factory) + { + clientFactory = factory; + _JSON_OPTIONS_gen = new global::System.Text.Json.JsonSerializerOptions() { PropertyNameCaseInsensitive = true }; + } + + [global::System.CodeDom.Compiler.GeneratedCode("AutoWasmApiGenerator.HttpServiceInvokerGenerator", "{{GeneratorVersion}}")] + public async global::System.Threading.Tasks.Task LogAsync(string model0) + { + var _client_gen = this.clientFactory.CreateClient("Test"); + var _request_gen = new global::System.Net.Http.HttpRequestMessage(); + _request_gen.Method = global::System.Net.Http.HttpMethod.Post; + var _url_gen = $"api/Test/Log/Log/{model0}"; + _request_gen.RequestUri = new global::System.Uri(_url_gen, UriKind.Relative); + var _response_gen = await _client_gen.SendAsync(_request_gen); + _response_gen.EnsureSuccessStatusCode(); + var _str_gen = await _response_gen.Content.ReadAsStringAsync(); + bool.TryParse(_str_gen, out var val); + return val; + } + } + } + """; + } + + #endregion + + #region Wag00008 + + private const string TestWag00008 = @"using AutoWasmApiGenerator +namespace AutoWasmApiGenerator.Test; + +public record TestModel0(string Name, int Age); + +public record TestModel1(string Name, double Salary); + +[WebController] +[ApiInvokerGenerate] +public interface ITest +{ + [WebMethod(Method = WebMethod.Post)] + Task LogAsync([WebMethodParameterBinding(BindingType.FromBody)] TestModel0 model0, [WebMethodParameterBinding(BindingType.FromBody)] TestModel1 model1); +} +"; + + private const string TestWag00008Result = @"using AutoWasmApiGenerator"; + + #endregion + + #region Wag00009 + + private const string TestWag00009_0 = @"namespace AutoWasmApiGenerator.Test; + +[WebController(Authorize = true)] +[ApiInvokerGenerate] +public interface ITest +{ + Task Log(string message); +}"; + + private const string TestWag00009_ValidReturnType = @"namespace AutoWasmApiGenerator.Test; + +[WebController(Authorize = true)] +[ApiInvokerGenerate] +public interface ITest +{ + Task Log(string message); +}"; + + private static string TestWag00009_ValidReturnTypeResult = null!; // Updated in UpdateWag00009TestString + + private static void UpdateWag00009TestString() + { + TestWag00009_ValidReturnTypeResult = + $$""" + // + #pragma warning disable + #nullable enable + + using Microsoft.Extensions.DependencyInjection; + + namespace AutoWasmApiGenerator.Test + { + [global::System.CodeDom.Compiler.GeneratedCode("AutoWasmApiGenerator.HttpServiceInvokerGenerator", "{{GeneratorVersion}}")] + /// + public partial class TestApiInvoker : global::AutoWasmApiGenerator.Test.ITest + { + private readonly global::System.Text.Json.JsonSerializerOptions _JSON_OPTIONS_gen; + + private readonly global::System.Net.Http.IHttpClientFactory clientFactory; + + private readonly global::AutoWasmApiGenerator.IHttpClientHeaderHandler headerHandler; + + public TestApiInvoker(global::System.Net.Http.IHttpClientFactory factory, global::System.IServiceProvider services) + { + clientFactory = factory; + headerHandler = services.GetService() ?? global::AutoWasmApiGenerator.DefaultHttpClientHeaderHandler.Default; + _JSON_OPTIONS_gen = new global::System.Text.Json.JsonSerializerOptions() { PropertyNameCaseInsensitive = true }; + } + + [global::System.CodeDom.Compiler.GeneratedCode("AutoWasmApiGenerator.HttpServiceInvokerGenerator", "{{GeneratorVersion}}")] + public async global::System.Threading.Tasks.Task Log(string message) + { + var _client_gen = this.clientFactory.CreateClient("Test"); + var _request_gen = new global::System.Net.Http.HttpRequestMessage(); + _request_gen.Method = global::System.Net.Http.HttpMethod.Post; + await headerHandler.SetRequestHeaderAsync(_request_gen, global::System.Threading.CancellationToken.None); + var _url_gen = $"api/Test/Log"; + _request_gen.RequestUri = new global::System.Uri(_url_gen, UriKind.Relative); + var _response_gen = await _client_gen.SendAsync(_request_gen); + _response_gen.EnsureSuccessStatusCode(); + var _str_gen = await _response_gen.Content.ReadAsStringAsync(); + bool.TryParse(_str_gen, out var val); + return val; + } + } + } + """; + } + + #endregion +} \ No newline at end of file diff --git a/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/HttpServiceInvokerGeneratorTest.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/HttpServiceInvokerGeneratorTest.cs new file mode 100644 index 0000000..f82b1b0 --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/HttpServiceInvokerGeneratorTest.cs @@ -0,0 +1,282 @@ +using System.Reflection; +using FluentAssertions; +using Generators.Shared; +using Microsoft.CodeAnalysis; + +namespace AutoWasmApiGenerator.Test; + +public static class ExecutableReferenceExtension +{ + public static PortableExecutableReference[] DistinctReferences(this IEnumerable types) + { + return types.Select(t => t.Assembly.Location).Distinct().Select(t => MetadataReference.CreateFromFile(t)) + .ToArray(); + } +} + +public partial class HttpServiceInvokerGeneratorTest : IncrementalSourceGeneratorTestBase +{ + private static readonly PortableExecutableReference[] References; + private static readonly string GeneratorVersion; + + static HttpServiceInvokerGeneratorTest() + { + References = new List + { + typeof(HttpServiceInvokerGenerator), + typeof(WebControllerAttribute) + }.DistinctReferences(); + GeneratorVersion = typeof(HttpServiceInvokerGenerator).Assembly.GetCustomAttribute() + ?.Version ?? throw new Exception("Unknown generator version"); + UpdateSuccessTestString(); + UpdateWag00009TestString(); + UpdateFromRouteTestString(); + } + + [Fact] + public void Test_ReferenceValidity() + { + var (compilation, _) = CreateDriver([TestSuccessCode, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + + var ret = compilation.GetAllSymbols(typeof(WebControllerAttribute).FullName!); + ret.Should().HaveCount(1); + } + + [Fact] + public void Test_SuccessGenerate() + { + // Get AssemblyMeta from AutoWasmApiGenerator + var (compilation, driver) = CreateDriver([TestSuccessCode, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + + // Run the generator + driver = driver.RunGenerators(compilation); + var results = driver.GetRunResult(); + var result = results.Results.Single(); + + // Assert that the generated sources are not empty + Assert.NotEmpty(result.GeneratedSources); + result.GeneratedSources.Should().HaveCount(1).And.Contain(r => r.HintName == "AutoWasmApiGenerator_Test_TestApiInvoker.g.cs"); + result.GeneratedSources[0].SourceText.ToString().ReplaceLineEndings().Should() + .BeEquivalentTo(TestSuccessResult.ReplaceLineEndings()); + } + + [Fact] + public void Test_Wag00002() + { + // Get AssemblyMeta from AutoWasmApiGenerator + var (compilation, driver) = CreateDriver([TestWag00002, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + // TODO: Add the test, however current error code is not used + } + + /// + /// ԣ + /// + [Fact] + public void Test_Wag00003() + { + // Get AssemblyMeta from AutoWasmApiGenerator + var (compilation, driver) = CreateDriver([TestWag00003, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + // TODO: Add the test, however current error code is not used + } + + /// + /// ԣ֧첽 + /// + [Fact] + public void Test_Wag00005() + { + // Get AssemblyMeta from AutoWasmApiGenerator + var (compilation, driver) = CreateDriver([TestWag00005, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + var result = driver.RunGenerators(compilation) + .GetRunResult(); + // result.Results.Should().BeEmpty(); + result.GeneratedTrees.Should().BeEmpty(); + result.Diagnostics.Should().HaveCount(1); + result.Diagnostics.Should().Contain(diagnostic => diagnostic.Id == "WAG00005"); + } + + /// + /// ԣ·δ·ɲ + /// + /// + [Fact] + public void Test_Wag00006_Fail() + { + // Get AssemblyMeta from AutoWasmApiGenerator + var (compilation, driver) = CreateDriver([TestWag00006, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + var results = driver.RunGenerators(compilation) + .GetRunResult(); + results.Diagnostics.Should() + .HaveCount(1) + .And.Contain(diagnostic => diagnostic.Id == "WAG00006"); + results.GeneratedTrees.Should().BeEmpty(); + } + + /// + /// ԣ·δ·ɲ + /// + /// + [Fact] + public void Test_FromRoute() + { + // Get AssemblyMeta from AutoWasmApiGenerator + var (compilation, driver) = CreateDriver([TestFromRoute, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + var results = driver.RunGenerators(compilation) + .GetRunResult(); + results.Diagnostics.Should().BeEmpty(); + results.GeneratedTrees.Should().NotBeEmpty(); + results.Results.Should().HaveCount(1); + var result = results.Results[0]; + result.GeneratedSources.Should().HaveCount(1); + string generated = result.GeneratedSources[0].SourceText.ToString().ReplaceLineEndings(); + generated.Should().BeEquivalentTo(TestFromRouteResult.ReplaceLineEndings()); + } + + + [Fact] + public void Test_Wag00007() + { + // Get AssemblyMeta from AutoWasmApiGenerator + var (compilation, driver) = CreateDriver([TestWag00007, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + var results = driver.RunGenerators(compilation) + .GetRunResult(); + results.Diagnostics.Should() + .HaveCount(1) + .And.Contain(diagnostic => diagnostic.Id == "WAG00007"); + results.GeneratedTrees.Should().BeEmpty(); + } + + /// + /// ԣö[FromBody] + /// + /// + [Fact] + public void Test_Wag00008() + { + // Get AssemblyMeta from AutoWasmApiGenerator + var (compilation, driver) = CreateDriver([TestWag00008, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + var results = driver.RunGenerators(compilation) + .GetRunResult(); + results.Diagnostics.Should() + .HaveCount(1) + .And.Contain(diagnostic => diagnostic.Id == "WAG00008"); + results.GeneratedTrees.Should().BeEmpty(); + } + + /// + /// ԣݲֵ֧ķֵ + /// + [Fact] + public void Test_Wag00009() + { + // Get AssemblyMeta from AutoWasmApiGenerator + var (compilation, driver) = CreateDriver([TestWag00009_0, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + var results = driver.RunGenerators(compilation) + .GetRunResult(); + results.Diagnostics.Should() + .HaveCount(1) + .And.Contain(diagnostic => diagnostic.Id == "WAG00009"); + results.GeneratedTrees.Should().BeEmpty(); + + // Test valid return type, wag00009 should not be triggered + (compilation, driver) = CreateDriver([TestWag00009_ValidReturnType, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + + results = driver.RunGenerators(compilation).GetRunResult(); + results.Diagnostics.Should() + .BeEmpty(); + results.Results.Should().HaveCount(1); + var result = results.Results[0]; + + result.GeneratedSources.Should().HaveCount(1); + result.GeneratedSources[0].SourceText.ToString().ReplaceLineEndings().Should() + .BeEquivalentTo(TestWag00009_ValidReturnTypeResult.ReplaceLineEndings()); + } + + [Fact] + public void Test_Wag00010() + { + // Get AssemblyMeta from AutoWasmApiGenerator + var (compilation, driver) = CreateDriver([TestWag00010, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + // TODO: Add the test, however current error code may not be triggered + } + + [Fact] + public void Test_Wag00011() + { + // Get AssemblyMeta from AutoWasmApiGenerator + var (compilation, driver) = CreateDriver([TestWag00011, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + var results = driver.RunGenerators(compilation) + .GetRunResult(); + results.Diagnostics.Should() + .HaveCount(1) + .And.Contain(diagnostic => diagnostic.Id == "WAG00011"); + results.GeneratedTrees.Should().BeEmpty(); + } + + + #region Example + + /* // this just an example snippet, not a real test + public void Test_Example() + { + // Get AssemblyMeta from AutoWasmApiGenerator + var (compilation, driver) = CreateDriver([Test0, AssemblyTag, Usings], + new HttpServiceInvokerGenerator().AsSourceGenerator(), References); + + // Run the generator + driver = driver.RunGenerators(compilation); + var results = driver.GetRunResult(); + var result = results.Results.Single(); + // + // Log the diagnostics + foreach (var diagnostic in result.Diagnostics) + { + Console.WriteLine(diagnostic.ToString()); + } + + // Log the generated sources + foreach (var generatedSource in result.GeneratedSources) + { + Console.WriteLine($"HintName: {generatedSource.HintName}"); + Console.WriteLine(generatedSource.SourceText.ToString()); + } + + // Assert that the generated sources are not empty + Assert.NotEmpty(result.GeneratedSources); + + // Update the compilation and rerun the generator + compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText("// dummy")); + driver = driver.RunGenerators(compilation); + + // Assert the driver doesn't recompute the output + results = driver.GetRunResult(); + result = results.Results.Single(); + var allOutputs = result.TrackedOutputSteps.SelectMany(outputStep => outputStep.Value) + .SelectMany(output => output.Outputs); + Assert.Collection(allOutputs, output => Assert.Equal(IncrementalStepRunReason.Cached, output.Reason)); + + // Assert the driver use the cached result from AssemblyName and Syntax + var assemblyNameOutputs = result.TrackedSteps["AssemblyName"].Single().Outputs; + Assert.Collection(assemblyNameOutputs, + output => Assert.Equal(IncrementalStepRunReason.Unchanged, output.Reason)); + + var syntaxOutputs = result.TrackedSteps["Syntax"].Single().Outputs; + Assert.Collection(syntaxOutputs, output => Assert.Equal(IncrementalStepRunReason.Unchanged, output.Reason)); + } + */ + + #endregion +} \ No newline at end of file diff --git a/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/IncrementalSourceGeneratorTestBase.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/IncrementalSourceGeneratorTestBase.cs new file mode 100644 index 0000000..f9dc3d8 --- /dev/null +++ b/src/AutoWasmApiGenerator/AutoWasmApiGenerator.Test/IncrementalSourceGeneratorTestBase.cs @@ -0,0 +1,22 @@ +using Basic.Reference.Assemblies; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace AutoWasmApiGenerator.Test; + +public abstract class IncrementalSourceGeneratorTestBase +{ + protected static (Compilation compilation, GeneratorDriver driver) CreateDriver(string[] source, ISourceGenerator sourceGenerator, params PortableExecutableReference[] references) + { + + var compilation = CSharpCompilation.Create("TestProject", + source.Select(t => CSharpSyntaxTree.ParseText(t)), + NetStandard20.References.All.AddRange(references), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + // Create the generator driver + var driver = CSharpGeneratorDriver.Create( + [sourceGenerator], + driverOptions: new GeneratorDriverOptions(disabledOutputs: IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)); + return (compilation, driver); + } +} \ No newline at end of file diff --git a/src/AutoWasmApiGenerator/Attributes/ApiInvokeNotSupportedAttribute.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/ApiInvokeNotSupportedAttribute.cs similarity index 100% rename from src/AutoWasmApiGenerator/Attributes/ApiInvokeNotSupportedAttribute.cs rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/ApiInvokeNotSupportedAttribute.cs diff --git a/src/AutoWasmApiGenerator/Attributes/ApiInvokerAssemblyAttribute.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/ApiInvokerAssemblyAttribute.cs similarity index 100% rename from src/AutoWasmApiGenerator/Attributes/ApiInvokerAssemblyAttribute.cs rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/ApiInvokerAssemblyAttribute.cs diff --git a/src/AutoWasmApiGenerator/Attributes/ApiInvokerGenerateAttribute.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/ApiInvokerGenerateAttribute.cs similarity index 100% rename from src/AutoWasmApiGenerator/Attributes/ApiInvokerGenerateAttribute.cs rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/ApiInvokerGenerateAttribute.cs diff --git a/src/AutoWasmApiGenerator/Attributes/NullableAttributes.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/NullableAttributes.cs similarity index 100% rename from src/AutoWasmApiGenerator/Attributes/NullableAttributes.cs rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/NullableAttributes.cs diff --git a/src/AutoWasmApiGenerator/Attributes/WebControllerAssemblyAttribute.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/WebControllerAssemblyAttribute.cs similarity index 100% rename from src/AutoWasmApiGenerator/Attributes/WebControllerAssemblyAttribute.cs rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/WebControllerAssemblyAttribute.cs diff --git a/src/AutoWasmApiGenerator/Attributes/WebControllerAttribute.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/WebControllerAttribute.cs similarity index 100% rename from src/AutoWasmApiGenerator/Attributes/WebControllerAttribute.cs rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/WebControllerAttribute.cs diff --git a/src/AutoWasmApiGenerator/Attributes/WebMethodAttribute.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/WebMethodAttribute.cs similarity index 100% rename from src/AutoWasmApiGenerator/Attributes/WebMethodAttribute.cs rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/WebMethodAttribute.cs diff --git a/src/AutoWasmApiGenerator/Attributes/WebMethodParameterBindingAttribute.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/WebMethodParameterBindingAttribute.cs similarity index 100% rename from src/AutoWasmApiGenerator/Attributes/WebMethodParameterBindingAttribute.cs rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/Attributes/WebMethodParameterBindingAttribute.cs diff --git a/src/AutoWasmApiGenerator/AutoWasmApiGenerator.csproj b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/AutoWasmApiGenerator.csproj similarity index 76% rename from src/AutoWasmApiGenerator/AutoWasmApiGenerator.csproj rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/AutoWasmApiGenerator.csproj index ec5a6cc..5d1a101 100644 --- a/src/AutoWasmApiGenerator/AutoWasmApiGenerator.csproj +++ b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/AutoWasmApiGenerator.csproj @@ -1,15 +1,9 @@  - + netstandard2.0;net6.0;net8.0 - latest - enable - true - Generated true True - MarvelTiter - 0.1.1 MIT readme.md True @@ -22,6 +16,11 @@ + + + true + build + diff --git a/src/AutoWasmApiGenerator/IHttpClientHeaderHandler.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/IHttpClientHeaderHandler.cs similarity index 66% rename from src/AutoWasmApiGenerator/IHttpClientHeaderHandler.cs rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/IHttpClientHeaderHandler.cs index 2bb6003..83acf99 100644 --- a/src/AutoWasmApiGenerator/IHttpClientHeaderHandler.cs +++ b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/IHttpClientHeaderHandler.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace AutoWasmApiGenerator; @@ -13,8 +14,9 @@ public interface IHttpClientHeaderHandler /// /// /// + /// /// - Task SetRequestHeaderAsync(HttpRequestMessage request); + Task SetRequestHeaderAsync(HttpRequestMessage request, CancellationToken token = default); } /// @@ -22,21 +24,21 @@ public interface IHttpClientHeaderHandler /// public class DefaultHttpClientHeaderHandler : IHttpClientHeaderHandler { - private DefaultHttpClientHeaderHandler() - { - - } - private static readonly Lazy lazy = new(() => new DefaultHttpClientHeaderHandler()); + private DefaultHttpClientHeaderHandler() { } + + private static readonly Lazy Lazy = new(() => new DefaultHttpClientHeaderHandler()); /// /// /// - public static IHttpClientHeaderHandler Default => lazy.Value; + public static IHttpClientHeaderHandler Default => Lazy.Value; + /// /// /// /// + /// /// - public Task SetRequestHeaderAsync(HttpRequestMessage request) + public Task SetRequestHeaderAsync(HttpRequestMessage request, CancellationToken token = default) { return Task.CompletedTask; } diff --git a/src/AutoWasmApiGenerator/Models/BindingType.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/Models/BindingType.cs similarity index 100% rename from src/AutoWasmApiGenerator/Models/BindingType.cs rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/Models/BindingType.cs diff --git a/src/AutoWasmApiGenerator/Models/IsExternalInit.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/Models/IsExternalInit.cs similarity index 100% rename from src/AutoWasmApiGenerator/Models/IsExternalInit.cs rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/Models/IsExternalInit.cs diff --git a/src/AutoWasmApiGenerator/Models/WebMethod.cs b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/Models/WebMethod.cs similarity index 100% rename from src/AutoWasmApiGenerator/Models/WebMethod.cs rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/Models/WebMethod.cs diff --git a/src/AutoWasmApiGenerator/readme.md b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/readme.md similarity index 91% rename from src/AutoWasmApiGenerator/readme.md rename to src/AutoWasmApiGenerator/AutoWasmApiGenerator/readme.md index 0a319ee..6573127 100644 --- a/src/AutoWasmApiGenerator/readme.md +++ b/src/AutoWasmApiGenerator/AutoWasmApiGenerator/readme.md @@ -111,3 +111,14 @@ Task Log3Async([WebMethodParameterBinding(BindingType.FromBody)] string me + FromBody 从请求正文中获取值。 + FromHeader 从 HTTP 标头中获取值。 + FromServices 从服务容器中获取值。 + + +## TODOs + +- [ ] 支持类partial功能 +- [ ] 支持方法partial功能 +- [ ] 支持方法名称自定义功能 +- [ ] 支持同步方法 +- [ ] 支持同时设置`[FromBody]`和`[FromForm]` (需要参考一下此功能是否可以实现) +- [ ] 增加全局配置 +- [ ] 整理Analyzer中的错误提示 diff --git a/src/AutoWasmApiGenerator/Directory.Build.props b/src/AutoWasmApiGenerator/Directory.Build.props new file mode 100644 index 0000000..699419b --- /dev/null +++ b/src/AutoWasmApiGenerator/Directory.Build.props @@ -0,0 +1,10 @@ + + + + 0.1.1 + latest + enable + MarvelTiter + AutoWasmApiGenerator + + diff --git a/src/Blazor.Test/Blazor.Test.Client/Blazor.Test.Client.csproj b/src/Blazor.Test/Blazor.Test.Client/Blazor.Test.Client.csproj index b476d21..093d759 100644 --- a/src/Blazor.Test/Blazor.Test.Client/Blazor.Test.Client.csproj +++ b/src/Blazor.Test/Blazor.Test.Client/Blazor.Test.Client.csproj @@ -19,8 +19,8 @@ - - + + diff --git a/src/Blazor.Test/Blazor.Test/Blazor.Test.csproj b/src/Blazor.Test/Blazor.Test/Blazor.Test.csproj index acef5ec..88e5920 100644 --- a/src/Blazor.Test/Blazor.Test/Blazor.Test.csproj +++ b/src/Blazor.Test/Blazor.Test/Blazor.Test.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/Blazor.Test/Blazor.Test/Class1.cs b/src/Blazor.Test/Blazor.Test/Class1.cs index 74e5609..fc807f7 100644 --- a/src/Blazor.Test/Blazor.Test/Class1.cs +++ b/src/Blazor.Test/Blazor.Test/Class1.cs @@ -49,7 +49,7 @@ public Task Log2Async(string message, CancellationToken token) public Task Log3Async(string message, string path, CancellationToken token) { throw new NotImplementedException(); - return Task.FromResult(message.Length > 5); + // return Task.FromResult(message.Length > 5); } } diff --git a/src/InjectTest/InjectTest.csproj b/src/InjectTest/InjectTest.csproj index dd31cb2..836cdc4 100644 --- a/src/InjectTest/InjectTest.csproj +++ b/src/InjectTest/InjectTest.csproj @@ -7,8 +7,8 @@ - + diff --git a/src/MT.Generators.sln b/src/MT.Generators.sln index f6c2c61..91a50cb 100644 --- a/src/MT.Generators.sln +++ b/src/MT.Generators.sln @@ -5,7 +5,7 @@ VisualStudioVersion = 17.11.35111.106 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject1", "TestProject1\TestProject1.csproj", "{6994C35E-E92F-418C-B5C9-B437FF25A6A0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoWasmApiGenerator", "AutoWasmApiGenerator\AutoWasmApiGenerator.csproj", "{9C1A1324-0111-468F-9644-F80F72A66DC5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoWasmApiGenerator", "AutoWasmApiGenerator\AutoWasmApiGenerator\AutoWasmApiGenerator.csproj", "{9C1A1324-0111-468F-9644-F80F72A66DC5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoInjectGenerator", "AutoInjectGenerator\AutoInjectGenerator.csproj", "{1CA123D0-5B2A-4BBD-A90E-B58E01A7B2CA}" EndProject @@ -16,8 +16,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoInject.Roslyn", "AutoInject.Roslyn\AutoInject.Roslyn.csproj", "{E48C1C4F-8111-4513-86EC-0C968E5437CD}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AutoWasm", "AutoWasm", "{0D1AD616-22D2-4D10-BD23-92040AFD90D5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoWasmApi.Roslyn", "AutoWasmApi.Roslyn\AutoWasmApi.Roslyn.csproj", "{625157FF-90BB-4950-9643-7BAEF5CEF6A8}" + ProjectSection(SolutionItems) = preProject + AutoWasmApiGenerator\Directory.Build.props = AutoWasmApiGenerator\Directory.Build.props + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazor.Test", "Blazor.Test\Blazor.Test\Blazor.Test.csproj", "{4A47713C-2332-40C1-9AAB-7B2A14D148A2}" EndProject @@ -43,6 +44,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{71D54F EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Generators.Shared", "Shared\Generators.Shared\Generators.Shared.shproj", "{0C3325BB-C186-4E45-A567-69DAF5411E8B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoWasmApiGenerator.Test", "AutoWasmApiGenerator\AutoWasmApiGenerator.Test\AutoWasmApiGenerator.Test.csproj", "{3ED9CDA7-B256-4B4C-85DF-E1EA62715428}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoWasmApi.Roslyn", "AutoWasmApiGenerator\AutoWasmApi.Roslyn\AutoWasmApi.Roslyn.csproj", "{144DCF75-901E-40EE-8449-FC404D57017D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,10 +74,6 @@ Global {E48C1C4F-8111-4513-86EC-0C968E5437CD}.Debug|Any CPU.Build.0 = Debug|Any CPU {E48C1C4F-8111-4513-86EC-0C968E5437CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {E48C1C4F-8111-4513-86EC-0C968E5437CD}.Release|Any CPU.Build.0 = Release|Any CPU - {625157FF-90BB-4950-9643-7BAEF5CEF6A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {625157FF-90BB-4950-9643-7BAEF5CEF6A8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {625157FF-90BB-4950-9643-7BAEF5CEF6A8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {625157FF-90BB-4950-9643-7BAEF5CEF6A8}.Release|Any CPU.Build.0 = Release|Any CPU {4A47713C-2332-40C1-9AAB-7B2A14D148A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4A47713C-2332-40C1-9AAB-7B2A14D148A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {4A47713C-2332-40C1-9AAB-7B2A14D148A2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -101,6 +102,14 @@ Global {6FFF3104-CCD7-4916-A911-AEB9A5D35536}.Debug|Any CPU.Build.0 = Debug|Any CPU {6FFF3104-CCD7-4916-A911-AEB9A5D35536}.Release|Any CPU.ActiveCfg = Release|Any CPU {6FFF3104-CCD7-4916-A911-AEB9A5D35536}.Release|Any CPU.Build.0 = Release|Any CPU + {3ED9CDA7-B256-4B4C-85DF-E1EA62715428}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3ED9CDA7-B256-4B4C-85DF-E1EA62715428}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3ED9CDA7-B256-4B4C-85DF-E1EA62715428}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3ED9CDA7-B256-4B4C-85DF-E1EA62715428}.Release|Any CPU.Build.0 = Release|Any CPU + {144DCF75-901E-40EE-8449-FC404D57017D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {144DCF75-901E-40EE-8449-FC404D57017D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {144DCF75-901E-40EE-8449-FC404D57017D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {144DCF75-901E-40EE-8449-FC404D57017D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -110,7 +119,6 @@ Global {9C1A1324-0111-468F-9644-F80F72A66DC5} = {0D1AD616-22D2-4D10-BD23-92040AFD90D5} {1CA123D0-5B2A-4BBD-A90E-B58E01A7B2CA} = {FD7A81EE-6E54-461B-8766-BB36EB5F31D8} {E48C1C4F-8111-4513-86EC-0C968E5437CD} = {FD7A81EE-6E54-461B-8766-BB36EB5F31D8} - {625157FF-90BB-4950-9643-7BAEF5CEF6A8} = {0D1AD616-22D2-4D10-BD23-92040AFD90D5} {4A47713C-2332-40C1-9AAB-7B2A14D148A2} = {47FB0C42-25F4-49C1-A98C-D6536116B894} {20CA19C8-3036-4D1B-B095-C59A4C76B8D0} = {47FB0C42-25F4-49C1-A98C-D6536116B894} {8638056F-E36D-4EBC-A4EA-A7AE22DDC161} = {FCEAEFA9-132B-4945-9E97-53612B8A5CBF} @@ -119,14 +127,17 @@ Global {26DD2068-DD76-4FF9-8FB6-8E354188F7F7} = {B1852D0E-A185-491C-B81D-03D1AC5EF97C} {6FFF3104-CCD7-4916-A911-AEB9A5D35536} = {B1852D0E-A185-491C-B81D-03D1AC5EF97C} {0C3325BB-C186-4E45-A567-69DAF5411E8B} = {71D54FEF-E30B-4829-9EF2-C27DF766E235} + {3ED9CDA7-B256-4B4C-85DF-E1EA62715428} = {47FB0C42-25F4-49C1-A98C-D6536116B894} + {144DCF75-901E-40EE-8449-FC404D57017D} = {0D1AD616-22D2-4D10-BD23-92040AFD90D5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E16846FC-C2B2-43ED-A534-5726B5C9DDEE} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution Shared\Generators.Shared\Generators.Shared.projitems*{0c3325bb-c186-4e45-a567-69daf5411e8b}*SharedItemsImports = 13 + Shared\Generators.Shared\Generators.Shared.projitems*{144dcf75-901e-40ee-8449-fc404d57017d}*SharedItemsImports = 5 Shared\Generators.Shared\Generators.Shared.projitems*{26dd2068-dd76-4ff9-8fb6-8e354188f7f7}*SharedItemsImports = 5 - Shared\Generators.Shared\Generators.Shared.projitems*{625157ff-90bb-4950-9643-7baef5cef6a8}*SharedItemsImports = 5 + Shared\Generators.Shared\Generators.Shared.projitems*{3ed9cda7-b256-4b4c-85df-e1ea62715428}*SharedItemsImports = 5 Shared\Generators.Shared\Generators.Shared.projitems*{ba595e23-bd3a-4b36-a094-06e5814fe5e9}*SharedItemsImports = 5 Shared\Generators.Shared\Generators.Shared.projitems*{e48c1c4f-8111-4513-86ec-0c968e5437cd}*SharedItemsImports = 5 EndGlobalSection diff --git a/src/Shared b/src/Shared index 559b46e..e48adca 160000 --- a/src/Shared +++ b/src/Shared @@ -1 +1 @@ -Subproject commit 559b46e1d4721bb665d35acc66fbc36536f7989e +Subproject commit e48adcac152e0a66651a03f5395b4023ec333247