Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into ubuntu-latest
Browse files Browse the repository at this point in the history
  • Loading branch information
romfir committed Sep 9, 2023
2 parents 2852b26 + ed1f954 commit ef959bc
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 31 deletions.
11 changes: 2 additions & 9 deletions OpenApiLINQPadDriver/Extensions/ReflectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ public static void SetProperty<T, TP>(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)
{
Expand All @@ -32,10 +28,7 @@ public static void SetField<T, TP>(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);
}
Expand Down
6 changes: 0 additions & 6 deletions OpenApiLINQPadDriver/OpenApiContextDriverProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,6 @@ private T GetValue<T>(Func<string?, T> 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<int, int> 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);

Expand Down
8 changes: 4 additions & 4 deletions OpenApiLINQPadDriver/OpenApiLINQPadDriver.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@

<PropertyGroup>
<AssemblyName>OpenApiLINQPadDriver</AssemblyName>
<Version>0.0.2-alpha</Version>
<Version>0.0.3-alpha</Version>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<Authors>Damian Romanowski (romfir22@gmail.com)</Authors>
<Copyright>Copyright © Damian Romanowski 2023-$([System.DateTime]::Now.Year)</Copyright>
<ApplicationIcon>OpenAPI.ico</ApplicationIcon>
<AssemblyTitle>LINQPad 7/6 Open API Driver</AssemblyTitle>
<AssemblyTitle>LINQPad 7 Open API Driver</AssemblyTitle>
<Description>$(AssemblyTitle).</Description>
</PropertyGroup>

Expand All @@ -46,14 +46,14 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EnablePackageValidation>true</EnablePackageValidation>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Debug_Publish_To_LINQPad_Folder'">
<OutputPath>$(localappdata)\LINQPad\Drivers\DataContext\NetCore\OpenApiLINQPadDriver</OutputPath>
<OutputPath>$(localappdata)\LINQPad\Drivers\DataContext\NetCore\$(AssemblyName)</OutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<UseCommonOutputDirectory>true</UseCommonOutputDirectory>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
Expand Down
33 changes: 22 additions & 11 deletions OpenApiLINQPadDriver/SchemaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ internal static List<ExplorerItem> 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!);

Expand All @@ -39,7 +43,7 @@ internal static List<ExplorerItem> GetSchemaAndBuildAssembly(OpenApiContextDrive

var codeGeneratedByNSwag = generator.GenerateFile();

MeasureTimeAndRestartStopWatch("Generating NSwag classes");
MeasureTimeAndAddTimeExecutionExplorerItem("Generating NSwag classes");

var clientSourceCode = endpointGrouping switch
{
Expand All @@ -48,19 +52,19 @@ internal static List<ExplorerItem> 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<ExplorerItem>();

Expand All @@ -81,13 +85,13 @@ internal static List<ExplorerItem> 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;

Expand Down Expand Up @@ -125,15 +129,22 @@ static ExplorerItem GenerateErrorExplorerItem(IReadOnlyCollection<string> errors
}

ExplorerItem CreateExplorerItemForTimeMeasurement()
=> new("Times", ExplorerItemKind.Schema, ExplorerIcon.Box)
=> new("Execution Times", ExplorerItemKind.Schema, ExplorerIcon.Box)
{
Children = new List<ExplorerItem>()
};

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<ExplorerItem> CreateExplorerItemsWithGeneratedCode(string codeGeneratedByNSwag, string clientSourceCode)
Expand Down
165 changes: 164 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,164 @@
# OpenApiLINQPadDriver
# 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", "<token>");
};
}
```
```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", "<token>");
}
```
### 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

0 comments on commit ef959bc

Please sign in to comment.