1

I'm trying to make a simple REST api with the ability to filter resources with some simple url query parameters e.g /Profile?firstName=Brian&age=26. I don't need any advanced filtering just find anything that matches all.

I'm using Minimal APIs in .NET 6 and MongoDB.Driver (2.17.1).

I've got a "solution" that works but its error prone and isn't very scalable or flexible.

How would like be able to read the query parameters without hardcoding the specific name and type for each endpoint without losing the type of the parameter. If possible use the Filter builder instead of json.

Current "solution"

Part of the Profile class

public class Profile : Entity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    ...
    public List<BasePreference> Preferences { get; set; }

    public Profile()
    {
        Preferences = new List<BasePreference>();
    }
}

App.MapGet("/profile",Test2);

private static async Task<IResult> Test2(
    IRepository<Profile> repo,
    string? firstName,
    string? lastName,
    int? age)
{
    Dictionary<string, object?> queryDict= new Dictionary<string, object?>();
    queryDict.Add(nameof(firstName),firstName);
    queryDict.Add(nameof(lastName),lastName);
    queryDict.Add(nameof(age), age);

    foreach (var pair in queryDict)
    {
        if(pair.Value is null)
        {
            data.Remove(pair.Key);
        }
    }

    var query = JsonSerializer.Serialize(data);

    var content = await repo.ReadMany(query);

    return Results.Ok(content);
}

MongoDB Repository snippet

private IMongoCollection<T> collection;

public MongoDBRepository(IOptions<MongoDBSettings> mongoDBSettings)
{
    MongoClient client = new MongoClient(mongoDBSettings.Value.ConnectionURI);

    IMongoDatabase database = client.GetDatabase(mongoDBSettings.Value.DatabaseName);

    collection = database.GetCollection<T>(typeof(T).Name.ToLower());
}

public async Task<List<T>> ReadMany(string query)
{
    FilterDefinition<T> filter = query;

    List<T> list = await collection.Find(filter).ToListAsync();

    return list;
}

Example document in MongoDB

My problems with this approach is manifold:

  • Repeatedly implementing this solution for multiple resources would be mind-numbing and error prone
  • Capitalization needs to match inside MongoDB exactly
  • Changes to Model classes would require multiple changes in different parts of the project
  • It can't handle any "complex" data types. e.g DateTime, Guid.

Things I've tried

  • I've tried just converting everything to a Json string and ignoring types, but age:"25" =/= age:25 according to MongoDB.

  • I've then searched for ways to allow "25" == 25 in mongoDB but haven't had any luck

  • I've tried creating a ProfileQueryModel that only contains the queryable properties and then using [FromParameters] but couldn't get the binding to work, and was also unsure how to convert it to a FilterDefinition

  • I've tried mapping the search query to a Dictionary<string,object> but with no luck. I've read that .NET 7 might include a way to make that possible, but that hasn't released yet.

  • I've tried having the query parameters just be a single JSON object e.g /profiles?query={"firstName":"Brian","age":25"} but was once again stuck on isn't a string

What I'm looking for

I don't need to be able to filter elements in arrays or sub objects. Just simple equality on all given fields.

Ideally, I'd want a solution where I don't use Json as a FilterDefinition as that seems less likely to break with more advanced data types and would also remove the need for any Json conversions. But I've been pulling my hair out for over a week now and I'm happy with any improvement I can get.

Thanks in advance and thanks for reading this far.

1 Answer 1

0

Can you just iterate over the parameters and build the filter string for mongo?

like:

 foreach(string key in Request.QueryString) 
{
    var value = Request.QueryString[key];
}

see also: similar

Added: I did it like this, with help from this post by looping throught the Request.Query and using a FilterDefinitionBuilder

Route:

[HttpGet]
        [Route("dynamicFilter")]
        public async Task<IActionResult> GetFilter()
        {
            if (Request.Query != null)
            {
                var stream = await _Repository.GetFilter(Request.Query);
                if (stream == null)
                {
                    return NotFound();
                }
                return Ok(stream);
            }

            return BadRequest("Parameter not found or is invalid");    
        }

        public async Task<List<Message>> GetFilter(IQueryCollection iRequest)
        {
            FilterDefinition<Message> filterDef = BuildFilterDef(iRequest);
            //List<Message> lTest = _Collection.Find(filterDef).ToList();
            return await _Collection.Find(filterDef).ToListAsync();
        }

BuildFilter (Query string parse)

public static FilterDefinition<Message> BuildFilterDef(IQueryCollection iRequest)
            {
                FilterDefinitionBuilder<Message> builder = Builders<Message>.Filter;
                //var testhardCodedQuery = (builder.Eq("field1", "1") & builder.Eq("field2", "2"));
    
                FilterDefinition<Message> dynamicQuery = builder.Empty;
                var andFilters = builder.Empty;
    
                foreach (KeyValuePair<string, StringValues> filter in iRequest)
                {
                    if (filter.Value != "")
                    {
                        andFilters &= builder.Eq(filter.Key, filter.Value);
                        dynamicQuery = builder.And(andFilters);
    
                    }
                }
                return dynamicQuery;
            }

Not this is only for & operator, need to build out of OR (builder.OR)

Sign up to request clarification or add additional context in comments.

2 Comments

This does not solve my problem, as that would convert my values to a string. e.g: "myurl.com?age=25" would result in "25" (as a string) which would not match the 25 (as an int) in mongodb.
See edit above for expanded version of how i solved

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.