Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/msbuild.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: MSBuild Tasks

on:
workflow_dispatch:
push:
paths-ignore:
- '**/*'
- '!.github/workflows/msbuild.yml'
- '!src/**/*.csproj'
- '!src/**/*.cs'
branches:
- 'main'
pull_request:
paths-ignore:
- '**/*'
- '!.github/workflows/msbuild.yml'
- '!src/**/*.csproj'
- '!src/**/*.cs'
branches:
- '*'

jobs:
build:
name: 👌 Verify build
runs-on: windows-latest
steps:
- name: 🛒 Checkout repository
uses: actions/checkout@v4
- name: 🛠️ Prepare
working-directory: test/MSBuild
run: |
dotnet restore ../../src/Refitter.sln
dotnet build -c release ../../src/Refitter/Refitter.csproj
dotnet build -c release ../../src/Refitter.MSBuild/Refitter.MSBuild.csproj
dotnet pack -c release ../../src/Refitter.MSBuild/Refitter.MSBuild.csproj -o .
nuget add .\Refitter.MSBuild.1.0.0.nupkg -source .
dotnet add package Refitter.MSBuild -s .
- name: 🛠️ Build
working-directory: test/MSBuild
run: dotnet build -v d -filelogger
- name: 🧪 Build Output
working-directory: test/MSBuild
run: cat msbuild.log
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,4 @@ test/GeneratedCode/
test/Apizr/GeneratedCode
test/MultipleFiles/**/*.cs
test/HelloWorld/HelloWorld/Output.cs
test/MSBuild/refitter.msbuild/
178 changes: 178 additions & 0 deletions src/Refitter.MSBuild/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
## MSBuild Tasks for Refitter

Refitter is available as custom MSBuild tasks that uses the [Refitter.Core](https://github.com/christianhelle/refitter/tree/main/src/Refitter.Core) library for generating a REST API Client using the [Refit](https://github.com/reactiveui/refit) library. Refitter can generate the Refit interface from OpenAPI specifications. Refitter could format the generated Refit interface to be managed by [Apizr](https://www.apizr.net) (v6+) and generate some registration helpers too.


### .Refitter File format

The following is an example `.refitter` file

```js
{
"openApiPath": "/path/to/your/openAPI", // Required
"namespace": "Org.System.Service.Api.GeneratedCode", // Optional. Default=GeneratedCode
"naming": {
"useOpenApiTitle": false, // Optional. Default=true
"interfaceName": "MyApiClient" // Optional. Default=ApiClient
},
"generateContracts": true, // Optional. Default=true
"generateXmlDocCodeComments": true, // Optional. Default=true
"generateStatusCodeComments": true, // Optional. Default=true
"addAutoGeneratedHeader": true, // Optional. Default=true
"addAcceptHeaders": true, // Optional. Default=true
"returnIApiResponse": false, // Optional. Default=false
"responseTypeOverride": { // Optional. Default={}
"File_Upload": "IApiResponse",
"File_Download": "System.Net.Http.HttpContent"
},
"generateOperationHeaders": true, // Optional. Default=true
"typeAccessibility": "Public", // Optional. Values=Public|Internal. Default=Public
"useCancellationTokens": false, // Optional. Default=false
"useIsoDateFormat": false, // Optional. Default=false
"multipleInterfaces": "ByEndpoint", // Optional. May be one of "ByEndpoint" or "ByTag"
"generateDeprecatedOperations": false, // Optional. Default=true
"operationNameTemplate": "{operationName}Async", // Optional. Must contain {operationName} when multipleInterfaces != ByEndpoint
"optionalParameters": false, // Optional. Default=false
"outputFolder": "../CustomOutput" // Optional. Default=./Generated
"outputFilename": "RefitInterface.cs", // Optional. Default=Output.cs for CLI tool
"additionalNamespaces": [ // Optional
"Namespace1",
"Namespace2"
],
"includeTags": [ // Optional. OpenAPI Tag to include when generating code
"Pet",
"Store",
"User"
],
"includePathMatches": [ // Optional. Only include Paths that match the provided regular expression
"^/pet/.*",
"^/store/.*"
],
"useDynamicQuerystringParameters": true, // Optional. Default=false
"usePolymorphicSerialization": false, // Optional. Default=false
"dependencyInjectionSettings": { // Optional
"baseUrl": "https://petstore3.swagger.io/api/v3", // Optional. Leave this blank to set the base address manually
"httpMessageHandlers": [ // Optional
"AuthorizationMessageHandler",
"TelemetryMessageHandler"
],
"transientErrorHandler": "Polly", // Optional. Value=None|Polly|HttpResilience
"maxRetryCount": 3, // Optional. Default=6
"firstBackoffRetryInSeconds": 0.5 // Optional. Default=1.0
},
"apizrSettings": { // Optional
"withRequestOptions": true, // Optional. Default=true
"withRegistrationHelper": true, // Optional. Default=false
"withCacheProvider": "InMemory", // Optional. Values=None|Akavache|MonkeyCache|InMemory|DistributedAsString|DistributedAsByteArray. Default=None
"withPriority": true, // Optional. Default=false
"withMediation": true, // Optional. Default=false
"withOptionalMediation": true, // Optional. Default=false
"withMappingProvider": "AutoMapper", // Optional. Values=None|AutoMapper|Mapster. Default=None
"withFileTransfer": true // Optional. Default=false
},
"codeGeneratorSettings": { // Optional. Default settings are the values set in this example
"requiredPropertiesMustBeDefined": true,
"generateDataAnnotations": true,
"anyType": "object",
"dateType": "System.DateTimeOffset",
"dateTimeType": "System.DateTimeOffset",
"timeType": "System.TimeSpan",
"timeSpanType": "System.TimeSpan",
"arrayType": "System.Collections.Generic.ICollection",
"dictionaryType": "System.Collections.Generic.IDictionary",
"arrayInstanceType": "System.Collections.ObjectModel.Collection",
"dictionaryInstanceType": "System.Collections.Generic.Dictionary",
"arrayBaseType": "System.Collections.ObjectModel.Collection",
"dictionaryBaseType": "System.Collections.Generic.Dictionary",
"propertySetterAccessModifier": "",
"generateImmutableArrayProperties": false,
"generateImmutableDictionaryProperties": false,
"handleReferences": false,
"jsonSerializerSettingsTransformationMethod": null,
"generateJsonMethods": false,
"enforceFlagEnums": false,
"inlineNamedDictionaries": false,
"inlineNamedTuples": true,
"inlineNamedArrays": false,
"generateOptionalPropertiesAsNullable": false,
"generateNullableReferenceTypes": false,
"generateNativeRecords": false,
"generateDefaultValues": true,
"inlineNamedAny": false,
"excludedTypeNames": [
"ExcludedTypeFoo",
"ExcludedTypeBar"
]
}
}
```

- `openApiPath` - points to the OpenAPI Specifications file. This can be the path to a file stored on disk, relative to the `.refitter` file. This can also be a URL to a remote file that will be downloaded over HTTP/HTTPS
- `namespace` - the namespace used in the generated code. If not specified, this defaults to `GeneratedCode`
- `naming.useOpenApiTitle` - a boolean indicating whether the OpenApi title should be used. Default is `true`
- `naming.interfaceName` - the name of the generated interface. The generated code will automatically prefix this with `I` so if this set to `MyApiClient` then the generated interface is called `IMyApiClient`. Default is `ApiClient`
- `generateContracts` - a boolean indicating whether contracts should be generated. A use case for this is several API clients use the same contracts. Default is `true`
- `generateXmlDocCodeComments` - a boolean indicating whether XML doc comments should be generated. Default is `true`
- `addAutoGeneratedHeader` - a boolean indicating whether XML doc comments should be generated. Default is `true`
- `addAcceptHeaders` - a boolean indicating whether to add accept headers [Headers("Accept: application/json")]. Default is `true`
- `returnIApiResponse` - a boolean indicating whether to return `IApiResponse<T>` objects. Default is `false`
- `responseTypeOverride` - a dictionary with operation ids (as specified in the OpenAPI document) and a particular return type to use. The types are wrapped in a task, but otherwise unmodified (so make sure to specify or import their namespaces). Default is `{}`
- `generateOperationHeaders` - a boolean indicating whether to use operation headers in the generated methods. Default is `true`
- `typeAccessibility` - the generated type accessibility. Possible values are `Public` and `Internal`. Default is `Public`
- `useCancellationTokens` - Use cancellation tokens in the generated methods. Default is `false`
- `useIsoDateFormat` - Set to `true` to explicitly format date query string parameters in ISO 8601 standard date format using delimiters (for example: 2023-06-15). Default is `false`
- `multipleInterfaces` - Set to `ByEndpoint` to generate an interface for each endpoint, or `ByTag` to group Endpoints by their Tag (like SwaggerUI groups them).
- `outputFolder` - a string describing a relative path to a desired output folder. Default is `./Generated`
- `outputFilename` - Output filename. Default is `Output.cs` when used from the CLI tool, otherwise its the .refitter filename. So `Petstore.refitter` becomes `Petstore.cs`.
- `additionalNamespaces` - A collection of additional namespaces to include in the generated file. A use case for this is when you want to reuse contracts from a different namespace than the generated code. Default is empty
- `includeTags` - A collection of tags to use a filter for including endpoints that contain this tag.
- `includePathMatches` - A collection of regular expressions used to filter paths.
- `generateDeprecatedOperations` - a boolean indicating whether deprecated operations should be generated or skipped. Default is `true`
- `operationNameTemplate` - Generate operation names using pattern. This must contain the string {operationName}. An example usage of this could be `{operationName}Async` to suffix all method names with Async. When using `"multipleIinterfaces": "ByEndpoint"`, This is name of the Execute() method in the interface
- `optionalParameters` - Generate non-required parameters as nullable optional parameters
- `useDynamicQuerystringParameters`: Set to `true` to wrap multiple query parameters into a single complex one. Default is `false` (no wrapping).
- `dependencyInjectionSettings` - Setting this will generated extension methods to `IServiceCollection` for configuring Refit clients. See https://github.com/reactiveui/refit?tab=readme-ov-file#dynamic-querystring-parameters for more information.
- `baseUrl` - Used as the HttpClient base address. Leave this blank to manually set the base URL
- `httpMessageHandlers` - A collection of `HttpMessageHandler` that is added to the HttpClient pipeline
- `transientErrorHandler` - This is the transient error handler to use. Possible values are `None`, `Polly`, and `HttpResilience`. Default is `None`
- `maxRetryCount` - This is the max retry count used in the Polly retry policy. Default is 6
- `firstBackoffRetryInSeconds` - This is the duration of the initial retry backoff. Default is 1 second
- `apizrSettings` - Setting this will format Refit interface to be managed by Apizr. See https://www.apizr.net for more information
- `withRequestOptions` - Tells if the Refit interface methods should have a final IApizrRequestOptions options parameter
- `withRegistrationHelper` - Tells if Refitter should generate Apizr registration helpers (extended with dependencyInjectionSettings set, otherwise static)
- `withCacheProvider` - Set the cache provider to be used
- `withPriority` - Tells if Apizr should handle request priority
- `withMediation` - Tells if Apizr should handle request mediation (extended only)
- `withOptionalMediation` - Tells if Apizr should handle optional request mediation (extended only)
- `withMappingProvider` - Set the mapping provider to be used
- `withFileTransfer` - Tells if Apizr should handle file transfer
- `codeGeneratorSettings` - Setting this allows customization of the NSwag generated types and contracts
- `requiredPropertiesMustBeDefined` - Default is true,
- `generateDataAnnotations` - Default is true,
- `anyType` - Default is `object`,
- `dateType` - Default is `System.DateTimeOffset`,
- `dateTimeType` - Default is `System.DateTimeOffset`,
- `timeType` - Default is `System.TimeSpan`,
- `timeSpanType` - Default is `System.TimeSpan`,
- `arrayType` - Default is `System.Collections.Generic.ICollection`,
- `dictionaryType` - Default is `System.Collections.Generic.IDictionary`,
- `arrayInstanceType` - Default is `System.Collections.ObjectModel.Collection`,
- `dictionaryInstanceType` - Default is `System.Collections.Generic.Dictionary`,
- `arrayBaseType` - Default is `System.Collections.ObjectModel.Collection`,
- `dictionaryBaseType` - Default is `System.Collections.Generic.Dictionary`,
- `propertySetterAccessModifier` - Default is ``,
- `generateImmutableArrayProperties` - Default is false,
- `generateImmutableDictionaryProperties` - Default is false,
- `handleReferences` - Default is false,
- `jsonSerializerSettingsTransformationMethod` - Default is null,
- `generateJsonMethods` - Default is false,
- `enforceFlagEnums` - Default is false,
- `inlineNamedDictionaries` - Default is false,
- `inlineNamedTuples` - Default is true,
- `inlineNamedArrays` - Default is false,
- `generateOptionalPropertiesAsNullable` - Default is false,
- `generateNullableReferenceTypes` - Default is false,
- `generateNativeRecords` - Default is false
- `generateDefaultValues` - Default is true
- `inlineNamedAny` - Default is false
- `excludedTypeNames` - Default is empty
30 changes: 30 additions & 0 deletions src/Refitter.MSBuild/Refitter.MSBuild.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Product>Refitter MSBuild Tasks</Product>
<PackageId>Refitter.MSBuild</PackageId>
<AssemblyName>Refitter.MSBuild</AssemblyName>
<RootNamespace>Refitter.MSBuild</RootNamespace>
<Title>Refitter.MSBuild</Title>
<Description>A MSBuid task that uses Refitter for generating a REST API Client using the Refit library from OpenAPI specifications.</Description>
<GenerateDependencyFile>true</GenerateDependencyFile>
<BuildOutputTargetFolder>tasks</BuildOutputTargetFolder>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build.Utilities.Core"
Version="17.12.6"
PrivateAssets="all"
ExcludeAssets="runtime"/>
</ItemGroup>

<ItemGroup>
<Content Include="$(AssemblyName).props" PackagePath="build" />
<Content Include="$(AssemblyName).targets" PackagePath="build" />
<Content Include="$(AssemblyName).props" PackagePath="tasks" />
<Content Include="$(AssemblyName).targets" PackagePath="tasks" />
<Content Include="../Refitter/bin/$(Configuration)/net8.0/**/*" PackagePath="tasks" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions src/Refitter.MSBuild/Refitter.MSBuild.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project TreatAsLocalProperty="TaskFolder;TaskAssembly">

<PropertyGroup>
<TaskFolder>$(MSBuildThisFileDirectory)..\tasks</TaskFolder>
<TaskAssembly>$(TaskFolder)\netstandard2.0\Refitter.MSBuild.dll</TaskAssembly>
<RefitterVersion>$(Version)</RefitterVersion>
</PropertyGroup>

<UsingTask TaskName="Refitter.MSBuild.RefitterGenerateTask"
AssemblyFile="$(TaskAssembly)" />

</Project>
10 changes: 10 additions & 0 deletions src/Refitter.MSBuild/Refitter.MSBuild.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project>

<Target Name="Refitter" BeforeTargets="BeforeBuild">
<RefitterGenerateTask ProjectFileDirectory="$(MSBuildProjectDirectory)" />
<ItemGroup>
<Compile Include="**/*.cs" />
</ItemGroup>
</Target>

</Project>
109 changes: 109 additions & 0 deletions src/Refitter.MSBuild/RefitterGenerateTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System.Diagnostics;
using System.Reflection;
using MSBuildTask = Microsoft.Build.Utilities.Task;

namespace Refitter.MSBuild;

public class RefitterGenerateTask : MSBuildTask
{
public string ProjectFileDirectory { get; set; }

public override bool Execute()
{
TryLogCommandLine($"Starting {nameof(RefitterGenerateTask)}");
TryLogCommandLine($"Looking for .refitter files under {ProjectFileDirectory}");

var files = Directory.GetFiles(
ProjectFileDirectory,
"*.refitter",
SearchOption.AllDirectories);

TryLogCommandLine($"Found {files.Length} .refitter files...");

foreach (var file in files)
{
TryLogCommandLine($"Processing {file}");
TryExecuteRefitter(file);
}

return true;
}

private void TryExecuteRefitter(string file)
{
try
{
StartProcess(file);
}
catch (Exception e)
{
TryLogErrorFromException(e);
}
}

private void StartProcess(string file)
{
var assembly = Assembly.GetExecutingAssembly();
var packageFolder = Path.GetDirectoryName(assembly.Location);
var refitterDll = $"{packageFolder}\\..\\refitter.dll";
TryLogCommandLine("Starting " + refitterDll);

using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"{refitterDll} --settings-file {file}",
WorkingDirectory = Path.GetDirectoryName(file)!,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = true,
}
};

process.ErrorDataReceived += (_, args) => TryLogError(args.Data);
process.OutputDataReceived += (_, args) => TryLogCommandLine(args.Data);
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
process.WaitForExit();
}

private void TryLogErrorFromException(Exception e)
{
try
{
Log.LogErrorFromException(e);
}
catch
{
// Ignore
}
}

private void TryLogCommandLine(string text)
{
try
{
Log.LogCommandLine(text);
}
catch
{
// ignore
}
}

private void TryLogError(string text)
{
try
{
Log.LogError(text);
}
catch
{
// ignore
}
}
}
Loading