From ed1f954beb4264d4e53244aa38b60eef6f9bf901 Mon Sep 17 00:00:00 2001 From: Damian Romanowski <33402460+romfir@users.noreply.github.com> Date: Sun, 10 Sep 2023 01:54:49 +0200 Subject: [PATCH] Created Readme * Update README.md * csproj cleanup * Cleanup of unused methods * Removed Stopwatch and used timestamps for NET 7 and above --- .../Extensions/ReflectionExtensions.cs | 11 +- .../OpenApiContextDriverProperties.cs | 6 - .../OpenApiLINQPadDriver.csproj | 8 +- OpenApiLINQPadDriver/SchemaBuilder.cs | 33 ++-- README.md | 165 +++++++++++++++++- 5 files changed, 192 insertions(+), 31 deletions(-) diff --git a/OpenApiLINQPadDriver/Extensions/ReflectionExtensions.cs b/OpenApiLINQPadDriver/Extensions/ReflectionExtensions.cs index 3d57197..6cd6c8e 100644 --- a/OpenApiLINQPadDriver/Extensions/ReflectionExtensions.cs +++ b/OpenApiLINQPadDriver/Extensions/ReflectionExtensions.cs @@ -12,11 +12,7 @@ public static void SetProperty(this T obj, string propertyName, TP value, var type = obj.GetType(); var propertyType = type.GetProperty(propertyName, bindingFlags)?.DeclaringType; - var property = propertyType?.GetProperty(propertyName, bindingFlags); - - if (property == null) - throw new InvalidOperationException( - $"Cannot find property '{propertyName}' in object of type {typeof(T)}"); + var property = propertyType?.GetProperty(propertyName, bindingFlags) ?? throw new InvalidOperationException($"Cannot find property '{propertyName}' in object of type {typeof(T)}"); if (!property.CanWrite) { @@ -32,10 +28,7 @@ public static void SetField(this T obj, string fieldName, TP value, Bindi { var type = obj.GetType(); - var field = type.GetField(fieldName, bindingFlags); - - if (field == null) - throw new InvalidOperationException($"Cannot find field '{fieldName}' in object of type {typeof(T)}"); + var field = type.GetField(fieldName, bindingFlags) ?? throw new InvalidOperationException($"Cannot find field '{fieldName}' in object of type {typeof(T)}"); field.SetValue(obj, value); } diff --git a/OpenApiLINQPadDriver/OpenApiContextDriverProperties.cs b/OpenApiLINQPadDriver/OpenApiContextDriverProperties.cs index d154333..d5c7cee 100644 --- a/OpenApiLINQPadDriver/OpenApiContextDriverProperties.cs +++ b/OpenApiLINQPadDriver/OpenApiContextDriverProperties.cs @@ -96,12 +96,6 @@ private T GetValue(Func convert, T defaultValue, [CallerMemberNam private bool GetValue(bool defaultValue, [CallerMemberName] string callerMemberName = "") => GetValue(static v => v.ToBoolSafe(), defaultValue, callerMemberName)!.Value; - //private int GetValue(int defaultValue, Func adjustValueFunc, [CallerMemberName] string callerMemberName = "") => - // adjustValueFunc(GetValue(static v => v.ToIntSafe(), defaultValue, callerMemberName)!.Value); - - //private string? GetValue(string defaultValue, [CallerMemberName] string callerMemberName = "") => - // GetValue(static v => v, defaultValue, callerMemberName); - private string? GetValue(string defaultValue, [CallerMemberName] string callerMemberName = "") => GetValue(static v => v, defaultValue, callerMemberName); diff --git a/OpenApiLINQPadDriver/OpenApiLINQPadDriver.csproj b/OpenApiLINQPadDriver/OpenApiLINQPadDriver.csproj index 81b3d15..3792bd2 100644 --- a/OpenApiLINQPadDriver/OpenApiLINQPadDriver.csproj +++ b/OpenApiLINQPadDriver/OpenApiLINQPadDriver.csproj @@ -18,12 +18,12 @@ OpenApiLINQPadDriver - 0.0.2-alpha + 0.0.3-alpha true Damian Romanowski (romfir22@gmail.com) Copyright © Damian Romanowski 2023-$([System.DateTime]::Now.Year) OpenAPI.ico - LINQPad 7/6 Open API Driver + LINQPad 7 Open API Driver $(AssemblyTitle). @@ -45,14 +45,14 @@ snupkg true true + true - $(localappdata)\LINQPad\Drivers\DataContext\NetCore\OpenApiLINQPadDriver + $(localappdata)\LINQPad\Drivers\DataContext\NetCore\$(AssemblyName) false false true - true diff --git a/OpenApiLINQPadDriver/SchemaBuilder.cs b/OpenApiLINQPadDriver/SchemaBuilder.cs index c6978cd..5e90d2d 100644 --- a/OpenApiLINQPadDriver/SchemaBuilder.cs +++ b/OpenApiLINQPadDriver/SchemaBuilder.cs @@ -21,13 +21,17 @@ internal static List GetSchemaAndBuildAssembly(OpenApiContextDrive ref string typeName) { typeName = TypedDataContextName; - var timeExplorerItem = CreateExplorerItemForTimeMeasurement(); + +#if NET7_0_OR_GREATER + var initialTimeStamp = Stopwatch.GetTimestamp(); +#else var stopWatch = Stopwatch.StartNew(); +#endif var document = AsyncHelper.RunSync(() => OpenApiDocumentHelper.GetFromUriAsync(new Uri(openApiContextDriverProperties.OpenApiDocumentUri!))); - MeasureTimeAndRestartStopWatch("Downloading document"); + MeasureTimeAndAddTimeExecutionExplorerItem("Downloading document"); document.SetServer(openApiContextDriverProperties.ApiUri!); @@ -39,7 +43,7 @@ internal static List GetSchemaAndBuildAssembly(OpenApiContextDrive var codeGeneratedByNSwag = generator.GenerateFile(); - MeasureTimeAndRestartStopWatch("Generating NSwag classes"); + MeasureTimeAndAddTimeExecutionExplorerItem("Generating NSwag classes"); var clientSourceCode = endpointGrouping switch { @@ -48,19 +52,19 @@ internal static List GetSchemaAndBuildAssembly(OpenApiContextDrive _ => throw new InvalidOperationException() }; - MeasureTimeAndRestartStopWatch("Generating clients partials"); + MeasureTimeAndAddTimeExecutionExplorerItem("Generating clients partials"); var compileResult = DataContextDriver.CompileSource(new CompilationInput { FilePathsToReference = openApiContextDriverProperties.GetCoreFxReferenceAssemblies() - .Append(typeof(JsonConvert).Assembly.Location) + .Append(typeof(JsonConvert).Assembly.Location) //required for code generation, otherwise NSwag will use lowest possible version 10.0.1 .ToArray(), #pragma warning disable SYSLIB0044 //this is the only way to read this assembly, LINQPad does not give any other reference to it OutputPath = assemblyToBuild.CodeBase, SourceCode = new[] { codeGeneratedByNSwag, clientSourceCode } }); - MeasureTimeAndRestartStopWatch("Compiling code"); + MeasureTimeAndAddTimeExecutionExplorerItem("Compiling code"); var explorerItems = new List(); @@ -81,13 +85,13 @@ internal static List GetSchemaAndBuildAssembly(OpenApiContextDrive var assemblyWithGeneratedCode = Assembly.LoadFile(assemblyToBuild.CodeBase!); #pragma warning restore SYSLIB0044 - MeasureTimeAndRestartStopWatch("Loading assembly from file"); + MeasureTimeAndAddTimeExecutionExplorerItem("Loading assembly from file"); var contextType = assemblyWithGeneratedCode.GetType(nameSpace, TypedDataContextName); explorerItems.AddRange(ReflectionSchemaBuilder.GenerateExplorerItems(contextType, endpointGrouping)); - MeasureTimeAndRestartStopWatch("Reading assembly using reflection and generating schema"); + MeasureTimeAndAddTimeExecutionExplorerItem("Reading assembly using reflection and generating schema"); return explorerItems; @@ -125,15 +129,22 @@ static ExplorerItem GenerateErrorExplorerItem(IReadOnlyCollection errors } ExplorerItem CreateExplorerItemForTimeMeasurement() - => new("Times", ExplorerItemKind.Schema, ExplorerIcon.Box) + => new("Execution Times", ExplorerItemKind.Schema, ExplorerIcon.Box) { Children = new List() }; - void MeasureTimeAndRestartStopWatch(string name) + void MeasureTimeAndAddTimeExecutionExplorerItem(string name) { - timeExplorerItem.Children.Add(new ExplorerItem(name + " " + stopWatch.Elapsed, ExplorerItemKind.Property, ExplorerIcon.Blank)); +#if NET7_0_OR_GREATER + var temp = initialTimeStamp; + initialTimeStamp = Stopwatch.GetTimestamp(); + var elapsed = Stopwatch.GetElapsedTime(temp, initialTimeStamp); +#else + var elapsed = stopWatch.Elapsed; stopWatch.Restart(); +#endif + timeExplorerItem.Children.Add(new ExplorerItem(name + " " + elapsed, ExplorerItemKind.Property, ExplorerIcon.Blank)); } static List CreateExplorerItemsWithGeneratedCode(string codeGeneratedByNSwag, string clientSourceCode) diff --git a/README.md b/README.md index 5bf25e3..daf35ba 100644 --- a/README.md +++ b/README.md @@ -1 +1,164 @@ -# OpenApiLINQPadDriver \ No newline at end of file +# OpenApiLINQPadDriver for LINQPad 7 +[![Latest build](https://github.com/romfir/OpenApiLINQPadDriver/workflows/Build/badge.svg)](https://github.com/romfir/OpenApiLINQPadDriver/actions) +[![NuGet](https://img.shields.io/nuget/v/OpenApiLINQPadDriver)](https://www.nuget.org/packages/OpenApiLINQPadDriver) +[![Downloads](https://img.shields.io/nuget/dt/OpenApiLINQPadDriver)](https://www.nuget.org/packages/OpenApiLINQPadDriver) +[![License](https://img.shields.io/badge/license-MIT-yellow)](https://opensource.org/licenses/MIT) + +## Description ## + +OpenApiLINQPadDriver is LINQPad 7 dynamic data context driver for creating C# clients based on [Open API](https://www.openapis.org)/[Swagger](https://swagger.io/specification/) specifications + +* Specification is read using [NJsonSchema](https://github.com/RicoSuter/NJsonSchema) and clients are generated using [NSwag](https://github.com/RicoSuter/NSwag) + +## Websites ## + +* [This project](https://github.com/romfir/OpenApiLINQPadDriver) +* [Original project for LINQPad 5](https://github.com/seba76/SwaggerContextDriver) +* UI is heavily inspired by [CsvLINQPadDriver](https://github.com/i2van/CsvLINQPadDriver) + +## Downloads ## +generation of lpx6 files is on the roadmap, for now we only support instalation via nuget + +## Prerequisites ## + +* [LINQPad 7](https://www.linqpad.net/LINQPad7.aspx): [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0)/[.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0) + +## Installation ## + +### LINQPad 7 ### + +#### NuGet #### + +[![NuGet](https://img.shields.io/nuget/v/OpenApiLINQPadDriver)](https://www.nuget.org/packages/OpenApiLINQPadDriver) + +* Open LINQPad 7. +* Click `Add connection` link. +* Click button `View more drivers...` +* Click radio button `Show all drivers` and type `OpenApiLINQPadDriver` (for now it is also required to check `Include Prerelease` checkbox) +* Install. +* In case of working in environments without internet access it is possible to manually download nuget package and configure `Package Source` to point to a folder where it is located + +## Usage ## + +Open API Connection can be added the same way as any other connection. + +* Click `Add Connection` +* Select `OpenApi Driver` +* Enter `Open Api Json Uri` or click `Get from disk` and pick it from file +* Manually enter `API Uri` or click `Get from swagger.json`, if servers are found in the specification uri of the first one will be picked +* Set settings +* Click `OK` +* Client should start generation, you can use it by right clicking on it and choosing `Use in Current Query` or by picking it from `Connection` select +* It is possible to drag method name from the tree view on the left to the query +* Example code using [PetStore API](https://petstore.swagger.io/v2/swagger.json) +```csharp +async Task Main() +{ + var newPetId = System.Random.Shared.NextInt64(); + await PetClient.AddPetAsync(new Pet() + { + Id = newPetId, + Name = "Dino", + Category = new Category + { + Id = 123, + Name = "Dog" + } + }); + + await PetClient.GetPetByIdAsync(newPetId).Dump(); +} +``` +### Refreshing client ### +* Right click on the connection and click `Refresh` +* Or use shortcut `Shift+Alt+D` + +## Configuration Options ## + +### Client Generation ### + +* Endpoint grouping - how methods will be grouped in generated client + * `Multiple clients from first tag and operationName` - usually first tag corresponds to ASP.NET controller, so this will group methods by controller + * `Single client from OperationId and OperationName` - this will put all endpoints in one class +* Json library - library used in generated client for serialization/deserialization, for specification reading [NJsonSchema](https://github.com/RicoSuter/NJsonSchema) uses `Newstonsoft.Json` + * `System.Text.Json` + * `Newstonsoft.Json` +* Class style + * `POCOs (Plain Old C# Objects)` + * `Classes implementing the INotifyPropertyChanged interface` + * `Classes implementing the Prism base class` - WIP, the library that is required is not added to the compilation + * `Records - read only POCOs (Plain Old C# Objects)` +* Generate sync methods - by default sync methods will not be generated + +### Misc ### + +* Debug info: show additional driver debug info, e.g. generated data context sources and add `Execution Times` explorer item with exectuion times of parts of the generation pipeline +* Remember this connection: connection will be available on next run. +* Contains production data: files contain production data. + +### PrepareRequestFunction ### +* Each generated client has `PrepareRequestFunction` Func, for `Multiple clients from first tag and operationName` mode, helper set only Func is also generted to set them all at once +* This Func will be run on each method exectuion before making a http request, it is run in `PrepareRequest` partial methods generated by [NSwag](https://github.com/RicoSuter/NSwag) +* It can be used to set additional headers or other http client settings +* Example usage +```csharp +async Task Main() +{ + PrepareRequestFunction = (httpClient, requestMessage, url) => + { + requestMessage.Headers.Add("UserId", "9"); + requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", ""); + }; +} +``` +```csharp +async Task Main() +{ + PrepareRequestFunction = PrepareRequest; +} + +private void PrepareRequest(HttpClient httpClient, HttpRequestMessage requestMessage, string url) +{ + requestMessage.Headers.Add("UserId", "9"); + requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", ""); +} +``` +### Known Issues ### +* Code does not compile when there are parameters with the same name, but with case differences in the single endpoint and they come from different locations eg `[FromQuery] int test, [FromHeader] int Test` [related issue](https://github.com/RicoSuter/NSwag/issues/2560) + +## Credits ## + +### Tools ### + +* [LINQPad 7](https://www.linqpad.net/LINQPad7.aspx) +* [LINQPad Command-Line and Scripting (LPRun)](https://www.linqpad.net/lprun.aspx) + +### Libraries ### + +* [NJsonSchema](https://github.com/RicoSuter/NJsonSchema) +* [NSwag](https://github.com/RicoSuter/NSwag) +* [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) - required by NSwag, included to bump version + +### Development ### +* [OpenApiLINQPadDriver.csproj](https://github.com/romfir/OpenApiLINQPadDriver/blob/master/OpenApiLINQPadDriver/OpenApiLINQPadDriver.csproj) contains special `Debug_Publish_To_LINQPad_Folder` debug build configuration, if it is chosen, code will be build only targeting `net7.0-windows` with additional properties: +https://github.com/romfir/OpenApiLINQPadDriver/blob/master/OpenApiLINQPadDriver/OpenApiLINQPadDriver.csproj#L50-L56 +* LINQPad can pick drivers from `\LINQPad\Drivers\DataContext\NetCore` folder +* Additionaly when exceptions will be thrown it will be possible to attach a debugger: +https://github.com/romfir/OpenApiLINQPadDriver/blob/master/OpenApiLINQPadDriver/OpenApiContextDriver.cs#L11-L20 + +### Roadmap ### +* Allow injection of own httpClient +* Unit tests +* `PrepareRequest` with string builder overload +* `ProcessResponse` +* `PrepareRequest` and `ProcessResponse` async overload that could be set via a setting +* Methods parameters and responses in tree view +* Auto dump response +* Auth helper methods eg. `SetBearerToken` +* Check if it is possible to add summary to generated methods +* Add `Prism.Mvvm` to compilation when prism class style is picked +* When multiple servers are found allow selection +* LINQPad 5 support +* Examples (include in the nuget) +* Expose JsonSerializerSettings setter on multi client setup +* Expose ReadResponseAsString on multi client setup