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
1 change: 1 addition & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ exclude_paths:
- '**/Models/**' # Domain and DTO models
- '**/Properties/**' # launchSettings.json or AssemblyInfo.cs
- '**/Utilities/**' # Helper extensions or static classes
- '**/Validators/**' # FluentValidation validators
- 'test/**/*' # Entire test suite (unit + integration)
2 changes: 1 addition & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
uses: actions/checkout@v4

- name: Run tests and generate Cobertura coverage reports
run: dotnet test --results-directory "coverage" --collect:"Code Coverage;Format=cobertura" --settings .runsettings
run: dotnet test --results-directory "coverage" --collect:"XPlat Code Coverage" --settings .runsettings

- name: Install dotnet-coverage tool
run: dotnet tool install --global dotnet-coverage
Expand Down
7 changes: 5 additions & 2 deletions .runsettings
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="Code Coverage" uri="datacollector://Microsoft/CodeCoverage/2.0">
<DataCollector friendlyName="XPlat Code Coverage">
<Configuration>
<Format>cobertura</Format>
<SkipAutoProps>true</SkipAutoProps>
<BasePath>$(MSBuildProjectDirectory)</BasePath>
<CodeCoverage>
<!-- Exclude all test projects -->
<ModulePaths>
<!-- Exclude all test projects -->
<Exclude>
<ModulePath>.*\.Tests\.dll$</ModulePath>
</Exclude>
Expand Down
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@
"[csharp]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "csharpier.csharpier-vscode"
},
"sonarlint.connectedMode.project": {
"connectionId": "nanotaboada",
"projectKey": "nanotaboada_Dotnet.Samples.AspNetCore.WebApi"
}
}
77 changes: 38 additions & 39 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
# Codecov repository YAML

# Codecov Repository YAML
# https://docs.codecov.com/docs/codecov-yaml
coverage:

# https://docs.codecov.com/docs/commit-status
coverage:
# https://docs.codecov.com/docs/commit-status
status:

project:
default:
target: 80% # Default target for all components
threshold: 10% # Allowable drop in coverage without failing
if_not_found: success # If no coverage report is found, don't fail
if_ci_failed: error # CI failure should fail coverage check

patch:
default:
target: 80% # Target for changed lines
threshold: 10% # Allowable drop in coverage without failing
target: 80%
threshold: 100%
if_not_found: success
if_ci_failed: success
patch:
default:
target: 0%
threshold: 100%
if_not_found: success

# https://docs.codecov.com/docs/components#component-options
component_management:

default_rules: # default rules that will be inherited by all components
default_rules:
statuses:
- type: project # components that don't have a status defined will have a project type one
- type: project
target: auto
branches:
- "!main"

individual_components:
- component_id: controllers # this is an identifier that should not be changed
name: Controllers # this is a display name, and can be changed freely
- component_id: controllers
name: Controllers
paths:
- 'src/Dotnet.Samples.AspNetCore.WebApi/Controllers/'
- component_id: services
Expand All @@ -38,28 +37,28 @@ component_management:

comment:
layout: "header, diff, flags, components"
behavior: default
require_changes: false

# https://docs.codecov.com/docs/ignoring-paths
ignore:
# Ignoring specific file yypes
- '**/*.sln' # Solution files
- '**/*.csproj' # C# project files
- '**/*.json' # JSON config files (e.g., appsettings)
- '**/*.yml' # YAML config files (e.g., pipelines)
- '**/*.png' # Image assets (e.g., Swagger diagram)
- .*\.sln
- .*\.csproj
- .*\.json
- .*\.yml
- .*\.png
- '**/*.md'

# Ignoring a specific folder
- 'src/**/Data/**/*' # Repositories, DbContext, database files
- 'src/**/Enums/**/*' # Enums like Position
- 'src/**/Mappings/**/*' # AutoMapper profiles
- 'src/**/Migrations/**/*' # EF Core migration artifacts
- 'src/**/Models/**/*' # Domain and DTO models
- 'src/**/Properties/**/*' # launchSettings.json or other system files
- 'src/**/Utilities/**/*' # Static helper and extension classes
- 'test' # Any file in the test folder (unit/integration/utils)
- .*\/test\/.*
- .*\/Program\.cs
- '**/LICENSE'
- '**/README.md'

# Ignoring Specific Files At All Depths
- '**/Program.cs' # ASP.NET Core entry point
- '**/.gitignore' # Git ignore file
- '**/LICENSE' # License text
- '**/README.md' # Project readme
- .*\/Data\/.*
- .*\/Enums\/.*
- .*\/Mappings\/.*
- .*\/Migrations\/.*
- .*\/Models\/.*
- .*\/Properties\/.*
- .*\/Utilities\/.*
- .*\/Validators\/.*
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
using System.Net.Mime;
using Dotnet.Samples.AspNetCore.WebApi.Models;
using Dotnet.Samples.AspNetCore.WebApi.Services;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;

namespace Dotnet.Samples.AspNetCore.WebApi.Controllers;

[ApiController]
[Route("[controller]")]
[Produces("application/json")]
public class PlayerController(IPlayerService playerService, ILogger<PlayerController> logger)
: ControllerBase
public class PlayerController(
IPlayerService playerService,
ILogger<PlayerController> logger,
IValidator<PlayerRequestModel> validator
) : ControllerBase
{
private readonly IPlayerService _playerService = playerService;
private readonly ILogger<PlayerController> _logger = logger;
private readonly IValidator<PlayerRequestModel> validator = validator;

/* -------------------------------------------------------------------------
* HTTP POST
Expand All @@ -30,25 +35,32 @@
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IResult> PostAsync([FromBody] PlayerRequestModel player)
{
if (!ModelState.IsValid)
var validation = await validator.ValidateAsync(player);

if (!validation.IsValid)
{
return TypedResults.BadRequest();
var errors = validation
.Errors.Select(error => new { error.PropertyName, error.ErrorMessage })
.ToArray();

_logger.LogWarning("POST validation failed: {@Errors}", errors);
return TypedResults.BadRequest(errors);

Check warning on line 49 in src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 11 lines is too similar to src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs:155
}
else if (await _playerService.RetrieveByIdAsync(player.Id) != null)

if (await _playerService.RetrieveByIdAsync(player.Id) != null)
{
return TypedResults.Conflict();
}
else
{
var result = await _playerService.CreateAsync(player);
return TypedResults.CreatedAtRoute(
routeName: "GetById",
routeValues: new { id = result.Id },
value: result
);
}

var result = await _playerService.CreateAsync(player);

return TypedResults.CreatedAtRoute(
routeName: "GetById",
routeValues: new { id = result.Id },
value: result
);
}

/* -------------------------------------------------------------------------
Expand Down Expand Up @@ -140,22 +152,28 @@
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> PutAsync([FromRoute] long id, [FromBody] PlayerRequestModel player)
{
if (!ModelState.IsValid)
var validation = await validator.ValidateAsync(player);

if (!validation.IsValid)
{
return TypedResults.BadRequest();
var errors = validation
.Errors.Select(error => new { error.PropertyName, error.ErrorMessage })
.ToArray();

_logger.LogWarning("PUT /players/{Id} validation failed: {@Errors}", id, errors);
return TypedResults.BadRequest(errors);

Check warning on line 166 in src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 11 lines is too similar to src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs:38
}
else if (await _playerService.RetrieveByIdAsync(id) == null)

if (await _playerService.RetrieveByIdAsync(id) == null)
{
return TypedResults.NotFound();
}
else
{
await _playerService.UpdateAsync(player);

return TypedResults.NoContent();
}
await _playerService.UpdateAsync(player);

return TypedResults.NoContent();
}

/* -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
Expand Down
53 changes: 41 additions & 12 deletions src/Dotnet.Samples.AspNetCore.WebApi/Enums/Position.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,47 @@ public class Position : Enumeration
public static readonly Position LeftWinger = new(11, "Left Winger", "LW");

private Position(int id, string name, string abbr)
: base(id, name)
{
Abbr = abbr;
}
: base(id, name) => Abbr = abbr;

public static Position? FromAbbr(string abbr)
{
return GetAll<Position>().FirstOrDefault(position => position.Abbr == abbr);
}
/// <summary>
/// Returns a Position object based on the abbreviation.
/// </summary>
/// <remarks>
/// This method searches through all the Position objects and returns the one
/// that matches the provided abbreviation. If no match is found, it returns null.
/// </remarks>
/// <param name="abbr">The abbreviation of the Position.</param>
/// <returns>
/// A Position object if found; otherwise, null.
/// </returns>
public static Position? FromAbbr(string abbr) =>
GetAll<Position>().FirstOrDefault(position => position.Abbr == abbr);

public static Position? FromId(int id)
{
return GetAll<Position>().FirstOrDefault(position => position.Id == id);
}
/// <summary>
/// Returns a Position object based on the ID.
/// </summary>
/// <remarks>
/// This method searches through all the Position objects and returns the one
/// that matches the provided ID. If no match is found, it returns null.
/// </remarks>
/// <param name="id">The ID of the Position.</param>
/// <returns>
/// A Position object if found; otherwise, null.
/// </returns>
public static Position? FromId(int id) =>
GetAll<Position>().FirstOrDefault(position => position.Id == id);

/// <summary>
/// Checks if the provided abbreviation is valid.
/// </summary>
/// <remarks>
/// This method checks if the provided abbreviation is not null or empty and
/// if it corresponds to a valid Position object.
/// </remarks>
/// <param name="abbr">The abbreviation to check.</param>
/// <returns>
/// True if the abbreviation is valid; otherwise, false.
/// </returns>
public static bool IsValidAbbr(string? abbr) =>
!string.IsNullOrWhiteSpace(abbr) && FromAbbr(abbr!) is not null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,21 @@
/// the required fields are provided and that the data is in the correct format.
/// </remarks>
public class PlayerRequestModel
{
public long Id { get; set; }

[Required]
public string? FirstName { get; set; }

public string? MiddleName { get; set; }

[Required]
public string? LastName { get; set; }

public DateTime? DateOfBirth { get; set; }

[Required]
public int SquadNumber { get; set; }

[Required]
public string? AbbrPosition { get; set; }

Check warning on line 28 in src/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerRequestModel.cs

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 14 lines is too similar to src/Dotnet.Samples.AspNetCore.WebApi/Models/Player.cs:12
public string? Team { get; set; }

public string? League { get; set; }
Expand Down
32 changes: 10 additions & 22 deletions src/Dotnet.Samples.AspNetCore.WebApi/Program.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.Reflection;
using Dotnet.Samples.AspNetCore.WebApi.Data;
using Dotnet.Samples.AspNetCore.WebApi.Mappings;
using Dotnet.Samples.AspNetCore.WebApi.Models;
using Dotnet.Samples.AspNetCore.WebApi.Services;
using Dotnet.Samples.AspNetCore.WebApi.Utilities;
using Dotnet.Samples.AspNetCore.WebApi.Validators;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using Serilog;
Expand Down Expand Up @@ -44,34 +47,19 @@
builder.Services.AddScoped<IPlayerService, PlayerService>();
builder.Services.AddMemoryCache();
builder.Services.AddAutoMapper(typeof(PlayerMappingProfile));
builder.Services.AddScoped<IValidator<PlayerRequestModel>, PlayerRequestModelValidator>();

if (builder.Environment.IsDevelopment())
{
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc(
"v1",
new OpenApiInfo
{
Version = "1.0.0",
Title = "Dotnet.Samples.AspNetCore.WebApi",
Description =
"🧪 Proof of Concept for a Web API (Async) made with .NET 8 (LTS) and ASP.NET Core 8.0",
Contact = new OpenApiContact
{
Name = "GitHub",
Url = new Uri("https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi")
},
License = new OpenApiLicense
{
Name = "MIT License",
Url = new Uri("https://opensource.org/license/mit")
}
}
options.SwaggerDoc("v1", builder.Configuration.GetSection("SwaggerDoc").Get<OpenApiInfo>());
options.IncludeXmlComments(
Path.Combine(
AppContext.BaseDirectory,
$"{Assembly.GetExecutingAssembly().GetName().Name}.xml"
)
);

var filePath = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, filePath));
});
}

Expand Down
Loading
Loading