Skip to content

[FEATURE] Implement PATCH method for partial player updates #342

@nanotaboada

Description

@nanotaboada

Problem

Currently, the API only supports PUT operations for updating player data, which requires sending the entire player object even when only one or two fields need to be modified. This is inefficient and doesn't follow REST best practices for partial resource updates. Users need a way to update specific player fields without having to provide all player data.

Proposed Solution

Implement HTTP PATCH method endpoints to allow partial updates of player resources. The API should accept a PlayerPartialUpdateRequestModel and update only the fields provided in the request, leaving other fields unchanged.

Important constraint: Partial updates should allow modifying team-related data (position, squad number, team, league) but NOT personal details (FirstName, MiddleName, LastName, DateOfBirth). Personal information should only be modified via PUT (full update).

Endpoint to implement:

  • PATCH /players/squadnumber/{squadNumber} - Update specific fields of a player by squad number

Note: We use squad number (natural identifier) instead of internal ID, as squad numbers are unique and user-facing. The ID-based endpoint is admin-only.

Suggested Approach

1. Route/Controller Definition (Controllers/PlayerController.cs)

  • Add [HttpPatch("squadnumber/{squadNumber}")] endpoint with PatchBySquadNumberAsync(int squadNumber, PlayerPartialUpdateRequestModel model) method
  • Return TypedResults.Ok() (200), TypedResults.NotFound() (404), TypedResults.BadRequest() (400), or TypedResults.Conflict() (409)
  • Use primary constructor for dependency injection

2. Service Layer (Services/PlayerService.cs)

  • Create Task<PlayerResponseModel?> PatchBySquadNumberAsync(int squadNumber, PlayerPartialUpdateRequestModel model) method
  • If updating SquadNumber field itself, validate new squad number doesn't create duplicates
  • Clear cache after successful updates (_cache.Remove(CacheKey_RetrieveAsync))
  • Use repository layer to fetch, update, and save entity
  • Use AutoMapper to transform PlayerPartialUpdateRequestModel to entity updates
  • Add structured logging with ILogger<PlayerService>

3. Repository Layer (Repositories/PlayerRepository.cs)

  • Reuse existing repository method GetBySquadNumberAsync and UpdateAsync
  • Apply partial updates to entity using AutoMapper or manual property mapping
  • Ensure only non-null properties from request model are applied to entity

4. Model Definition (Models/)

  • Refactor existing models to follow Request/Response pattern:
    • Rename PlayerRequestModel to PlayerCreateRequestModel (for POST)
    • Create PlayerUpdateRequestModel class (for PUT - full replacement, all fields)
    • Create PlayerPartialUpdateRequestModel class with only team-related fields as nullable:
      public class PlayerPartialUpdateRequestModel
      {
          public int? SquadNumber { get; set; }
          public string? AbbrPosition { get; set; }
          public string? Team { get; set; }
          public string? League { get; set; }
          // Excludes: FirstName, MiddleName, LastName, DateOfBirth
      }
    • Keep PlayerResponseModel as is - already demonstrates data transformations:
      // Entity (Player) → Response (PlayerResponseModel) transformations:
      // - FirstName + MiddleName + LastName → FullName (concatenation)
      // - DateOfBirth (DateTime) → Birth (formatted string)
      // - SquadNumber → Dorsal (different naming)
      // - Team → Club (different naming)
      // - Starting11 (bool) → Starting11 ("Yes"/"No" string)
  • All models follow PascalCase with Model suffix
  • AutoMapper handles all transformations (configured in PlayerMappingProfile)

5. Validation (Validators/)

  • Create PlayerPartialUpdateRequestModelValidator with FluentValidation rules
  • Validate squad number range and uniqueness (async validator checking repository)
  • Validate position abbreviation using Position.FromAbbr() if position is provided
  • Register validator in ServiceCollectionExtensions.AddValidation()

6. AutoMapper Configuration (Mappings/PlayerMappingProfile.cs)

  • Add mapping from PlayerPartialUpdateRequestModel to Player entity
  • Configure to ignore null values (Condition(src => src != null))
  • Update existing mappings to reflect renamed models (PlayerCreateRequestModel, PlayerUpdateRequestModel)
  • Verify existing PlayerPlayerResponseModel mapping demonstrates data transformations:
    • FullName computed from FirstName + MiddleName + LastName
    • Birth formatted from DateOfBirth
    • Dorsal mapped from SquadNumber
    • Club mapped from Team
    • Starting11 converted from bool to "Yes"/"No" string
  • This mapping demonstrates AutoMapper's transformation capabilities

7. Testing (test/Dotnet.Samples.AspNetCore.WebApi.Tests/)

  • Create PlayerServiceTests test methods for PATCH operations using Given-When-Then pattern
  • Create PlayerControllerTests test methods for PATCH endpoint
  • Test partial updates (single field, multiple fields)
  • Test squad number uniqueness validation when updating to new squad number
  • Test 404 response for non-existent player by squad number
  • Test that personal details cannot be modified via PATCH
  • Test cache invalidation after PATCH operations
  • Use PlayerFakes and PlayerMocks utilities for test data
  • Add [Trait("Category", "Unit")] attributes
  • Follow AAA pattern (Arrange, Act, Assert)

8. Code Style

  • Use primary constructors for dependency injection
  • Format with CSharpier (opinionated style)
  • Follow async/await patterns consistently (async Task<T> return types)
  • Use var for local variables when type is obvious
  • Add XML documentation comments for public methods
  • Use structured logging patterns with Serilog

Acceptance Criteria

  • Models refactored to use Request/Response pattern:
    • PlayerResponseModel for GET operations
    • PlayerCreateRequestModel for POST operations (renamed from PlayerRequestModel)
    • PlayerUpdateRequestModel for PUT operations (new)
    • PlayerPartialUpdateRequestModel for PATCH operations (excludes personal details)
  • PATCH endpoint /players/squadnumber/{squadNumber} successfully updates team-related player fields
  • Personal details (FirstName, MiddleName, LastName, DateOfBirth) cannot be modified via PATCH
  • Only fields provided in request body are updated (other fields remain unchanged)
  • Squad number uniqueness is validated when updating to new squad number via FluentValidation
  • Appropriate HTTP status codes returned (200 OK, 404 Not Found, 400 Bad Request, 409 Conflict)
  • Cache is invalidated after successful PATCH operations
  • All tests pass with dotnet test
  • Test coverage includes new service and controller method
  • Code formatted with CSharpier
  • Swagger/OpenAPI documentation reflects new model names and PATCH endpoint
  • Structured logging includes relevant context (squad numbers)

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETPull requests that update .NET codeenhancementNew feature or requestplanningEnables automatic issue planning with CodeRabbitpriority mediumPlanned enhancement. Queue for upcoming work.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions