From 21ae1a74ece01d621e590fb8662871a8965f8dc7 Mon Sep 17 00:00:00 2001 From: javierdfm Date: Fri, 9 Jan 2026 16:12:30 +0100 Subject: [PATCH 1/8] feat: add channel batch updates support --- src/Clients/ChannelBatchUpdater.cs | 201 +++++++++++++++++++++++++++++ src/Clients/ChannelClient.cs | 8 ++ src/Clients/IChannelClient.cs | 12 ++ src/Models/Channel.cs | 108 ++++++++++++++++ tests/ChannelBatchUpdaterTests.cs | 179 +++++++++++++++++++++++++ tests/TestBase.cs | 2 +- 6 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 src/Clients/ChannelBatchUpdater.cs create mode 100644 tests/ChannelBatchUpdaterTests.cs diff --git a/src/Clients/ChannelBatchUpdater.cs b/src/Clients/ChannelBatchUpdater.cs new file mode 100644 index 0000000..165c8c2 --- /dev/null +++ b/src/Clients/ChannelBatchUpdater.cs @@ -0,0 +1,201 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using StreamChat.Models; + +namespace StreamChat.Clients +{ + /// + /// Provides convenience methods for batch channel operations. + /// + public class ChannelBatchUpdater + { + private readonly IChannelClient _client; + + public ChannelBatchUpdater(IChannelClient client) + { + _client = client; + } + + /// + /// Adds members to channels matching the filter. + /// + public async Task AddMembersAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.AddMembers, + Filter = filter, + Members = members, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Removes members from channels matching the filter. + /// + public async Task RemoveMembersAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.RemoveMembers, + Filter = filter, + Members = members, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Invites members to channels matching the filter. + /// + public async Task InviteMembersAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.InviteMembers, + Filter = filter, + Members = members, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Assigns roles to members in channels matching the filter. + /// + public async Task AssignRolesAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.AssignRoles, + Filter = filter, + Members = members, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Adds moderators to channels matching the filter. + /// + public async Task AddModeratorsAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.AddModerators, + Filter = filter, + Members = members, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Removes moderator role from members in channels matching the filter. + /// + public async Task DemoteModeratorsAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.DemoteModerators, + Filter = filter, + Members = members, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Hides channels matching the filter for the specified members. + /// + public async Task HideAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.Hide, + Filter = filter, + Members = members, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Shows channels matching the filter for the specified members. + /// + public async Task ShowAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.Show, + Filter = filter, + Members = members, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Archives channels matching the filter for the specified members. + /// + public async Task ArchiveAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.Archive, + Filter = filter, + Members = members, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Unarchives channels matching the filter for the specified members. + /// + public async Task UnarchiveAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.Unarchive, + Filter = filter, + Members = members, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Updates data on channels matching the filter. + /// + public async Task UpdateDataAsync(ChannelsBatchFilters filter, ChannelDataUpdate data) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.UpdateData, + Filter = filter, + Data = data, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Adds filter tags to channels matching the filter. + /// + public async Task AddFilterTagsAsync(ChannelsBatchFilters filter, IEnumerable tags) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.AddFilterTags, + Filter = filter, + FilterTagsUpdate = tags, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Removes filter tags from channels matching the filter. + /// + public async Task RemoveFilterTagsAsync(ChannelsBatchFilters filter, IEnumerable tags) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.RemoveFilterTags, + Filter = filter, + FilterTagsUpdate = tags, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + } +} diff --git a/src/Clients/ChannelClient.cs b/src/Clients/ChannelClient.cs index f20a9d3..d90ff3d 100644 --- a/src/Clients/ChannelClient.cs +++ b/src/Clients/ChannelClient.cs @@ -38,6 +38,14 @@ public async Task DeleteChannelsAsync(IEnumerable UpdateChannelsBatchAsync(ChannelsBatchOptions options) + => await ExecuteRequestAsync("channels/batch", + HttpMethod.PUT, + HttpStatusCode.OK, + options); + + public ChannelBatchUpdater BatchUpdater() => new ChannelBatchUpdater(this); + public async Task HideAsync(string channelType, string channelId, string userId, bool clearHistory = false) => await ExecuteRequestAsync($"channels/{channelType}/{channelId}/hide", HttpMethod.POST, diff --git a/src/Clients/IChannelClient.cs b/src/Clients/IChannelClient.cs index 53e5763..f3c4e77 100644 --- a/src/Clients/IChannelClient.cs +++ b/src/Clients/IChannelClient.cs @@ -50,6 +50,18 @@ Task AssignRolesAsync(string channelType, string channelI /// https://getstream.io/chat/docs/dotnet-csharp/channel_delete/?language=csharp Task DeleteChannelsAsync(IEnumerable cids, bool hardDelete = false); + /// + /// Updates channels in batch based on the provided options. + /// This is an asynchronous operation and the returned value is a task Id. + /// You can use to check the status of the task. + /// + Task UpdateChannelsBatchAsync(ChannelsBatchOptions options); + + /// + /// Returns a instance for convenient batch channel operations. + /// + ChannelBatchUpdater BatchUpdater(); + /// /// Takes away moderators status from given user ids. /// diff --git a/src/Models/Channel.cs b/src/Models/Channel.cs index bfc34ab..9785a0b 100644 --- a/src/Models/Channel.cs +++ b/src/Models/Channel.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Runtime.Serialization; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace StreamChat.Models { @@ -155,4 +157,110 @@ public class ChannelUnmuteRequest : ChannelMuteRequest public class ChannelUnmuteResponse : ChannelMuteResponse { } + + [JsonConverter(typeof(StringEnumConverter))] + public enum ChannelBatchOperation + { + [EnumMember(Value = "addMembers")] + AddMembers, + + [EnumMember(Value = "removeMembers")] + RemoveMembers, + + [EnumMember(Value = "inviteMembers")] + InviteMembers, + + [EnumMember(Value = "assignRoles")] + AssignRoles, + + [EnumMember(Value = "addModerators")] + AddModerators, + + [EnumMember(Value = "demoteModerators")] + DemoteModerators, + + [EnumMember(Value = "hide")] + Hide, + + [EnumMember(Value = "show")] + Show, + + [EnumMember(Value = "archive")] + Archive, + + [EnumMember(Value = "unarchive")] + Unarchive, + + [EnumMember(Value = "updateData")] + UpdateData, + + [EnumMember(Value = "addFilterTags")] + AddFilterTags, + + [EnumMember(Value = "removeFilterTags")] + RemoveFilterTags, + } + + public class ChannelBatchMemberRequest + { + [JsonProperty("user_id")] + public string UserId { get; set; } + + [JsonProperty("channel_role")] + public string ChannelRole { get; set; } + } + + public class ChannelDataUpdate + { + [JsonProperty("frozen")] + public bool? Frozen { get; set; } + + [JsonProperty("disabled")] + public bool? Disabled { get; set; } + + [JsonProperty("custom")] + public Dictionary Custom { get; set; } + + [JsonProperty("team")] + public string Team { get; set; } + + [JsonProperty("config_overrides")] + public Dictionary ConfigOverrides { get; set; } + + [JsonProperty("auto_translation_enabled")] + public bool? AutoTranslationEnabled { get; set; } + + [JsonProperty("auto_translation_language")] + public string AutoTranslationLanguage { get; set; } + } + + public class ChannelsBatchFilters + { + [JsonProperty("cids")] + public object Cids { get; set; } + + [JsonProperty("types")] + public object Types { get; set; } + + [JsonProperty("filter_tags")] + public object FilterTags { get; set; } + } + + public class ChannelsBatchOptions + { + [JsonProperty("operation")] + public ChannelBatchOperation Operation { get; set; } + + [JsonProperty("filter")] + public ChannelsBatchFilters Filter { get; set; } + + [JsonProperty("members")] + public IEnumerable Members { get; set; } + + [JsonProperty("data")] + public ChannelDataUpdate Data { get; set; } + + [JsonProperty("filter_tags_update")] + public IEnumerable FilterTagsUpdate { get; set; } + } } diff --git a/tests/ChannelBatchUpdaterTests.cs b/tests/ChannelBatchUpdaterTests.cs new file mode 100644 index 0000000..15ebb68 --- /dev/null +++ b/tests/ChannelBatchUpdaterTests.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using NUnit.Framework; +using StreamChat.Clients; +using StreamChat.Models; + +namespace StreamChatTests +{ + /// Tests for + /// + /// The tests follow arrange-act-assert pattern divided by empty lines. + /// Please make sure to follow the pattern to keep the consistency. + /// + [TestFixture] + public class ChannelBatchUpdaterTests : TestBase + { + [Test] + public async Task TestUpdateChannelsBatchWithValidOptionsAsync() + { + var ch1 = await CreateChannelAsync(createdByUserId: (await UpsertNewUserAsync()).Id); + var ch2 = await CreateChannelAsync(createdByUserId: (await UpsertNewUserAsync()).Id); + var userToAdd = await UpsertNewUserAsync(); + + var filter = new ChannelsBatchFilters + { + Cids = new Dictionary + { + { "$in", new[] { ch1.Cid, ch2.Cid } }, + }, + }; + + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.AddMembers, + Filter = filter, + Members = new[] { new ChannelBatchMemberRequest { UserId = userToAdd.Id } }, + }; + + var response = await _channelClient.UpdateChannelsBatchAsync(options); + + response.TaskId.Should().NotBeNullOrEmpty(); + } + + [Test] + public async Task TestChannelBatchUpdaterAddMembersAsync() + { + var user1 = await UpsertNewUserAsync(); + var ch1 = await CreateChannelAsync(createdByUserId: user1.Id); + var ch2 = await CreateChannelAsync(createdByUserId: user1.Id); + + var user2 = await UpsertNewUserAsync(); + var user3 = await UpsertNewUserAsync(); + + var filter = new ChannelsBatchFilters + { + Cids = new Dictionary + { + { "$in", new[] { ch1.Cid, ch2.Cid } }, + }, + }; + + var members = new[] + { + new ChannelBatchMemberRequest { UserId = user2.Id }, + new ChannelBatchMemberRequest { UserId = user3.Id }, + }; + + var updater = _channelClient.BatchUpdater(); + var response = await updater.AddMembersAsync(filter, members); + + response.TaskId.Should().NotBeNullOrEmpty(); + await WaitUntilTaskSucceedsAsync(response.TaskId); + + var ch1Members = await _channelClient.QueryMembersAsync(new QueryMembersRequest + { + Type = ch1.Type, + Id = ch1.Id, + FilterConditions = new Dictionary(), + }); + + var ch2Members = await _channelClient.QueryMembersAsync(new QueryMembersRequest + { + Type = ch2.Type, + Id = ch2.Id, + FilterConditions = new Dictionary(), + }); + + ch1Members.Members.Should().Contain(m => m.UserId == user2.Id); + ch1Members.Members.Should().Contain(m => m.UserId == user3.Id); + ch2Members.Members.Should().Contain(m => m.UserId == user2.Id); + ch2Members.Members.Should().Contain(m => m.UserId == user3.Id); + } + + [Test] + public async Task TestChannelBatchUpdaterRemoveMembersAsync() + { + var user1 = await UpsertNewUserAsync(); + var user2 = await UpsertNewUserAsync(); + var user3 = await UpsertNewUserAsync(); + + var ch1 = await CreateChannelAsync(createdByUserId: user1.Id, members: new[] { user1.Id, user2.Id, user3.Id }); + var ch2 = await CreateChannelAsync(createdByUserId: user1.Id, members: new[] { user1.Id, user2.Id, user3.Id }); + + var filter = new ChannelsBatchFilters + { + Cids = new Dictionary + { + { "$in", new[] { ch1.Cid, ch2.Cid } }, + }, + }; + + var members = new[] + { + new ChannelBatchMemberRequest { UserId = user2.Id }, + }; + + var updater = _channelClient.BatchUpdater(); + var response = await updater.RemoveMembersAsync(filter, members); + + response.TaskId.Should().NotBeNullOrEmpty(); + await WaitUntilTaskSucceedsAsync(response.TaskId); + + var ch1Members = await _channelClient.QueryMembersAsync(new QueryMembersRequest + { + Type = ch1.Type, + Id = ch1.Id, + FilterConditions = new Dictionary(), + }); + + var ch2Members = await _channelClient.QueryMembersAsync(new QueryMembersRequest + { + Type = ch2.Type, + Id = ch2.Id, + FilterConditions = new Dictionary(), + }); + + ch1Members.Members.Should().NotContain(m => m.UserId == user2.Id); + ch2Members.Members.Should().NotContain(m => m.UserId == user2.Id); + } + + [Test] + public async Task TestChannelBatchUpdaterArchiveAsync() + { + var user1 = await UpsertNewUserAsync(); + var user2 = await UpsertNewUserAsync(); + + var ch1 = await CreateChannelAsync(createdByUserId: user1.Id, members: new[] { user1.Id, user2.Id }); + var ch2 = await CreateChannelAsync(createdByUserId: user1.Id, members: new[] { user1.Id, user2.Id }); + + var filter = new ChannelsBatchFilters + { + Cids = new Dictionary + { + { "$in", new[] { ch1.Cid, ch2.Cid } }, + }, + }; + + var members = new[] + { + new ChannelBatchMemberRequest { UserId = user1.Id }, + }; + + var updater = _channelClient.BatchUpdater(); + var response = await updater.ArchiveAsync(filter, members); + + response.TaskId.Should().NotBeNullOrEmpty(); + await WaitUntilTaskSucceedsAsync(response.TaskId); + + var ch1State = await _channelClient.GetOrCreateAsync(ch1.Type, ch1.Id, new ChannelGetRequest()); + var ch1Member = ch1State.Members.FirstOrDefault(m => m.UserId == user1.Id); + + ch1Member.Should().NotBeNull(); + ch1Member.ArchivedAt.Should().NotBeNull(); + } + } +} diff --git a/tests/TestBase.cs b/tests/TestBase.cs index 3e96d44..fb8f52f 100644 --- a/tests/TestBase.cs +++ b/tests/TestBase.cs @@ -212,7 +212,7 @@ protected async Task TryMultipleAsync(Func testBody, } } - private async Task WaitUntilTaskSucceedsAsync(string taskId) + protected async Task WaitUntilTaskSucceedsAsync(string taskId) { try { From c1a984c65e45e2735bf0e1adf951bd75e4eb5d02 Mon Sep 17 00:00:00 2001 From: javierdfm Date: Fri, 9 Jan 2026 16:24:25 +0100 Subject: [PATCH 2/8] fix: enum --- src/Models/Channel.cs | 3 +- src/Utils/EnumMemberStringEnumConverter.cs | 58 ++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/Utils/EnumMemberStringEnumConverter.cs diff --git a/src/Models/Channel.cs b/src/Models/Channel.cs index 9785a0b..4cb9f49 100644 --- a/src/Models/Channel.cs +++ b/src/Models/Channel.cs @@ -3,6 +3,7 @@ using System.Runtime.Serialization; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using StreamChat.Utils; namespace StreamChat.Models { @@ -158,7 +159,6 @@ public class ChannelUnmuteResponse : ChannelMuteResponse { } - [JsonConverter(typeof(StringEnumConverter))] public enum ChannelBatchOperation { [EnumMember(Value = "addMembers")] @@ -249,6 +249,7 @@ public class ChannelsBatchFilters public class ChannelsBatchOptions { [JsonProperty("operation")] + [JsonConverter(typeof(Utils.EnumMemberStringEnumConverter))] public ChannelBatchOperation Operation { get; set; } [JsonProperty("filter")] diff --git a/src/Utils/EnumMemberStringEnumConverter.cs b/src/Utils/EnumMemberStringEnumConverter.cs new file mode 100644 index 0000000..ca81892 --- /dev/null +++ b/src/Utils/EnumMemberStringEnumConverter.cs @@ -0,0 +1,58 @@ +using System; +using System.Reflection; +using Newtonsoft.Json; +using StreamChat.Utils; + +namespace StreamChat.Utils +{ + /// + /// A JsonConverter that serializes enums as strings, respecting EnumMember attributes. + /// + public class EnumMemberStringEnumConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + var enumValue = (Enum)value; + writer.WriteValue(enumValue.ToEnumMemberString()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + if (reader.TokenType == JsonToken.String) + { + var stringValue = reader.Value.ToString(); + var enumValues = Enum.GetValues(objectType); + + foreach (Enum enumValue in enumValues) + { + var enumMemberString = enumValue.ToEnumMemberString(); + if (string.Equals(enumMemberString, stringValue, StringComparison.OrdinalIgnoreCase)) + { + return enumValue; + } + } + + // Fallback to standard enum parsing + return Enum.Parse(objectType, stringValue, true); + } + + throw new JsonSerializationException($"Unexpected token type: {reader.TokenType}"); + } + + public override bool CanConvert(Type objectType) + { + return objectType.GetTypeInfo().IsEnum; + } + } +} From 278e38076e1210aaf52211796649e7513013bfed Mon Sep 17 00:00:00 2001 From: javierdfm Date: Fri, 9 Jan 2026 16:29:13 +0100 Subject: [PATCH 3/8] fix: enum conversion --- src/Utils/EnumMemberStringEnumConverter.cs | 23 ++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Utils/EnumMemberStringEnumConverter.cs b/src/Utils/EnumMemberStringEnumConverter.cs index ca81892..d07a1d5 100644 --- a/src/Utils/EnumMemberStringEnumConverter.cs +++ b/src/Utils/EnumMemberStringEnumConverter.cs @@ -1,7 +1,8 @@ using System; +using System.Linq; using System.Reflection; +using System.Runtime.Serialization; using Newtonsoft.Json; -using StreamChat.Utils; namespace StreamChat.Utils { @@ -18,8 +19,15 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s return; } - var enumValue = (Enum)value; - writer.WriteValue(enumValue.ToEnumMemberString()); + var enumType = value.GetType(); + var enumValue = value.ToString(); + var memberInfo = enumType.GetTypeInfo().DeclaredMembers + .SingleOrDefault(x => x.Name == enumValue); + + var enumMemberAttribute = memberInfo?.GetCustomAttribute(false); + var stringValue = enumMemberAttribute?.Value ?? enumValue; + + writer.WriteValue(stringValue); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) @@ -33,10 +41,17 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { var stringValue = reader.Value.ToString(); var enumValues = Enum.GetValues(objectType); + var enumTypeInfo = objectType.GetTypeInfo(); foreach (Enum enumValue in enumValues) { - var enumMemberString = enumValue.ToEnumMemberString(); + var enumValueString = enumValue.ToString(); + var memberInfo = enumTypeInfo.DeclaredMembers + .SingleOrDefault(x => x.Name == enumValueString); + + var enumMemberAttribute = memberInfo?.GetCustomAttribute(false); + var enumMemberString = enumMemberAttribute?.Value ?? enumValueString; + if (string.Equals(enumMemberString, stringValue, StringComparison.OrdinalIgnoreCase)) { return enumValue; From af93775f84134d3c197e79d3c88cadfee6308b51 Mon Sep 17 00:00:00 2001 From: javierdfm Date: Fri, 9 Jan 2026 16:30:19 +0100 Subject: [PATCH 4/8] fix: lint --- src/Utils/EnumMemberStringEnumConverter.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Utils/EnumMemberStringEnumConverter.cs b/src/Utils/EnumMemberStringEnumConverter.cs index d07a1d5..560808c 100644 --- a/src/Utils/EnumMemberStringEnumConverter.cs +++ b/src/Utils/EnumMemberStringEnumConverter.cs @@ -23,10 +23,10 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s var enumValue = value.ToString(); var memberInfo = enumType.GetTypeInfo().DeclaredMembers .SingleOrDefault(x => x.Name == enumValue); - + var enumMemberAttribute = memberInfo?.GetCustomAttribute(false); var stringValue = enumMemberAttribute?.Value ?? enumValue; - + writer.WriteValue(stringValue); } @@ -42,22 +42,22 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var stringValue = reader.Value.ToString(); var enumValues = Enum.GetValues(objectType); var enumTypeInfo = objectType.GetTypeInfo(); - + foreach (Enum enumValue in enumValues) { var enumValueString = enumValue.ToString(); var memberInfo = enumTypeInfo.DeclaredMembers .SingleOrDefault(x => x.Name == enumValueString); - + var enumMemberAttribute = memberInfo?.GetCustomAttribute(false); var enumMemberString = enumMemberAttribute?.Value ?? enumValueString; - + if (string.Equals(enumMemberString, stringValue, StringComparison.OrdinalIgnoreCase)) { return enumValue; } } - + // Fallback to standard enum parsing return Enum.Parse(objectType, stringValue, true); } From 8d6458b5015c6fb28ae0eba3d021483f941e9856 Mon Sep 17 00:00:00 2001 From: javierdfm Date: Fri, 9 Jan 2026 16:54:16 +0100 Subject: [PATCH 5/8] fix: fixing enum converter --- src/Models/Channel.cs | 5 +- src/Utils/EnumMemberStringEnumConverter.cs | 73 ---------------------- 2 files changed, 2 insertions(+), 76 deletions(-) delete mode 100644 src/Utils/EnumMemberStringEnumConverter.cs diff --git a/src/Models/Channel.cs b/src/Models/Channel.cs index 4cb9f49..2e2278d 100644 --- a/src/Models/Channel.cs +++ b/src/Models/Channel.cs @@ -3,7 +3,6 @@ using System.Runtime.Serialization; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using StreamChat.Utils; namespace StreamChat.Models { @@ -159,6 +158,7 @@ public class ChannelUnmuteResponse : ChannelMuteResponse { } + [JsonConverter(typeof(StringEnumConverter))] public enum ChannelBatchOperation { [EnumMember(Value = "addMembers")] @@ -248,8 +248,7 @@ public class ChannelsBatchFilters public class ChannelsBatchOptions { - [JsonProperty("operation")] - [JsonConverter(typeof(Utils.EnumMemberStringEnumConverter))] + [JsonProperty("operation", DefaultValueHandling = DefaultValueHandling.Include)] public ChannelBatchOperation Operation { get; set; } [JsonProperty("filter")] diff --git a/src/Utils/EnumMemberStringEnumConverter.cs b/src/Utils/EnumMemberStringEnumConverter.cs deleted file mode 100644 index 560808c..0000000 --- a/src/Utils/EnumMemberStringEnumConverter.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Runtime.Serialization; -using Newtonsoft.Json; - -namespace StreamChat.Utils -{ - /// - /// A JsonConverter that serializes enums as strings, respecting EnumMember attributes. - /// - public class EnumMemberStringEnumConverter : JsonConverter - { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - return; - } - - var enumType = value.GetType(); - var enumValue = value.ToString(); - var memberInfo = enumType.GetTypeInfo().DeclaredMembers - .SingleOrDefault(x => x.Name == enumValue); - - var enumMemberAttribute = memberInfo?.GetCustomAttribute(false); - var stringValue = enumMemberAttribute?.Value ?? enumValue; - - writer.WriteValue(stringValue); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - { - return null; - } - - if (reader.TokenType == JsonToken.String) - { - var stringValue = reader.Value.ToString(); - var enumValues = Enum.GetValues(objectType); - var enumTypeInfo = objectType.GetTypeInfo(); - - foreach (Enum enumValue in enumValues) - { - var enumValueString = enumValue.ToString(); - var memberInfo = enumTypeInfo.DeclaredMembers - .SingleOrDefault(x => x.Name == enumValueString); - - var enumMemberAttribute = memberInfo?.GetCustomAttribute(false); - var enumMemberString = enumMemberAttribute?.Value ?? enumValueString; - - if (string.Equals(enumMemberString, stringValue, StringComparison.OrdinalIgnoreCase)) - { - return enumValue; - } - } - - // Fallback to standard enum parsing - return Enum.Parse(objectType, stringValue, true); - } - - throw new JsonSerializationException($"Unexpected token type: {reader.TokenType}"); - } - - public override bool CanConvert(Type objectType) - { - return objectType.GetTypeInfo().IsEnum; - } - } -} From 23a7a9fd33d3825b2d694870bd69e77f8a290707 Mon Sep 17 00:00:00 2001 From: javierdfm Date: Thu, 15 Jan 2026 09:04:32 +0100 Subject: [PATCH 6/8] fix: attempt to fix tests --- src/Clients/ChannelBatchUpdater.cs | 25 +++++++++++++------------ src/Models/Channel.cs | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Clients/ChannelBatchUpdater.cs b/src/Clients/ChannelBatchUpdater.cs index 165c8c2..ab62e1e 100644 --- a/src/Clients/ChannelBatchUpdater.cs +++ b/src/Clients/ChannelBatchUpdater.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using StreamChat.Models; @@ -25,7 +26,7 @@ public async Task AddMembersAsync(ChannelsBatchFilters f { Operation = ChannelBatchOperation.AddMembers, Filter = filter, - Members = members, + Members = members?.ToList(), }; return await _client.UpdateChannelsBatchAsync(options); } @@ -39,7 +40,7 @@ public async Task RemoveMembersAsync(ChannelsBatchFilter { Operation = ChannelBatchOperation.RemoveMembers, Filter = filter, - Members = members, + Members = members?.ToList(), }; return await _client.UpdateChannelsBatchAsync(options); } @@ -53,7 +54,7 @@ public async Task InviteMembersAsync(ChannelsBatchFilter { Operation = ChannelBatchOperation.InviteMembers, Filter = filter, - Members = members, + Members = members?.ToList(), }; return await _client.UpdateChannelsBatchAsync(options); } @@ -67,7 +68,7 @@ public async Task AssignRolesAsync(ChannelsBatchFilters { Operation = ChannelBatchOperation.AssignRoles, Filter = filter, - Members = members, + Members = members?.ToList(), }; return await _client.UpdateChannelsBatchAsync(options); } @@ -81,7 +82,7 @@ public async Task AddModeratorsAsync(ChannelsBatchFilter { Operation = ChannelBatchOperation.AddModerators, Filter = filter, - Members = members, + Members = members?.ToList(), }; return await _client.UpdateChannelsBatchAsync(options); } @@ -95,7 +96,7 @@ public async Task DemoteModeratorsAsync(ChannelsBatchFil { Operation = ChannelBatchOperation.DemoteModerators, Filter = filter, - Members = members, + Members = members?.ToList(), }; return await _client.UpdateChannelsBatchAsync(options); } @@ -109,7 +110,7 @@ public async Task HideAsync(ChannelsBatchFilters filter, { Operation = ChannelBatchOperation.Hide, Filter = filter, - Members = members, + Members = members?.ToList(), }; return await _client.UpdateChannelsBatchAsync(options); } @@ -123,7 +124,7 @@ public async Task ShowAsync(ChannelsBatchFilters filter, { Operation = ChannelBatchOperation.Show, Filter = filter, - Members = members, + Members = members?.ToList(), }; return await _client.UpdateChannelsBatchAsync(options); } @@ -137,7 +138,7 @@ public async Task ArchiveAsync(ChannelsBatchFilters filt { Operation = ChannelBatchOperation.Archive, Filter = filter, - Members = members, + Members = members?.ToList(), }; return await _client.UpdateChannelsBatchAsync(options); } @@ -151,7 +152,7 @@ public async Task UnarchiveAsync(ChannelsBatchFilters fi { Operation = ChannelBatchOperation.Unarchive, Filter = filter, - Members = members, + Members = members?.ToList(), }; return await _client.UpdateChannelsBatchAsync(options); } @@ -179,7 +180,7 @@ public async Task AddFilterTagsAsync(ChannelsBatchFilter { Operation = ChannelBatchOperation.AddFilterTags, Filter = filter, - FilterTagsUpdate = tags, + FilterTagsUpdate = tags?.ToList(), }; return await _client.UpdateChannelsBatchAsync(options); } @@ -193,7 +194,7 @@ public async Task RemoveFilterTagsAsync(ChannelsBatchFil { Operation = ChannelBatchOperation.RemoveFilterTags, Filter = filter, - FilterTagsUpdate = tags, + FilterTagsUpdate = tags?.ToList(), }; return await _client.UpdateChannelsBatchAsync(options); } diff --git a/src/Models/Channel.cs b/src/Models/Channel.cs index 2e2278d..9785a0b 100644 --- a/src/Models/Channel.cs +++ b/src/Models/Channel.cs @@ -248,7 +248,7 @@ public class ChannelsBatchFilters public class ChannelsBatchOptions { - [JsonProperty("operation", DefaultValueHandling = DefaultValueHandling.Include)] + [JsonProperty("operation")] public ChannelBatchOperation Operation { get; set; } [JsonProperty("filter")] From 88a2aee5fac0cfd2b7a74f20db08b9398f2a2da1 Mon Sep 17 00:00:00 2001 From: javierdfm Date: Thu, 15 Jan 2026 13:32:34 +0100 Subject: [PATCH 7/8] fix: another attempt --- src/Models/Channel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Channel.cs b/src/Models/Channel.cs index 9785a0b..2e2278d 100644 --- a/src/Models/Channel.cs +++ b/src/Models/Channel.cs @@ -248,7 +248,7 @@ public class ChannelsBatchFilters public class ChannelsBatchOptions { - [JsonProperty("operation")] + [JsonProperty("operation", DefaultValueHandling = DefaultValueHandling.Include)] public ChannelBatchOperation Operation { get; set; } [JsonProperty("filter")] From 17aba18daee3f4d582f7a569aa58913d6f94126d Mon Sep 17 00:00:00 2001 From: javierdfm Date: Fri, 16 Jan 2026 17:22:51 +0100 Subject: [PATCH 8/8] fix: tests --- src/Clients/ChannelClient.cs | 2 +- tests/ChannelBatchUpdaterTests.cs | 31 +++++++++++++++++-------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Clients/ChannelClient.cs b/src/Clients/ChannelClient.cs index d90ff3d..f7ec886 100644 --- a/src/Clients/ChannelClient.cs +++ b/src/Clients/ChannelClient.cs @@ -41,7 +41,7 @@ public async Task DeleteChannelsAsync(IEnumerable UpdateChannelsBatchAsync(ChannelsBatchOptions options) => await ExecuteRequestAsync("channels/batch", HttpMethod.PUT, - HttpStatusCode.OK, + HttpStatusCode.Created, options); public ChannelBatchUpdater BatchUpdater() => new ChannelBatchUpdater(this); diff --git a/tests/ChannelBatchUpdaterTests.cs b/tests/ChannelBatchUpdaterTests.cs index 15ebb68..c00e0e0 100644 --- a/tests/ChannelBatchUpdaterTests.cs +++ b/tests/ChannelBatchUpdaterTests.cs @@ -123,22 +123,25 @@ public async Task TestChannelBatchUpdaterRemoveMembersAsync() response.TaskId.Should().NotBeNullOrEmpty(); await WaitUntilTaskSucceedsAsync(response.TaskId); - var ch1Members = await _channelClient.QueryMembersAsync(new QueryMembersRequest - { - Type = ch1.Type, - Id = ch1.Id, - FilterConditions = new Dictionary(), - }); - - var ch2Members = await _channelClient.QueryMembersAsync(new QueryMembersRequest + await WaitForAsync(async () => { - Type = ch2.Type, - Id = ch2.Id, - FilterConditions = new Dictionary(), - }); + var ch1Members = await _channelClient.QueryMembersAsync(new QueryMembersRequest + { + Type = ch1.Type, + Id = ch1.Id, + FilterConditions = new Dictionary(), + }); - ch1Members.Members.Should().NotContain(m => m.UserId == user2.Id); - ch2Members.Members.Should().NotContain(m => m.UserId == user2.Id); + var ch2Members = await _channelClient.QueryMembersAsync(new QueryMembersRequest + { + Type = ch2.Type, + Id = ch2.Id, + FilterConditions = new Dictionary(), + }); + + return ch1Members.Members.All(m => m.UserId != user2.Id) + && ch2Members.Members.All(m => m.UserId != user2.Id); + }, timeout: 20000, delay: 1000); } [Test]