diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..bcb2323 --- /dev/null +++ b/.config/dotnet-tools.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index b5f39e6..d9748ca 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..d6c6091 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,9 @@ + + + $(SolutionDir)openapi.yaml + + + + + + \ No newline at end of file diff --git a/WebApiBootstrap.sln b/WebApiBootstrap.sln index 99d06e3..e0290eb 100644 --- a/WebApiBootstrap.sln +++ b/WebApiBootstrap.sln @@ -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 diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..60b3bc0 --- /dev/null +++ b/openapi.yaml @@ -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 \ No newline at end of file diff --git a/src/WebApiBootstrap.Api/Convert.cs b/src/WebApiBootstrap.Api/Convert.cs new file mode 100644 index 0000000..5e9a732 --- /dev/null +++ b/src/WebApiBootstrap.Api/Convert.cs @@ -0,0 +1,22 @@ +namespace WebApiBootstrap.Api; + +internal static class Convert +{ + /// + /// Converts an object to a data transfer object (DTO). + /// + /// The object to convert. + /// The type of DTO. + /// The DTO. + /// + /// This method is a shorthand for . It allows using the method group syntax: + /// var dtos = models.Select(Convert.ToDto); + /// instead of the lambda syntax: + /// var dtos = models.Select(model => model.ToDto()); + /// + /// + public static T ToDto(this ISerializableAs serializable) + { + return serializable.ToDto(); + } +} \ No newline at end of file diff --git a/src/WebApiBootstrap.Api/GlobalUsings.cs b/src/WebApiBootstrap.Api/GlobalUsings.cs new file mode 100644 index 0000000..08c54ef --- /dev/null +++ b/src/WebApiBootstrap.Api/GlobalUsings.cs @@ -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; \ No newline at end of file diff --git a/src/WebApiBootstrap.Api/ISerializableAs.cs b/src/WebApiBootstrap.Api/ISerializableAs.cs new file mode 100644 index 0000000..45710a1 --- /dev/null +++ b/src/WebApiBootstrap.Api/ISerializableAs.cs @@ -0,0 +1,10 @@ +namespace WebApiBootstrap.Api; + +/// +/// An object that can be converted to a data transfer object (DTO) of type . +/// +/// The type of DTO. +internal interface ISerializableAs +{ + T ToDto(); +} \ No newline at end of file diff --git a/src/WebApiBootstrap.Api/Models/WeatherForecast.cs b/src/WebApiBootstrap.Api/Models/WeatherForecast.cs new file mode 100644 index 0000000..76226e8 --- /dev/null +++ b/src/WebApiBootstrap.Api/Models/WeatherForecast.cs @@ -0,0 +1,17 @@ +namespace WebApiBootstrap.Api.Models; + +internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) + : ISerializableAs +{ + 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); + } +} \ No newline at end of file diff --git a/src/WebApiBootstrap.Api/Program.cs b/src/WebApiBootstrap.Api/Program.cs index 00ff539..7cab004 100644 --- a/src/WebApiBootstrap.Api/Program.cs +++ b/src/WebApiBootstrap.Api/Program.cs @@ -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(); \ No newline at end of file diff --git a/src/WebApiBootstrap.Api/WebApiBootstrap.Api.csproj b/src/WebApiBootstrap.Api/WebApiBootstrap.Api.csproj index 9b01f30..0c7d4a7 100644 --- a/src/WebApiBootstrap.Api/WebApiBootstrap.Api.csproj +++ b/src/WebApiBootstrap.Api/WebApiBootstrap.Api.csproj @@ -1,16 +1,24 @@ - - net8.0 - enable - enable - true - WebApiBootstrap - + + net8.0 + enable + enable + true + WebApiBootstrap.Api + - - - - + + + + - + + + + + + + + + \ No newline at end of file diff --git a/src/WebApiBootstrap.Client/RefitterInterface.g.cs b/src/WebApiBootstrap.Client/RefitterInterface.g.cs new file mode 100644 index 0000000..60b3d53 --- /dev/null +++ b/src/WebApiBootstrap.Client/RefitterInterface.g.cs @@ -0,0 +1,27 @@ +// +// This code was generated by Refitter. +// + + +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 + { + /// OK + /// Thrown when the request returns a non-success status code. + [Headers("Accept: application/json")] + [Get("/weatherforecast")] + Task> Execute(CancellationToken cancellationToken = default); + } + + +} \ No newline at end of file diff --git a/src/WebApiBootstrap.Client/WebApiBootstrap.Client.csproj b/src/WebApiBootstrap.Client/WebApiBootstrap.Client.csproj new file mode 100644 index 0000000..5cc5830 --- /dev/null +++ b/src/WebApiBootstrap.Client/WebApiBootstrap.Client.csproj @@ -0,0 +1,46 @@ + + + + netstandard2.0 + disable + enable + 10 + + + + + + + + + + + + + + + + + %5E + + + + + %5C + + + + + + + + \ No newline at end of file diff --git a/src/WebApiBootstrap.Contracts/WeatherForecast.cs b/src/WebApiBootstrap.Contracts/WeatherForecast.cs new file mode 100644 index 0000000..7e5eaf4 --- /dev/null +++ b/src/WebApiBootstrap.Contracts/WeatherForecast.cs @@ -0,0 +1,5 @@ +using System; + +namespace WebApiBootstrap.Contracts; + +public record WeatherForecast(DateOnly Date, int TemperatureC, int? TemperatureF, string? Summary); \ No newline at end of file diff --git a/src/WebApiBootstrap.Contracts/WebApiBootstrap.Contracts.csproj b/src/WebApiBootstrap.Contracts/WebApiBootstrap.Contracts.csproj new file mode 100644 index 0000000..a06fe4e --- /dev/null +++ b/src/WebApiBootstrap.Contracts/WebApiBootstrap.Contracts.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + disable + annotations + 10 + + + + + + + + + + + + \ No newline at end of file