Skip to content

Latest commit

 

History

History
266 lines (191 loc) · 13.3 KB

5-Employee-Search.md

File metadata and controls

266 lines (191 loc) · 13.3 KB

Step 5 - Employee Search

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


Functional requirement

The employee search will allow the following criteria to be searched:

  • First and last name using wildcard (starts with, ends with and contains); e.g. abc*, *abc or *abc*.
  • Gender selection; zero, one or more.
  • Start date range (from/to).
  • Option to include terminated employees (default is to exclude).

The search should also support paging. The results should be returned in last name, first name and start date order.

Examples of the endpoint are as follows (or any combination thereof).

Endpoint Description
GET /employees?lastName=smi* all employees whose last name starts with smi.
GET /employees?gender=f all female employees.
GET /employees?startFrom=2000-01-01&startTo=2002-12-31 all employess who started between 01-Jan-2000 and 31-Dec-2002 (inclusive).
GET /employess?lastName=smi*&includeTerminated=true all employees whose last name starts with smi, both current and terminated.
GET /employees?gender=f&$skip=10&$take25 all female employees with paging (skipping the first 10 employees and getting the next 25 in sequence).

Data repository

During the initial database set up process the Employee table was enabled via code-generation leveraging an Entity Framework model class. This will be used to perform the query operations so that we can leverage .NET's LINQ-style query filtering and sorting.


Selection criteria

The API selection criteria will be enabled by defining a class with all of the requisite properties. Beef will then expose each of the properties individually within the API Controller to enable their usage. Beef can also automatically enable paging support when selected to do so; therefore, this does not need to be enabled via the selection criteria directly.

Add the following entity code-gen configuration after all the other existing entities within entity.beef-5.yaml (MyEf.Hr.CodeGen project).

  # Creating an EmployeeArgs entity
  # - Genders will support a list (none or more) reference data values.
  # - StartFrom, StartTo and IncludeTerminated are all Nullable so we can tell whether a value was provided or not.
  # - The IsIncludeTerminated overrides the JsonName to meet the stated requirement name of includeTerminated.
- { name: EmployeeArgs, text: '{{Employee}} search arguments',
    properties: [
      { name: FirstName },
      { name: LastName },
      { name: Genders, type: ^Gender, refDataList: true },
      { name: StartFrom, type: 'DateTime?', dateTimeTransform: DateOnly },
      { name: StartTo, type: 'DateTime?', dateTimeTransform: DateOnly },
      { name: IsIncludeTerminated, jsonName: includeTerminated, type: 'bool?' }
    ]
  }

The requisite GetByArgs operation needs to be added to the Employee entity configuration.

Add the following to the array of operations after the existing Delete operation.

      # Search operation
      # - Type is GetColl that indicates that a collection is the expected result for the query-based operation.
      # - ReturnType is overriding the default Employee as we want to use EmployeeBase (reduced set of fields).
      # - Paging indicates that paging support is required and to be automatically enabled for the operation.
      # - Parameter specifies that a single parameter with a type of EmployeeArgs (defined later) is required and that the value should be validated.
      { name: GetByArgs, type: GetColl, paging: true, returnType: EmployeeBase,
        parameters: [
          { name: Args, type: EmployeeArgs, validator: EmployeeArgsValidator }
        ]
      }

Execute the code-generation using the command line (within MyEf.Hr.CodeGen base directory).

dotnet run entity

Data access logic

Within the entity code-gen configuration where auto-implementation is not possible then the data access logic must be specified explicitly. Beef will still generate the boilerplate/skeleton wrapping logic for the data access to ensure consistency, and will invoke a corresponding *OnImplementation method to perform (which the developer is then required to implement).

This logic must be implemented by the developer in a non-generated partial class. A new EmployeeData.cs must be created within MyEf.Hr.Business/Data; do not change any files under the Generated folder as these will be overridden during the next code-gen execution.

This new EmployeeData.cs (non-generated) logic will need to be extended to support the new GetByArgs.

For query operations generally we do not implement using the custom *OnImplementation approach; as the primary code, with the exception of the actual search criteria can be generated successfully. As such, in this case Beef will have generated an extension delegate named _getByArgsOnQuery to enable. This extension delegate will be passed in the IQueryable<EfModel.Employee> so that filtering and sorting, etc. can be applied, as well as the search arguments (EmployeeArgs). Note: no paging is passed, or needs to be applied, as Beef will apply this automatically.

Extensions within Beef are leveraged by implementing the partial constructor method (EmployeeDataCtor) and providing an implementation for the requisite extension delegate (_getByArgsOnQuery). The With methods are enabled by CoreEx to simplify the code logic to apply the filter only where the value is not null, plus specifically handle the likes of wildcards. Also note usage of IgnoreAutoIncludes (a standard Entity Framework capability) to avoid the cost of loading related data that is not needed for this query.

Within the MyEf.Hr.Business/Data folder, create EmployeeData.cs and implement as follows:

partial void EmployeeDataCtor()
{
    // Implement the GetByArgs OnQuery search/filtering logic.
    _getByArgsOnQuery = (q, args) =>
    {
        _ef.WithWildcard(args?.FirstName, w => q = q.Where(x => EF.Functions.Like(x.FirstName!, w)));
        _ef.WithWildcard(args?.LastName, w => q = q.Where(x => EF.Functions.Like(x.LastName!, w)));
        _ef.With(args?.Genders, g => q = q.Where(x => g.ToCodeList().Contains(x.GenderCode)));
        _ef.With(args?.StartFrom, f => q = q.Where(x => x.StartDate >= f));
        _ef.With(args?.StartTo, t => q = q.Where(x => x.StartDate <= t));

        if (args?.IsIncludeTerminated is null || !args.IsIncludeTerminated.Value)
            q = q.Where(x => x.TerminationDate == null);

                return q.IgnoreAutoIncludes().OrderBy(x => x.LastName).ThenBy(x => x.FirstName).ThenBy(x => x.StartDate);
            };
        }
    }
}

Validation

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

namespace MyEf.Hr.Business.Validation;

/// <summary>
/// Represents a <see cref="EmployeeArgs"/> validator.
/// </summary>
public class EmployeeArgsValidator : Validator<EmployeeArgs>
{
    /// <summary>
    /// Initializes a new instance of the <see cref="EmployeeValidator"/> class.
    /// </summary>
    public EmployeeArgsValidator()
    {
        Property(x => x.FirstName).Common(CommonValidators.PersonName).Wildcard();
        Property(x => x.LastName).Common(CommonValidators.PersonName).Wildcard();
        Property(x => x.Genders).AreValid();
        Property(x => x.StartFrom).CompareProperty(CompareOperator.LessThanEqual, x => x.StartTo);
    }
}

End-to-End testing

Now that we've implemented GetByArgs search functionality, we can re-add the appropriate tests. Do so by un-commenting the region GetByArgs within MyEf.Hr.Test/Apis/EmployeeTest.cs.

As extra homework, you should also consider implementing unit testing for the validator.


Verify

At this stage we now have added and tested the employee search, in addition to the employee CRUD APIs.

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

Check the output of code gen tool. There should have been 2 new and 9 updated files similar to the below output:

MyEf.Hr.CodeGen Complete. [1818ms, Files: Unchanged = 16, Updated = 9, Created = 2, TotalLines = 1584]

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

The following tests were newly added and should pass:

A210_GetByArgs_All
A220_GetByArgs_All_Paging
A230_GetByArgs_FirstName
A240_GetByArgs_LastName
A250_GetByArgs_LastName_IncludeTerminated
A260_GetByArgs_Gender
A270_GetByArgs_Empty
A280_GetByArgs_FieldSelection
A290_GetByArgs_RefDataText
A300_GetByArgs_ArgsError

OData-like Query

The above search capability is great and for most use cases is perfectly acceptable; however, it is not as flexible as the likes of OData or GraphQL.

Therefore, depending on the functional needs a more flexible query capability may be required. There is basic, explicit, OData-like support available where this need arises; this will now be added to the MyEf.Hr solution. See CoreEx.Data.Querying for more information on the underlying implementation.

Note: Where OData and GraphQL are specifically required then they would need to be implemented separately as the Avanade accelerators do not provide this functionality out-of-the-box.


OData-like requirements

A single query endpoint will be added to enable, and support the same requirements using OData-like filtering:

Endpoint Description
GET /employees/query?$filter=startswith(lastName, 'smi*') all employees whose last name starts with smi.
GET /employees/query?$filter=gender eq 'f' all female employees.
GET /employees/query?$filter=startdate ge 2000-01-01 and startdate le 2002-12-31 all employess who started between 01-Jan-2000 and 31-Dec-2002 (inclusive).
GET /employees/query?$filter=startswith(lastName, 'smi*') and (terminated eq null || terminated ne null) all employees whose last name starts with smi, both current and terminated.
GET /employees?gender=f&$skip=10&$take25 all female employees with paging (skipping the first 10 employees and getting the next 25 in sequence).

Code-generation

The following code-gen should be added after the GetByArgs configuration; this will add the query endpoint to the Employee entity.

      # Query operation
      # - Type is GetColl that indicates that a collection is the expected result for the query-based operation.
      # - ReturnType is overriding the default Employee as we want to use EmployeeBase (reduced set of fields).
      # - Query indicates that OData-like $filter/$order support is to be enabled.
      # - Paging indicates that paging support is required and to be automatically enabled for the operation.
      # - WebApiRoute specifies the route of 'query' to be used for the operation.
      { name: GetByQuery, type: GetColl, query: true, paging: true, returnType: EmployeeBase, webApiRoute: query }

Execute the code-generation again using the command line (within MyEf.Hr.CodeGen base directory) and the various artefacts will be updated (generated).

Data access logic

The EmployeeData.cs partial class (non generated) will need to be extended to support the new GetByQuery operation.

Firstly, the QueryArgsConfig must be instantiated with the desired configuration. This will explicitly add support for the specified fields to achieve the desired functionality:

public partial class EmployeeData
{
    private static readonly QueryArgsConfig _config = QueryArgsConfig.Create()
        .WithFilter(filter => filter
            .AddField<string>(nameof(Employee.LastName), c => c.WithOperators(QueryFilterOperator.AllStringOperators).WithUpperCase())
            .AddField<string>(nameof(Employee.FirstName), c => c.WithOperators(QueryFilterOperator.AllStringOperators).WithUpperCase())
            .AddReferenceDataField<Gender>(nameof(Employee.Gender), nameof(EfModel.Employee.GenderCode))
            .AddField<DateTime>(nameof(Employee.StartDate))
            .AddNullField(nameof(Employee.Termination), nameof(EfModel.Employee.TerminationDate), c => c.WithDefault(new QueryStatement($"{nameof(EfModel.Employee.TerminationDate)} == null"))))
        .WithOrderBy(orderby => orderby
            .AddField(nameof(Employee.LastName))
            .AddField(nameof(Employee.FirstName))
            .WithDefault($"{nameof(Employee.LastName)}, {nameof(Employee.FirstName)}"));

Secondly, the GetByQuery operation must be implemented. Add the following to the end of the EmployeeDataCtor method to enable the functionality:

        // Implement the GetByQuery OnQuery search/filtering logic using OData-like query syntax.
        _getByQueryOnQuery = (q, args) => q.IgnoreAutoIncludes().Where(_config, args).OrderBy(_config, args);

End-to-End testing

Now that we've implemented GetByQuery search functionality, we can re-add the appropriate tests. Do so by un-commenting the region GetByQuery within MyEf.Hr.Test/Apis/EmployeeTest.cs. Within test explorer, run the EmployeeTest set of tests and confirm they all pass.


Next Step

Next we will implement the employee termination endpoint.