Table of Contents

πŸ” Search Users with Filters

Now that you can create users, let’s implement a query endpoint that allows clients to search for users using filters, pagination, and sorting β€” all powered by PotatoFinder.

πŸ“ Place all files for this feature in:

Potato.Example.Application/Features/Users/Search/

πŸ’‘ Protip: Add a Usings.cs file to avoid repeating common imports:

global using static LanguageExt.Prelude;
global using static Potato.PotatoModule;
global using LanguageExt;
global using Potato;
global using Potato.Example.Domain;

πŸ“„ SearchUsersQuery.cs

namespace Potato.Examples.Application;

/// <summary>
/// A flexible search query for users using PotatoFilter, pagination, and optional sorting.
/// </summary>
/// <param name="Filter">The filter to apply to the node tree.</param>
/// <param name="Cursor">Pagination options (e.g. first, after).</param>
/// <param name="Sort">Optional sorting by property and direction.</param>
public record SearchUsersQuery(
    PotatoFilter Filter,
    PotatoCursor Cursor,
    PotatoSort? Sort
) : PotatoQuery<PotatoEdgeCollection<UserNode>>;

πŸ“„ SearchUsersQuery.Handler.cs

using System;
using System.Diagnostics;
using Microsoft.Extensions.Logging;

namespace Potato.Examples.Application;

/// <summary>
/// Handles execution of <see cref="SearchUsersQuery"/> to retrieve paginated user nodes.
/// Automatically enforces type filtering to ensure only <see cref="UserNode"/> results are returned.
/// </summary>
public class SearchUsersQueryHandler(
    ILogger<SearchUsersQueryHandler> logger,
    PotatoFinder<Node> finder
) : PotatoQueryHandler<SearchUsersQuery, PotatoEdgeCollection<UserNode>>
{
    /// <summary>
    /// Handles the <see cref="SearchUsersQuery"/> by applying the provided filter, sort, and pagination parameters.
    /// Ensures only <see cref="UserNode"/> results are returned, even if the input filter is broader.
    /// </summary>
    /// <param name="query">The search query containing filter, cursor, and optional sort.</param>
    /// <param name="cancellationToken">A cancellation token for cooperative request cancellation.</param>
    /// <returns>A paginated collection of <see cref="UserNode"/> results or a failure.</returns>
    public override async Task<Either<PotatoFailure, PotatoEdgeCollection<UserNode>>> Handle(SearchUsersQuery query, CancellationToken cancellationToken)
    {
        try
        {
            using Activity? activity = Activity.Current?.Source.StartActivity();

            logger.LogQueryRecieved(query);

            return await finder.Find(
                    Filter: PotatoFilter.And(query.Filter, PotatoFilter.OfType<UserNode>()),
                    Cursor: query.Cursor,
                    Sort: query.Sort
                )
                .MapAsync(c => c.Cast<UserNode>())
                .HandlerFinished(logger, query);
        }
        catch (Exception ex)
        {
            logger.LogQueryFailed(query, ex);
            return UnexpectedFailure(ex);
        }
    }
}

πŸ“„ SearchUsersQuery.Extension.cs

using Microsoft.AspNetCore.Mvc;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Maps the search endpoint for querying <see cref="UserNode"/>s.
/// </summary>
public static class SearchUsersQueryExtension
{
    public static RouteHandlerBuilder MapSearchUsers(this WebApplication app)
    {
        return app.MapPut("/users/search", async (
            [FromBody] SearchUsersQuery query,
            [FromServices] PotatoCommandExecutor executor
        ) =>
        {
            return await executor.Execute<SearchUsersQuery, PotatoEdgeCollection<UserNode>>(query);
        });
    }
}

πŸ“Œ Register the endpoint in Program.cs:

app.MapSearchUsers();

πŸ“„ Potato.Example.Application.http

Use this file in VS Code with the REST Client extension to test both CreateUserCommand and SearchUsersQuery.

@host = http://localhost
@port = 5122

PUT {{host}}:{{port}}/users/create
Content-Type: application/json

{
  "username": "JohnDoe"
}

###

PUT {{host}}:{{port}}/users/search
Content-Type: application/json

{
  "filter": {
    "$type": "PotatoPropertyEqualsFilter",
    "property": "Username",
    "value": "JohnDoe"
  },
  "cursor": {
    "first": 10
  },
  "sort": {
    "property": "Username",
    "direction": "Ascending"
  }
}

βœ… Summary

You now have:

  • βœ… A SearchUsersQuery to filter, page, and sort users
  • βœ… A handler that uses PotatoFinder and filters only UserNode entries
  • βœ… A minimal API endpoint wired with PotatoCommandExecutor
  • βœ… A .http file for fast testing with REST Client

➑️ Next: Add OpenAPI schema filters and examples to your Swagger docs.

Would you like help drafting the OpenAPI schema filter for UserNode?