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: 37 additions & 6 deletions DotnetClientGenerator/CSharpClientGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public string GenerateClient(ParsedApiSpec spec, ClientGeneratorOptions options)
sb.AppendLine($"namespace {namespaceName};");
sb.AppendLine();

GenerateModelClasses(sb, spec.Schemas);
GenerateModelClasses(sb, spec.Schemas, options.GenerateModelInterfaces);

// Generate response types for each endpoint
foreach (var endpoint in spec.Endpoints)
Expand Down Expand Up @@ -301,21 +301,26 @@ private string ToPascalCase(string input)
return char.ToUpperInvariant(input[0]) + input[1..];
}

private void GenerateModelClasses(StringBuilder sb, IDictionary<string, OpenApiSchema?> schemas)
private void GenerateModelClasses(StringBuilder sb, IDictionary<string, OpenApiSchema?> schemas, bool generateInterfaces)
{
foreach (var schema in schemas)
{
if (schema.Value != null)
{
GenerateModelClass(sb, schema.Key, schema.Value);
if (generateInterfaces)
{
GenerateModelInterface(sb, schema.Key, schema.Value);
sb.AppendLine();
}
GenerateModelClass(sb, schema.Key, schema.Value, generateInterfaces);
sb.AppendLine();
}
}
}

private void GenerateModelClass(StringBuilder sb, string className, OpenApiSchema schema)
private void GenerateModelInterface(StringBuilder sb, string className, OpenApiSchema schema)
{
sb.AppendLine($"public class {className}");
sb.AppendLine($"public interface I{className}");
sb.AppendLine("{");

if (schema.Properties != null)
Expand All @@ -325,7 +330,33 @@ private void GenerateModelClass(StringBuilder sb, string className, OpenApiSchem
var propertyName = ToPascalCase(property.Key);
var propertyType = GetCSharpTypeFromSchema(property.Value);
var isRequired = schema.Required?.Contains(property.Key) == true;


if (!isRequired && !propertyType.EndsWith("?") && IsNullableType(property.Value))
{
propertyType += "?";
}

sb.AppendLine($" {propertyType} {propertyName} {{ get; }}");
}
}

sb.AppendLine("}");
}

private void GenerateModelClass(StringBuilder sb, string className, OpenApiSchema schema, bool generateInterface)
{
string interfaceImplementation = generateInterface ? $" : I{className}" : "";
sb.AppendLine($"public class {className}{interfaceImplementation}");
sb.AppendLine("{");

if (schema.Properties != null)
{
foreach (var property in schema.Properties)
{
var propertyName = ToPascalCase(property.Key);
var propertyType = GetCSharpTypeFromSchema(property.Value);
var isRequired = schema.Required?.Contains(property.Key) == true;

if (!isRequired && !propertyType.EndsWith("?") && IsNullableType(property.Value))
{
propertyType += "?";
Expand Down
1 change: 1 addition & 0 deletions DotnetClientGenerator/ClientGeneratorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public class ClientGeneratorOptions
public string? ClassName { get; init; }
public string? Namespace { get; init; }
public bool GenerateInterface { get; init; }
public bool GenerateModelInterfaces { get; init; }
}
22 changes: 15 additions & 7 deletions DotnetClientGenerator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,19 @@
Description = "Generate an interface for the client class"
};

Option<bool> modelInterfacesOption = new("--generate-model-interfaces", "-m")
{
Description = "Generate interfaces for model classes"
};

RootCommand rootCommand = new("A tool for generating C# API clients from OpenAPI specifications");
rootCommand.Options.Add(inputOption);
rootCommand.Options.Add(outputOption);
rootCommand.Options.Add(classNameOption);
rootCommand.Options.Add(namespaceOption);
rootCommand.Options.Add(watchOption);
rootCommand.Options.Add(interfaceOption);
rootCommand.Options.Add(modelInterfacesOption);

rootCommand.SetAction(async (parseResult, _) =>
{
Expand All @@ -51,20 +57,21 @@
var namespaceName = parseResult.GetValue(namespaceOption)!;
var watch = parseResult.GetValue(watchOption);
var generateInterface = parseResult.GetValue(interfaceOption);

var generateModelInterfaces = parseResult.GetValue(modelInterfacesOption);

try
{
await GenerateClient(input, output, className, namespaceName, generateInterface);
await GenerateClient(input, output, className, namespaceName, generateInterface, generateModelInterfaces);

if (watch)
{
Console.WriteLine($"👀 Watching {input} for changes...");

using FileSystemWatcher watcher = new FileSystemWatcher(Path.GetDirectoryName(Path.GetFullPath(input)) ?? ".", Path.GetFileName(input));
watcher.Changed += async (_, _) =>
{
Console.WriteLine("🔄 File changed, regenerating...");
await GenerateClient(input, output, className, namespaceName, generateInterface);
await GenerateClient(input, output, className, namespaceName, generateInterface, generateModelInterfaces);
};
watcher.EnableRaisingEvents = true;

Expand All @@ -81,7 +88,7 @@

return await rootCommand.Parse(args).InvokeAsync();

static async Task GenerateClient(string input, string output, string className, string namespaceName, bool generateInterface)
static async Task GenerateClient(string input, string output, string className, string namespaceName, bool generateInterface, bool generateModelInterfaces)
{
Console.WriteLine("🚀 Generating C# API client...");
Console.WriteLine($"📥 Input: {input}");
Expand All @@ -93,12 +100,13 @@ static async Task GenerateClient(string input, string output, string className,

Console.WriteLine($"🏗️ Generating code for {spec.Schemas.Count} models and {spec.Endpoints.Count} endpoints...");
CSharpClientGenerator generator = new();

ClientGeneratorOptions options = new()
{
ClassName = className,
Namespace = namespaceName,
GenerateInterface = generateInterface
GenerateInterface = generateInterface,
GenerateModelInterfaces = generateModelInterfaces
};

string clientCode = generator.GenerateClient(spec, options);
Expand Down
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ dotnet-client-generator --input openapi.json --output ApiClient.cs --watch
| `--output` | `-o` | Output file path for generated C# client | Yes |
| `--class-name` | `-c` | Name of the generated client class | No (default: "ApiClient") |
| `--namespace` | `-n` | Namespace for the generated client | No (default: "GeneratedClient") |
| `--generate-interface` | `-g` | Generate an interface for the client class | No |
| `--generate-model-interfaces` | `-m` | Generate interfaces for model classes | No |
| `--watch` | `-w` | Watch input file for changes and regenerate | No |

## Generated Client Features
Expand All @@ -77,6 +79,8 @@ The generated C# client includes:
- **Query Parameters**: Automatic query string building
- **Path Parameters**: Automatic URL path interpolation
- **Request Bodies**: JSON serialization for POST/PUT operations
- **Interface Generation**: Optional generation of interfaces for dependency injection
- **Model Interfaces**: Optional generation of interfaces for model classes

## Example Generated Client

Expand Down Expand Up @@ -131,6 +135,63 @@ var pets = await client.ListPets(limit: 10);
await client.CreatePet(new { name = "Fluffy", tag = "cat" });
```

## Interface Generation

### Client Interface

Generate an interface for the client class using `--generate-interface`:

```bash
dotnet-client-generator -i openapi.json -o ApiClient.cs --generate-interface
```

This generates:

```csharp
public interface IApiClient
{
Task<GetUserResponse> GetUser(int userId);
// ... other methods
}

public class ApiClient : IApiClient
{
// ... implementation
}
```

### Model Interfaces

Generate interfaces for model classes using `--generate-model-interfaces`:

```bash
dotnet-client-generator -i openapi.json -o ApiClient.cs --generate-model-interfaces
```

This generates:

```csharp
public interface IPet
{
long Id { get; }
string Name { get; }
string? Tag { get; }
}

public class Pet : IPet
{
public long Id { get; set; }
public string Name { get; set; }
public string? Tag { get; set; }
}
```

Model interfaces are useful for:
- Dependency injection and mocking in tests
- Creating abstraction layers
- Enforcing read-only contracts
- Supporting polymorphism in domain models

## Development

### Building
Expand Down