add client and contracts project

This commit is contained in:
Nick Seguin 2024-02-13 18:37:09 -06:00
parent e97126f9f4
commit 21739e098e
Signed by: nseguin
GPG key ID: 68C99FA84079021D
15 changed files with 306 additions and 67 deletions

18
.config/dotnet-tools.json Normal file
View file

@ -0,0 +1,18 @@
{
"version": 1,
"isRoot": true,
"tools": {
"swashbuckle.aspnetcore.cli": {
"version": "6.5.0",
"commands": [
"swagger"
]
},
"refitter": {
"version": "0.9.7",
"commands": [
"refitter"
]
}
}
}

View file

@ -104,7 +104,7 @@ csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_prefer_static_local_function = true:warning
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:silent
# Code-block preferences
csharp_prefer_braces = true:silent
@ -256,31 +256,31 @@ dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interfaces.required_modifiers =
dotnet_naming_symbols.interfaces.required_modifiers =
dotnet_naming_symbols.enums.applicable_kinds = enum
dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.enums.required_modifiers =
dotnet_naming_symbols.enums.required_modifiers =
dotnet_naming_symbols.events.applicable_kinds = event
dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.events.required_modifiers =
dotnet_naming_symbols.events.required_modifiers =
dotnet_naming_symbols.methods.applicable_kinds = method
dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.methods.required_modifiers =
dotnet_naming_symbols.methods.required_modifiers =
dotnet_naming_symbols.properties.applicable_kinds = property
dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.properties.required_modifiers =
dotnet_naming_symbols.properties.required_modifiers =
dotnet_naming_symbols.public_fields.applicable_kinds = field
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_fields.required_modifiers =
dotnet_naming_symbols.public_fields.required_modifiers =
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_fields.required_modifiers =
dotnet_naming_symbols.private_fields.required_modifiers =
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
@ -288,15 +288,15 @@ dotnet_naming_symbols.private_static_fields.required_modifiers = static
dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types_and_namespaces.required_modifiers =
dotnet_naming_symbols.types_and_namespaces.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters.required_modifiers =
dotnet_naming_symbols.type_parameters.required_modifiers =
dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
@ -304,7 +304,7 @@ dotnet_naming_symbols.private_constant_fields.required_modifiers = const
dotnet_naming_symbols.local_variables.applicable_kinds = local
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
dotnet_naming_symbols.local_variables.required_modifiers =
dotnet_naming_symbols.local_variables.required_modifiers =
dotnet_naming_symbols.local_constants.applicable_kinds = local
dotnet_naming_symbols.local_constants.applicable_accessibilities = local
@ -312,7 +312,7 @@ dotnet_naming_symbols.local_constants.required_modifiers = const
dotnet_naming_symbols.parameters.applicable_kinds = parameter
dotnet_naming_symbols.parameters.applicable_accessibilities = *
dotnet_naming_symbols.parameters.required_modifiers =
dotnet_naming_symbols.parameters.required_modifiers =
dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
@ -328,37 +328,36 @@ dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readon
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_symbols.local_functions.applicable_accessibilities = *
dotnet_naming_symbols.local_functions.required_modifiers =
dotnet_naming_symbols.local_functions.required_modifiers =
# Naming styles
dotnet_naming_style.pascalcase.required_prefix =
dotnet_naming_style.pascalcase.required_suffix =
dotnet_naming_style.pascalcase.word_separator =
dotnet_naming_style.pascalcase.required_prefix =
dotnet_naming_style.pascalcase.required_suffix =
dotnet_naming_style.pascalcase.word_separator =
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_style.ipascalcase.required_prefix = I
dotnet_naming_style.ipascalcase.required_suffix =
dotnet_naming_style.ipascalcase.word_separator =
dotnet_naming_style.ipascalcase.required_suffix =
dotnet_naming_style.ipascalcase.word_separator =
dotnet_naming_style.ipascalcase.capitalization = pascal_case
dotnet_naming_style.tpascalcase.required_prefix = T
dotnet_naming_style.tpascalcase.required_suffix =
dotnet_naming_style.tpascalcase.word_separator =
dotnet_naming_style.tpascalcase.required_suffix =
dotnet_naming_style.tpascalcase.word_separator =
dotnet_naming_style.tpascalcase.capitalization = pascal_case
dotnet_naming_style._camelcase.required_prefix = _
dotnet_naming_style._camelcase.required_suffix =
dotnet_naming_style._camelcase.word_separator =
dotnet_naming_style._camelcase.required_suffix =
dotnet_naming_style._camelcase.word_separator =
dotnet_naming_style._camelcase.capitalization = camel_case
dotnet_naming_style.camelcase.required_prefix =
dotnet_naming_style.camelcase.required_suffix =
dotnet_naming_style.camelcase.word_separator =
dotnet_naming_style.camelcase.required_prefix =
dotnet_naming_style.camelcase.required_suffix =
dotnet_naming_style.camelcase.word_separator =
dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_style.s_camelcase.required_prefix = s_
dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case
dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case

9
Directory.Build.props Normal file
View file

@ -0,0 +1,9 @@
<Project>
<PropertyGroup>
<OpenApiDocPath>$(SolutionDir)openapi.yaml</OpenApiDocPath>
</PropertyGroup>
<Target Name="RestoreDotNetTools" BeforeTargets="GenerateOpenApiDoc">
<Exec Command='dotnet tool restore --configfile "$(SolutionDir).config/dotnet-tools.json"'/>
</Target>
</Project>

View file

@ -7,6 +7,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3E83E620-E6C
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiBootstrap.Api", "src\WebApiBootstrap.Api\WebApiBootstrap.Api.csproj", "{5C60CE8C-0E08-40F2-B428-3F942AC49065}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiBootstrap.Contracts", "src\WebApiBootstrap.Contracts\WebApiBootstrap.Contracts.csproj", "{97D2070C-40DD-46D6-BF73-6A037960C745}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiBootstrap.Client", "src\WebApiBootstrap.Client\WebApiBootstrap.Client.csproj", "{B09D031D-33D4-4CA4-B242-011EE829FAEB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -20,8 +24,18 @@ Global
{5C60CE8C-0E08-40F2-B428-3F942AC49065}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C60CE8C-0E08-40F2-B428-3F942AC49065}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C60CE8C-0E08-40F2-B428-3F942AC49065}.Release|Any CPU.Build.0 = Release|Any CPU
{97D2070C-40DD-46D6-BF73-6A037960C745}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{97D2070C-40DD-46D6-BF73-6A037960C745}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97D2070C-40DD-46D6-BF73-6A037960C745}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97D2070C-40DD-46D6-BF73-6A037960C745}.Release|Any CPU.Build.0 = Release|Any CPU
{B09D031D-33D4-4CA4-B242-011EE829FAEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B09D031D-33D4-4CA4-B242-011EE829FAEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B09D031D-33D4-4CA4-B242-011EE829FAEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B09D031D-33D4-4CA4-B242-011EE829FAEB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5C60CE8C-0E08-40F2-B428-3F942AC49065} = {3E83E620-E6C9-48B9-B8BE-02B4CDD07FE5}
{97D2070C-40DD-46D6-BF73-6A037960C745} = {3E83E620-E6C9-48B9-B8BE-02B4CDD07FE5}
{B09D031D-33D4-4CA4-B242-011EE829FAEB} = {3E83E620-E6C9-48B9-B8BE-02B4CDD07FE5}
EndGlobalSection
EndGlobal

38
openapi.yaml Normal file
View file

@ -0,0 +1,38 @@
openapi: 3.0.1
info:
title: 'WebApiBootstrap.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'
version: '1.0'
paths:
/weatherforecast:
get:
tags:
- 'WebApiBootstrap.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'
operationId: GetWeatherForecast
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/WeatherForecast'
components:
schemas:
WeatherForecast:
type: object
properties:
date:
type: string
format: date
temperatureC:
type: integer
format: int32
temperatureF:
type: integer
format: int32
nullable: true
summary:
type: string
nullable: true
additionalProperties: false

View file

@ -0,0 +1,22 @@
namespace WebApiBootstrap.Api;
internal static class Convert
{
/// <summary>
/// Converts an object to a data transfer object (DTO).
/// </summary>
/// <param name="serializable">The object to convert.</param>
/// <typeparam name="T">The type of DTO.</typeparam>
/// <returns>The DTO.</returns>
/// <remarks>
/// This method is a shorthand for <see cref="ISerializableAs{T}.ToDto" />. It allows using the method group syntax:
/// <code> var dtos = models.Select(Convert.ToDto); </code>
/// instead of the lambda syntax:
/// <code> var dtos = models.Select(model => model.ToDto()); </code>
/// </remarks>
/// <seealso cref="ISerializableAs{T}" />
public static T ToDto<T>(this ISerializableAs<T> serializable)
{
return serializable.ToDto();
}
}

View file

@ -0,0 +1,9 @@
// Allows using "Dto" as a namespace prefix instead of suffix, so "Dto.WeatherForecast" instead of "WeatherForecastDto"
global using Dto = WebApiBootstrap.Contracts;
// Unless we explicitly use the "Dto." prefix, we assume that we're referring to the Models namespace.
global using WebApiBootstrap.Api.Models;
// Allows using "Convert.ToDto" instead of "Dto.Convert.ToDto"
global using Convert = WebApiBootstrap.Api.Convert;

View file

@ -0,0 +1,10 @@
namespace WebApiBootstrap.Api;
/// <summary>
/// An object that can be converted to a data transfer object (DTO) of type <typeparamref name="T" />.
/// </summary>
/// <typeparam name="T">The type of DTO.</typeparam>
internal interface ISerializableAs<out T>
{
T ToDto();
}

View file

@ -0,0 +1,17 @@
namespace WebApiBootstrap.Api.Models;
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
: ISerializableAs<Dto.WeatherForecast>
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public static WeatherForecast FromDto(Dto.WeatherForecast dto)
{
return new WeatherForecast(dto.Date, dto.TemperatureC, dto.Summary);
}
public Dto.WeatherForecast ToDto()
{
return new Dto.WeatherForecast(Date, TemperatureC, TemperatureF, Summary);
}
}

View file

@ -1,44 +1,42 @@
var builder = WebApplication.CreateBuilder(args);
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
WebApplication app = builder.Build();
var app = builder.Build();
app.UseSwagger();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[]
string[] summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering",
"Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.MapGet(
"/weatherforecast",
() =>
{
Dto.WeatherForecast[] forecast = Enumerable.Range(1, 5)
.Select(
index => new WeatherForecast(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]))
.Select(Convert.ToDto)
.ToArray();
app.Run();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
app.Run();

View file

@ -1,16 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<RootNamespace>WebApiBootstrap</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<RootNamespace>WebApiBootstrap.Api</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
</ItemGroup>
</Project>
<ItemGroup>
<ProjectReference Include="../WebApiBootstrap.Contracts/WebApiBootstrap.Contracts.csproj"/>
</ItemGroup>
<Target Name="GenerateOpenApiDoc" AfterTargets="AfterBuild" Condition="'$(OpenApiDocPath)' != ''">
<Exec Command='dotnet swagger tofile --yaml --output "$(OpenApiDocPath)" "$(TargetPath)" "v1"'/>
</Target>
</Project>

View file

@ -0,0 +1,27 @@
// <auto-generated>
// This code was generated by Refitter.
// </auto-generated>
using Refit;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using WebApiBootstrap.Contracts;
namespace WebApiBootstrap.Client
{
[System.CodeDom.Compiler.GeneratedCode("Refitter", "0.9.7.0")]
public partial interface IGetWeatherForecastEndpoint
{
/// <returns>OK</returns>
/// <throws cref="ApiException">Thrown when the request returns a non-success status code.</throws>
[Headers("Accept: application/json")]
[Get("/weatherforecast")]
Task<ICollection<WeatherForecast>> Execute(CancellationToken cancellationToken = default);
}
}

View file

@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>10</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nullable" Version="1.3.1" PrivateAssets="all"/>
<PackageReference Include="IsExternalInit" Version="1.0.3" PrivateAssets="all"/>
<PackageReference Include="Refit" Version="7.0.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../WebApiBootstrap.Contracts/WebApiBootstrap.Contracts.csproj"/>
</ItemGroup>
<!-- Splitting a command into multiple lines requires a ^ on Windows and \ on Linux. Please find a better way. -->
<Choose>
<When Condition="'$(OS)' == 'Windows_NT'">
<PropertyGroup>
<Separator>%5E</Separator>
</PropertyGroup>
</When>
<Otherwise>
<PropertyGroup>
<Separator>%5C</Separator>
</PropertyGroup>
</Otherwise>
</Choose>
<Target Name="GenerateRefitInterface" BeforeTargets="BeforeBuild" Condition="'$(OpenApiDocPath)' != '' And Exists($(OpenApiDocPath))">
<Exec Command="dotnet refitter '$(OpenApiDocPath)' $(Separator)
--output $(ProjectDir)RefitterInterface.g.cs $(Separator)
--namespace 'WebApiBootstrap.Client' $(Separator)
--interface-only $(Separator)
--cancellation-tokens $(Separator)
--additional-namespace 'WebApiBootstrap.Contracts' $(Separator)
--multiple-interfaces ByEndpoint $(Separator)
--no-logging $(Separator)
--no-banner $(Separator)"
/>
</Target>
</Project>

View file

@ -0,0 +1,5 @@
using System;
namespace WebApiBootstrap.Contracts;
public record WeatherForecast(DateOnly Date, int TemperatureC, int? TemperatureF, string? Summary);

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>annotations</Nullable>
<LangVersion>10</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nullable" Version="1.3.1" PrivateAssets="all"/>
<PackageReference Include="IsExternalInit" Version="1.0.3" PrivateAssets="all"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Portable.System.DateTimeOnly" Version="8.0.0"/>
</ItemGroup>
</Project>