Skip to content

Commit 845228b

Browse files
authored
Merge pull request #1 from darthfabar/development
Development
2 parents 19fd5f7 + 011bd26 commit 845228b

27 files changed

+862
-65
lines changed

.github/workflows/build.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,14 @@ jobs:
3636
- name: 'Git Fetch Tags'
3737
run: git fetch --tags
3838
shell: pwsh
39-
- name: 'Install .NET Core SDK'
39+
- name: Setup .NET Core 3.1
4040
uses: actions/setup-dotnet@v1
41+
with:
42+
dotnet-version: 3.1.x
43+
- name: Setup .NET Core 5.0
44+
uses: actions/setup-dotnet@v1
45+
with:
46+
dotnet-version: 5.0.x
4147
- name: 'Dotnet Tool Restore'
4248
run: dotnet tool restore
4349
shell: pwsh

ContractModelsAttributeCheck.sln

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
6565
.github\workflows\release-drafter.yml = .github\workflows\release-drafter.yml
6666
EndProjectSection
6767
EndProject
68+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{D56CE5D4-0CEA-4EAE-8B6F-4602C3962AC0}"
69+
EndProject
70+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleWebApp", "Examples\SampleWebApp\SampleWebApp.csproj", "{FFEBDF88-C252-4DF1-AA7B-C3F0A5295932}"
71+
EndProject
72+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWebApp.Tests", "Examples\SampleWebApp.Tests\SampleWebApp.Tests.csproj", "{CF79DEFB-904B-47EE-8FC0-7E1ABAAEE872}"
73+
EndProject
6874
Global
6975
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7076
Debug|Any CPU = Debug|Any CPU
@@ -79,6 +85,14 @@ Global
7985
{528E66EB-2232-4534-A20D-9BC8A240BF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
8086
{528E66EB-2232-4534-A20D-9BC8A240BF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
8187
{528E66EB-2232-4534-A20D-9BC8A240BF2F}.Release|Any CPU.Build.0 = Release|Any CPU
88+
{FFEBDF88-C252-4DF1-AA7B-C3F0A5295932}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
89+
{FFEBDF88-C252-4DF1-AA7B-C3F0A5295932}.Debug|Any CPU.Build.0 = Debug|Any CPU
90+
{FFEBDF88-C252-4DF1-AA7B-C3F0A5295932}.Release|Any CPU.ActiveCfg = Release|Any CPU
91+
{FFEBDF88-C252-4DF1-AA7B-C3F0A5295932}.Release|Any CPU.Build.0 = Release|Any CPU
92+
{CF79DEFB-904B-47EE-8FC0-7E1ABAAEE872}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
93+
{CF79DEFB-904B-47EE-8FC0-7E1ABAAEE872}.Debug|Any CPU.Build.0 = Debug|Any CPU
94+
{CF79DEFB-904B-47EE-8FC0-7E1ABAAEE872}.Release|Any CPU.ActiveCfg = Release|Any CPU
95+
{CF79DEFB-904B-47EE-8FC0-7E1ABAAEE872}.Release|Any CPU.Build.0 = Release|Any CPU
8296
EndGlobalSection
8397
GlobalSection(SolutionProperties) = preSolution
8498
HideSolutionNode = FALSE
@@ -90,6 +104,8 @@ Global
90104
{528E66EB-2232-4534-A20D-9BC8A240BF2F} = {E1B24F25-B8A4-46EE-B7EB-7803DCFC543F}
91105
{EFE1E5ED-D337-4874-82EC-D9FA0BC7D3AB} = {F20E2797-D1E3-4321-91BB-FAE54954D2A0}
92106
{841C67EF-BBB2-4730-8E29-22FF3FD54306} = {EFE1E5ED-D337-4874-82EC-D9FA0BC7D3AB}
107+
{FFEBDF88-C252-4DF1-AA7B-C3F0A5295932} = {D56CE5D4-0CEA-4EAE-8B6F-4602C3962AC0}
108+
{CF79DEFB-904B-47EE-8FC0-7E1ABAAEE872} = {D56CE5D4-0CEA-4EAE-8B6F-4602C3962AC0}
93109
EndGlobalSection
94110
GlobalSection(ExtensibilityGlobals) = postSolution
95111
SolutionGuid = {73F36209-F8D6-4066-8951-D97729F773CF}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"iisSettings": {
3+
"windowsAuthentication": false,
4+
"anonymousAuthentication": true,
5+
"iisExpress": {
6+
"applicationUrl": "http://localhost:62620/",
7+
"sslPort": 44324
8+
}
9+
},
10+
"profiles": {
11+
"IIS Express": {
12+
"commandName": "IISExpress",
13+
"launchBrowser": true,
14+
"environmentVariables": {
15+
"ASPNETCORE_ENVIRONMENT": "Development"
16+
}
17+
},
18+
"SampleWebApp.Tests": {
19+
"commandName": "Project",
20+
"launchBrowser": true,
21+
"environmentVariables": {
22+
"ASPNETCORE_ENVIRONMENT": "Development"
23+
},
24+
"applicationUrl": "https://localhost:5001;http://localhost:5000"
25+
}
26+
}
27+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
6+
<IsPackable>false</IsPackable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="FluentAssertions" Version="5.10.3" />
11+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.13" />
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
13+
<PackageReference Include="xunit" Version="2.4.1" />
14+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
15+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
16+
<PrivateAssets>all</PrivateAssets>
17+
</PackageReference>
18+
<PackageReference Include="coverlet.collector" Version="3.0.3">
19+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
20+
<PrivateAssets>all</PrivateAssets>
21+
</PackageReference>
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<ProjectReference Include="..\..\Source\ContractModelsAttributeCheck\ContractModelsAttributeCheck.csproj" />
26+
<ProjectReference Include="..\SampleWebApp\SampleWebApp.csproj" />
27+
</ItemGroup>
28+
29+
</Project>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using ContractModelsAttributeCheck;
2+
using FluentAssertions;
3+
using Microsoft.AspNetCore.Mvc.ApiExplorer;
4+
using Microsoft.AspNetCore.Mvc.Testing;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using SampleWebApp.Contract.V1;
7+
using SampleWebApp.Contract.V2;
8+
using System;
9+
using System.Linq;
10+
using System.Text.Json.Serialization;
11+
using System.Threading.Tasks;
12+
using Xunit;
13+
14+
namespace SampleWebApp.Tests
15+
{
16+
public class ValidateContractModelsAttributesTest: IClassFixture<WebApplicationFactory<SampleWebApp.Startup>>
17+
{
18+
private readonly WebApplicationFactory<SampleWebApp.Startup> _factory;
19+
private readonly Type[] _attributes = new[] { typeof(JsonPropertyNameAttribute), typeof(JsonIgnoreAttribute) };
20+
21+
public ValidateContractModelsAttributesTest(WebApplicationFactory<SampleWebApp.Startup> factory)
22+
{
23+
_factory = factory;
24+
}
25+
26+
[Theory(DisplayName ="Don't include models that come FromQuery")]
27+
[InlineData("v1", typeof(PagingParameters))]
28+
[InlineData("v2", typeof(PagingParametersV2))]
29+
public void Query_V1_ContractModels(string apiVersion, Type typeNotInList)
30+
{
31+
// Arrange
32+
var apiProvider = _factory.Services.GetService<IApiDescriptionGroupCollectionProvider>();
33+
var apiInfoForVersion = apiProvider.ApiDescriptionGroups.Items.FirstOrDefault(w => w.GroupName == apiVersion);
34+
var modelFinder = new ApiContractModelsFinder();
35+
// Act
36+
var contractTypes = modelFinder.GetUsedContractTypes(apiInfoForVersion, "application/json");
37+
38+
// Assert
39+
contractTypes.GetAllTypes().Should().NotContain(typeNotInList);
40+
}
41+
42+
[Fact]
43+
public void V1Models_Dont_Have_Attributes()
44+
{
45+
// Arrange
46+
var apiProvider = _factory.Services.GetService<IApiDescriptionGroupCollectionProvider>();
47+
var apiInfoForVersion = apiProvider.ApiDescriptionGroups.Items.FirstOrDefault(w => w.GroupName == "v1");
48+
var modelFinder = new ApiContractModelsAttributeChecker();
49+
// Act
50+
var validationResults = modelFinder.CheckAttributesOfApiContractTypes(apiInfoForVersion, _attributes, "application/json");
51+
52+
// Assert
53+
var typesWithMissingAttributes = validationResults.Where(w => w.HasRequiredAttribute);
54+
typesWithMissingAttributes.Should().BeEmpty();
55+
}
56+
57+
[Fact]
58+
public void V2Models_Have_Attributes()
59+
{
60+
// Arrange
61+
var apiProvider = _factory.Services.GetService<IApiDescriptionGroupCollectionProvider>();
62+
var apiInfoForVersion = apiProvider.ApiDescriptionGroups.Items.FirstOrDefault(w => w.GroupName == "v2");
63+
var modelFinder = new ApiContractModelsAttributeChecker();
64+
// Act
65+
var validationResults = modelFinder.CheckAttributesOfApiContractTypes(apiInfoForVersion, _attributes, "application/json");
66+
67+
// Assert
68+
var typesWithMissingAttributes = validationResults.Where(w => !w.HasRequiredAttribute);
69+
typesWithMissingAttributes.Should().BeEmpty();
70+
}
71+
72+
[Fact]
73+
public void Validate_All_Models()
74+
{
75+
// Arrange
76+
var apiProvider = _factory.Services.GetService<IApiDescriptionGroupCollectionProvider>();
77+
var modelFinder = new ApiContractModelsAttributeChecker();
78+
// Act
79+
var validationResults = modelFinder.CheckAttributesOfApiContractTypes(apiProvider, _attributes, "application/json");
80+
81+
// Assert
82+
var typesWithMissingAttributes = validationResults.Where(w => !w.HasRequiredAttribute);
83+
typesWithMissingAttributes.Should().NotBeEmpty();
84+
}
85+
}
86+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.AspNetCore.Mvc.ApiExplorer;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.OpenApi.Models;
4+
using Swashbuckle.AspNetCore.SwaggerGen;
5+
6+
namespace SampleWebApp
7+
{
8+
public class ConfigureSwaggerOptions : Microsoft.Extensions.Options.IConfigureOptions<SwaggerGenOptions>
9+
{
10+
readonly IApiVersionDescriptionProvider provider;
11+
12+
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) =>
13+
this.provider = provider;
14+
15+
public void Configure(SwaggerGenOptions options)
16+
{
17+
foreach (var description in provider.ApiVersionDescriptions)
18+
{
19+
options.SwaggerDoc(
20+
description.GroupName,
21+
new OpenApiInfo()
22+
{
23+
Title = $"Sample API {description.ApiVersion}",
24+
Version = description.ApiVersion.ToString(),
25+
});
26+
}
27+
}
28+
}
29+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace SampleWebApp.Contract.V1
2+
{
3+
public class Product
4+
{
5+
public int Id { get; internal set; }
6+
public string Name { get; set; }
7+
}
8+
9+
public class PagingParameters
10+
{
11+
public int PageNo { get; set; }
12+
13+
public int PageSize { get; set; } = 20;
14+
}
15+
16+
public class ProductCreate
17+
{
18+
public string Name { get; set; }
19+
}
20+
21+
public class ErrorResponse
22+
{
23+
public int StatusCode { get; set; }
24+
25+
public string Message { get; set; }
26+
}
27+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace SampleWebApp.Contract.V2
4+
{
5+
public class ProductV2
6+
{
7+
[JsonPropertyName("id")]
8+
public int Id { get; internal set; }
9+
[JsonPropertyName("name")]
10+
public string Name { get; set; }
11+
}
12+
13+
public class PagingParametersV2
14+
{
15+
[JsonPropertyName("pageNo")]
16+
public int PageNo { get; set; }
17+
18+
[JsonPropertyName("pageSize")]
19+
public int PageSize { get; set; } = 20;
20+
}
21+
22+
public class ProductCreateV2
23+
{
24+
[JsonPropertyName("name")]
25+
public string Name { get; set; }
26+
}
27+
28+
public class ErrorResponseV2
29+
{
30+
[JsonPropertyName("statusCode")]
31+
public int StatusCode { get; set; }
32+
33+
[JsonPropertyName("message")]
34+
public string Message { get; set; }
35+
}
36+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.AspNetCore.Mvc;
3+
using SampleWebApp.Contract.V1;
4+
using Swashbuckle.AspNetCore.Annotations;
5+
using System.Collections.Generic;
6+
7+
namespace SampleWebApp.Controllers.V1
8+
{
9+
[ApiVersion("1.0")]
10+
[ApiController]
11+
[Route("api/v{version:apiVersion}/[controller]")]
12+
[Produces("application/json")]
13+
public class ProductsController
14+
{
15+
[HttpGet]
16+
[SwaggerResponse(StatusCodes.Status200OK, "ok", typeof(IEnumerable<Product>))]
17+
[SwaggerResponse(StatusCodes.Status500InternalServerError, "internal error", typeof(ErrorResponse))]
18+
public ActionResult<IEnumerable<Product>> GetProducts()
19+
{
20+
return new[]
21+
{
22+
new Product { Id = 1, Name = "A product" },
23+
new Product { Id = 2, Name = "Another product" },
24+
};
25+
}
26+
27+
[HttpGet]
28+
[Route("paged")]
29+
[SwaggerResponse(StatusCodes.Status200OK, "ok", typeof(IEnumerable<Product>))]
30+
[SwaggerResponse(StatusCodes.Status500InternalServerError, "internal error", typeof(ErrorResponse))]
31+
public ActionResult<IEnumerable<Product>> GetProductsPaged([FromQuery] PagingParameters _)
32+
{
33+
return new[]
34+
{
35+
new Product { Id = 1, Name = "A product" },
36+
new Product { Id = 2, Name = "Another product" },
37+
};
38+
}
39+
40+
41+
[HttpPost]
42+
[SwaggerResponse(StatusCodes.Status200OK, "ok", typeof(Product))]
43+
[SwaggerResponse(StatusCodes.Status500InternalServerError, "internal error", typeof(ErrorResponse))]
44+
public IActionResult CreateProduct([FromBody]ProductCreate _)
45+
{
46+
return new OkObjectResult(new Product());
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)