Skip to content

christianhelle/refitter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Build Smoke Tests NuGet Quality Gate Status codecov

All Contributors

Refitter

Refitter is a tool for generating a C# REST API Client using the Refit library. Refitter can generate the Refit interface and contracts from OpenAPI specifications. Refitter could format the generated Refit interface to be managed by Apizr (v6+) and generate some registration helpers too.

Refitter comes in 2 forms:

CLI Tool

Installation:

The tool is packaged as a .NET Tool and is published to nuget.org. You can install the latest version of this tool like this:

dotnet tool install --global Refitter

Usage:

$ refitter --help
USAGE:
    refitter [URL or input file] [OPTIONS]

EXAMPLES:
    refitter ./openapi.json
    refitter https://petstore3.swagger.io/api/v3/openapi.yaml
    refitter ./openapi.json --settings-file ./openapi.refitter --output ./GeneratedCode.cs
    refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode" --output ./GeneratedCode.cs
    refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode" --internal
    refitter ./openapi.json --output ./IGeneratedCode.cs --interface-only
    refitter ./openapi.json --output ./GeneratedContracts.cs --contract-only
    refitter ./openapi.json --use-api-response
    refitter ./openapi.json --cancellation-tokens
    refitter ./openapi.json --no-operation-headers
    refitter ./openapi.json --no-accept-headers
    refitter ./openapi.json --use-iso-date-format
    refitter ./openapi.json --additional-namespace "Your.Additional.Namespace" --additional-namespace "Your.Other.Additional.Namespace"
    refitter ./openapi.json --multiple-interfaces ByEndpoint
    refitter ./openapi.json --tag Pet --tag Store --tag User
    refitter ./openapi.json --match-path '^/pet/.*'
    refitter ./openapi.json --trim-unused-schema
    refitter ./openapi.json --trim-unused-schema  --keep-schema '^Model$' --keep-schema '^Person.+'
    refitter ./openapi.json --no-deprecated-operations
    refitter ./openapi.json --operation-name-template '{operationName}Async'
    refitter ./openapi.json --optional-nullable-parameters
    refitter ./openapi.json --use-polymorphic-serialization

ARGUMENTS:
    [URL or input file]    URL or file path to OpenAPI Specification file

OPTIONS:
                                                DEFAULT
    -h, --help                                                   Prints help information
    -v, --version                                                Prints version information
    -s, --settings-file                                          Path to .refitter settings file. Specifying this will ignore all other settings (except for --output)
    -n, --namespace                             GeneratedCode    Default namespace to use for generated types
        --contracts-namespace                                    Default namespace to use for generated contracts
    -o, --output                                Output.cs        Path to Output file or folder (if multiple files are generated)
        --contracts-output                                       Output path for generated contracts. Enabling this automatically enables generating multiple files
        --no-auto-generated-header                               Don't add <auto-generated> header to output file
        --no-accept-headers                                      Don't add <Accept> header to output file
        --interface-only                                         Don't generate contract types
        --contract-only                                          Don't generate clients
        --use-api-response                                       Return Task<IApiResponse<T>> instead of Task<T>
        --use-observable-response                                Return IObservable instead of Task
        --internal                                               Set the accessibility of the generated types to 'internal'
        --cancellation-tokens                                    Use cancellation tokens
        --no-operation-headers                                   Don't generate operation headers
        --no-logging                                             Don't log errors or collect telemetry
        --additional-namespace                                   Add additional namespace to generated types
        --exclude-namespace                                      Exclude namespace on generated types
        --use-iso-date-format                                    Explicitly format date query string parameters in ISO 8601 standard date format using delimiters (2023-06-15)
        --multiple-interfaces                                    Generate a Refit interface for each endpoint. May be one of ByEndpoint, ByTag
        --multiple-files                                         Generate multiple files instead of a single large file.
                                                                 The output files can be the following:
                                                                 - RefitInterfaces.cs
                                                                 - DependencyInjection.cs
                                                                 - Contracts.cs
        --match-path                                             Only include Paths that match the provided regular expression. May be set multiple times
        --tag                                                    Only include Endpoints that contain this tag. May be set multiple times and result in OR'ed evaluation
        --skip-validation                                        Skip validation of the OpenAPI specification
        --no-deprecated-operations                               Don't generate deprecated operations
        --operation-name-template                                Generate operation names using pattern. When using --multiple-interfaces ByEndpoint, this is name of the Execute() method in the interface
        --optional-nullable-parameters                           Generate nullable parameters as optional parameters
        --trim-unused-schema                                     Removes unreferenced components schema to keep the generated output to a minimum
        --keep-schema                                            Force to keep matching schema, uses regular expressions. Use together with "--trim-unused-schema". Can be set multiple times
        --no-banner                                              Don't show donation banner
        --skip-default-additional-properties                     Set to true to skip default additional properties
        --operation-name-generator              Default          The NSwag IOperationNameGenerator implementation to use.
                                                                 May be one of:
                                                                 - Default
                                                                 - MultipleClientsFromOperationId
                                                                 - MultipleClientsFromPathSegments
                                                                 - MultipleClientsFromFirstTagAndOperationId
                                                                 - MultipleClientsFromFirstTagAndOperationName
                                                                 - MultipleClientsFromFirstTagAndPathSegments
                                                                 - SingleClientFromOperationId
                                                                 - SingleClientFromPathSegments
                                                                 See https://refitter.github.io/api/Refitter.Core.OperationNameGeneratorTypes.html for more information
        --immutable-records                                      Generate contracts as immutable records instead of classes
        --use-apizr                                              Use Apizr by:
                                                                 - Adding a final IApizrRequestOptions options parameter to all generated methods
                                                                 - Providing cancellation tokens by Apizr request options instead of a dedicated parameter
                                                                 - Using method overloads instead of optional parameters
                                                                 See https://refitter.github.io for more information and https://www.apizr.net to get started with Apizr
        --use-dynamic-querystring-parameters                     Enable wrapping multiple query parameters into a single complex one. Default is no wrapping.
                                                                 See https://github.com/reactiveui/refit?tab=readme-ov-file#dynamic-querystring-parameters for more information
        --use-polymorphic-serialization                          Use System.Text.Json polymorphic serialization.
                                                                 Replaces NSwag JsonInheritanceConverter attributes with System.Text.Json JsonPolymorphicAttributes.
                                                                 To have the native support of inheritance (de)serialization and fallback to base types when
                                                                 payloads with (yet) unknown types are offered by newer versions of an API
                                                                 See https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism for more information
        --disposable                                             Generate refit clients that implement IDisposable

To generate code from an OpenAPI specifications file, run the following:

$ refitter [path to OpenAPI spec file] --namespace "[Your.Namespace.Of.Choice.GeneratedCode]"

This will generate a file called Output.cs which contains the Refit interface and contract classes generated using NSwag

Source Generator

Refitter is available as a C# Source Generator that uses the Refitter.Core library for generating a REST API Client using the Refit library. Refitter can generate the Refit interface from OpenAPI specifications. Refitter could format the generated Refit interface to be managed by Apizr and generate some registration helpers too.

The Refitter source generator is a bit untraditional in a sense that it creates a folder called Generated in the same location as the .refitter file and generates files to disk under the Generated folder (can be changed with --outputFolder). The source generator output should be included in the project and committed to source control. This is done because there is no other way to trigger the Refit source generator to pickup the Refitter generated code

(Translation: I couldn't for the life of me figure how to get that to work, sorry)

Installation

The source generator is distributed as a NuGet package and should be installed to the project that will contain the generated code

dotnet add package Refitter.SourceGenerator

Usage

This source generator generates code based on any .refitter file included to the project as AdditionalFiles.

The generator can automatically detect all .refitter files inside the project that referenced the Refitter.SourceGenerator package and there is no need to include them manually as AdditionalFiles

.Refitter File format

The following is an example .refitter file

{
  "openApiPath": "/path/to/your/openAPI", // Required
  "namespace": "Org.System.Service.Api.GeneratedCode", // Optional. Default=GeneratedCode
  "contractsNamespace": "Org.System.Service.Api.GeneratedCode.Contracts", // Optional. Default=GeneratedCode
  "naming": {
    "useOpenApiTitle": false, // Optional. Default=true
    "interfaceName": "MyApiClient" // Optional. Default=ApiClient
  },
  "generateContracts": true, // Optional. Default=true
  "generateClients": 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
  "contractsOutputFolder": "../Contracts", // Optional. Default=NULL
  "generateMultipleFiles": false, // Optional. Default=false
  "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/.*"
  ],
  "trimUnusedSchema": false, // Optional. Default=false
  "keepSchemaPatterns": [ // Optional. Force to keep matching schema, uses regular expressions. Use together with trimUnusedSchema=true
    "^Model$",
    "^Person.+"
  ],
  "generateDefaultAdditionalProperties": true, // Optional. default=true
  "operationNameGenerator": "Default", // Optional. May be one of Default, MultipleClientsFromOperationId, MultipleClientsFromPathSegments, MultipleClientsFromFirstTagAndOperationId, MultipleClientsFromFirstTagAndOperationName, MultipleClientsFromFirstTagAndPathSegments, SingleClientFromOperationId, SingleClientFromPathSegments
  "immutableRecords": false,
  "useDynamicQuerystringParameters": true, // Optional. Default=false
  "usePolymorphicSerialization": true, // Optional. Default=false
  "generateDisposableClients": true, // 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"
    ],
    "usePolly": true, // DEPRECATED - Use "transientErrorHandler": "None|Polly|HttpResilience" instead
    "transientErrorHandler": "HttpResilience", // Optional. Set this to configure transient error handling with a retry policy that uses a jittered backoff. May be one of 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,
    "dateFormat": "yyyy-MM-dd",
    "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
  • generateClients: - a boolean indicating whether clients should be generated. A use case for this is to seperate clients and contracts in two generation
  • generateXmlDocCodeComments - a boolean indicating whether XML doc comments should be generated. Default is true
  • generateStatusCodeComments - a boolean indicating whether the XML docs for ApiException and IApiResponse contain detailed descriptions for every documented status code. 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
  • optionalParameters - Generate non-required parameters as nullable optional parameters
  • trimUnusedSchema - Removes unreferenced components schema to keep the generated output to a minimum
  • keepSchemaPatterns: A collection of regular expressions to force to keep matching schema. This is used together with trimUnusedSchema
  • generateDefaultAdditionalProperties: Set to false to skip default additional properties. Default is true
  • operationNameGenerator: The NSwag IOperationNameGenerator implementation to use. See https://refitter.github.io/api/Refitter.Core.OperationNameGeneratorTypes.html
  • immutableRecords: Set to true to generate contracts as immutable records instead of classes. Default is false
  • useDynamicQuerystringParameters: Set to true to wrap multiple query parameters into a single complex one. Default is false (no wrapping). See https://github.com/reactiveui/refit?tab=readme-ov-file#dynamic-querystring-parameters for more information.
  • dependencyInjectionSettings - Setting this will generated extension methods to IServiceCollection for configuring Refit clients
    • 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
    • usePolly - Set this to true to configure the HttpClient to use Polly using a retry policy with a jittered backoff. This is DEPRECATED, use transientErrorHandler instead
    • transientErrorHandler: Set this to configure transient error handling with a retry policy that uses a jittered backoff. See https://refitter.github.io/api/Refitter.Core.TransientErrorHandler.html
    • 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
    • dateFormat - Default is yyyy-MM-dd
    • excludedTypeNames - Default is empty

MSBuild

A common scenario for generating code from OpenAPI specifications is to do it at build time. This can be achieved using MSBuild tasks. An example of such an approach would be to include a .refitter file in the projects directory and execute the Refitter CLI from pre-build events

<Target Name="Refitter" AfterTargets="PreBuildEvent">
    <Exec WorkingDirectory="$(ProjectDir)" Command="refitter --settings-file .refitter --skip-validation" />
</Target>

The snippet above requires that Refitter is installed on the machine as a globally available dotnet tool. This might not be the case if you're running on a build agent from a CI/CD environment. In this case you might want to install Refitter as a local tool using a manifest file, as described in this tutorial

<Target Name="Refitter" AfterTargets="PreBuildEvent">
    <Exec WorkingDirectory="$(ProjectDir)" Command="dotnet tool restore" />
    <Exec WorkingDirectory="$(ProjectDir)" Command="refitter --settings-file .refitter --skip-validation" />
</Target>

The dotnet build process does will probably not have access to the package repository in which to download Refitter from, this is at least the case with Azure Pipelines and Azure Artifacts. To workaround this, you can provide a separate nuget.config that only uses nuget.org as a <packageSource>.

Something like this:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="NuGet" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

You might want to place the nuget.config file in another folder to avoid using it to build the .NET project, then you can specify this when executing dotnet tool restore

<Target Name="Refitter" AfterTargets="PreBuildEvent">
    <Exec WorkingDirectory="$(ProjectDir)" Command="dotnet tool restore --configfile refitter/nuget.config" />
    <Exec WorkingDirectory="$(ProjectDir)" Command="refitter --settings-file .refitter --skip-validation" />
</Target>

In the example above, the nuget.config file is placed under the refitter folder.

Refitter.MSBuild package

Refitter ships with an MSBuild custom task that is distributed as a NuGet package and includes the Refitter CLI binary. This will simplify generating code from OpenAPI specifications at build time.

To use the package, install Refitter.MSBuild

<ItemGroup>
    <PackageReference Include="Refitter.MSBuild" Version="1.5.0" />
</ItemGroup>

The MSBuild package includes a custom .target file which executes the RefitterGenerateTask custom task and looks something like this:

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

The RefitterGenerateTask task will scan the project folder for .refitter files and executes them all. By default, telemetry collection is enabled, and to opt-out of it you must specify <RefitterNoLogging>true</RefitterNoLogging> in the .csproj <PropertyGroup>

Using the generated code

Here's an example generated output from the Swagger Petstore example using the default settings

CLI Tool

$ refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode"

Source Generator .refitter file

{
  "openApiPath": "./openapi.json",
  "namespace": "Your.Namespace.Of.Choice.GeneratedCode"
}

Output (Snippet)

Full output is available here

using Refit;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace Your.Namespace.Of.Choice.GeneratedCode
{
    [System.CodeDom.Compiler.GeneratedCode("Refitter", "1.0.0.0")]
    public partial interface ISwaggerPetstore
    {
        /// <summary>Update an existing pet</summary>
        /// <remarks>Update an existing pet by Id</remarks>
        /// <param name="body">Update an existent pet in the store</param>
        /// <returns>Successful operation</returns>
        /// <exception cref="ApiException">
        /// Thrown when the request returns a non-success status code:
        /// <list type="table">
        /// <listheader>
        /// <term>Status</term>
        /// <description>Description</description>
        /// </listheader>
        /// <item>
        /// <term>400</term>
        /// <description>Invalid ID supplied</description>
        /// </item>
        /// <item>
        /// <term>404</term>
        /// <description>Pet not found</description>
        /// </item>
        /// <item>
        /// <term>405</term>
        /// <description>Validation exception</description>
        /// </item>
        /// </list>
        /// </exception>
        [Headers("Accept: application/xml, application/json")]
        [Put("/pet")]
        Task<Pet> UpdatePet([Body] Pet body);

        ...
    }
}

Here's an example generated output from the Swagger Petstore example configured to wrap the return type in IApiResponse<T>

CLI Tool

$ refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode" --use-api-response

Source Generator .refitter file

{
  "openApiPath": "./openapi.json",
  "namespace": "Your.Namespace.Of.Choice.GeneratedCode",
  "returnIApiResponse": true
}

Output (Snippet)

Full output is available here

using Refit;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace Your.Namespace.Of.Choice.GeneratedCode
{
    [System.CodeDom.Compiler.GeneratedCode("Refitter", "1.0.0.0")]
    public partial interface ISwaggerPetstore
    {
        /// <summary>Update an existing pet</summary>
        /// <remarks>Update an existing pet by Id</remarks>
        /// <param name="body">Update an existent pet in the store</param>
        /// <returns>
        /// A <see cref="Task"/> representing the <see cref="IApiResponse"/> instance containing the result:
        /// <list type="table">
        /// <listheader>
        /// <term>Status</term>
        /// <description>Description</description>
        /// </listheader>
        /// <item>
        /// <term>200</term>
        /// <description>Successful operation</description>
        /// </item>
        /// <item>
        /// <term>400</term>
        /// <description>Invalid ID supplied</description>
        /// </item>
        /// <item>
        /// <term>404</term>
        /// <description>Pet not found</description>
        /// </item>
        /// <item>
        /// <term>405</term>
        /// <description>Validation exception</description>
        /// </item>
        /// </list>
        /// </returns>
        [Headers("Accept: application/xml, application/json")]
        [Put("/pet")]
        Task<IApiResponse<Pet>> UpdatePet([Body] Pet body);

        ...
    }
}

Here's an example generated output from the Swagger Petstore example configured to generate an interface for each endpoint

CLI Tool

$ refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode" --multiple-interfaces ByEndpoint

Source Generator .refitter file

{
  "openApiPath": "./openapi.json",
  "namespace": "Your.Namespace.Of.Choice.GeneratedCode",
  "multipleInterfaces": "ByEndpoint"
}

Output (Snippet)

Full output is available here

/// <summary>Update an existing pet</summary>
[System.CodeDom.Compiler.GeneratedCode("Refitter", "1.0.0.0")]
public partial interface IUpdatePetEndpoint
{
    /// <summary>Update an existing pet</summary>
    /// <remarks>Update an existing pet by Id</remarks>
    /// <param name="body">Update an existent pet in the store</param>
    /// <returns>Successful operation</returns>
    /// <exception cref="ApiException">
    /// Thrown when the request returns a non-success status code:
    /// <list type="table">
    /// <listheader>
    /// <term>Status</term>
    /// <description>Description</description>
    /// </listheader>
    /// <item>
    /// <term>400</term>
    /// <description>Invalid ID supplied</description>
    /// </item>
    /// <item>
    /// <term>404</term>
    /// <description>Pet not found</description>
    /// </item>
    /// <item>
    /// <term>405</term>
    /// <description>Validation exception</description>
    /// </item>
    /// </list>
    /// </exception>
    [Headers("Accept: application/xml, application/json")]
    [Put("/pet")]
    Task<Pet> Execute([Body] Pet body);
}

Here's an example generated output from the Swagger Petstore example configured to generate an interface with dynamic querystring paremeters

CLI Tool

$ refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode" --use-dynamic-querystring-parameters

Output (Snippet)

Full output is available here

[System.CodeDom.Compiler.GeneratedCode("Refitter", "1.0.0.0")]
public partial interface ISwaggerPetstoreOpenAPI30
{
    /// <summary>Updates a pet in the store with form data</summary>
    /// <param name="petId">ID of pet that needs to be updated</param>
    /// <param name="queryParams">The dynamic querystring parameter wrapping all others.</param>
    /// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
    /// <exception cref="ApiException">
    /// Thrown when the request returns a non-success status code:
    /// <list type="table">
    /// <listheader>
    /// <term>Status</term>
    /// <description>Description</description>
    /// </listheader>
    /// <item>
    /// <term>405</term>
    /// <description>Invalid input</description>
    /// </item>
    /// </list>
    /// </exception>
    [Post("/pet/{petId}")]
    Task UpdatePetWithForm(long petId, [Query] UpdatePetWithFormQueryParams queryParams);
}

public class UpdatePetWithFormQueryParams
{
    /// <summary>
    /// Name of pet that needs to be updated
    /// </summary>
    [Query]
    public string Name { get; set; }

    /// <summary>
    /// Status of pet that needs to be updated
    /// </summary>
    [Query]
    public string Status { get; set; }
}

RestService

Here's an example usage of the generated code above

using Refit;
using System;
using System.Threading.Tasks;

namespace Your.Namespace.Of.Choice.GeneratedCode;

internal class Program
{
    private static async Task Main(string[] args)
    {
        var client = RestService.For<ISwaggerPetstore>("https://petstore3.swagger.io/api/v3");
        var pet = await client.GetPetById(1);

        Console.WriteLine("## Using Task<T> as return type ##");
        Console.WriteLine($"Name: {pet.Name}");
        Console.WriteLine($"Category: {pet.Category.Name}");
        Console.WriteLine($"Status: {pet.Status}");
        Console.WriteLine();

        var client2 = RestService.For<WithApiResponse.ISwaggerPetstore>("https://petstore3.swagger.io/api/v3");
        var response = await client2.GetPetById(2);

        Console.WriteLine("## Using Task<IApiResponse<T>> as return type ##");
        Console.WriteLine($"HTTP Status Code: {response.StatusCode}");
        Console.WriteLine($"Name: {response.Content.Name}");
        Console.WriteLine($"Category: {response.Content.Category.Name}");
        Console.WriteLine($"Status: {response.Content.Status}");
    }
}

The RestService class generates an implementation of ISwaggerPetstore that uses HttpClient to make its calls.

The code above when run will output something like this:

## Using Task<T> as return type ##
Name: Gatitotototo
Category: Chaucito
Status: Sold

## Using Task<IApiResponse<T>> as return type ##
HTTP Status Code: OK
Name: Gatitotototo
Category: Chaucito
Status: Sold

ASP.NET Core and HttpClientFactory

Here's an example Minimal API with the Refit.HttpClientFactory library:

using Refit;
using Your.Namespace.Of.Choice.GeneratedCode;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services
    .AddRefitClient<ISwaggerPetstore>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://petstore3.swagger.io/api/v3"));

var app = builder.Build();
app.MapGet(
        "/pet/{id:long}",
        async (ISwaggerPetstore petstore, long id) =>
        {
            try
            {
                return Results.Ok(await petstore.GetPetById(id));
            }
            catch (Refit.ApiException e)
            {
                return Results.StatusCode((int)e.StatusCode);
            }
        })
    .WithName("GetPetById")
    .WithOpenApi();

app.UseHttpsRedirection();
app.UseSwaggerUI();
app.UseSwagger();
app.Run();

.NET Core supports registering the generated ISwaggerPetstore interface via HttpClientFactory

The following request to the API above

$ curl -X 'GET' 'https://localhost:5001/pet/1' -H 'accept: application/json'

Returns a response that looks something like this:

{
  "id": 1,
  "name": "Special_char_owner_!@#$^&()`.testing",
  "photoUrls": [
    "https://petstore3.swagger.io/resources/photos/623389095.jpg"
  ],
  "tags": [],
  "status": "Sold"
}

Dependency Injection

Refitter supports generating bootstrapping code that allows the user to conveniently configure all generated Refit interfaces by calling a single extension method to IServiceCollection.

This is enabled through the .refitter settings file like this:

{
  "openApiPath": "../OpenAPI/v3.0/petstore.json",
  "namespace": "Petstore",
  "dependencyInjectionSettings": {
    "baseUrl": "https://petstore3.swagger.io/api/v3",
    "httpMessageHandlers": [ "TelemetryDelegatingHandler" ],
    "transientErrorHandler": "Polly",
    "maxRetryCount": 3,
    "firstBackoffRetryInSeconds": 0.5
  }
}

which will generate an extension method to IServiceCollection called ConfigureRefitClients(). The generated extension method depends on Refit.HttpClientFactory library and looks like this:

public static IServiceCollection ConfigureRefitClients(
    this IServiceCollection services,
    Action<IHttpClientBuilder>? builder = default,
    RefitSettings? settings = default)
{
    var clientBuilderISwaggerPetstore = services
        .AddRefitClient<ISwaggerPetstore>(settings)
        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://petstore3.swagger.io/api/v3"))
        .AddHttpMessageHandler<TelemetryDelegatingHandler>();

    clientBuilderISwaggerPetstore
        .AddPolicyHandler(
            HttpPolicyExtensions
                .HandleTransientHttpError()
                .WaitAndRetryAsync(
                    Backoff.DecorrelatedJitterBackoffV2(
                        TimeSpan.FromSeconds(0.5),
                        3)));

    builder?.Invoke(clientBuilderISwaggerPetstore);

    return services;
}

This comes in handy especially when generating multiple interfaces, by tag or endpoint. For example, the following .refitter settings file

{
  "openApiPath": "../OpenAPI/v3.0/petstore.json",
  "namespace": "Petstore",
  "multipleInterfaces": "ByTag",
  "dependencyInjectionSettings": {
    "baseUrl": "https://petstore3.swagger.io/api/v3",
    "httpMessageHandlers": [ "TelemetryDelegatingHandler" ],
    "transientErrorHandler": "Polly",
    "maxRetryCount": 3,
    "firstBackoffRetryInSeconds": 0.5
  }
}

Will generate a single ConfigureRefitClients() extension methods that may contain dependency injection configuration code for multiple interfaces like this

public static IServiceCollection ConfigureRefitClients(
    this IServiceCollection services,
    Action<IHttpClientBuilder>? builder = default,
    RefitSettings? settings = default)
{
    var clientBuilderIPetApi = services
        .AddRefitClient<IPetApi>(settings)
        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://petstore3.swagger.io/api/v3"))
        .AddHttpMessageHandler<TelemetryDelegatingHandler>();

    clientBuilderIPetApi
        .AddPolicyHandler(
            HttpPolicyExtensions
                .HandleTransientHttpError()
                .WaitAndRetryAsync(
                    Backoff.DecorrelatedJitterBackoffV2(
                        TimeSpan.FromSeconds(0.5),
                        3)));

    builder?.Invoke(clientBuilderIPetApi);

    var clientBuilderIStoreApi = services
        .AddRefitClient<IStoreApi>(settings)
        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://petstore3.swagger.io/api/v3"))
        .AddHttpMessageHandler<TelemetryDelegatingHandler>();

    clientBuilderIStoreApi
        .AddPolicyHandler(
            HttpPolicyExtensions
                .HandleTransientHttpError()
                .WaitAndRetryAsync(
                    Backoff.DecorrelatedJitterBackoffV2(
                        TimeSpan.FromSeconds(0.5),
                        3)));

    builder?.Invoke(clientBuilderIStoreApi);

    var clientBuilderIUserApi = services
        .AddRefitClient<IUserApi>(settings)
        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://petstore3.swagger.io/api/v3"))
        .AddHttpMessageHandler<TelemetryDelegatingHandler>();

    clientBuilderIUserApi
        .AddPolicyHandler(
            HttpPolicyExtensions
                .HandleTransientHttpError()
                .WaitAndRetryAsync(
                    Backoff.DecorrelatedJitterBackoffV2(
                        TimeSpan.FromSeconds(0.5),
                        3)));

    builder?.Invoke(clientBuilderIUserApi);

    return services;
}

Personally, they I use Refitter is to generate an interface per endpoint, so when generating code for a large and complex API, I might have several interfaces.

Apizr

Apizr is a Refit client manager that provides a set of features to enhance requesting experience with resilience, caching, priority, mediation, mapping, logging, authentication, file transfer capabilities and many more...

Generating the interfaces

Refitter supports generating Apizr formatted Refit interfaces that can be managed then by Apizr (v6+).

You can enable Apizr formatted Refit interface generation either:

  • With the --use-apizr command line argument
  • By setting the apizrSettings section in the .refitter settings file

Note that --use-apizr uses default Apizr settings with withRequestOptions set to true as recommended, while the .refitter settings file allows you to configure it deeper.

In both cases, it will format the generated Refit interfaces to be Apizr ready by:

  • Adding a final IApizrRequestOptions options parameter to all generated methods (if withRequestOptions is set to true)
  • Providing cancellation tokens by Apizr request options instead of a dedicated parameter (if withRequestOptions is set to true)
  • Using method overloads instead of optional parameters (note that setting useDynamicQuerystringParameters to true improve overloading experience)

From here, you're definitly free to use the formatted interface with Apizr by registering, configuring and using it following the Apizr documentation. But Refitter can go further by generating some helpers to make the configuration easier.

Generating the helpers

Refitter supports generating Apizr (v6+) bootstrapping code that allows the user to conveniently configure all generated Apizr formatted Refit interfaces by calling a single method. It could be either an extension method to IServiceCollection if DependencyInjectionSettings are set, or a static builder method if not.

To enable Apizr registration code generation for IServiceCollection, you need at least to set the withRegistrationHelper property to true and configure the DependencyInjectionSettings section in the .refitter settings file. This is what the .refitter settings file may look like, depending on you configuration:

{
  "openApiPath": "../OpenAPI/v3.0/petstore.json",
  "namespace": "Petstore",
  "useDynamicQuerystringParameters": true,
  "dependencyInjectionSettings": {
    "baseUrl": "https://petstore3.swagger.io/api/v3",
    "httpMessageHandlers": [ "MyDelegatingHandler" ],
    "transientErrorHandler": "HttpResilience",
    "maxRetryCount": 3,
    "firstBackoffRetryInSeconds": 0.5
  },
  "apizrSettings": {
    "withRequestOptions": true, // Recommended to include an Apizr request options parameter to Refit interface methods
    "withRegistrationHelper": true, // Mandatory to actually generate the Apizr registration extended method
    "withCacheProvider": "InMemory", // Optional, default is None
    "withPriority": true, // Optional, default is false
    "withMediation": true, // Optional, default is false
    "withOptionalMediation": true, // Optional, default is false
    "withMappingProvider": "AutoMapper", // Optional, default is None
    "withFileTransfer": true // Optional, default is false
  }
}

which will generate an extension method to IServiceCollection called ConfigurePetstoreApiApizrManager(). The generated extension method depends on Apizr.Extensions.Microsoft.DependencyInjection library and looks like this:

public static IServiceCollection ConfigurePetstoreApiApizrManager(
    this IServiceCollection services,
    Action<IApizrExtendedManagerOptionsBuilder>? optionsBuilder = null)
{
    optionsBuilder ??= _ => { }; // Default empty options if null
    optionsBuilder += options => options
        .WithBaseAddress("https://petstore3.swagger.io/api/v3", ApizrDuplicateStrategy.Ignore)
        .WithDelegatingHandler<MyDelegatingHandler>()
        .ConfigureHttpClientBuilder(builder => builder
            .AddStandardResilienceHandler(config =>
            {
                config.Retry = new HttpRetryStrategyOptions
                {
                    UseJitter = true,
                    MaxRetryAttempts = 3,
                    Delay = TimeSpan.FromSeconds(0.5)
                };
            }))
        .WithInMemoryCacheHandler()
        .WithAutoMapperMappingHandler()
        .WithPriority()
        .WithOptionalMediation()
        .WithFileTransferOptionalMediation();

    return services.AddApizrManagerFor<IPetstoreApi>(optionsBuilder);
}

This comes in handy especially when generating multiple interfaces, by tag or endpoint. For example, the following .refitter settings file

{
  "openApiPath": "../OpenAPI/v3.0/petstore.json",
  "namespace": "Petstore",
  "useDynamicQuerystringParameters": true,
  "multipleInterfaces": "ByTag",
  "naming": {
    "useOpenApiTitle": false,
    "interfaceName": "Petstore"
  },
  "dependencyInjectionSettings": {
    "baseUrl": "https://petstore3.swagger.io/api/v3",
    "httpMessageHandlers": [ "MyDelegatingHandler" ],
    "transientErrorHandler": "HttpResilience",
    "maxRetryCount": 3,
    "firstBackoffRetryInSeconds": 0.5
  },
  "apizrSettings": {
    "withRequestOptions": true, // Recommended to include an Apizr request options parameter to Refit interface methods
    "withRegistrationHelper": true, // Mandatory to actually generate the Apizr registration extended method
    "withCacheProvider": "InMemory", // Optional, default is None
    "withPriority": true, // Optional, default is false
    "withMediation": true, // Optional, default is false
    "withOptionalMediation": true, // Optional, default is false
    "withMappingProvider": "AutoMapper", // Optional, default is None
    "withFileTransfer": true // Optional, default is false
  }
}

Will generate a single ConfigurePetstoreApizrManagers() extension method that may contain dependency injection configuration code for multiple interfaces like this

public static IServiceCollection ConfigurePetstoreApizrManagers(
    this IServiceCollection services,
    Action<IApizrExtendedCommonOptionsBuilder>? optionsBuilder = null)
{
    optionsBuilder ??= _ => { }; // Default empty options if null
    optionsBuilder += options => options
        .WithBaseAddress("https://petstore3.swagger.io/api/v3", ApizrDuplicateStrategy.Ignore)
        .WithDelegatingHandler<MyDelegatingHandler>()
        .ConfigureHttpClientBuilder(builder => builder
            .AddStandardResilienceHandler(config =>
            {
                config.Retry = new HttpRetryStrategyOptions
                {
                    UseJitter = true,
                    MaxRetryAttempts = 3,
                    Delay = TimeSpan.FromSeconds(0.5)
                };
            }))
        .WithInMemoryCacheHandler()
        .WithAutoMapperMappingHandler()
        .WithPriority()
        .WithOptionalMediation()
        .WithFileTransferOptionalMediation();

    return services.AddApizr(
        registry => registry
            .AddManagerFor<IPetApi>()
            .AddManagerFor<IStoreApi>()
            .AddManagerFor<IUserApi>(),
        optionsBuilder);

}

Here, IPetApi, IStoreApi and IUserApi are the generated interfaces which share the same common configuration defined from the .refitter file.

To enable Apizr static builder code generation, you need at least to set the withRegistrationHelper property to true and leave the DependencyInjectionSettings section to null in the .refitter settings file. This is what the .refitter settings file may look like, depending on you configuration:

{
  "openApiPath": "../OpenAPI/v3.0/petstore.json",
  "namespace": "Petstore",
  "useDynamicQuerystringParameters": true,
  "apizrSettings": {
    "withRequestOptions": true, // Recommended to include an Apizr request options parameter to Refit interface methods
    "withRegistrationHelper": true, // Mandatory to actually generate the Apizr registration extended method
    "withCacheProvider": "Akavache", // Optional, default is None
    "withPriority": true, // Optional, default is false
    "withMappingProvider": "AutoMapper", // Optional, default is None
    "withFileTransfer": true // Optional, default is false
  }
}

which will generate a static builder method called BuildPetstore30ApizrManager(). The generated builder method depends on Apizr library and looks like this:

public static IApizrManager<ISwaggerPetstoreOpenAPI30> BuildPetstore30ApizrManager(Action<IApizrManagerOptionsBuilder> optionsBuilder)
{
    optionsBuilder ??= _ => { }; // Default empty options if null
    optionsBuilder += options => options
        .WithAkavacheCacheHandler()
        .WithAutoMapperMappingHandler(new MapperConfiguration(config => { /* YOUR_MAPPINGS_HERE */ }))
        .WithPriority();

    return ApizrBuilder.Current.CreateManagerFor<ISwaggerPetstoreOpenAPI30>(optionsBuilder);
}

This comes in handy especially when generating multiple interfaces, by tag or endpoint. For example, the following .refitter settings file

{
  "openApiPath": "../OpenAPI/v3.0/petstore.json",
  "namespace": "Petstore",
  "useDynamicQuerystringParameters": true,
  "multipleInterfaces": "ByTag",
  "naming": {
    "useOpenApiTitle": false,
    "interfaceName": "Petstore"
  },
  "dependencyInjectionSettings": {
    "baseUrl": "https://petstore3.swagger.io/api/v3",
    "httpMessageHandlers": [ "MyDelegatingHandler" ],
    "transientErrorHandler": "HttpResilience",
    "maxRetryCount": 3,
    "firstBackoffRetryInSeconds": 0.5
  },
  "apizrSettings": {
    "withRequestOptions": true, // Recommended to include an Apizr request options parameter to Refit interface methods
    "withRegistrationHelper": true, // Mandatory to actually generate the Apizr registration extended method
    "withCacheProvider": "InMemory", // Optional, default is None
    "withPriority": true, // Optional, default is false
    "withMediation": true, // Optional, default is false
    "withOptionalMediation": true, // Optional, default is false
    "withMappingProvider": "AutoMapper", // Optional, default is None
    "withFileTransfer": true // Optional, default is false
  }
}

Will generate a single BuildPetstoreApizrManagers() builder method that may contain configuration code for multiple interfaces like this

public static IApizrRegistry BuildPetstoreApizrManagers(Action<IApizrCommonOptionsBuilder> optionsBuilder)
{
    optionsBuilder ??= _ => { }; // Default empty options if null
    optionsBuilder += options => options
        .WithAkavacheCacheHandler()
        .WithAutoMapperMappingHandler(new MapperConfiguration(config => { /* YOUR_MAPPINGS_HERE */ }))
        .WithPriority();

    return ApizrBuilder.Current.CreateRegistry(
        registry => registry
            .AddManagerFor<IPetApi>()
            .AddManagerFor<IStoreApi>()
            .AddManagerFor<IUserApi>(),
        optionsBuilder);
}

Here, IPetApi, IStoreApi and IUserApi are the generated interfaces which share the same common configuration defined from the .refitter file.


Customizing the configuration

You may want to adjust apis configuration, for example, to add a custom header to requests. This can be done using the Action<TApizrOptionsBuilder> parameter while calling the generated method. To know how to make Apizr fit your needs, please refer to the Apizr documentation.

Using the managers

Once you called the generated method, you will get an IApizrManager<T> instance that you can use to make requests to the API. Here's an example of how to use it:

var result = await petstoreManager.ExecuteAsync((api, opt) => api.GetPetById(1, opt),
    options => options // Whatever final request options you want to apply
        .WithPriority(Priority.Background)
        .WithHeaders(["HeaderKey1: HeaderValue1"])
        .WithRequestTimeout("00:00:10")
        .WithCancellation(cts.Token));

Please head to the Apizr documentation to get more.

System requirements

.NET 8.0

Contributors

Philip Cox
Philip Cox

πŸ’»
Cameron MacFarland
Cameron MacFarland

πŸ’»
kgame
kgame

πŸ’»
Thomas Pettersen / Yrki
Thomas Pettersen / Yrki

πŸ’»
Artem
Artem

πŸ›
m7clarke
m7clarke

πŸ›
kirides
kirides

πŸ› πŸ’»
guillaumeserale
guillaumeserale

πŸ’» πŸ›
Dennis Brentjes
Dennis Brentjes

πŸ’» πŸ€”
Damian Hickey
Damian Hickey

πŸ›
richardhu-lmg
richardhu-lmg

πŸ›
brease-colin
brease-colin

πŸ›
angelofb
angelofb

πŸ’»
Dim Nogro
Dim Nogro

πŸ’»
yadanilov19
yadanilov19

πŸ€” πŸ’»
Daniel Powell
Daniel Powell

πŸ›
Ekkeir
Ekkeir

πŸ“– πŸ›
Waylon Martinez
Waylon Martinez

πŸ›
vkmadupa
vkmadupa

πŸ›
Noblix
Noblix

πŸ’» πŸ€”
Attila Hajdrik
Attila Hajdrik

πŸ€”
bielik01
bielik01

πŸ› πŸ€”
naaeef
naaeef

πŸ€”
Alireza Habibi
Alireza Habibi

πŸ›
Jeff Parker, PE
Jeff Parker, PE

πŸ›
jods
jods

πŸ€” πŸ›
Edimarquez Medeiros
Edimarquez Medeiros

πŸ’»
safakkesikci
safakkesikci

πŸ›
folbrecht
folbrecht

πŸ›
mortenlaursen
mortenlaursen

πŸ’»
manuel-fernandez-rodriguez
manuel-fernandez-rodriguez

πŸ›
Eli Yammine
Eli Yammine

πŸ›
kami-poi
kami-poi

πŸ€”
Xeevis
Xeevis

πŸ›
DJ4ddi
DJ4ddi

πŸ’» πŸ€”
direncancatalkaya
direncancatalkaya

πŸ’»
Robert Palmqvist
Robert Palmqvist

πŸ€” πŸ’»
Tim M
Tim M

πŸ“–
janfolbrecht
janfolbrecht

πŸ€” πŸ’»
Nick Seguin
Nick Seguin

πŸ’»
David Brink
David Brink

πŸ› πŸ’»
Stu Wilson
Stu Wilson

πŸ€” πŸ’»
sharpzilla
sharpzilla

πŸ€”
Tatu
Tatu

πŸ›
JΓ©rΓ©my BRUN-PICARD
JΓ©rΓ©my BRUN-PICARD

πŸ€” πŸ’» πŸ“–
Ed Barnard
Ed Barnard

πŸ€”
bastien.noel
bastien.noel

πŸ›
Meikel Philipp
Meikel Philipp

πŸ€”
Berk Selvi
Berk Selvi

πŸ€” πŸ’»
Joshua Ozeri
Joshua Ozeri

πŸ›
Ryan Heath
Ryan Heath

πŸ€” πŸ’»
Brian Brunner
Brian Brunner

πŸ€”
Frank Samiec
Frank Samiec

πŸ’»
Fabio Loreggian
Fabio Loreggian

πŸ›
geometrikal
geometrikal

πŸ›
Shubin Pavel
Shubin Pavel

πŸ’»
Wiebe Tijsma
Wiebe Tijsma

πŸ€”
Henri Demers
Henri Demers

πŸ›
Amund
Amund

πŸ’»

For tips and tricks on software development, check out my blog

If you find this useful and feel a bit generous then feel free to buy me a coffee β˜•