Skip to content

Latest commit

 

History

History
342 lines (255 loc) · 13.6 KB

7-Performance-Review.md

File metadata and controls

342 lines (255 loc) · 13.6 KB

Step 7 - Employee Performance Review

This will walk through the process of creating and testing the employee performance review capability.


Functional requirement

In simple terms, an employee performance review can occur at any time during an employee's employment. It captures the basic information: when, the reviewer, the outcome and related notes.


Data repository

A new PerformanceReview table and related Entity Framework (EF) model will be required to support.


Create Performance Review table

First step, as was previously the case, is to create the PerformanceReview table migration script. Execute the following within the MyEf.Hr.Database directory to create the migration script for the project.

dotnet run script create Hr PerformanceReview

Open the newly created migration script within MyEf.Hr.Database/Migrations and replace its contents with the following.

-- Create table: [Hr].[PerformanceReview]

BEGIN TRANSACTION

CREATE TABLE [Hr].[PerformanceReview] (
  [PerformanceReviewId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY,
  [EmployeeId] UNIQUEIDENTIFIER NOT NULL,
  [Date] DATETIME2 NULL,
  [PerformanceOutcomeCode] NVARCHAR(50) NULL,
  [Reviewer] NVARCHAR(100) NULL,
  [Notes] NVARCHAR(4000) NULL,
  [RowVersion] TIMESTAMP NOT NULL,
  [CreatedBy] NVARCHAR(250) NULL,
  [CreatedDate] DATETIME2 NULL,
  [UpdatedBy] NVARCHAR(250) NULL,
  [UpdatedDate] DATETIME2 NULL
);
	
COMMIT TRANSACTION

Create Reference Data table

To support the above a Hr.PerformanceOutcome reference data table is also required. Execute the following within the MyEf.Hr.Database directory to create the migration script. No further changes will be needed.

dotnet run script refdata Hr PerformanceOutcome

Reference Data data

Now that the Reference Data table exists it will need to be populated. Append the following YAML to the existing MyEf.Hr.Database/Data/RefData.yaml file.

  - $PerformanceOutcome:
    - DN: Does not meet expectations
    - ME: Meets expectations
    - EE: Exceeds expectations

Entity Framework model

For the performance review, Entity Framework will be used exclusively to support the full CRUD capabilities. Append the following after the other reference data tables within MyEf.Hr.Database/database.beef-5.yaml.

- name: PerformanceOutcome

Add the additional relationship to the Employee table by replacing existing table configuration.

  # References the Employee and related tables to implement the EF Model and infer the underlying schema.
- name: Employee
  relationships: [
    # Relationships can be code-generated (basic functionality), or handcrafted in the .NET code using the standard EntityFramework capabilities.
    # - One-to-many to EmergencyContacts table foreign key using EmployeeId column. Cascade the delete. Auto include collection on get and track for updates.
    { name: EmergencyContact, propertyName: EmergencyContacts, foreignKeyColumns: [ EmployeeId ], onDelete: ClientCascade, autoInclude: true },
    # - One-to-many to PerformanceReview table foreign key using EmployeeId column. Cascade the delete. Do _not_ auto include collection on get and track for updates (default).
    { name: PerformanceReview, propertyName: PerformanceReviews, foreignKeyColumns: [ EmployeeId ], onDelete: ClientCascade }
  ]

Finally add the PerformanceReview table to the end of the file to create the related Entity Framework model.

- name: PerformanceReview

Database management

Once the configuration has been completed then the database can be created/updated, the code-generation performed, and the corresponding reference data loaded into the corresponding tables.

At the command line execute the following command within the MyEf.Hr.Database directory to perform these updates. The log output will describe all actions that were performed.

dotnet run all

Reference Data API

The reference data API for the PerformanceOutcome needs to be added. Append the following to the end of refdata.beef.yaml within the MyEf.Hr.CodeGen project.

- { name: PerformanceOutcome, entityFrameworkModel: EfModel.PerformanceOutcome }

Once the reference data has been configured the code-generation can be performed. Use the following command line within the MyEf.Hr.CodeGen directory to generate.

dotnet run refdata

Performance Review API

To implement the core PerformanceReview operations the following will need to be performed:

  • Code configuration
  • Data access logic
  • Validation

Code generation

The PerformanceReview entity and operations configuration is required as follows, append to the end of entity.beef-5.yaml (MyEf.Hr.CodeGen project). Given that we are updating a single table row within the database, all of the operations will be able to be automatically implemented (generated) limiting the amount of code that needs to be added.

  # Creating a PerformanceReview entity
  # - Collection and CollectionResult required by GetByEmployeeId operation.
  # - WebApiRoutePrefix is explicitly specified overridding the default.
  # - Behavior specifies that a Get, Update, Patch and Delete operation should be implemeted following the standard pattern.
  # - EntityFrameworkModel specifies the corresponding model and will be auto-implemented (defaults from configuration hierarchy).
  # - EmployeeId is made immutable within mapper by specifing DataOperationTypes as AnyExceptUpdate; i.e. the value will never map (override) on an update.
- { name: PerformanceReview, collection: true, collectionResult: true, webApiRoutePrefix: reviews, behavior: gupd, validator: PerformanceReviewValidator, entityFrameworkModel: EfModel.PerformanceReview,
    properties: [
      { name: Id, type: Guid, text: '{{PerformanceReview}} identifier', primaryKey: true, dataName: PerformanceReviewId, dataAutoGenerated: true },
      { name: EmployeeId, text: '{{Employee.Id}} (value is immutable)', type: Guid, dataOperationTypes: AnyExceptUpdate },
      { name: Date, type: DateTime },
      { name: Outcome, type: ^PerformanceOutcome, dataName: PerformanceOutcomeCode },
      { name: Reviewer },
      { name: Notes },
      { name: ETag },
      { name: ChangeLog, type: ChangeLog }
    ],
    operations: [
      # Operations (in addition to the Get, Update, Patch and Delete).
      # GetByEmployeeId - this requires the EmployeeId to be passed in via the URI which is filtered within the developer extension.
      # Create - this requires the EmployeeId to be passed in via the URI which will override the value in the entity within the Manager layer (as defined by LayerPassing value of ToManagerSet).
      # The WebApiRoute with a ! prefix informs code-gen to use as-is versus prepending with the WebApiRoutePrefix; otherwise, would incorrectly result in 'reviews/employees/{employeeId}'.
      { name: GetByEmployeeId, type: GetColl, paging: true, webApiRoute: '!employees/{employeeId}/reviews',
        parameters: [
          { name: EmployeeId, text: '{{Employee}} identifier', type: Guid, isMandatory: true }
        ]
      },
      { name: Create, type: Create, webApiRoute: '!employees/{employeeId}/reviews',
        parameters: [
          { name: EmployeeId, text: '{{Employee}} identifier', type: Guid, layerPassing: ToManagerSet, isMandatory: true }
        ]
      }
    ]
  }

Once configured the code-generation can be performed. Use the following command line within the MyEf.Hr.CodeGen directory to generate.

dotnet run entity

Data access logic

The generated PerformanceReviewData.cs logic will need to be extended to support the filtering for the GetByEmployeeId operation. This logic must be implemented by the developer in a non-generated partial class. Create a new PerformanceReviewData.cs class within MyEf.Hr.Business/Data.

To implement the filtering, the extension delegate named _getByEmployeeIdOnQuery is implemented. Implement the class as follows:

namespace MyEf.Hr.Business.Data;

public partial class PerformanceReviewData
{
    partial void PerformanceReviewDataCtor()
    {
        _getByEmployeeIdOnQuery = (q, employeeId) => q.Where(x => x.EmployeeId == employeeId).OrderByDescending(x => x.Date);
    }
}

Validation

Within the MyEf.Hr.Business/Validation folder create PerformanceReviewValidator.cs and implement as follows:

namespace MyEf.Hr.Business.Validation;

/// <summary>
/// Represents a <see cref="PerformanceReview"/> validator.
/// </summary>
public class PerformanceReviewValidator : Validator<PerformanceReview>
{
    private readonly IPerformanceReviewManager _performanceReviewManager;
    private readonly IEmployeeManager _employeeManager;

    /// <summary>
    /// Initializes a new instance of the <see cref="PerformanceReviewValidator"/> class.
    /// </summary>
    public PerformanceReviewValidator(IPerformanceReviewManager performanceReviewManager, IEmployeeManager employeeManager)
    {
        _performanceReviewManager = performanceReviewManager ?? throw new ArgumentNullException(nameof(performanceReviewManager));
        _employeeManager = employeeManager ?? throw new ArgumentNullException(nameof(employeeManager));

        Property(x => x.EmployeeId).Mandatory();
        Property(x => x.Date).Mandatory().CompareValue(CompareOperator.LessThanEqual, _ => CoreEx.ExecutionContext.Current.Timestamp, _ => "today");
        Property(x => x.Notes).String(4000);
        Property(x => x.Reviewer).Mandatory().String(256);
        Property(x => x.Outcome).Mandatory().IsValid();
    }

    /// <summary>
    /// Add further validation logic.
    /// </summary>
    protected override async Task<Result> OnValidateAsync(ValidationContext<PerformanceReview> context, CancellationToken cancellationToken)
    {
        // Exit where the EmployeeId is not valid.
        if (context.HasError(x => x.EmployeeId))
            return Result.Success;

        // Ensure that the EmployeeId, on Update, has not been changed as it is immutable.
        if (ExecutionContext.OperationType == OperationType.Update)
        {
            var prr = await Result.GoAsync(_performanceReviewManager.GetAsync(context.Value.Id))
                                  .When(v => v is null, _ => Result.NotFoundError()).ConfigureAwait(false);

            if (prr.IsFailure)
                return prr.AsResult();

            if (context.Value.EmployeeId != prr.Value.EmployeeId)
                return Result.Done(() => context.AddError(x => x.EmployeeId, ValidatorStrings.ImmutableFormat));
        }

        // Check that the referenced Employee exists, and the review data is within the bounds of their employment.
        return await Result.GoAsync(_employeeManager.GetAsync(context.Value.EmployeeId)).Then(e =>
        {
            if (e == null)
                context.AddError(x => x.EmployeeId, ValidatorStrings.ExistsFormat);
            else
            {
                if (!context.HasError(x => x.Date))
                {
                    if (context.Value.Date < e.StartDate)
                        context.AddError(x => x.Date, "{0} must not be prior to the Employee starting.");
                    else if (e.Termination != null && context.Value.Date > e.Termination.Date)
                        context.AddError(x => x.Date, "{0} must not be after the Employee has terminated.");
                }
            }
        }).AsResult().ConfigureAwait(false);
    }
}

End-to-End testing

For the purposes of this sample peform the following:

For the end-to-end testing to function the performance review related data must first be populated into the database; append the following into the existing Data.yaml (MyEf.Hr.Test/Data).

  - PerformanceReview:
    - { PerformanceReviewId: 1, EmployeeId: 2, Date: 2014-03-15, PerformanceOutcomeCode: DN, Reviewer: [email protected], Notes: Work quality low. }
    - { PerformanceReviewId: 2, EmployeeId: 1, Date: 2016-11-12, PerformanceOutcomeCode: EE, Reviewer: [email protected], Notes: They are awesome! }
    - { PerformanceReviewId: 3, EmployeeId: 2, Date: 2014-01-15, PerformanceOutcomeCode: DN, Reviewer: [email protected], Notes: Work quality below standard. }

Review and execute the tests and ensure they all pass as expected.


Verify

At this stage we now have added and tested the performance review capabilities. All the desired functional requirements have now been implemented.

To verify, build the solution and ensure no compilation errors.

Within test explorer, run the PerformanceReviewTest and PerformanceReviewValidatorTest set of tests and confirm they all pass.

The following tests were newly added and should pass:

A110_Get
A110_Get_NotFound
A210_GetByEmployeeId_NotFound
A220_GetByEmployeeId
A220_GetByEmployeeId_Last
B110_Create
C110_Update_NotFound
C120_Update_Concurrency
C130_Update
E110_Delete
A110_Validate_Initial
A120_Validate_BadData
A130_Validate_BeforeStarting
A140_Validate_AfterTermination
A150_Validate_EmployeeNotFound
A160_Validate_EmployeeImmutable
A170_Validate_CreateOK
A180_Validate_UpdateOK

Next Step

Next we will consider the addition of Event-driven architecture capabilities, starting by implementing the Transactional Outbox