-
-
Notifications
You must be signed in to change notification settings - Fork 232
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(templates): add httpclient source-generator to Boilerplate proje…
- Loading branch information
Showing
52 changed files
with
723 additions
and
138 deletions.
There are no files selected for viewing
4 changes: 2 additions & 2 deletions
4
.github/workflows/adminpanel.cd.yml → .github/workflows/admin-sample.cd.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 2 additions & 2 deletions
4
.github/workflows/todosample.cd.yml → .github/workflows/todo-sample.cd.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/ActionParameter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
using Microsoft.CodeAnalysis; | ||
|
||
namespace Bit.SourceGenerators; | ||
|
||
public class ActionParameter | ||
{ | ||
public string Name { get; set; } = default!; | ||
|
||
public ITypeSymbol Type { get; set; } = default!; | ||
} |
23 changes: 23 additions & 0 deletions
23
src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/ControllerAction.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
using System.Collections.Generic; | ||
using Microsoft.CodeAnalysis; | ||
|
||
namespace Bit.SourceGenerators; | ||
|
||
public class ControllerAction | ||
{ | ||
public IMethodSymbol Method { get; set; } = default!; | ||
|
||
public ITypeSymbol ReturnType { get; set; } = default!; | ||
|
||
public bool DoesReturnSomething => ReturnType.ToDisplayString() is not "System.Threading.Tasks.Task" or "System.Threading.Tasks.ValueTask"; | ||
|
||
public string HttpMethod { get; set; } = default!; | ||
|
||
public string Url { get; set; } = default!; | ||
|
||
public List<ActionParameter> Parameters { get; set; } = []; | ||
|
||
public ActionParameter? BodyParameter { get; set; } | ||
|
||
public bool HasCancellationToken { get; set; } | ||
} |
120 changes: 120 additions & 0 deletions
120
src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/HttpClientProxySourceGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
using System; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Text; | ||
using Microsoft.CodeAnalysis; | ||
|
||
namespace Bit.SourceGenerators; | ||
|
||
[Generator] | ||
public class HttpClientProxySourceGenerator : ISourceGenerator | ||
{ | ||
public void Initialize(GeneratorInitializationContext context) | ||
{ | ||
context.RegisterForSyntaxNotifications(() => new HttpClientProxySyntaxReceiver()); | ||
} | ||
|
||
public void Execute(GeneratorExecutionContext context) | ||
{ | ||
if (context.SyntaxContextReceiver is not HttpClientProxySyntaxReceiver receiver || receiver.IControllers.Any() is false) | ||
{ | ||
return; | ||
} | ||
|
||
StringBuilder generatedClasses = new(); | ||
|
||
foreach (var iController in receiver.IControllers) | ||
{ | ||
StringBuilder generatedMethods = new(); | ||
|
||
foreach (var action in iController.Actions) | ||
{ | ||
string parameters = string.Join(", ", action.Parameters.Select(p => $"{p.Type.ToDisplayString()} {p.Name}")); | ||
|
||
var hasQueryString = action.Url.Contains('?'); | ||
|
||
generatedMethods.AppendLine($@" | ||
public async {action.ReturnType.ToDisplayString()} {action.Method.Name}({parameters}) | ||
{{ | ||
{$@"var url = $""{action.Url}"";"} | ||
var dynamicQS = GetDynamicQueryString(); | ||
if (dynamicQS is not null) | ||
{{ | ||
url += {(action.Url.Contains('?') ? "'&'" : "'?'")} + dynamicQS; | ||
}} | ||
{(action.DoesReturnSomething ? $@"return (await prerenderStateService.GetValue(url, async () => | ||
{{" : string.Empty)} | ||
using var request = new HttpRequestMessage(HttpMethod.{action.HttpMethod}, url); | ||
{(action.BodyParameter is not null ? $@"request.Content = JsonContent.Create({action.BodyParameter.Name}, options.GetTypeInfo<{action.BodyParameter.Type.ToDisplayString()}>());" : string.Empty)} | ||
using var response = await httpClient.SendAsync(request{(action.HasCancellationToken ? ", cancellationToken" : string.Empty)}); | ||
{(action.DoesReturnSomething ? ($"return await response.Content.ReadFromJsonAsync(options.GetTypeInfo<{action.ReturnType.GetUnderlyingType().ToDisplayString()}>(){(action.HasCancellationToken ? ", cancellationToken" : string.Empty)});}}))!;") : string.Empty)} | ||
}} | ||
"); | ||
} | ||
|
||
generatedClasses.AppendLine($@" | ||
internal class {iController.ClassName}(HttpClient httpClient, JsonSerializerOptions options, IPrerenderStateService prerenderStateService) : AppControllerBase, {iController.Symbol.ToDisplayString()} | ||
{{ | ||
{generatedMethods} | ||
}}"); | ||
} | ||
|
||
StringBuilder finalSource = new(@$" | ||
using System.Text.Json; | ||
using System.Web; | ||
namespace Microsoft.Extensions.DependencyInjection; | ||
[global::System.CodeDom.Compiler.GeneratedCode(""Bit.SourceGenerators"",""{BitSourceGeneratorUtil.GetPackageVersion()}"")] | ||
[global::System.Diagnostics.DebuggerNonUserCode] | ||
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] | ||
internal static class IHttpClientServiceCollectionExtensions | ||
{{ | ||
public static void AddTypedHttpClients(this IServiceCollection services) | ||
{{ | ||
{string.Join(Environment.NewLine, receiver.IControllers.Select(i => $" services.TryAddTransient<{i.Symbol.ToDisplayString()}, {i.ClassName}>();"))} | ||
}} | ||
internal class AppControllerBase | ||
{{ | ||
Dictionary<string, object?> queryString = []; | ||
public void AddQueryString(string key, object? value) | ||
{{ | ||
queryString.Add(key, value); | ||
}} | ||
public void AddQueryStrings(Dictionary<string, object?> queryString) | ||
{{ | ||
foreach (var key in queryString.Keys) | ||
{{ | ||
AddQueryString(key, queryString[key]); | ||
}} | ||
}} | ||
protected string? GetDynamicQueryString() | ||
{{ | ||
if (queryString is not {{ Count: > 0 }}) | ||
return null; | ||
var collection = HttpUtility.ParseQueryString(string.Empty); | ||
foreach (var key in queryString.Keys) | ||
{{ | ||
collection.Add(key, queryString[key]?.ToString()); | ||
}} | ||
queryString.Clear(); | ||
return collection.ToString(); | ||
}} | ||
}} | ||
{generatedClasses} | ||
}} | ||
"); | ||
|
||
context.AddSource($"HttpClientProxy.cs", finalSource.ToString()); | ||
} | ||
} |
83 changes: 83 additions & 0 deletions
83
src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/HttpClientProxySyntaxReceiver.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using DoLess.UriTemplates; | ||
using System.Web; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
||
namespace Bit.SourceGenerators; | ||
|
||
public class HttpClientProxySyntaxReceiver : ISyntaxContextReceiver | ||
{ | ||
public List<IController> IControllers { get; } = []; | ||
|
||
public void OnVisitSyntaxNode(GeneratorSyntaxContext syntaxNode) | ||
{ | ||
if (syntaxNode.Node is InterfaceDeclarationSyntax interfaceDeclarationSyntax | ||
&& interfaceDeclarationSyntax.BaseList is not null | ||
&& interfaceDeclarationSyntax.BaseList.Types.Any(t => t.Type.ToString() == "IAppController")) | ||
{ | ||
var model = syntaxNode.SemanticModel.Compilation.GetSemanticModel(interfaceDeclarationSyntax.SyntaxTree); | ||
var controllerSymbol = (ITypeSymbol)model.GetDeclaredSymbol(interfaceDeclarationSyntax)!; | ||
bool isController = controllerSymbol.IsIController(); | ||
|
||
if (isController == true) | ||
{ | ||
var controllerName = controllerSymbol.Name[1..].Replace("Controller", string.Empty); | ||
|
||
var route = controllerSymbol | ||
.GetAttributes() | ||
.FirstOrDefault(a => a.AttributeClass?.Name.StartsWith("Route") is true)? | ||
.ConstructorArguments | ||
.FirstOrDefault() | ||
.Value? | ||
.ToString() | ||
?.Replace("[controller]", controllerName) ?? string.Empty; | ||
|
||
var actions = controllerSymbol.GetMembers() | ||
.OfType<IMethodSymbol>() | ||
.Where(m => m.MethodKind == MethodKind.Ordinary) | ||
.Select(m => new ControllerAction | ||
{ | ||
Method = m, | ||
ReturnType = m.ReturnType, | ||
HttpMethod = m.GetHttpMethod(), | ||
Url = m.Name, | ||
Parameters = m.Parameters.Select(y => new ActionParameter | ||
{ | ||
Name = y.Name, | ||
Type = y.Type | ||
}).ToList() | ||
}).ToList(); | ||
|
||
foreach (var action in actions) | ||
{ | ||
var uriTemplate = UriTemplate.For($"{route}{action.Method.GetAttributes() | ||
.FirstOrDefault(a => a.AttributeClass?.Name.StartsWith("Http") is true)? | ||
.ConstructorArguments.FirstOrDefault().Value?.ToString()}".Replace("[action]", action.Method.Name)); | ||
|
||
foreach (var parameter in action.Parameters) | ||
{ | ||
uriTemplate.WithParameter(parameter.Name, $"{{{parameter.Name}}}"); | ||
} | ||
|
||
string url = HttpUtility.UrlDecode(uriTemplate.ExpandToString()); | ||
|
||
// if there is a parameter that is not a cancellation token and is not in the route template, then it is the body parameter | ||
action.BodyParameter = action.Parameters.FirstOrDefault(p => p.Type.ToDisplayString() is not "System.Threading.CancellationToken" && url.Contains($"{{{p.Name}}}") is false); | ||
action.HasCancellationToken = action.Parameters.Any(p => p.Type.ToDisplayString() is "System.Threading.CancellationToken"); | ||
action.Url = url; | ||
} | ||
|
||
IControllers.Add(new IController | ||
{ | ||
Actions = actions, | ||
Name = controllerName, | ||
ClassName = controllerSymbol.Name[1..], | ||
Symbol = controllerSymbol, | ||
Syntax = interfaceDeclarationSyntax | ||
}); | ||
} | ||
} | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/IController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using System.Collections.Generic; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis; | ||
|
||
namespace Bit.SourceGenerators; | ||
|
||
public class IController | ||
{ | ||
public string Name { get; set; } = default!; | ||
|
||
public string ClassName { get; set; } = default!; | ||
|
||
public ITypeSymbol Symbol { get; set; } = default!; | ||
|
||
public InterfaceDeclarationSyntax Syntax { get; set; } = default!; | ||
|
||
public List<ControllerAction> Actions { get; set; } = []; | ||
} | ||
|
33 changes: 33 additions & 0 deletions
33
src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/ITypeSymboExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
using System.Linq; | ||
|
||
namespace Microsoft.CodeAnalysis; | ||
|
||
public static class ITypeSymbolExtensions | ||
{ | ||
public static bool IsIController(this ITypeSymbol type) | ||
{ | ||
return type.AllInterfaces.Any(x => x.Name == "IAppController"); | ||
} | ||
|
||
public static string GetHttpMethod(this IMethodSymbol method) | ||
{ | ||
return method.GetAttributes().FirstOrDefault(a => a.AttributeClass?.Name.StartsWith("Http") is true)?.AttributeClass?.Name switch | ||
{ | ||
"HttpGetAttribute" => "Get", | ||
"HttpPostAttribute" => "Post", | ||
"HttpPutAttribute" => "Put", | ||
"HttpDeleteAttribute" => "Delete", | ||
"HttpPatchAttribute" => "Patch", | ||
_ => "Get" | ||
}; | ||
} | ||
|
||
public static ITypeSymbol GetUnderlyingType(this ITypeSymbol typeSymbol) | ||
{ | ||
return typeSymbol switch | ||
{ | ||
INamedTypeSymbol namedTypeSymbol => namedTypeSymbol.TypeArguments.FirstOrDefault() ?? namedTypeSymbol, | ||
_ => typeSymbol | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.