From 19ccdb90260cbc7b490240d4133a1aa1380e2157 Mon Sep 17 00:00:00 2001 From: MattMo Date: Fri, 3 Feb 2023 13:33:28 -0800 Subject: [PATCH 1/5] Working on a client code generation feature to help speed up using an api built with this library. --- Rest.Net.Example/Program.cs | 8 + Rest.Net/CodeGenerator.cs | 78 +++++++ Rest.Net/CodeWriter.cs | 398 ++++++++++++++++++++++++++++++++++++ Rest.Net/Route.cs | 2 +- Rest.Net/RouteGroup.cs | 37 ++++ 5 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 Rest.Net/CodeGenerator.cs create mode 100644 Rest.Net/CodeWriter.cs create mode 100644 Rest.Net/RouteGroup.cs diff --git a/Rest.Net.Example/Program.cs b/Rest.Net.Example/Program.cs index d9523fc..cc46daa 100644 --- a/Rest.Net.Example/Program.cs +++ b/Rest.Net.Example/Program.cs @@ -34,6 +34,10 @@ namespace MontoyaTech.Rest.Net.Example new Route(HttpRequestMethod.Get, "/json", Json) ); + Console.WriteLine(CodeGenerator.GenerateCSharpClient(listener.Routes)); + + Console.ReadLine(); + listener.RequestPreProcessEvent += (HttpListenerContext context) => { Console.WriteLine("Request start: " + context.Request.RawUrl); return true; @@ -56,21 +60,25 @@ namespace MontoyaTech.Rest.Net.Example listener.Block(); } + [RouteGroup("Test")] public static HttpListenerResponse Status(HttpListenerContext context) { return context.Response.WithStatus(HttpStatusCode.OK).WithText("Everything is operational. 👍"); } + [RouteGroup("Test")] public static HttpListenerResponse Add(HttpListenerContext context, double a, double b) { return context.Response.WithStatus(HttpStatusCode.OK).WithText((a + b).ToString()); } + [RouteGroup("Test")] public static HttpListenerResponse Signup(HttpListenerContext context, User user) { return context.Response.WithStatus(HttpStatusCode.OK).WithText("User:" + user.Name); } + [RouteGroup("Test")] public static HttpListenerResponse Json(HttpListenerContext context) { return context.Response.WithStatus(HttpStatusCode.OK).WithJson(new User("Rest.Net")); diff --git a/Rest.Net/CodeGenerator.cs b/Rest.Net/CodeGenerator.cs new file mode 100644 index 0000000..794dd52 --- /dev/null +++ b/Rest.Net/CodeGenerator.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net +{ + /// + /// A class that can take a set of routes and generate code + /// for a client that can be used to interact with them. + /// + public class CodeGenerator + { + /// + /// Generates a CSharp client for a given set of routes. + /// + /// + /// + public static string GenerateCSharpClient(IList routes) + { + var writer = new CodeWriter(); + + writer.WriteLine("public class Client").WriteLine("{").Indent(); + writer.WriteSpacer(); + writer.WriteLine("public string BaseUrl = null;"); + writer.WriteSpacer(); + writer.WriteLine("public Client(string baseUrl)").WriteLine("{").Indent(); + writer.WriteLine("this.BaseUrl = baseUrl;"); + writer.Outdent().WriteLine("}"); + + GenerateCSharpRouteClasses(routes, writer); + + writer.Outdent().WriteLine("}"); + + return writer.ToString(); + } + + private static void GenerateCSharpRouteClasses(IList routes, CodeWriter writer) + { + var groups = new Dictionary>(); + + //Go through all the routes and group them. + foreach (var route in routes) + { + //Get the method that this route is tied to. + var methodInfo = route.Target.GetMethodInfo(); + + //See if this method defines the group for us. + var routeGroup = methodInfo.GetCustomAttribute(); + + //Group this route. + string group = (routeGroup != null ? routeGroup.Name : methodInfo.DeclaringType.Name); + + if (groups.ContainsKey(group)) + groups[group].Add(route); + else + groups.Add(group, new List() { route }); + } + } + + private static void GenerateCSharpRouteClass(string name, List routes, CodeWriter writer) + { + } + + /// + /// Generates a Javascript client for a given set of routes. + /// + /// + /// + public static string GenerateJavascriptClient(IList routes) + { + return null; + } + } +} diff --git a/Rest.Net/CodeWriter.cs b/Rest.Net/CodeWriter.cs new file mode 100644 index 0000000..df40ac3 --- /dev/null +++ b/Rest.Net/CodeWriter.cs @@ -0,0 +1,398 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net +{ + /// + /// The outline of a writer that helps with generating code. + /// + internal class CodeWriter + { + /// + /// The internal string builder. + /// + private StringBuilder Builder = new StringBuilder(); + + /// + /// The current number of characters written.. + /// + public int Length + { + get + { + return Builder.Length; + } + } + + /// + /// The current number of indents. + /// + private int Indents = 0; + + /// + /// Whether or not the writer is pending an indent that needs + /// to be handled. + /// + private bool PendingIndent = false; + + /// + /// Creates a new default CodeWriter. + /// + public CodeWriter() { } + + /// + /// Creates a new default text writer and copies the indent data from the passed + /// text writer. + /// + /// + public CodeWriter(CodeWriter writer) + { + this.Indents = writer.Indents; + + //If we have indents, then we must set pending indent. + if (this.Indents > 0) + this.PendingIndent = true; + } + + /// + /// Writes text to the writer. + /// + /// + public CodeWriter Write(string text) + { + if (this.PendingIndent) + { + for (var i = 0; i < Indents; i++) + this.Builder.Append(' '); + + this.PendingIndent = false; + } + + this.Builder.Append(text); + + return this; + } + + /// + /// Writes a character to the writer. + /// + /// + public CodeWriter Write(char @char) + { + if (this.PendingIndent) + { + for (var i = 0; i < Indents; i++) + this.Builder.Append(' '); + + this.PendingIndent = false; + } + + this.Builder.Append(@char); + + return this; + } + + /// + /// Writes text to the writer and a newline. + /// + /// + public CodeWriter WriteLine(string text) + { + if (this.PendingIndent) + { + for (var i = 0; i < Indents; i++) + this.Builder.Append(' '); + + this.PendingIndent = false; + } + + this.Builder.Append(text); + this.Builder.Append('\n'); + this.PendingIndent = true; + + return this; + } + + /// + /// Writes a character and new line to the writer. + /// + /// + public CodeWriter WriteLine(char @char) + { + if (this.PendingIndent) + { + for (var i = 0; i < Indents; i++) + this.Builder.Append(' '); + + this.PendingIndent = false; + } + + this.Builder.Append(@char); + this.Builder.Append('\n'); + this.PendingIndent = true; + + return this; + } + + /// + /// Writes text to the writer if the condition is met. + /// + /// + /// + /// + public CodeWriter WriteAssert(bool condition, string text) + { + if (condition) + return this.Write(text); + + return this; + } + + /// + /// Writes a character to the writer if the condition is met. + /// + /// + /// + /// + public CodeWriter WriteAssert(bool condition, char @char) + { + if (condition) + return this.Write(@char); + + return this; + } + + /// + /// Writes text to the writer and a newline if the condition is met. + /// + /// + /// + /// + public CodeWriter WriteLineAssert(bool condition, string text) + { + if (condition) + return this.WriteLine(text); + + return this; + } + + /// + /// Writes a character and new line to the writer if the condition is met. + /// + /// + /// + /// + public CodeWriter WriteLineAssert(bool condition, char @char) + { + if (condition) + return this.WriteLine(@char); + + return this; + } + + /// + /// Writes the contents of another text writer into this one. + /// + /// + public CodeWriter Write(CodeWriter writer) + { + this.Builder.Append(writer.Builder.ToString()); + + return this; + } + + /// + /// Inserts a new line to the writer. + /// + public CodeWriter NewLine() + { + this.Builder.Append('\n'); + this.PendingIndent = true; + + return this; + } + + /// + /// Inserts a new line to the writer if the condition is met. + /// + public CodeWriter NewLineAssert(bool condition) + { + if (condition) + { + this.Builder.Append('\n'); + this.PendingIndent = true; + } + + return this; + } + + /// + /// Writes a space to the writer unless there is already one. + /// + /// + public CodeWriter WriteSpacer() + { + if (this.Builder.Length >= 1 && this.Builder[this.Builder.Length - 1] != ' ') + this.Builder.Append(' '); + + return this; + } + + /// + /// Writes a separator to the writer unless one wouldn't make sense. + /// + /// + public CodeWriter WriteSeparator() + { + if (this.Builder.Length > 0) + { + var offset = this.Builder.Length - 1; + var prev = this.Builder[offset]; + while (offset >= 0 && (prev == ' ' || prev == '\t' || prev == '\n' || prev == '\r')) + { + offset--; + prev = this.Builder[offset]; + } + + if (prev != '(' && prev != '{' && prev != '[' && prev != ',') + this.Builder.Append(", "); + } + + return this; + } + + /// + /// Writes a separator to the writer and text if a condition is met unless a separator here isn't needed, then just text is written. + /// + /// + /// + /// + public CodeWriter WriteSeparatorAssert(bool condition, string text) + { + if (condition) + { + if (this.Builder.Length > 0) + { + var offset = this.Builder.Length - 1; + var prev = this.Builder[offset]; + while (offset >= 0 && (prev == ' ' || prev == '\t' || prev == '\n' || prev == '\r')) + { + offset--; + prev = this.Builder[offset]; + } + + if (prev != '(' && prev != '{' && prev != '[' && prev != ',') + this.Builder.Append(", "); + } + + if (text != null) + this.Builder.Append(text); + } + + return this; + } + + /// + /// Writes a blank line as a spacer to the writer + /// unless there is already one. + /// + public CodeWriter WriteBreak() + { + if (this.Builder.Length >= 2) + { + var prevChar = this.Builder[this.Builder.Length - 2]; + + if (prevChar != '\n') + { + this.Builder.Append('\n'); + this.PendingIndent = true; + } + } + else + { + this.Builder.Append('\n'); + this.PendingIndent = true; + } + + return this; + } + + /// + /// Indents the writer one time. + /// + public CodeWriter Indent() + { + this.Indents += 4; + + return this; + } + + /// + /// Indents the writer one time if the condition is met. + /// + public CodeWriter IndentAssert(bool condition) + { + if (condition) + this.Indents += 4; + + return this; + } + + /// + /// Removes one indent from the writer. + /// + public CodeWriter Outdent() + { + if (this.Indents > 0) + this.Indents -= 4; + + return this; + } + + /// + /// Removes one indent from the writer if the condition is met. + /// + public CodeWriter OutdentAssert(bool condition) + { + if (condition && this.Indents > 0) + this.Indents -= 4; + + return this; + } + + /// + /// Resets all indents from the writer. + /// + public CodeWriter ResetIndents() + { + this.Indents = 0; + + return this; + } + + /// + /// Removes the last character from the writer. + /// + /// + public CodeWriter Remove() + { + if (this.Builder.Length > 0) + this.Builder.Remove(this.Builder.Length - 1, 1); + + return this; + } + + /// + /// Gets all the written data from the writer. + /// + /// + public override string ToString() + { + return this.Builder.ToString(); + } + } +} diff --git a/Rest.Net/Route.cs b/Rest.Net/Route.cs index 784f67a..2484fde 100644 --- a/Rest.Net/Route.cs +++ b/Rest.Net/Route.cs @@ -25,7 +25,7 @@ namespace MontoyaTech.Rest.Net /// /// The target function to invoke if this route is invoked. /// - private Func Target; + internal Func Target; /// /// Whether or not to close the response after the route is invoked. diff --git a/Rest.Net/RouteGroup.cs b/Rest.Net/RouteGroup.cs new file mode 100644 index 0000000..3bee727 --- /dev/null +++ b/Rest.Net/RouteGroup.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net +{ + /// + /// The outline of an attribute that controls what group a route belongs to. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RouteGroup : Attribute + { + /// + /// The name of the group this Route belongs to. + /// + public string Name = null; + + /// + /// Creates a new default route group. + /// + public RouteGroup() { } + + /// + /// Creates a new route group with the name of the group. + /// + /// + public RouteGroup(string name) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Must not be null or empty", nameof(name)); + + this.Name = name; + } + } +} From 6bb01464e7d115ff558f6bf3f9efa5e31f2f298d Mon Sep 17 00:00:00 2001 From: MattMo Date: Sat, 4 Feb 2023 08:26:09 -0800 Subject: [PATCH 2/5] Working on C# client code generation. --- Rest.Net.Example/Client.cs | 47 +++++++++++++++ Rest.Net.Example/Program.cs | 15 ++++- Rest.Net/CodeGenerator.cs | 61 +++++++++++++++++-- Rest.Net/Route.cs | 114 +++++++++++++++++++++++++++++++++++- Rest.Net/RouteGroup.cs | 2 +- Rest.Net/RouteRequest.cs | 34 +++++++++++ Rest.Net/RouteResponse.cs | 34 +++++++++++ 7 files changed, 298 insertions(+), 9 deletions(-) create mode 100644 Rest.Net.Example/Client.cs create mode 100644 Rest.Net/RouteRequest.cs create mode 100644 Rest.Net/RouteResponse.cs diff --git a/Rest.Net.Example/Client.cs b/Rest.Net.Example/Client.cs new file mode 100644 index 0000000..7401906 --- /dev/null +++ b/Rest.Net.Example/Client.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net.Example +{ + public class Client + { + public string BaseUrl = null; + + public TestFunctions Test; + + public class TestFunctions + { + public Client Client; + + public TestFunctions(Client client) + { + this.Client = client; + } + + public void Status() + { + } + + public void Add() + { + } + + public void Signup() + { + } + + public void Json() + { + } + } + + public Client(string baseUrl) + { + this.BaseUrl = baseUrl; + this.Test = new TestFunctions(this); + } + } +} diff --git a/Rest.Net.Example/Program.cs b/Rest.Net.Example/Program.cs index cc46daa..b46a3f4 100644 --- a/Rest.Net.Example/Program.cs +++ b/Rest.Net.Example/Program.cs @@ -34,7 +34,9 @@ namespace MontoyaTech.Rest.Net.Example new Route(HttpRequestMethod.Get, "/json", Json) ); - Console.WriteLine(CodeGenerator.GenerateCSharpClient(listener.Routes)); + string code = CodeGenerator.GenerateCSharpClient(listener.Routes); + + Console.WriteLine(code); Console.ReadLine(); @@ -61,24 +63,35 @@ namespace MontoyaTech.Rest.Net.Example } [RouteGroup("Test")] + [RouteResponse(typeof(string))] public static HttpListenerResponse Status(HttpListenerContext context) { return context.Response.WithStatus(HttpStatusCode.OK).WithText("Everything is operational. 👍"); } [RouteGroup("Test")] + [RouteResponse(typeof(string))] public static HttpListenerResponse Add(HttpListenerContext context, double a, double b) { return context.Response.WithStatus(HttpStatusCode.OK).WithText((a + b).ToString()); } [RouteGroup("Test")] + [RouteResponse(typeof(string))] public static HttpListenerResponse Signup(HttpListenerContext context, User user) { return context.Response.WithStatus(HttpStatusCode.OK).WithText("User:" + user.Name); } [RouteGroup("Test")] + [RouteRequest(typeof(User))] + public static HttpListenerResponse SignupRequest(HttpListenerContext context) + { + return context.Response.WithStatus(HttpStatusCode.OK); + } + + [RouteGroup("Test")] + [RouteResponse(typeof(User))] public static HttpListenerResponse Json(HttpListenerContext context) { return context.Response.WithStatus(HttpStatusCode.OK).WithJson(new User("Rest.Net")); diff --git a/Rest.Net/CodeGenerator.cs b/Rest.Net/CodeGenerator.cs index 794dd52..449522b 100644 --- a/Rest.Net/CodeGenerator.cs +++ b/Rest.Net/CodeGenerator.cs @@ -24,21 +24,25 @@ namespace MontoyaTech.Rest.Net var writer = new CodeWriter(); writer.WriteLine("public class Client").WriteLine("{").Indent(); - writer.WriteSpacer(); writer.WriteLine("public string BaseUrl = null;"); - writer.WriteSpacer(); + + GenerateCSharpRouteClasses(routes, writer, out List routeClasses); + + writer.WriteBreak(); writer.WriteLine("public Client(string baseUrl)").WriteLine("{").Indent(); writer.WriteLine("this.BaseUrl = baseUrl;"); - writer.Outdent().WriteLine("}"); - GenerateCSharpRouteClasses(routes, writer); + foreach (var @class in routeClasses) + writer.WriteLine($"this.{@class} = new {@class}Functions(this);"); + + writer.Outdent().WriteLine("}"); writer.Outdent().WriteLine("}"); return writer.ToString(); } - private static void GenerateCSharpRouteClasses(IList routes, CodeWriter writer) + private static void GenerateCSharpRouteClasses(IList routes, CodeWriter writer, out List routeClasses) { var groups = new Dictionary>(); @@ -46,11 +50,15 @@ namespace MontoyaTech.Rest.Net foreach (var route in routes) { //Get the method that this route is tied to. - var methodInfo = route.Target.GetMethodInfo(); + var methodInfo = route.GetTarget().GetMethodInfo(); //See if this method defines the group for us. var routeGroup = methodInfo.GetCustomAttribute(); + //If there wasn't a route group on the method, see if there is one on the declaring type. + if (routeGroup == null) + routeGroup = methodInfo.DeclaringType.GetCustomAttribute(); + //Group this route. string group = (routeGroup != null ? routeGroup.Name : methodInfo.DeclaringType.Name); @@ -59,10 +67,51 @@ namespace MontoyaTech.Rest.Net else groups.Add(group, new List() { route }); } + + //Generate classes for these groups + foreach (var group in groups) + GenerateCSharpRouteClass(group.Key, group.Value, writer); + + //Set the route classes + routeClasses = new List(); + routeClasses.AddRange(groups.Select(group => group.Key)); } private static void GenerateCSharpRouteClass(string name, List routes, CodeWriter writer) { + writer.WriteBreak(); + + writer.WriteLine($"public {name}Functions {name};"); + + writer.WriteBreak(); + + writer.WriteLine($"public class {name}Functions").WriteLine("{").Indent(); + + writer.WriteLine("public Client Client;"); + + writer.WriteBreak(); + + writer.WriteLine($"public {name}Functions(Client client)").WriteLine("{").Indent(); + + writer.WriteLine("this.Client = client;"); + + writer.Outdent().WriteLine("}"); + + foreach (var route in routes) + GenerateCSharpRouteFunction(route, writer); + + writer.Outdent().WriteLine("}"); + } + + private static void GenerateCSharpRouteFunction(Route route, CodeWriter writer) + { + writer.WriteBreak(); + + var methodInfo = route.GetTarget().GetMethodInfo(); + + writer.WriteLine($"public void {methodInfo.Name}()").WriteLine("{").Indent(); + + writer.Outdent().WriteLine("}"); } /// diff --git a/Rest.Net/Route.cs b/Rest.Net/Route.cs index 2484fde..304442a 100644 --- a/Rest.Net/Route.cs +++ b/Rest.Net/Route.cs @@ -25,7 +25,7 @@ namespace MontoyaTech.Rest.Net /// /// The target function to invoke if this route is invoked. /// - internal Func Target; + private Func Target; /// /// Whether or not to close the response after the route is invoked. @@ -47,6 +47,13 @@ namespace MontoyaTech.Rest.Net /// public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -117,6 +124,15 @@ namespace MontoyaTech.Rest.Net { this.Target.Invoke(context); } + + /// + /// Returns the target delegate for this route. + /// + /// + public virtual Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -125,6 +141,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -138,6 +161,11 @@ namespace MontoyaTech.Rest.Net { this.Target.DynamicInvoke(context, RouteArgumentConverter.Convert(arguments[0])); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -146,6 +174,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -163,6 +198,11 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[1]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -171,6 +211,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -189,6 +236,11 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[2]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -197,6 +249,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -216,6 +275,11 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[3]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -224,6 +288,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -244,6 +315,11 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[4]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -252,6 +328,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -273,6 +356,11 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[5]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -281,6 +369,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -303,6 +398,11 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[6]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -311,6 +411,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -334,5 +441,10 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[7]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } } diff --git a/Rest.Net/RouteGroup.cs b/Rest.Net/RouteGroup.cs index 3bee727..bb8ee78 100644 --- a/Rest.Net/RouteGroup.cs +++ b/Rest.Net/RouteGroup.cs @@ -9,7 +9,7 @@ namespace MontoyaTech.Rest.Net /// /// The outline of an attribute that controls what group a route belongs to. /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class RouteGroup : Attribute { /// diff --git a/Rest.Net/RouteRequest.cs b/Rest.Net/RouteRequest.cs new file mode 100644 index 0000000..4ab72e0 --- /dev/null +++ b/Rest.Net/RouteRequest.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net +{ + /// + /// The outline of an attribute that defines a routes request type. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RouteRequest : Attribute + { + /// + /// The type of the request. + /// + public Type RequestType = null; + + /// + /// Creates a default route request. + /// + public RouteRequest() { } + + /// + /// Creates a route request with the request type. + /// + /// + public RouteRequest(Type requestType) + { + this.RequestType = requestType; + } + } +} diff --git a/Rest.Net/RouteResponse.cs b/Rest.Net/RouteResponse.cs new file mode 100644 index 0000000..525fc04 --- /dev/null +++ b/Rest.Net/RouteResponse.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net +{ + /// + /// The outline of an attribute that defines a routes response type. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RouteResponse : Attribute + { + /// + /// The type of the request. + /// + public Type ResponseType = null; + + /// + /// Creates a default route response. + /// + public RouteResponse() { } + + /// + /// Creates a route response with the response type. + /// + /// + public RouteResponse(Type responseType) + { + this.ResponseType = responseType; + } + } +} From 74f8921f7ae6219b80fbdc26507afd0ce7197988 Mon Sep 17 00:00:00 2001 From: MattMo Date: Sat, 4 Feb 2023 10:37:30 -0800 Subject: [PATCH 3/5] Improved the code generator. More to come. --- Rest.Net.Example/Client.cs | 82 ++++--- Rest.Net.Example/Program.cs | 4 + Rest.Net/CodeGenerator.cs | 441 ++++++++++++++++++++++++++++++++---- Rest.Net/CodeWriter.cs | 22 +- 4 files changed, 475 insertions(+), 74 deletions(-) diff --git a/Rest.Net.Example/Client.cs b/Rest.Net.Example/Client.cs index 7401906..7a4392b 100644 --- a/Rest.Net.Example/Client.cs +++ b/Rest.Net.Example/Client.cs @@ -6,42 +6,68 @@ using System.Threading.Tasks; namespace MontoyaTech.Rest.Net.Example { + using System; + using System.Net.Http; + public class Client { - public string BaseUrl = null; + public string BaseUrl; - public TestFunctions Test; + private HttpClient HttpClient; - public class TestFunctions - { - public Client Client; - - public TestFunctions(Client client) - { - this.Client = client; - } - - public void Status() - { - } - - public void Add() - { - } - - public void Signup() - { - } - - public void Json() - { - } - } + public TestApi Test; public Client(string baseUrl) { this.BaseUrl = baseUrl; - this.Test = new TestFunctions(this); + this.Test = new TestApi(this); + this.HttpClient = new HttpClient(); + this.HttpClient.DefaultRequestHeaders.Add("Accept", "*/*"); + this.HttpClient.DefaultRequestHeaders.Add("Connection", "keep-alive"); + this.HttpClient.DefaultRequestHeaders.Add("Accept-Encoding", "identity"); + } + + public class TestApi + { + public Client Client; + + public TestApi(Client client) + { + this.Client = client; + } + + public string Status() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{this.Client.BaseUrl}/status"); + return default; + } + + public string Add(double a, double b) + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{this.Client.BaseUrl}/add/{a}/{b}"); + return default; + } + + public string Signup(User user) + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{this.Client.BaseUrl}/signup/{user}"); + return default; + } + + public User Json() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{this.Client.BaseUrl}/json"); + return default; + } + } + + public class User + { + public string Name; + + public System.Collections.Generic.List List; + + public ulong Property { get; set; } } } } diff --git a/Rest.Net.Example/Program.cs b/Rest.Net.Example/Program.cs index b46a3f4..febc655 100644 --- a/Rest.Net.Example/Program.cs +++ b/Rest.Net.Example/Program.cs @@ -14,6 +14,10 @@ namespace MontoyaTech.Rest.Net.Example { public string Name = null; + public List List = null; + + public ulong Property { get; set; } + public User(string name) { this.Name = name; diff --git a/Rest.Net/CodeGenerator.cs b/Rest.Net/CodeGenerator.cs index 449522b..b10f414 100644 --- a/Rest.Net/CodeGenerator.cs +++ b/Rest.Net/CodeGenerator.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Data; using System.Linq; +using System.Net.Http; using System.Reflection; using System.Text; using System.Threading.Tasks; +using System.Xml.Linq; namespace MontoyaTech.Rest.Net { @@ -14,45 +17,249 @@ namespace MontoyaTech.Rest.Net /// public class CodeGenerator { - /// - /// Generates a CSharp client for a given set of routes. - /// - /// - /// - public static string GenerateCSharpClient(IList routes) + private static bool IsTypeDotNet(Type type) { - var writer = new CodeWriter(); + if (type.Assembly.GetName().Name == "System.Private.CoreLib") + return true; - writer.WriteLine("public class Client").WriteLine("{").Indent(); - writer.WriteLine("public string BaseUrl = null;"); - - GenerateCSharpRouteClasses(routes, writer, out List routeClasses); - - writer.WriteBreak(); - writer.WriteLine("public Client(string baseUrl)").WriteLine("{").Indent(); - writer.WriteLine("this.BaseUrl = baseUrl;"); - - foreach (var @class in routeClasses) - writer.WriteLine($"this.{@class} = new {@class}Functions(this);"); - - writer.Outdent().WriteLine("}"); - - writer.Outdent().WriteLine("}"); - - return writer.ToString(); + return false; } - private static void GenerateCSharpRouteClasses(IList routes, CodeWriter writer, out List routeClasses) + private static List FindTypeDependencies(Type type) + { + var dependencies = new HashSet(); + + if (IsTypeDotNet(type)) + return dependencies.ToList(); + + dependencies.Add(type); + + var arguments = type.GetGenericArguments(); + + if (arguments != null) + { + foreach (var argument in arguments) + { + var types = FindTypeDependencies(argument); + + for (int i = 0; i < types.Count; i++) + if (!dependencies.Contains(types[i])) + dependencies.Add(types[i]); + } + } + + var fields = type.GetFields(); + + if (fields != null) + { + foreach (var field in fields) + { + if (field.IsPublic) + { + var types = FindTypeDependencies(field.FieldType); + + for (int i = 0; i < types.Count; i++) + if (!dependencies.Contains(types[i])) + dependencies.Add(types[i]); + } + } + } + + var properties = type.GetProperties(); + + if (properties != null) + { + foreach (var property in properties) + { + if (property.GetSetMethod() != null && property.GetGetMethod() != null) + { + var types = FindTypeDependencies(property.PropertyType); + + for (int i = 0; i < types.Count; i++) + if (!dependencies.Contains(types[i])) + dependencies.Add(types[i]); + } + } + } + + return dependencies.ToList(); + } + + private static List FindRouteDependencies(Route route) + { + var dependencies = new HashSet(); + + var methodInfo = route.GetTarget().GetMethodInfo(); + + if (methodInfo != null) + { + var parameters = methodInfo.GetParameters(); + + if (parameters != null) + { + for (int i = 1; i < parameters.Length; i++) + { + var types = FindTypeDependencies(parameters[i].ParameterType); + + foreach (var type in types) + if (!dependencies.Contains(type)) + dependencies.Add(type); + } + } + + var routeRequest = methodInfo.GetCustomAttribute(); + + if (routeRequest != null) + { + var types = FindTypeDependencies(routeRequest.RequestType); + + foreach (var type in types) + if (!dependencies.Contains(type)) + dependencies.Add(type); + } + + var routeResponse = methodInfo.GetCustomAttribute(); + + if (routeResponse != null) + { + var types = FindTypeDependencies(routeResponse.ResponseType); + + foreach (var type in types) + if (!dependencies.Contains(type)) + dependencies.Add(type); + } + } + + return dependencies.ToList(); + } + + private static List FindRoutesDependencies(List routes) + { + var dependencies = new HashSet(); + + foreach (var route in routes) + { + var types = FindRouteDependencies(route); + + if (types != null) + for (int i = 0; i < types.Count; i++) + if (!dependencies.Contains(types[i])) + dependencies.Add(types[i]); + } + + return dependencies.ToList(); + } + + private static string GetTypeFullyResolvedName(Type type) + { + if (IsTypeDotNet(type)) + { + var typeCode = Type.GetTypeCode(type); + + switch (typeCode) + { + case TypeCode.Boolean: + return "bool"; + case TypeCode.Byte: + return "byte"; + case TypeCode.Char: + return "char"; + case TypeCode.DateTime: + return "DateTime"; + case TypeCode.Decimal: + return "decimal"; + case TypeCode.Double: + return "double"; + case TypeCode.Int16: + return "short"; + case TypeCode.Int32: + return "int"; + case TypeCode.Int64: + return "long"; + case TypeCode.SByte: + return "sbyte"; + case TypeCode.Single: + return "float"; + case TypeCode.String: + return "string"; + case TypeCode.UInt16: + return "ushort"; + case TypeCode.UInt32: + return "uint"; + case TypeCode.UInt64: + return "ulong"; + default: + var builder = new StringBuilder(); + + builder.Append(type.Namespace); + + int genericSymbol = type.Name.IndexOf('`'); + + if (genericSymbol == -1) + builder.Append(".").Append(type.Name); + else + builder.Append(".").Append(type.Name.Substring(0, genericSymbol)); + + var genericArguments = type.GetGenericArguments(); + + if (genericArguments != null && genericArguments.Length > 0) + { + builder.Append("<"); + + for (int i = 0; i < genericArguments.Length; i++) + { + if (i > 0) + builder.Append(", "); + + builder.Append(GetTypeFullyResolvedName(genericArguments[i])); + } + + builder.Append(">"); + } + + return builder.ToString(); + } + } + else + { + var builder = new StringBuilder(); + + int genericSymbol = type.Name.IndexOf('`'); + + if (genericSymbol == -1) + builder.Append(type.Name); + else + builder.Append(type.Name.Substring(0, genericSymbol)); + + var genericArguments = type.GetGenericArguments(); + + if (genericArguments != null && genericArguments.Length > 0) + { + builder.Append("<"); + + for (int i = 0; i < genericArguments.Length; i++) + { + if (i > 0) + builder.Append(", "); + + builder.Append(GetTypeFullyResolvedName(genericArguments[i])); + } + + builder.Append(">"); + } + + return builder.ToString(); + } + } + + private static Dictionary> FindRouteGroups(List routes) { var groups = new Dictionary>(); - //Go through all the routes and group them. foreach (var route in routes) { - //Get the method that this route is tied to. var methodInfo = route.GetTarget().GetMethodInfo(); - //See if this method defines the group for us. var routeGroup = methodInfo.GetCustomAttribute(); //If there wasn't a route group on the method, see if there is one on the declaring type. @@ -68,30 +275,121 @@ namespace MontoyaTech.Rest.Net groups.Add(group, new List() { route }); } - //Generate classes for these groups - foreach (var group in groups) - GenerateCSharpRouteClass(group.Key, group.Value, writer); - - //Set the route classes - routeClasses = new List(); - routeClasses.AddRange(groups.Select(group => group.Key)); + return groups; } - private static void GenerateCSharpRouteClass(string name, List routes, CodeWriter writer) + public static string GenerateCSharpClient(List routes) + { + var includedTypes = FindRoutesDependencies(routes); + + var routeGroups = FindRouteGroups(routes); + + var writer = new CodeWriter(); + + writer.WriteLine("using System;"); + writer.WriteLine("using System.Net.Http;"); + + writer.WriteBreak().WriteLine("public class Client").WriteLine("{").Indent(); + + writer.WriteBreak().WriteLine("public string BaseUrl;"); + + writer.WriteBreak().WriteLine("private HttpClient HttpClient;"); + + //Create fields foreach route group so they can be accessed. + foreach (var group in routeGroups) + writer.WriteBreak().WriteLine($"public {group.Key}Api {group.Key};"); + + //Create the client constructor + writer.WriteBreak().WriteLine("public Client(string baseUrl)").WriteLine("{").Indent(); + + //Init the base url + writer.WriteLine("this.BaseUrl = baseUrl;"); + + //Init all the route group fields + foreach (var group in routeGroups) + writer.WriteLine($"this.{group.Key} = new {group.Key}Api(this);"); + + //Init the http client + writer.WriteLine("this.HttpClient = new HttpClient();"); + writer.WriteLine(@"this.HttpClient.DefaultRequestHeaders.Add(""Accept"", ""*/*"");"); + writer.WriteLine(@"this.HttpClient.DefaultRequestHeaders.Add(""Connection"", ""keep-alive"");"); + writer.WriteLine(@"this.HttpClient.DefaultRequestHeaders.Add(""Accept-Encoding"", ""identity"");"); + + writer.Outdent().WriteLine("}"); + + GenerateCSharpRouteGroups(routeGroups, writer); + + GenerateCSharpIncludedTypes(includedTypes, writer); + + writer.Outdent().WriteLine("}"); + + return writer.ToString(); + } + + private static void GenerateCSharpIncludedTypes(List types, CodeWriter writer) + { + foreach (var type in types) + GenerateCSharpIncludedType(type, writer); + } + + private static void GenerateCSharpIncludedType(Type type, CodeWriter writer) { writer.WriteBreak(); - writer.WriteLine($"public {name}Functions {name};"); + writer.WriteLine($"public class {type.Name}").WriteLine("{").Indent(); + var fields = type.GetFields(); + + if (fields != null) + foreach (var field in fields) + if (field.IsPublic) + GenerateCSharpIncludedField(field, writer); + + var properties = type.GetProperties(); + + if (properties != null) + foreach (var property in properties) + if (property.GetSetMethod() != null && property.GetGetMethod() != null) + GenerateCSharpIncludedProperty(property, writer); + + writer.Outdent().WriteLine("}"); + + System.Diagnostics.Debugger.Break(); + } + + private static void GenerateCSharpIncludedField(FieldInfo field, CodeWriter writer) + { writer.WriteBreak(); - writer.WriteLine($"public class {name}Functions").WriteLine("{").Indent(); + writer.WriteLine($"public {GetTypeFullyResolvedName(field.FieldType)} {field.Name};"); + } + + private static void GenerateCSharpIncludedProperty(PropertyInfo property, CodeWriter writer) + { + writer.WriteBreak(); + + writer.WriteLine($"public {GetTypeFullyResolvedName(property.PropertyType)} {property.Name} {{ get; set; }}"); + } + + private static void GenerateCSharpRouteGroups(Dictionary> groups, CodeWriter writer) + { + foreach (var group in groups) + GenerateCSharpRouteGroup(group.Key, group.Value, writer); + } + + private static void GenerateCSharpRouteGroup(string name, List routes, CodeWriter writer) + { + writer.WriteBreak(); + + writer.WriteLine($"public class {name}Api").WriteLine("{").Indent(); + + writer.WriteBreak(); writer.WriteLine("public Client Client;"); writer.WriteBreak(); - writer.WriteLine($"public {name}Functions(Client client)").WriteLine("{").Indent(); + writer.WriteLine($"public {name}Api(Client client)").WriteLine("{").Indent(); writer.WriteLine("this.Client = client;"); @@ -109,16 +407,69 @@ namespace MontoyaTech.Rest.Net var methodInfo = route.GetTarget().GetMethodInfo(); - writer.WriteLine($"public void {methodInfo.Name}()").WriteLine("{").Indent(); + var routeRequest = methodInfo.GetCustomAttribute(); + + var routeResponse = methodInfo.GetCustomAttribute(); + + writer.Write($"public {(routeResponse == null ? "void" : GetTypeFullyResolvedName(routeResponse.ResponseType))} {methodInfo.Name}("); + + var parameters = methodInfo.GetParameters(); + + if (parameters != null) + { + for (int i = 1; i < parameters.Length; i++) + { + writer.WriteSeparator(); + writer.Write(GetTypeFullyResolvedName(parameters[i].ParameterType)).Write(" ").Write(parameters[i].Name); + } + } + + if (routeRequest != null) + { + writer.WriteSeparator(); + writer.Write(GetTypeFullyResolvedName(routeRequest.RequestType)).Write(" request"); + } + + writer.WriteLine(")").WriteLine("{").Indent(); + + //Generate code to send a request + /* + * var response = HttpClient.Send(new HttpRequestMessage(HttpMethod.Post, $"{Auth0Url}/oauth/token") + { + Content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("code", code), + new KeyValuePair("redirect_uri", redirectUrl), + new KeyValuePair("client_id", ClientId) + }) + }) + */ + + writer.Write($"var request = new HttpRequestMessage("); + + switch (route.Method.ToLower()) + { + case "post": + writer.Write("HttpMethod.Post"); + break; + case "get": + writer.Write("HttpMethod.Get"); + break; + default: + throw new NotSupportedException("Unsupport route method:" + route.Method); + } + + writer.WriteSeparator().Write('$').WriteString($"{{this.Client.BaseUrl}}/{route.Syntax}"); + + writer.WriteLine(");"); + + if (routeResponse != null) + writer.WriteLine("return default;"); writer.Outdent().WriteLine("}"); } - /// - /// Generates a Javascript client for a given set of routes. - /// - /// - /// public static string GenerateJavascriptClient(IList routes) { return null; diff --git a/Rest.Net/CodeWriter.cs b/Rest.Net/CodeWriter.cs index df40ac3..b28521a 100644 --- a/Rest.Net/CodeWriter.cs +++ b/Rest.Net/CodeWriter.cs @@ -77,6 +77,26 @@ namespace MontoyaTech.Rest.Net return this; } + /// + /// Writes text to the writer surrounded by quotes. + /// + /// + /// + public CodeWriter WriteString(string text) + { + if (this.PendingIndent) + { + for (var i = 0; i < Indents; i++) + this.Builder.Append(' '); + + this.PendingIndent = false; + } + + this.Builder.Append('"').Append(text).Append('"'); + + return this; + } + /// /// Writes a character to the writer. /// @@ -306,7 +326,7 @@ namespace MontoyaTech.Rest.Net { var prevChar = this.Builder[this.Builder.Length - 2]; - if (prevChar != '\n') + if (prevChar != '\n' && prevChar != '{' && prevChar != '(' && prevChar != '[') { this.Builder.Append('\n'); this.PendingIndent = true; From 1475159f1cc0f375f21f0ee41ca90601cddc36be Mon Sep 17 00:00:00 2001 From: MattMo Date: Sun, 5 Feb 2023 10:59:29 -0800 Subject: [PATCH 4/5] Added RouteName and Request/Response handling to the code generator. More work is needed. --- Rest.Net.Example/Client.cs | 71 +++++++++++++++++++++++++++----- Rest.Net.Example/Program.cs | 51 ++++++++++++++--------- Rest.Net/CodeGenerator.cs | 80 +++++++++++++++++++++++++++---------- Rest.Net/RouteListener.cs | 11 +++++ Rest.Net/RouteName.cs | 38 ++++++++++++++++++ 5 files changed, 199 insertions(+), 52 deletions(-) create mode 100644 Rest.Net/RouteName.cs diff --git a/Rest.Net.Example/Client.cs b/Rest.Net.Example/Client.cs index 7a4392b..8947e6b 100644 --- a/Rest.Net.Example/Client.cs +++ b/Rest.Net.Example/Client.cs @@ -13,14 +13,17 @@ namespace MontoyaTech.Rest.Net.Example { public string BaseUrl; - private HttpClient HttpClient; + public HttpClient HttpClient; public TestApi Test; + public AuthApi Auth; + public Client(string baseUrl) { this.BaseUrl = baseUrl; this.Test = new TestApi(this); + this.Auth = new AuthApi(this); this.HttpClient = new HttpClient(); this.HttpClient.DefaultRequestHeaders.Add("Accept", "*/*"); this.HttpClient.DefaultRequestHeaders.Add("Connection", "keep-alive"); @@ -38,26 +41,72 @@ namespace MontoyaTech.Rest.Net.Example public string Status() { - var request = new HttpRequestMessage(HttpMethod.Get, $"{this.Client.BaseUrl}/status"); - return default; + var message = new HttpRequestMessage(HttpMethod.Get, $"{this.Client.BaseUrl}/status"); + + var response = this.Client.HttpClient.Send(message); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + return Newtonsoft.Json.JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + else + throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); } public string Add(double a, double b) { - var request = new HttpRequestMessage(HttpMethod.Post, $"{this.Client.BaseUrl}/add/{a}/{b}"); - return default; + var message = new HttpRequestMessage(HttpMethod.Post, $"{this.Client.BaseUrl}/add/{a}/{b}"); + + var response = this.Client.HttpClient.Send(message); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + return Newtonsoft.Json.JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + else + throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); + } + } + + public class AuthApi + { + public Client Client; + + public AuthApi(Client client) + { + this.Client = client; } - public string Signup(User user) + public bool UserExists(string name) { - var request = new HttpRequestMessage(HttpMethod.Post, $"{this.Client.BaseUrl}/signup/{user}"); - return default; + var message = new HttpRequestMessage(HttpMethod.Get, $"{this.Client.BaseUrl}/auth/{name}"); + + var response = this.Client.HttpClient.Send(message); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + return Newtonsoft.Json.JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + else + throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); } - public User Json() + public void Signup(User request) { - var request = new HttpRequestMessage(HttpMethod.Get, $"{this.Client.BaseUrl}/json"); - return default; + var message = new HttpRequestMessage(HttpMethod.Post, $"{this.Client.BaseUrl}/auth/signup"); + + message.Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(request)); + + var response = this.Client.HttpClient.Send(message); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); + } + + public User Get() + { + var message = new HttpRequestMessage(HttpMethod.Get, $"{this.Client.BaseUrl}/auth"); + + var response = this.Client.HttpClient.Send(message); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + return Newtonsoft.Json.JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + else + throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); } } diff --git a/Rest.Net.Example/Program.cs b/Rest.Net.Example/Program.cs index febc655..b434522 100644 --- a/Rest.Net.Example/Program.cs +++ b/Rest.Net.Example/Program.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using System.Net; using MontoyaTech.Rest.Net; +using System.Net.Mime; namespace MontoyaTech.Rest.Net.Example { @@ -18,15 +19,12 @@ namespace MontoyaTech.Rest.Net.Example public ulong Property { get; set; } + public User() { } + public User(string name) { this.Name = name; } - - public static explicit operator User(string input) - { - return new User(input.ToString()); - } } public static void Main(string[] args) @@ -34,24 +32,23 @@ namespace MontoyaTech.Rest.Net.Example var listener = new RouteListener(8080, new Route(HttpRequestMethod.Get, "/status", Status), new Route(HttpRequestMethod.Post, "/add/{a}/{b}", Add), - new Route(HttpRequestMethod.Post, "/signup/{username}", Signup), - new Route(HttpRequestMethod.Get, "/json", Json) + new Route(HttpRequestMethod.Get, "/auth/{username}", Exists), + new Route(HttpRequestMethod.Post, "/auth/signup", Signup), + new Route(HttpRequestMethod.Get, "/auth/", Json) ); string code = CodeGenerator.GenerateCSharpClient(listener.Routes); Console.WriteLine(code); - Console.ReadLine(); - listener.RequestPreProcessEvent += (HttpListenerContext context) => { - Console.WriteLine("Request start: " + context.Request.RawUrl); + Console.WriteLine($"[{context.Request.HttpMethod}] Request start: " + context.Request.RawUrl); return true; }; listener.RequestPostProcessEvent += (HttpListenerContext context) => { - Console.WriteLine("Request end: " + context.Request.RawUrl); + Console.WriteLine($"[{context.Request.HttpMethod}] Request end: " + context.Request.RawUrl); }; listener.Start(); @@ -61,7 +58,11 @@ namespace MontoyaTech.Rest.Net.Example foreach (var route in listener.Routes) Console.WriteLine($"- [{route.Method}] {route.Syntax}"); - Console.WriteLine($"Rest api server running at http://localhost:{listener.Port}"); + Console.WriteLine($"Rest api server running at {listener.BaseUrl}"); + + var client = new Client(listener.BaseUrl); + + var result = client.Auth.Get(); listener.Block(); } @@ -80,12 +81,6 @@ namespace MontoyaTech.Rest.Net.Example return context.Response.WithStatus(HttpStatusCode.OK).WithText((a + b).ToString()); } - [RouteGroup("Test")] - [RouteResponse(typeof(string))] - public static HttpListenerResponse Signup(HttpListenerContext context, User user) - { - return context.Response.WithStatus(HttpStatusCode.OK).WithText("User:" + user.Name); - } [RouteGroup("Test")] [RouteRequest(typeof(User))] @@ -94,7 +89,25 @@ namespace MontoyaTech.Rest.Net.Example return context.Response.WithStatus(HttpStatusCode.OK); } - [RouteGroup("Test")] + [RouteGroup("Auth")] + [RouteName("UserExists")] + [RouteResponse(typeof(bool))] + public static HttpListenerResponse Exists(HttpListenerContext context, string name) + { + Console.WriteLine("Auth.Exists called, name:" + name); + + return context.Response.WithStatus(HttpStatusCode.OK).WithJson(true); + } + + [RouteGroup("Auth")] + [RouteRequest(typeof(User))] + public static HttpListenerResponse Signup(HttpListenerContext context) + { + return context.Response.WithStatus(HttpStatusCode.OK); + } + + [RouteGroup("Auth")] + [RouteName("Get")] [RouteResponse(typeof(User))] public static HttpListenerResponse Json(HttpListenerContext context) { diff --git a/Rest.Net/CodeGenerator.cs b/Rest.Net/CodeGenerator.cs index b10f414..c1f6b79 100644 --- a/Rest.Net/CodeGenerator.cs +++ b/Rest.Net/CodeGenerator.cs @@ -293,7 +293,7 @@ namespace MontoyaTech.Rest.Net writer.WriteBreak().WriteLine("public string BaseUrl;"); - writer.WriteBreak().WriteLine("private HttpClient HttpClient;"); + writer.WriteBreak().WriteLine("public HttpClient HttpClient;"); //Create fields foreach route group so they can be accessed. foreach (var group in routeGroups) @@ -353,8 +353,6 @@ namespace MontoyaTech.Rest.Net GenerateCSharpIncludedProperty(property, writer); writer.Outdent().WriteLine("}"); - - System.Diagnostics.Debugger.Break(); } private static void GenerateCSharpIncludedField(FieldInfo field, CodeWriter writer) @@ -407,11 +405,14 @@ namespace MontoyaTech.Rest.Net var methodInfo = route.GetTarget().GetMethodInfo(); + var routeName = methodInfo.GetCustomAttribute(); + var routeRequest = methodInfo.GetCustomAttribute(); var routeResponse = methodInfo.GetCustomAttribute(); - writer.Write($"public {(routeResponse == null ? "void" : GetTypeFullyResolvedName(routeResponse.ResponseType))} {methodInfo.Name}("); + //Construct the routes request function + writer.Write($"public {(routeResponse == null ? "void" : GetTypeFullyResolvedName(routeResponse.ResponseType))} {(routeName == null ? methodInfo.Name : routeName.Name)}("); var parameters = methodInfo.GetParameters(); @@ -432,21 +433,8 @@ namespace MontoyaTech.Rest.Net writer.WriteLine(")").WriteLine("{").Indent(); - //Generate code to send a request - /* - * var response = HttpClient.Send(new HttpRequestMessage(HttpMethod.Post, $"{Auth0Url}/oauth/token") - { - Content = new FormUrlEncodedContent(new[] - { - new KeyValuePair("grant_type", "authorization_code"), - new KeyValuePair("code", code), - new KeyValuePair("redirect_uri", redirectUrl), - new KeyValuePair("client_id", ClientId) - }) - }) - */ - - writer.Write($"var request = new HttpRequestMessage("); + //Generate the message code + writer.WriteBreak().Write($"var message = new HttpRequestMessage("); switch (route.Method.ToLower()) { @@ -460,12 +448,60 @@ namespace MontoyaTech.Rest.Net throw new NotSupportedException("Unsupport route method:" + route.Method); } - writer.WriteSeparator().Write('$').WriteString($"{{this.Client.BaseUrl}}/{route.Syntax}"); + writer.WriteSeparator().Write('$').Write('"').Write("{this.Client.BaseUrl}"); - writer.WriteLine(");"); + //Reconstruct the route syntax into a request url. + var components = route.Syntax.Split('/'); + int argumentIndex = 0; + foreach (var component in components) + { + if (!string.IsNullOrWhiteSpace(component)) + { + writer.Write('/'); + if (component.StartsWith("{")) + { + writer.Write("{").Write(parameters[argumentIndex++ + 1].Name).Write("}"); + } + else if (component == "*") + { + writer.Write("*"); + } + else if (component == "**") + { + break; + } + else + { + writer.Write(component); + } + } + } + + writer.Write('"').WriteLine(");"); + + //Add the request content if any. + if (routeRequest != null) + { + writer.WriteBreak().WriteLine("message.Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(request));"); + } + + //Generate the response code + writer.WriteBreak().WriteLine("var response = this.Client.HttpClient.Send(message);"); + + //Handle the response if (routeResponse != null) - writer.WriteLine("return default;"); + { + writer.WriteBreak().WriteLine("if (response.StatusCode == System.Net.HttpStatusCode.OK)").Indent(); + writer.WriteLine($"return Newtonsoft.Json.JsonConvert.DeserializeObject<{GetTypeFullyResolvedName(routeResponse.ResponseType)}>(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());"); + writer.Outdent().WriteLine("else").Indent(); + writer.WriteLine(@"throw new Exception(""Unexpected Http Response StatusCode:"" + response.StatusCode);").Outdent(); + } + else + { + writer.WriteBreak().WriteLine("if (response.StatusCode == System.Net.HttpStatusCode.OK)").Indent(); + writer.WriteLine(@"throw new Exception(""Unexpected Http Response StatusCode:"" + response.StatusCode);").Outdent(); + } writer.Outdent().WriteLine("}"); } diff --git a/Rest.Net/RouteListener.cs b/Rest.Net/RouteListener.cs index 60ab12f..f5ce862 100644 --- a/Rest.Net/RouteListener.cs +++ b/Rest.Net/RouteListener.cs @@ -29,6 +29,17 @@ namespace MontoyaTech.Rest.Net /// public ushort Port = 8081; + /// + /// Returns the BaseUrl for this RouteListener. + /// + public string BaseUrl + { + get + { + return $"http://localhost:{this.Port}"; + } + } + /// /// An event to preprocess routes before the route is executed. /// diff --git a/Rest.Net/RouteName.cs b/Rest.Net/RouteName.cs new file mode 100644 index 0000000..467c268 --- /dev/null +++ b/Rest.Net/RouteName.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net +{ + /// + /// The outline of an attribute that allows you to rename an attribute in + /// the output code generation. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RouteName : Attribute + { + /// + /// The name of this Route. + /// + public string Name = null; + + /// + /// Creates a new default RouteName. + /// + public RouteName() { } + + /// + /// Creates a new RouteName with a given name. + /// + /// + public RouteName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Cannot be null or whitespace", nameof(name)); + + this.Name = name; + } + } +} From 42c4682c892b43fba804efdd8f5fb1f64500e952 Mon Sep 17 00:00:00 2001 From: MattMo Date: Sun, 5 Feb 2023 12:28:37 -0800 Subject: [PATCH 5/5] Bumped package version to 1.1.9. Added documentation and cleaned up code. Going to merge into Master. --- Rest.Net.Example/Client.cs | 11 +- Rest.Net.Example/Program.cs | 2 +- ...odeGenerator.cs => ClientCodeGenerator.cs} | 111 ++++++++++++++++-- Rest.Net/Rest.Net.csproj | 2 +- Rest.Net/RouteListener.cs | 10 ++ 5 files changed, 119 insertions(+), 17 deletions(-) rename Rest.Net/{CodeGenerator.cs => ClientCodeGenerator.cs} (80%) diff --git a/Rest.Net.Example/Client.cs b/Rest.Net.Example/Client.cs index 8947e6b..4307c2e 100644 --- a/Rest.Net.Example/Client.cs +++ b/Rest.Net.Example/Client.cs @@ -8,6 +8,7 @@ namespace MontoyaTech.Rest.Net.Example { using System; using System.Net.Http; + using Newtonsoft.Json; public class Client { @@ -46,7 +47,7 @@ namespace MontoyaTech.Rest.Net.Example var response = this.Client.HttpClient.Send(message); if (response.StatusCode == System.Net.HttpStatusCode.OK) - return Newtonsoft.Json.JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); else throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); } @@ -58,7 +59,7 @@ namespace MontoyaTech.Rest.Net.Example var response = this.Client.HttpClient.Send(message); if (response.StatusCode == System.Net.HttpStatusCode.OK) - return Newtonsoft.Json.JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); else throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); } @@ -80,7 +81,7 @@ namespace MontoyaTech.Rest.Net.Example var response = this.Client.HttpClient.Send(message); if (response.StatusCode == System.Net.HttpStatusCode.OK) - return Newtonsoft.Json.JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); else throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); } @@ -89,7 +90,7 @@ namespace MontoyaTech.Rest.Net.Example { var message = new HttpRequestMessage(HttpMethod.Post, $"{this.Client.BaseUrl}/auth/signup"); - message.Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(request)); + message.Content = new StringContent(JsonConvert.SerializeObject(request)); var response = this.Client.HttpClient.Send(message); @@ -104,7 +105,7 @@ namespace MontoyaTech.Rest.Net.Example var response = this.Client.HttpClient.Send(message); if (response.StatusCode == System.Net.HttpStatusCode.OK) - return Newtonsoft.Json.JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); else throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); } diff --git a/Rest.Net.Example/Program.cs b/Rest.Net.Example/Program.cs index b434522..a4e6d87 100644 --- a/Rest.Net.Example/Program.cs +++ b/Rest.Net.Example/Program.cs @@ -37,7 +37,7 @@ namespace MontoyaTech.Rest.Net.Example new Route(HttpRequestMethod.Get, "/auth/", Json) ); - string code = CodeGenerator.GenerateCSharpClient(listener.Routes); + string code = ClientCodeGenerator.GenerateCSharpClient(listener.Routes); Console.WriteLine(code); diff --git a/Rest.Net/CodeGenerator.cs b/Rest.Net/ClientCodeGenerator.cs similarity index 80% rename from Rest.Net/CodeGenerator.cs rename to Rest.Net/ClientCodeGenerator.cs index c1f6b79..9b7f815 100644 --- a/Rest.Net/CodeGenerator.cs +++ b/Rest.Net/ClientCodeGenerator.cs @@ -15,8 +15,13 @@ namespace MontoyaTech.Rest.Net /// A class that can take a set of routes and generate code /// for a client that can be used to interact with them. /// - public class CodeGenerator + public class ClientCodeGenerator { + /// + /// Returns whether or not a given type belongs to DotNet. + /// + /// + /// private static bool IsTypeDotNet(Type type) { if (type.Assembly.GetName().Name == "System.Private.CoreLib") @@ -25,6 +30,11 @@ namespace MontoyaTech.Rest.Net return false; } + /// + /// Finds all the dependencies for a given type and returns them. + /// + /// + /// private static List FindTypeDependencies(Type type) { var dependencies = new HashSet(); @@ -85,6 +95,11 @@ namespace MontoyaTech.Rest.Net return dependencies.ToList(); } + /// + /// Finds all the types that a given route depends on to function. + /// + /// + /// private static List FindRouteDependencies(Route route) { var dependencies = new HashSet(); @@ -133,6 +148,11 @@ namespace MontoyaTech.Rest.Net return dependencies.ToList(); } + /// + /// Finds all the types that a given set of routes depend on to function. + /// + /// + /// private static List FindRoutesDependencies(List routes) { var dependencies = new HashSet(); @@ -150,6 +170,11 @@ namespace MontoyaTech.Rest.Net return dependencies.ToList(); } + /// + /// Returns the fully resolve name for a given type. + /// + /// + /// private static string GetTypeFullyResolvedName(Type type) { if (IsTypeDotNet(type)) @@ -252,6 +277,11 @@ namespace MontoyaTech.Rest.Net } } + /// + /// Finds all the route groupings from a set of routes and returns those groups. + /// + /// + /// private static Dictionary> FindRouteGroups(List routes) { var groups = new Dictionary>(); @@ -278,7 +308,24 @@ namespace MontoyaTech.Rest.Net return groups; } - public static string GenerateCSharpClient(List routes) + /// + /// Generates a CSharpClient from a RouteListener. + /// + /// + /// The name of the Client class, default is Client. + /// The generated C# code. + public static string GenerateCSharpClient(RouteListener listener, string name = "Client") + { + return GenerateCSharpClient(listener.Routes, name); + } + + /// + /// Generates a CSharpClient from a given set of Routes. + /// + /// + /// The name of the Client class, default is Client. + /// + public static string GenerateCSharpClient(List routes, string name = "Client") { var includedTypes = FindRoutesDependencies(routes); @@ -288,8 +335,9 @@ namespace MontoyaTech.Rest.Net writer.WriteLine("using System;"); writer.WriteLine("using System.Net.Http;"); + writer.WriteLine("using Newtonsoft.Json;"); - writer.WriteBreak().WriteLine("public class Client").WriteLine("{").Indent(); + writer.WriteBreak().WriteLine($"public class {name}").WriteLine("{").Indent(); writer.WriteBreak().WriteLine("public string BaseUrl;"); @@ -326,12 +374,22 @@ namespace MontoyaTech.Rest.Net return writer.ToString(); } + /// + /// Generates C# for a set of included types. + /// + /// + /// private static void GenerateCSharpIncludedTypes(List types, CodeWriter writer) { foreach (var type in types) GenerateCSharpIncludedType(type, writer); } + /// + /// Generates C# for a given included type. + /// + /// + /// private static void GenerateCSharpIncludedType(Type type, CodeWriter writer) { writer.WriteBreak(); @@ -355,6 +413,11 @@ namespace MontoyaTech.Rest.Net writer.Outdent().WriteLine("}"); } + /// + /// Generates C# for a field inside an included type. + /// + /// + /// private static void GenerateCSharpIncludedField(FieldInfo field, CodeWriter writer) { writer.WriteBreak(); @@ -362,6 +425,11 @@ namespace MontoyaTech.Rest.Net writer.WriteLine($"public {GetTypeFullyResolvedName(field.FieldType)} {field.Name};"); } + /// + /// Generates C# for a property inside an included type. + /// + /// + /// private static void GenerateCSharpIncludedProperty(PropertyInfo property, CodeWriter writer) { writer.WriteBreak(); @@ -369,12 +437,23 @@ namespace MontoyaTech.Rest.Net writer.WriteLine($"public {GetTypeFullyResolvedName(property.PropertyType)} {property.Name} {{ get; set; }}"); } + /// + /// Generates C# for a set of route groups. + /// + /// + /// private static void GenerateCSharpRouteGroups(Dictionary> groups, CodeWriter writer) { foreach (var group in groups) GenerateCSharpRouteGroup(group.Key, group.Value, writer); } + /// + /// Generates C# for a given route group. + /// + /// + /// + /// private static void GenerateCSharpRouteGroup(string name, List routes, CodeWriter writer) { writer.WriteBreak(); @@ -399,6 +478,12 @@ namespace MontoyaTech.Rest.Net writer.Outdent().WriteLine("}"); } + /// + /// Generates a C# function for a given route. + /// + /// + /// + /// private static void GenerateCSharpRouteFunction(Route route, CodeWriter writer) { writer.WriteBreak(); @@ -411,9 +496,10 @@ namespace MontoyaTech.Rest.Net var routeResponse = methodInfo.GetCustomAttribute(); - //Construct the routes request function + //Generate the route function header writer.Write($"public {(routeResponse == null ? "void" : GetTypeFullyResolvedName(routeResponse.ResponseType))} {(routeName == null ? methodInfo.Name : routeName.Name)}("); + //Generate the functions parameters var parameters = methodInfo.GetParameters(); if (parameters != null) @@ -482,9 +568,7 @@ namespace MontoyaTech.Rest.Net //Add the request content if any. if (routeRequest != null) - { - writer.WriteBreak().WriteLine("message.Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(request));"); - } + writer.WriteBreak().WriteLine("message.Content = new StringContent(JsonConvert.SerializeObject(request));"); //Generate the response code writer.WriteBreak().WriteLine("var response = this.Client.HttpClient.Send(message);"); @@ -493,7 +577,7 @@ namespace MontoyaTech.Rest.Net if (routeResponse != null) { writer.WriteBreak().WriteLine("if (response.StatusCode == System.Net.HttpStatusCode.OK)").Indent(); - writer.WriteLine($"return Newtonsoft.Json.JsonConvert.DeserializeObject<{GetTypeFullyResolvedName(routeResponse.ResponseType)}>(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());"); + writer.WriteLine($"return JsonConvert.DeserializeObject<{GetTypeFullyResolvedName(routeResponse.ResponseType)}>(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());"); writer.Outdent().WriteLine("else").Indent(); writer.WriteLine(@"throw new Exception(""Unexpected Http Response StatusCode:"" + response.StatusCode);").Outdent(); } @@ -503,12 +587,19 @@ namespace MontoyaTech.Rest.Net writer.WriteLine(@"throw new Exception(""Unexpected Http Response StatusCode:"" + response.StatusCode);").Outdent(); } + //Close off the route function. writer.Outdent().WriteLine("}"); } - public static string GenerateJavascriptClient(IList routes) + /// + /// Generates a Javascript client from a given set of routes. + /// + /// + /// + /// + public static string GenerateJavascriptClient(List routes) { - return null; + throw new NotImplementedException(); } } } diff --git a/Rest.Net/Rest.Net.csproj b/Rest.Net/Rest.Net.csproj index 34bfb69..66a48a1 100644 --- a/Rest.Net/Rest.Net.csproj +++ b/Rest.Net/Rest.Net.csproj @@ -17,7 +17,7 @@ MontoyaTech.Rest.Net MontoyaTech.Rest.Net True - 1.1.8 + 1.1.9 Logo_Symbol_Black_Outline.png diff --git a/Rest.Net/RouteListener.cs b/Rest.Net/RouteListener.cs index f5ce862..67dd520 100644 --- a/Rest.Net/RouteListener.cs +++ b/Rest.Net/RouteListener.cs @@ -219,5 +219,15 @@ namespace MontoyaTech.Rest.Net if (!Thread.Yield()) Thread.Sleep(1000); } + + /// + /// Generates a C# client from this Route Listener and returns the code. + /// + /// The name of the Client. Default is Client. + /// + public string GenerateCSharpClient(string name = "Client") + { + return ClientCodeGenerator.GenerateCSharpClient(this, name); + } } }