microsoftgraph / microsoftgraph/msgraph-sdk-dotnet

Members@delta is empty during initial group delta sync under AdditionalData

Open
#3,102 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

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

Description

Describe the bug

After updating the MS Graph SDK from 5.37 to 5.105. Members@delta is empty during initial group delta sync. Below is the code snippet & response attached for both versions.

Expected behavior

Expecting members delta under Additional data as shown
"AdditionalData": { "members@delta": [ { "@odata.type": "#microsoft.graph.user", "id": "<Guid>" }, { "@odata.type": "#microsoft.graph.user", "id": "<Guid>" }, { "@odata.type": "#microsoft.graph.user", "id": "<Guid>" } ] },

with latest SDK getting the empty response as show below
"AdditionalData": { "members@delta": {} },

How to reproduce

Code used to reproduce.


private static async Task DebugGroupMembersSyncWorkflow(GraphServiceClient client, string groupId = null!, string deltaLink = null!)
    {
        var nextLink = deltaLink;
        do
        {
            var deltaResponse = string.IsNullOrEmpty(nextLink)
                ? await GetDeltaResponse(client, groupId).ConfigureAwait(false)
                : await GetDeltaResponse(client, new Uri(nextLink)).ConfigureAwait(false);

            nextLink = deltaResponse.NextLink;
            deltaLink = deltaResponse.DeltaLink;

            if (!string.IsNullOrEmpty(nextLink))
            {
                var memberCount = deltaResponse.MemberIds.Count();
                var removedCount = deltaResponse.RemovedMemberIds.Count();
                var hasNextLink = !string.IsNullOrEmpty(deltaResponse.NextLink);

                Console.WriteLine($"MemberIds count: {memberCount}, RemovedMemberIds count: {removedCount}, HasNextLink: {hasNextLink}");
            }

            Console.WriteLine($"HasNextLink: {!string.IsNullOrEmpty(deltaResponse.NextLink)}, HasDeltaLink: {!string.IsNullOrEmpty(deltaResponse.DeltaLink)}");
            
        } while (!string.IsNullOrEmpty(nextLink));

        Console.WriteLine($"Completed group members delta sync and has DeltaSyncLink {!string.IsNullOrEmpty(deltaLink)}");
        Console.WriteLine("Would you want to run delta sync again using the delta link? (y/n): ");
        var input = Console.ReadLine();

        if(input == "y")
        {
            await DebugGroupMembersSyncWorkflow(client, deltaLink: deltaLink);
        }
    }

    private static async Task<GroupMembersDeltaResponse> GetDeltaResponse(GraphServiceClient client, string groupId)
    {
        var deltaResponse = await client.Groups.Delta.GetAsDeltaGetResponseAsync(
            config =>
            {
                config.QueryParameters.Filter = $"id eq '{groupId}'";
                config.QueryParameters.Select = ["displayName", "members"];
            },
            CancellationToken.None).ConfigureAwait(false);

        string serlializedResponse = System.Text.Json.JsonSerializer.Serialize(deltaResponse);

        return deltaResponse!.GetGroupMembersDelta();
    }

    private static async Task<GroupMembersDeltaResponse> GetDeltaResponse(GraphServiceClient client, Uri url)
    {
        var deltaResponse = await client.Groups.Delta
                                .WithUrl(url.ToString())
                                .GetAsDeltaGetResponseAsync(cancellationToken: CancellationToken.None).ConfigureAwait(false);

        ArgumentNullException.ThrowIfNull(deltaResponse, nameof(deltaResponse));

        return deltaResponse.GetGroupMembersDelta();
    }

    private static GroupMembersDeltaResponse GetGroupMembersDelta(this Microsoft.Graph.Groups.Delta.DeltaGetResponse deltaGetResponse)
    {
        ArgumentNullException.ThrowIfNull(deltaGetResponse, nameof(deltaGetResponse));

        var group = deltaGetResponse.Value?.FirstOrDefault();
        if (group == null)
        {
            return new GroupMembersDeltaResponse
            {
                NextLink = deltaGetResponse.OdataNextLink ?? string.Empty,
                DeltaLink = deltaGetResponse.OdataDeltaLink ?? string.Empty,
            };
        }

        var groupMembersDeltaResponse = new GroupMembersDeltaResponse
        {
            GroupId = group.Id!,
            GroupName = group.DisplayName!,
            MemberIds = Enumerable.Empty<string>(),
            RemovedMemberIds = Enumerable.Empty<string>(),
            NextLink = deltaGetResponse.OdataNextLink!,
            DeltaLink = deltaGetResponse.OdataDeltaLink!,
        };

        if (group.AdditionalData.TryGetValue("members@delta", out var membersDelta) && membersDelta is JsonElement membersDeltaElement)
        {
            var members = System.Text.Json.JsonSerializer.Deserialize<IEnumerable<GraphUserRef>>(membersDeltaElement.GetRawText());
            groupMembersDeltaResponse.MemberIds = members?.Where(x => x.Removed == null).Select(x => x.Id) ?? Enumerable.Empty<string>();
            groupMembersDeltaResponse.RemovedMemberIds = members?.Where(x => x.Removed != null).Select(x => x.Id) ?? Enumerable.Empty<string>();
        }

        return groupMembersDeltaResponse;
    }
}


public record GraphUserRef(
    [JsonPropertyName("@odata.type")] string ODataType,
    [JsonPropertyName("id")] string Id,
    [JsonPropertyName("@removed")] string? Removed
);

public class GroupMembersDeltaResponse
{
    /// <summary>
    /// Gets or sets the unique identifier of the group.
    /// </summary>
    public string GroupId { get; set; } = string.Empty;

    /// <summary>
    /// Gets or sets the name of the group.
    /// </summary>
    public string GroupName { get; set; } = string.Empty;

    /// <summary>
    /// Gets or sets the member IDs of the group.
    /// </summary>
    public IEnumerable<string> MemberIds { get; set; } = Array.Empty<string>();

    /// <summary>
    /// Gets or sets the removed member IDs of the group.
    /// </summary>
    public IEnumerable<string> RemovedMemberIds { get; set; } = Array.Empty<string>();

    /// <summary>
    /// Gets or sets the next link for pagination.
    /// </summary>
    public string NextLink { get; set; } = string.Empty;

    /// <summary>
    /// Gets or sets the URL that can be used to retrieve changes since the last request.
    /// </summary>
    public string DeltaLink { get; set; } = string.Empty;
}
SDK Version

5.105

Latest version known to work for scenario above?

5.37

Known Workarounds

Downgrading the package works for now.

Debug output
Click to expand log ```
</details>


### Configuration

_No response_

### 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 with the reproduced GraphServiceClient.Groups.Delta.GetAsDeltaGetResponseAsync calls and compare the serialized AdditionalData response between SDK 5.37 and 5.105. Trace how the initial group delta response handles members@delta, then add a regression test for the reported scenario. Done means members@delta contains the returned member objects instead of an empty object.

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
Quiet
Clarity
Needs clarification
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.