microsoftgraph / microsoftgraph/msgraph-sdk-dotnet

Different filter escaping behaviour in batch and direct requests

Open
#2,708 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

type:bug
Dominant language
C#
Stars
789
Forks
264
Avg merge
15h 17m
Merged PRs (30d)
3

Description

Describe the bug

I'm trying to find a user and a selection of their properties through their email. Currently I'm building a filter to find said user like so:

  • filter: identities/any(id:id/issuer eq ' ' and id/issuerAssignedId eq '{emailAddress}')

For most email addresses, the filtering works as expected for both direct requests using _GraphServiceClient.Users.GetAsync() as well as batch requests and building the requests with _GraphServiceClient.Users.ToGetRequestInformation().

I've noticed that some of our users aren't returned by the batch requests but provide valid responses through a direct request.
The emails have in common that they contain a '+' character, e.g. test+mailPostfix@mail.com. Upon further investigation I've noticed that the Graph API can't handle the '+' character and needs it to be escaped.
Apparently, the filter string is escaped when requesting _GraphServiceClient.Users.GetAsync() but not when using _GraphServiceClient.Users.ToGetRequestInformation() followed by a batch request.

Expected behavior

The filter behaviour should be equal throughout the SDK and not differ between usages. I believe the filter should also be escaped when build a request and sending it through a batch request.

How to reproduce

Assuming I have a list of emails for which I want some B2C properties like the display name.

When I use the the SDK by sending the requests one by one like so, everything works as expected:

var usersResponse = await _GraphServiceClient.Users
            .GetAsync(requestConfig =>
            {
                requestConfig.QueryParameters.Top = 999;
                requestConfig.QueryParameters.Select = ["displayName", "identities", "otherMails", "id"];
                requestConfig.QueryParameters.Filter = $"identities/any(id:id/issuer eq ' ' and id/issuerAssignedId eq '{emailAddress}')";
            }, cancellationToken);

if (usersResponse == null)
{
    return null;
}

var userList = new List<User>();
var pageIterator = PageIterator<User, UserCollectionResponse>.CreatePageIterator(_GraphServiceClient, usersResponse, user =>
{
    userList.Add(user);
    return true;
});
await pageIterator.IterateAsync(cancellationToken);

return userList;

But creating a batch request when the list of email addresses might become bigger creates empty results for emails with a '+' character. The request is built like this:

var batchRequestContent = new BatchRequestContentCollection(_GraphServiceClient);
var requestIdsForEmails = new Dictionary<string, string>();
foreach (var emailAddress in emailAddresses)
{
     var identityRequest = _GraphServiceClient.Users.ToGetRequestInformation(requestConfig =>
     {
          requestConfig.QueryParameters.Select = UserSelections;
          requestConfig.QueryParameters.Filter = $"identities/any(id:id/issuer eq ' ' and id/issuerAssignedId eq '{emailAddress}')"
     });

     var identityRequestId = await batchRequestContent.AddBatchRequestStepAsync(identityRequest);

     requestIdsForEmails.Add(emailAddress, [identityRequestId, otherMailRequestId]);
}
var batchResponse = await _GraphServiceClient.Batch.PostAsync(batchRequestContent, cancellationToken);

var emailsWithUsers = new Dictionary<string, IEnumerable<User>>();
foreach (var emailWithRequestIds in requestIdsForEmails)
{
     try
     {
          var userResponse = await batchResponse.GetResponseByIdAsync<UserCollectionResponse>(emailWithRequestIds.Value);

          emailsWithUsers.Add(emailWithRequestIds.Key, userResponse.Value);
      }
      catch (Exception)
      {
          emailsWithUsers.Add(emailWithRequestIds.Key, []);
      }
}
return emailsWithUsers;
SDK Version

5.59.0

Latest version known to work for scenario above?

No response

Known Workarounds

I've fixed the issue by explicitly escaping the emailAddress before using it in the filter:

var escapedEmail = Uri.EscapeDataString(emailAddress);
var identityRequest = _GraphServiceClient.Users.ToGetRequestInformation(requestConfig =>
{
    requestConfig.QueryParameters.Select = UserSelections;
    requestConfig.QueryParameters.Filter = $"identities/any(id:id/issuer eq ' ' and id/issuerAssignedId eq '{escapedEmail }')"
});

But this creates an odd situation since escaping the emailAddress with a direct request to _GraphServiceClient.Users.GetAsync() breaks the direct request. So I need to handle the email addresses differently which creates a confusing situation in the code base.

Debug output

No response

Configuration
  • OS: Windows 11
  • architecture x64
Other information

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the difference between Users.GetAsync() and Users.ToGetRequestInformation() in a batch request with an email containing '+'. Compare how the filter is serialized in both paths, and verify completion when equivalent requests return the same user results without requiring different caller-side escaping.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
api
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.